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以降では次のように動きます。

  1. error_bad_lines=False の行で TypeError: read_csv() got an unexpected keyword argument 'error_bad_lines' が出ます
  2. そのエラーを except: pass が黙って捨てます
  3. 仮に読めたとしても、DataFrame.append() も2.0で削除されているので、ここでも例外になって捨てられます
  4. 最後に 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 です。指定できる値は次のとおりです。

以前の書き方

今の書き方

動き

error_bad_lines=False

on_bad_lines="skip"

列数の合わない行を黙って捨てる

warn_bad_lines=True

on_bad_lines="warn"

捨てつつ警告を出す

(なし)

on_bad_lines=関数(engine="python" が必要)

捨てた行を自分で受け取れる

df.append(other)

pd.concat([df, other], ignore_index=True)

行を連結する

指示文どおり 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

grep -nE "error_bad_lines|warn_bad_lines|\.append\(|utcnow" import_meisai.py

「pandas 2.0以降で削除されたAPIを使わずに書き直して」

アクセス間隔・timeoutが無い

grep -nE "requests\.(get|post)|time\.sleep" import_meisai.py

「全リクエストにtimeoutを付け、連続アクセスには間隔を空けて」

例外で黙って止まる

grep -n -A1 "except" import_meisai.py(直後が pass や continue だけの箇所を見る)

「except: pass を禁止。ログを残し、終了コードを1にして」

APIキー・URLの直書き

grep -niE "(key|token|password|webhook|secret)\s*=\s*['\"]" import_meisai.py

「秘密の値は環境変数から読むように」

上の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>&1

AIは 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.target

Persistent=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が基本です。共有サーバーは制限と個人情報の扱いを確かめてから選びます。