ChatGPTやClaude Codeに「為替レートの表を毎日CSVに保存して」と頼むと、今でも pd.read_html(res.text) と書いたコードが出てきます。pandas read_html の StringIO 対応が入っていないこのコードは、警告を出しながらしばらくは動きます。しかしタイムアウトが無い、表の取り違えに気づけない、失敗しても黙って止まる、といった問題も抱えています。この記事では、AIへの指示文を実物で示しながら、毎日止まらない取り込みスクリプトに直すまでを手順にします。
pandas 2.1でread_htmlの文字列渡しが非推奨に:AIが今も書く古い形
何が変わったのか
pandas 2.1から、read_html() にHTMLの文字列をそのまま渡す書き方が非推奨になりました。今は io.StringIO で包み、「ファイルのように読めるもの」として渡します。StringIOは、文字列をファイルの代わりに扱えるようにする標準ライブラリのクラスです。
以前の書き方 | 今の書き方 | |
|---|---|---|
表の読み込み |
|
|
実行時の挙動 | FutureWarning(将来の仕様変更の予告)が出る | 警告なし |
将来のpandas | 警告文に「将来のバージョンで削除」と明記されている | そのまま動く |
古い書き方で実行すると、次の警告が出ます。
FutureWarning: Passing literal html to 'read_html' is deprecated and will be removed in a future version. To read from a literal string, wrap it in a 'StringIO' object.警告なので処理自体は続きます。そのため、毎日cronで回していると誰も気づきません。ある日pandasを更新した時点で、取り込みが止まります。どのバージョンで削除されるかは、お使いの環境で pip show pandas を実行し、pandas公式ドキュメントのリリースノートで確認してください。
AIはなぜ古い形を出すのか
生成AIは、学習した大量の過去コードから「よくある書き方」を出します。read_html(res.text) はpandas 2.1より前に長く使われていたため、今でも出てきやすい形です。
Stack Overflowの2025年調査では、AIコーディングツールを使う開発者が84%に達しています。一方で、AIの精度を信頼する人は40%から29%に下がりました。最大の不満として45%が挙げたのは「惜しいが惜しいだけのコード(almost right, but not quite)」です。read_htmlのコードは、まさにこの「惜しい」の典型です。
ChatGPT・Claude Codeへの指示文と、出てきたコードの直し方
実行環境
- Python 3(
python3 -Vで確認。3.10以上を想定) pip install pandas lxml requests- pandasは2.1以上を前提にします。StringIOで包む書き方は2.1未満でも動きますが、警告が出るのは2.1以上です。
pip show pandasで確認してください lxmlは、read_htmlがHTMLを解析するときに使うライブラリです。これが無いとImportErrorで止まります
最初の指示と、AIが出したコード
多くの人が最初に送るのは、このくらいの指示です。
https://example.com/rates にある為替レートの表を取得して、
rates.csv に毎日追記するPythonスクリプトを書いてください。すると、次のようなコードがよく返ってきます。
import pandas as pd
import requests
url = "https://example.com/rates"
try:
res = requests.get(url)
tables = pd.read_html(res.text)
df = tables[0]
df.to_csv("rates.csv", mode="a", index=False)
print("保存しました")
except Exception as e:
print(e)手元で1回動かすと、CSVはできます。しかし、毎日回すと問題が出る箇所が6つあります。
AIのコードで確認すべき6箇所
見る場所 | 問題 | 毎日回すと何が起きるか |
|---|---|---|
| 非推奨の文字列渡し | pandasを更新した日に止まる |
|
| requestsは既定でタイムアウトしない。相手が応答しないと永久に待ち、次の日の実行まで残り続ける |
| 「ページで最初の表」を決め打ちしている | サイト側が広告や別の表を上に足した日から、別の表を黙って保存し続ける |
| エラーページでも処理を続ける | 404やメンテナンス画面の表を取り込む |
| 失敗しても正常終了する | cronからは成功に見える。CSVが何日も更新されていなくても気づけない |
| 毎回ヘッダー行を書き、日付列も無い | CSVの途中にヘッダーが何行も混ざり、どの日のデータか分からない |
今回のコードには出てきませんが、Slack等への通知を頼むとWebhookのURLやAPIキーをコードに直書きしてくることがあります。これも同じく確認対象です。キーは環境変数から os.getenv() で読む形に直させてください。
直させる指示文
「動きません」だけではなく、直してほしい点を箇条書きで渡します。これが一番確実です。
次の条件で書き直してください。
- pandas 2.1以降を前提に、read_html には io.StringIO で包んで渡す
- read_html は match= で「通貨」という文字を含む表だけを取る。tables[0] の決め打ちはしない
- 取れた表に「通貨」「仲値」の列が無ければ例外にする
- requests には timeout=(5, 30) を付け、raise_for_status() を呼ぶ
- 通信エラーは60秒空けて最大3回まで再試行する。アクセスは1日1回だけ
- User-Agent に連絡先URL https://example.com/contact を入れる
- 先頭に「取得日」列を足し、同じ日のデータが既にあれば追記しない
- ヘッダーはファイルが無いときだけ書く
- logging でファイルにログを残し、失敗したら終了コード1で終わる
- except で例外を握りつぶさない
- 文字化け対策として、文字コードが判定できないときは推定値を使う直った後のコード
"""為替レート表を1日1回取得してCSVに追記する"""
import logging
import sys
import time
from datetime import date
from io import StringIO
from pathlib import Path
import pandas as pd
import requests
URL = "https://example.com/rates"
OUT = Path(__file__).with_name("rates.csv")
HEADERS = {"User-Agent": "rates-collector/1.0 (+https://example.com/contact)"}
REQUIRED = {"通貨", "仲値"}
logging.basicConfig(
filename=Path(__file__).with_name("fetch.log"),
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
def fetch_html(url: str) -> str:
for attempt in range(3):
try:
res = requests.get(url, headers=HEADERS, timeout=(5, 30))
res.raise_for_status()
if res.encoding is None or res.encoding.lower() == "iso-8859-1":
res.encoding = res.apparent_encoding
return res.text
except requests.RequestException as e:
logging.warning("取得失敗 %d回目: %s", attempt + 1, e)
if attempt < 2:
time.sleep(60)
raise RuntimeError("3回とも取得できなかった")
def parse_table(html: str) -> pd.DataFrame:
tables = pd.read_html(StringIO(html), match="通貨")
df = tables[0]
missing = REQUIRED - set(df.columns.astype(str))
if missing:
raise ValueError(f"想定した列が無い: {missing}")
return df
def append_csv(df: pd.DataFrame) -> None:
today = date.today().isoformat()
if OUT.exists():
done = pd.read_csv(OUT, usecols=["取得日"])["取得日"].astype(str)
if today in set(done):
logging.info("本日分は取得済み")
return
df.insert(0, "取得日", today)
df.to_csv(OUT, mode="a", index=False, header=not OUT.exists(), encoding="utf-8")
logging.info("%d行を追記", len(df))
def main() -> int:
try:
append_csv(parse_table(fetch_html(URL)))
except Exception:
logging.exception("取り込み失敗")
print("取り込み失敗。fetch.log を確認", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())ブロックごとの説明
- 定数の部分:URL・保存先・User-Agent・必須の列名をまとめて冒頭に置いています。保存先は
Path(__file__).with_name()で「スクリプトと同じフォルダ」を指します。cronから実行すると作業フォルダがホームになり、CSVが思わぬ場所にできることがあります。この書き方はその対策です。 - logging.basicConfig:ログを
fetch.logに日時付きで残します。printの出力は、cronで回すとどこにも残らないことがあります。 - fetch_html():
timeout=(5, 30)は「接続に5秒、応答の受信に30秒」まで待つという意味です。raise_for_status()は、404や500などのエラー応答を例外に変えます。サーバーが文字コードを伝えてこないとき、requestsはISO-8859-1とみなします。その場合だけ本文から推定したapparent_encodingに切り替えて、日本語の文字化けを防いでいます。通信エラーのときは60秒空けて、合計3回まで試します。 - parse_table():ここが今回の主役です。
StringIO(html)で包んで渡すので、FutureWarningは出ません。match="通貨"を付けると、表の中に「通貨」という文字を含む表だけが返ります。該当する表が1つも無ければ、read_htmlはValueErrorを出します。さらに列名を確かめるので、サイトの構造が変わった日に「別の表を黙って保存する」ことが無くなります。match=には、取りたい表の見出しや列名に実際に書かれている語を入れてください。 - append_csv():先頭に「取得日」列を足します。同じ日の行が既にあれば何もしません。手動でもう一度実行しても、データが二重になりません。ヘッダーはファイルが無いときだけ書きます。
- main():失敗したら、ログに詳しい原因(トレースバック)を残します。標準エラーにも1行出し、終了コード1で終わります。cronやsystemdから「失敗した」と分かるようにするための部分です。
表の見出しが2段になっているサイトでは、列名が MultiIndex(2段の列名)になります。この場合、上の列名チェックは通りません。AIに「列名が2段なら1段に結合してから確認して」と追加で指示してください。
read_htmlで取る前に:robots.txt・利用規約・アクセス間隔
- 公式のCSV・APIを先に探す:為替・株価・統計は、配信元がCSVダウンロードやAPIを用意していることが多いデータです。あればそちらを使うのが、相手にとっても自分にとっても安定します。
- robots.txtを確認する:
https://example.com/robots.txtを開き、取得したいパスがDisallow(取得禁止)になっていないかを見ます。 - 利用規約を読む:株価や指数のデータには、自動取得や再配布を禁じているものがあります。社内の集計用でも、規約で禁止されていれば取得しません。
- 1日1回・1ページで足りる設計にする:今回のコードは、1日1回だけ取りに行きます。再試行も60秒空けて最大3回までです。AIに「全ページを並列で取って」と頼むと、相手に負荷をかけるコードが出てきます。並列化はさせないでください。
- ブロックされたら止める:403が返る、CAPTCHA(人間かどうかの確認画面)が出るといった場合は、相手が自動取得を望んでいないという意思表示です。User-Agentの偽装などで回避せず、取得自体を見直します。
毎日の取り込みをどこで動かすか:手元PC・共有サーバーのcron・VPSの止まる条件
3つの置き場所の比較
置き場所 | 向いている条件 | 止まる条件 |
|---|---|---|
手元のPC(cron・タスクスケジューラ) | まず試したい。PCが毎日決まった時刻に起動している | スリープ・シャットダウン中は実行されない。cronは逃した回を後から実行しないので、その日は丸ごと抜ける |
共有レンタルサーバーのcron | 1日1回・数十秒で終わる処理。管理画面からcronを設定できる | プランによっては、pip installが使えない、実行時間に上限がある、外部への通信が制限されている。導入前に契約先のマニュアルで確認が要る |
VPS(systemd timer) | 取得先が増える、Playwrightなどブラウザを動かす、常に動かしたい | OSの更新・ディスク容量・再起動後の確認まで自分で管理する。放置するとディスクが一杯になって止まる |
read_htmlで静的な表を1日1回取るだけなら、共有サーバーのcronで十分なことが多いです。表がJavaScriptで後から描画されるページだと、read_htmlでは取れません。その場合はブラウザを動かす必要があり、VPSを選ぶことになります。
cronで動かす場合
crontab -e で、次の1行を追加します。平日の7:30に実行する例です。週末は為替が動かず同じ表を取るだけなので、1-5(月〜金)にしています。
30 7 * * 1-5 cd /home/username/rates && /home/username/rates/.venv/bin/python fetch_rates.py- pythonは絶対パスで書く:cronは環境変数PATHが最小限なので、
pythonと書くと、pandasを入れていない別のPythonが使われることがあります - 時刻はサーバーのタイムゾーン:
dateコマンドで確認します。海外のサーバーだと9時間ずれることがあります - 失敗の知らせ:cronの実行結果をメールで受け取れる環境なら、失敗時に標準エラーへ出した1行が届きます。届かない環境では、
fetch.logの最後に「追記」が出ているかを定期的に確認します - 登録の確認:
crontab -lで、登録した行が入っているかを確かめます
VPSで動かす場合
VPSなら、cronよりsystemd timerが向いています。Persistent=true を付けると、再起動などで逃した回を起動後に実行してくれるからです。設定の手順はPlaywright定期実行をsystemdで 2026年10月にまとめています。read_htmlのスクリプトでも同じ形で使えます。
サーバーごとにpandasやlxmlを入れ直すのが面倒な場合は、依存パッケージをスクリプトの冒頭に書いておく方法もあります。その書き方はPEP723とuv run --script 2026年10月版で解説しています。
止まったことに気づくための最低限
- 失敗したら終了コード1で終わる(このコードは対応済み)
- ログに「本日分は取得済み」か「◯行を追記」が毎日1行残る
- 週に一度はCSVの最終行の取得日を見る。2営業日以上古ければ、どこかで止まっています
AIに書かせたコードは「1回動いた」時点では完成ではありません。上の3点を満たして、ようやく「止まったら分かる」状態になります。
関連する選択肢
毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。
※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。
pandas read_html StringIO化で毎日の取り込みを止めないための要点
ChatGPTやClaude Codeが出す pd.read_html(res.text) は、pandas 2.1で非推奨になった書き方です。StringIO(res.text) で包む形に直させましょう。あわせて次の点も直させます。
tables[0]の決め打ちをやめ、match=と列名チェックで表を特定するtimeoutとraise_for_status()を付ける- 例外を握りつぶさず、ログを残して終了コード1で終わる
AIへの指示は「動かない」ではなく、直してほしい条件を箇条書きで渡すのが確実です。取得前にはrobots.txtと利用規約を確認し、アクセスは1日1回に抑えます。
置き場所の目安は次のとおりです。
- まず試すなら手元のPC。ただしスリープ中は抜ける
- 静的な表を1日1回取るだけなら共有サーバーのcron
- ブラウザが必要なページや取得先が増えたらVPSのsystemd timer
どこに置く場合も、「止まったら分かる」仕組みまで用意して完成です。