ChatGPTに「銀行やECの明細CSVを毎朝取り込むPythonを書いて」と頼むと、pd.read_csv(..., error_bad_lines=False) を使ったコードがよく出てきます。この引数はpandas 2.0で削除されており、今のpandasでは動きません。しかもAIのコードは例外を握りつぶしがちなので、エラーも出ないまま毎朝何も取り込まれていない、ということが起きます。この記事では、on_bad_lines と encoding="cp932" で書き直させるAIへの指示文を実物で示します。そのうえで、毎日止めずに動かす置き場所まで説明します。
ChatGPTが書く明細CSV取り込みコードは、pandas 2.0以降では何もせず終わる
前提にする実行環境
- Python 3.12(2026年時点で推奨安定版とされている系列)
- pandas:2026年9月17日に3.0.6が出ています。この記事のコードは
error_bad_linesが削除された 2.0以降 を前提にしています。手元の版はpip show pandasで確認してください - requests:失敗をチャットに通知する部分だけで使います
python3 -m venv .venv
. .venv/bin/activate
pip install "pandas>=2.0" requests
pip show pandas # Version: の行で実際に入った版を確認pandas>=2.0 と下限を指定するのは、1.x系が残っている環境だと古い書き方がそのまま通ってしまい、別のPCやサーバーに移した日に初めて壊れるからです。最初から2.0以降で揃えておけば、古い書き方はその場でエラーになり、すぐ気づけます。
最初にAIへ出した指示
コードを書き慣れていない人が出しがちな、ごく普通の頼み方です。
銀行とネットショップの利用明細CSVを、毎朝まとめて1つの台帳CSVに追記する
Pythonスクリプトを書いてください。ダウンロードフォルダにあるCSVを全部読み込んで、
ledger.csv に足していく形でお願いします。壊れた行があっても止まらないようにしてください。出てきたコード
import glob
import pandas as pd
ledger = pd.read_csv("ledger.csv")
for f in glob.glob("/home/username/Downloads/*.csv"):
try:
df = pd.read_csv(f, error_bad_lines=False, encoding="shift_jis")
ledger = ledger.append(df)
except:
pass
ledger.to_csv("ledger.csv", index=False)一見それらしく見えますが、pandas 2.0以降では次のように動きます。
error_bad_lines=Falseの行でTypeError: read_csv() got an unexpected keyword argument 'error_bad_lines'が出ます- そのエラーを
except: passが黙って捨てます - 仮に読めたとしても、
DataFrame.append()も2.0で削除されているので、ここでも例外になって捨てられます - 最後に
ledger.csvが元の内容のまま上書き保存され、スクリプトは正常終了します
「壊れた行があっても止まらないように」という指示を、AIが「どんな例外も無視する」と解釈した結果です。毎朝実行されて終了コードも0なので、台帳が更新されていないことに何日も気づけません。このコードには、ほかにも危うい点が3つあります。
shift_jis:国内向けの明細CSVは、Excelで開けるようShift_JIS系で出力されることが多いです。ただし①や㈱のような機種依存文字は、厳密なshift_jisでは読めません。拡張部分まで含むcp932を指定します"ledger.csv"の相対パス:cronなどから実行すると作業ディレクトリが変わり、別の場所に台帳ができます- 同じファイルを毎朝読み直す:ダウンロードフォルダのCSVは消えないので、同じ明細が毎日二重、三重に追記されます
on_bad_lines と encoding="cp932" で書き直させる指示文と、直った後のコード
直させるときの指示文
「動かないので直して」だけでは、AIは同じ前提のまま別の書き方を試すだけになりがちです。前提(バージョン)・禁止事項・期待する動きを具体的に渡します。
次の条件で書き直してください。
- pandas は 2.0 以降(pip show pandas で確認済み)。error_bad_lines と
DataFrame.append は削除されているので使わない。on_bad_lines と pd.concat を使う
- 明細CSVの文字コードは UTF-8(BOM付き) か cp932。順に試して、どちらでも読めなければエラーにする
- 列数が合わない行は読み飛ばしてよいが、何行飛ばしたかを必ずログに残す
- except: pass は禁止。失敗したら終了コードを1にして、ログに例外の内容を残す
- パスはすべて絶対パスか、環境変数で渡す
- 取り込み済みのCSVは done フォルダへ移し、同じファイルを二度取り込まない
- 失敗の通知先URLはコードに書かず、環境変数 NOTIFY_WEBHOOK_URL から読む
- requests を使う箇所には必ず timeout を付ける直った後のコード(import_meisai.py)
AIの出力を、確認しながら整えたものです。ブロックごとに説明します。
import os
import sys
import shutil
import logging
from pathlib import Path
import pandas as pd
import requests
BASE = Path(os.environ.get("MEISAI_DIR", "/home/username/meisai"))
INBOX = BASE / "inbox" # 明細CSVを置く場所
DONE = BASE / "done" # 取り込み済みの移動先
OUT = BASE / "ledger.csv" # 台帳
logging.basicConfig(
filename=BASE / "import.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)設定ブロック:置き場所は環境変数 MEISAI_DIR で変えられるようにして、既定値も絶対パスにしています。cronから呼ばれても、作業ディレクトリに左右されません。ログはファイルに残すので、後から「いつ、何行読み飛ばしたか」を確認できます。
def read_statement(path):
bad_lines = []
def keep_bad(line):
bad_lines.append(line)
return None # この行は読み飛ばす
for enc in ("utf-8-sig", "cp932"):
try:
df = pd.read_csv(
path,
encoding=enc,
on_bad_lines=keep_bad,
engine="python",
thousands=",",
)
return df, bad_lines
except UnicodeDecodeError:
bad_lines.clear()
raise ValueError(f"文字コードを判定できません: {path.name}")読み込みブロック(主役):旧来の error_bad_lines=False に当たるのが on_bad_lines です。指定できる値は次のとおりです。
以前の書き方 | 今の書き方 | 動き |
|---|---|---|
|
| 列数の合わない行を黙って捨てる |
|
| 捨てつつ警告を出す |
(なし) |
| 捨てた行を自分で受け取れる |
|
| 行を連結する |
指示文どおり on_bad_lines="skip" でも動きます。ただし明細の場合、読み飛ばされた行は金額がそのまま消えた行です。末尾の「合計」行のように捨ててよい行もありますが、本当に飛ばしてよかったのかは件数を見ないと判断できません。そこで関数を渡して、飛ばした行を手元に集めています。関数を渡すには engine="python" の指定が必要です。
文字コードは utf-8-sig(BOM付きUTF-8も読める)を先に試し、失敗したら cp932 に切り替えます。cp932のファイルはUTF-8としては読めずに失敗することがほとんどなので、この順番にしています。thousands="," は 1,234 のような桁区切り付きの金額を数値として読むための指定です。
def notify(text):
url = os.environ.get("NOTIFY_WEBHOOK_URL")
if not url:
return
try:
requests.post(url, json={"text": text}, timeout=(3, 10))
except requests.exceptions.RequestException:
logging.exception("通知の送信に失敗しました")通知ブロック:通知先のURLは環境変数から読み、コードには書きません。timeout=(3, 10) は「接続3秒・応答待ち10秒」の意味です。これが無いと、通知先が応答しないときにスクリプトごと止まり続けます。timeoutを付け忘れたAIコードの典型的な症状は requests timeout忘れ|AIコード2026年10月 にまとめています。
def main():
DONE.mkdir(parents=True, exist_ok=True)
files = sorted(INBOX.glob("*.csv"))
if not files:
logging.info("新しい明細はありません")
return 0
frames, imported = [], []
for path in files:
if (DONE / path.name).exists():
logging.warning("%s は取り込み済みのため飛ばします", path.name)
continue
df, bad = read_statement(path)
if bad:
logging.warning("%s: %d行を読み飛ばしました 先頭=%s",
path.name, len(bad), bad[0])
notify(f"{path.name}: {len(bad)}行を読み飛ばしました。ログを確認してください")
df["source_file"] = path.name
frames.append(df)
imported.append(path)
if not frames:
return 0
new = pd.concat(frames, ignore_index=True)
if OUT.exists():
new = pd.concat([pd.read_csv(OUT, encoding="utf-8-sig"), new],
ignore_index=True)
new.to_csv(OUT, index=False, encoding="utf-8-sig")
for path in imported:
shutil.move(str(path), DONE / path.name)
logging.info("%dファイルを取り込みました", len(imported))
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as e:
logging.exception("取り込みに失敗しました")
notify(f"明細の取り込みに失敗しました: {type(e).__name__}")
sys.exit(1)本体ブロック:取り込みが済んだファイルは done へ移すので、同じファイルを二度読みません。ここでは行単位の重複削除(drop_duplicates)はあえて使っていません。同じ日・同じ店・同じ金額の支払いが2回あるのは明細ではよくあることで、行単位で重複を消すと本物の取引まで消えるからです。期間が重なるCSVを落とさないことを運用で守るか、明細に取引番号の列があればそれを重複判定のキーにするよう、AIに追加で指示してください。
最後の try/except は、例外をログに残したうえで終了コード1を返します。旧コードの except: pass との違いは、失敗が外から見えることです。cronやsystemdが「失敗した」と判断できるのは、この終了コードがあるからです。
AIが出したコードで自分で確認する4箇所(古いAPI・間隔なし・例外の握りつぶし・キー直書き)
2026年9月29日のOpenAI DevDay 2026ではGPT-6.1 Solが発表されるなど、モデルは更新され続けています。それでも、学習した時点の古いAPIが混ざる問題は無くなっていません。現場でも、コードレビュー担当の約9割がAI生成コードの普及で負担が増えたと感じていて、「動いてテストも通るのに不安が残る」という声が出ています。自分で書き慣れていなくても、次の4箇所は grep で機械的に探せます。
確認箇所 | 探し方 | 見つけたらAIに返す一文 |
|---|---|---|
削除・非推奨のAPI |
| 「pandas 2.0以降で削除されたAPIを使わずに書き直して」 |
アクセス間隔・timeoutが無い |
| 「全リクエストにtimeoutを付け、連続アクセスには間隔を空けて」 |
例外で黙って止まる |
| 「except: pass を禁止。ログを残し、終了コードを1にして」 |
APIキー・URLの直書き |
| 「秘密の値は環境変数から読むように」 |
上の4つに加えて、次の4観点を人が一度確認してから運用に乗せるのが確実です。
- 要件適合:「壊れた行で止まらない」が「全部無視する」になっていないか
- 暗黙の仕様:明細では、同じ金額の取引が2回あるのは正常なこと
- テスト設計:わざと列数の合わない行を入れたCSVで試し、ログに件数が出るか
- 依存範囲:pandasの版、Pythonの版、置き場所のパス
ほかに古いAPIが出やすい箇所としては、datetime.utcnow() の非推奨化があります。これはcronで時刻がずれる原因にもなります(utcnow非推奨とcron時刻ずれ|AIコード2026年10月)。
明細の取得元への配慮:自動ログインはさせず、公式のCSV出力を使う
AIに「ダウンロードも自動化して」と頼むと、ログイン画面をSeleniumで操作するコードが出てくることがあります。銀行やECの会員ページに保存したパスワードで自動ログインさせたり、二段階認証を回避させたりする書き方は、利用規約に反する可能性が高く、アカウント停止の原因にもなります。この記事では扱いません。
- 取得は公式の手段に限る:画面からのCSVダウンロードを人が行い、
inboxに置く。サービスが明細取得のAPIや連携機能を公式に提供している場合は、その利用条件の範囲で使う - 利用規約を先に読む:会員ページの自動取得を禁じているサービスは多いです。robots.txtは公開ページ向けの取り決めなので、ログイン後のページについては規約の方が判断基準になります
- アクセス間隔:公開ページから何かを取る処理を足す場合は、1日1回で足りるかをまず考え、複数ページを取るなら1リクエストごとに数秒以上空けます
- 明細は個人情報:台帳とログには取引先や金額が残ります。置き場所の権限(
chmod 600)と、どのマシンに置くかを決めてから運用を始めます
毎朝7時に止めずに動かす置き場所:手元PC・共有レンタルサーバーのcron・VPS
DevDay 2026では常時稼働型のAIエージェント「dots」も発表されました。ただ、毎朝同じCSVを取り込むような決まった処理は、OS標準のスケジューラで十分に動きます。大事なのは「どこで動かすか」と「そこが止まる条件」を知っておくことです。
置き場所 | 向いている条件 | 止まる条件 |
|---|---|---|
手元のPC(Linuxはcron、Macはlaunchd、Windowsはタスクスケジューラ) | CSVを手でダウンロードしていて、そのPCに明細が既にある | スリープ中・電源オフ・ログアウト。実行時刻にPCが閉じていると、その日の分が飛ぶ |
共有レンタルサーバーのcron | 数秒で終わる処理で、明細をサーバーに置いてよい | 実行時間や同時プロセス数の制限、提供側のPythonやpandasの版が固定、パッケージを入れられない。明細を第三者のサーバーに置くことになる |
VPS(systemd timer) | 明細の受け渡しまで自動化し、取り逃しを許したくない | OS更新やディスクの空き不足を放置したとき、契約切れ。保守は自分の責任になる |
cronで動かす場合
0 7 * * * MEISAI_DIR=/home/username/meisai /home/username/meisai/.venv/bin/python /home/username/meisai/import_meisai.py >> /home/username/meisai/cron.log 2>&1AIは python3 import_meisai.py と書きがちです。しかしcronから呼ばれる python3 は、venvで入れたpandasとは別のPythonです。古いpandasが入っていたり、そもそも入っていなかったりするので、venvの python を絶対パスで書きます。cronの時刻はそのマシンのタイムゾーンで解釈される点にも注意してください。
VPS(myserver)でsystemd timerを使う場合
# ~/.config/systemd/user/meisai.service
[Service]
Type=oneshot
Environment=MEISAI_DIR=/home/username/meisai
EnvironmentFile=/home/username/meisai/.env
ExecStart=/home/username/meisai/.venv/bin/python /home/username/meisai/import_meisai.py
# ~/.config/systemd/user/meisai.timer
[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true
[Install]
WantedBy=timers.targetPersistent=true を付けておくと、停止していた時間帯の実行を起動時に取り戻します。.env には NOTIFY_WEBHOOK_URL=... を書き、chmod 600 にしてからGitの管理対象外にします。ログアウト後も動かすには loginctl enable-linger username が必要です。終了コード1を返したかどうかは systemctl --user status meisai.service と journalctl --user -u meisai.service で確認できます。
Macで手元実行する場合は、launchdの登録方法が変わっているので launchd定期実行はbootstrapで|2026年10月 を参照してください。
関連する選択肢
毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。
※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。
まとめ:error_bad_lines は on_bad_lines に、except: pass は終了コード1に
ChatGPTに明細CSVの取り込みを書かせると、pandas 2.0で削除された error_bad_lines=False と DataFrame.append() が出やすくなります。しかも except: pass と組み合わさると、エラーも出さずに毎朝何も取り込まないコードになります。直させるときは、pandasの版、on_bad_lines と pd.concat の使用、utf-8-sig と cp932 の順での読み込み、読み飛ばした件数のログ、例外の握りつぶしの禁止、秘密の値は環境変数から読むこと、を具体的に指示します。受け取ったコードは grep で4箇所を確認してください。置き場所は、CSVを手で落とすなら手元PC、取り逃しを許さないならVPSのsystemd timerが基本です。共有サーバーは制限と個人情報の扱いを確かめてから選びます。