ChatGPTやClaude Codeに「取引先のCSVを毎朝取り込むスクリプト」を頼むと、今でも pd.read_csv(..., error_bad_lines=False) を含むコードが出てくることがあります。この引数はpandas 2.0で削除されたので、そのまま動かすと止まります。単純に on_bad_lines="skip" に置き換えると今度は崩れた行が黙って消えます。この記事では、AIへの指示文の実物を使って pandas on_bad_lines の "warn" と callable で崩れた行を退避する形に直させ、それを毎朝止めずに動かすところまでを扱います。
error_bad_lines=False はpandas 2.0で削除され、今は on_bad_lines を使う
「列数が合わない行」が混ざったCSVを読み込むときの書き方は、pandasのバージョンによって次のように変わっています。
やりたいこと | 以前の書き方 | 今の書き方(pandas 2.x) |
|---|---|---|
崩れた行で止める |
|
|
崩れた行を黙って捨てる |
|
|
捨てるが、行番号を表示する |
|
|
崩れた行を受け取って、自分で処理する | (手段なし) |
|
古い書き方のままpandas 2.xで実行すると、次のように引数そのものが認識されずに止まります。
TypeError: read_csv() got an unexpected keyword argument 'error_bad_lines'AIがこの書き方を出しがちなのは、ネット上に残っている古い記事やQ&Aの量が多いからです。2026年10月時点では、ChatGPTはGPT-6.1 SolやGPT-6 Luna、GitHub CopilotではClaude Opus 5.5への移行が予定されているなど、モデルは次々に新しくなっています。それでも古いAPIは混ざります。モデルが新しいかどうかではなく、出てきたコードを見て判断する必要があります。
手元のpandasのバージョンは、次のコマンドで確認できます。
pip show pandas
python3 -Vこの記事のコードは Python 3.10以上・pandas 2.x を前提にしています。必要なパッケージは次の2つです。
python3 -m venv .venv
. .venv/bin/activate
pip install pandas requestsUbuntuなどでシステムの pip install が拒否される場合は、externally-managedはvenvで|26年10月 の手順で仮想環境を作ってください。なお、2026年10月21日にPython 3.15.1・3.14.9・3.13.17のセキュリティリリースが出ています。同梱の pip が 26.2.2(certifi 2026.10.19入り)に更新されているので、既存のvenvでも python -m pip install --upgrade pip を一度実行しておくと安心です。
AIへの最初の指示と、出てきたコードで確認すべき4箇所
最初の指示(よくある頼み方)
取引先から毎朝もらうCSV(Shift_JIS)をURLからダウンロードして読み込み、
列が崩れた行は飛ばしてSQLiteに追記するPythonスクリプトを書いて。出てきたコード(典型例)
import pandas as pd
import sqlite3
import requests
API_KEY = "sk-xxxxxxxxxxxxxxxx"
url = "https://example.com/export/orders.csv"
r = requests.get(url, headers={"Authorization": f"Bearer {API_KEY}"})
open("orders.csv", "wb").write(r.content)
try:
df = pd.read_csv("orders.csv", encoding="shift_jis",
error_bad_lines=False, warn_bad_lines=True)
conn = sqlite3.connect("orders.db")
df.to_sql("orders", conn, if_exists="append", index=False)
except Exception as e:
print(e)手元で一度動いたように見えても、毎朝の自動実行に載せると問題が出ます。次の4箇所は、コードを書き慣れていなくても目で探せます。
- 古いAPI:
error_bad_lines・warn_bad_linesという語があれば、pandas 2.xでは動きません。 - 例外で黙って止まる:
except Exception as e: print(e)は、エラーを表示するだけで正常終了(終了コード0)扱いになります。cronやsystemdからは「成功」に見えるので、何日も取り込みが止まっていても気づけません。 - APIキーの直書き:
API_KEY = "..."のように文字列で書かれていたら要修正です。ファイルを共有したりGitに載せたりした時点で漏れます。 - 通信の待ち時間と失敗の確認が無い:
timeoutが無いと相手が応答しない日に永久に待ち続けます。raise_for_status()が無いと、エラーページのHTMLをCSVとして保存してしまいます。複数ファイルを連続で取得するなら、アクセスの間隔(time.sleep)があるかも見ます。
もう1つ見落としやすい点があります。単純に on_bad_lines="skip" へ置き換えると動くようにはなりますが、崩れた行は記録されずに消えます。取引先CSVなら、その1行は1件分の受注や請求かもしれません。「捨てる」ではなく「退避して後で人が見る」形にするのが今回の主眼です。
直させる指示と、on_bad_lines の callable で崩れた行を退避する今の書き方
直させる指示(そのまま貼れる形)
次の条件で書き直して。
1. pandas 2.x で動かすので error_bad_lines / warn_bad_lines は使わない。
on_bad_lines に関数を渡し、崩れた行は捨てずに日付付きの別CSVへ退避する。
engine="python" を指定する。
2. 取引先コードの先頭の0が消えないよう、全列を文字列で読む。
3. APIキーは環境変数 ORDERS_API_KEY から読む。無ければ終了コード1で止める。
4. requests は Session を使い、timeout=30 と raise_for_status() を付ける。
5. 例外は握りつぶさず logging に出し、終了コード1で終わる。
6. 退避した行数をログに出し、全体の1割を超えたら取り込まずに終了コード1にする。指示のポイントは、使ってほしくない引数名を名指しすることと、「失敗したら終了コード1」と書くことです。「エラー処理を入れて」とだけ頼むと、AIは最初のコードと同じ try/except で握りつぶす形を出しやすくなります。
手元で調べるときは on_bad_lines="warn"
どの行が崩れているかをまず知りたいだけなら、次の1行で十分です。
df = pd.read_csv("orders.csv", encoding="cp932", on_bad_lines="warn")崩れた行の行番号と列数が警告として表示され、その行は読み飛ばされます。ただし表示されるだけでファイルには残りません。毎朝の自動実行では誰も画面を見ていないので、次の callable 版にします。
直った後のコード
import csv
import logging
import os
import sqlite3
import sys
from datetime import date
from pathlib import Path
import pandas as pd
import requests
BASE = Path(__file__).resolve().parent
CSV_URL = "https://example.com/export/orders.csv"
BAD_RATIO_LIMIT = 0.1
logging.basicConfig(
filename=BASE / "orders_import.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
log = logging.getLogger(__name__)
def fetch_csv(url, api_key, dest):
with requests.Session() as s:
r = s.get(url, headers={"Authorization": f"Bearer {api_key}"}, timeout=30)
r.raise_for_status()
dest.write_bytes(r.content)
def load_csv(path):
bad_rows = []
def keep_bad(fields):
bad_rows.append(fields)
return None # None を返すとその行は DataFrame に入らない
df = pd.read_csv(
path,
encoding="cp932",
engine="python",
on_bad_lines=keep_bad,
dtype=str,
)
return df, bad_rows
def quarantine(bad_rows):
out = BASE / "quarantine" / f"bad_{date.today():%Y%m%d}.csv"
out.parent.mkdir(exist_ok=True)
with out.open("w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows(bad_rows)
return out
def main():
api_key = os.environ.get("ORDERS_API_KEY")
if not api_key:
log.error("環境変数 ORDERS_API_KEY がありません")
return 1
src = BASE / "orders.csv"
fetch_csv(CSV_URL, api_key, src)
df, bad_rows = load_csv(src)
total = len(df) + len(bad_rows)
if total == 0:
log.error("CSVにデータ行がありません")
return 1
if bad_rows:
out = quarantine(bad_rows)
log.warning("崩れた行 %d 件を %s に退避しました", len(bad_rows), out.name)
if len(bad_rows) > total * BAD_RATIO_LIMIT:
log.error("崩れた行が全体の1割を超えたので取り込みを中止しました")
return 1
with sqlite3.connect(BASE / "orders.db") as conn:
df.to_sql("orders", conn, if_exists="append", index=False)
log.info("%d 件を取り込みました", len(df))
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Exception:
log.exception("取り込みに失敗しました")
sys.exit(1)ブロックごとの説明
- 先頭の定数と logging:ログをスクリプトと同じフォルダの
orders_import.logに書きます。cronやsystemdから動かすと画面が無いので、printではなくファイルに残します。パスをPath(__file__)基準にしているのは、cronが実行時のカレントディレクトリを保証しないためです。 fetch_csv():requests.Session()で取得し、timeout=30で30秒応答が無ければ例外にします。raise_for_status()は404や500のときに例外を出すので、エラーページをCSVとして保存する事故を防げます。load_csv():ここが今回の主役です。on_bad_linesに渡したkeep_badは、列数が多すぎる行が見つかるたびに区切った後の値のリストを受け取ります。リストをbad_rowsに貯めてNoneを返すと、その行は表に入らず、内容は手元に残ります。関数を渡す形はengine="python"のときに使えます(対応範囲はpandasのバージョンで変わりうるので、pip show pandasの版の公式ドキュメント「read_csv」で確認してください)。dtype=strは「00123」のようなコードが数値の123に化けるのを防ぎます。quarantine():退避した行をquarantine/bad_20261026.csvのような日付付きファイルに書きます。翌朝の実行で上書きされず、人があとで開いて取引先に確認できます。main():キーが無い・データが0行・崩れた行が1割超、のいずれかなら 1を返します。1割を超えるのは、取引先が列を足したなどの形式変更であることが多く、そのまま取り込むと大半を失うためです。- 最後の
try/except:予想外の例外もlog.exceptionで行番号付きで記録し、必ず終了コード1で終わります。最初のAIのコードとの違いは、握りつぶさず「失敗した」と外に伝える点です。
callable でも拾えない崩れ方がある
on_bad_lines が反応するのは列が多すぎる行だけです。列が足りない行はエラーにならず、足りない列が空(NaN)で埋められて取り込まれます。取引先コードや金額のように空だと困る列があるなら、取り込み前に df["取引先コード"].isna().sum() のように空の件数を数え、0でなければ同じように退避か中止にするよう、追加でAIに指示してください。
取引先CSVのダウンロード元への配慮:利用規約・robots.txt・アクセス間隔
CSVを取引先のWebシステムや公開サイトから取ってくる場合は、相手のサーバーを使わせてもらっている立場です。
- 公式の手段を優先する:エクスポート機能やAPIが用意されているなら、画面をスクレイピングせずそちらを使います。取得方法を取引先と取り決めておけば、仕様変更も事前に教えてもらえます。
- 利用規約とrobots.txtを確認する:自動取得を禁じている場合は使いません。ログインが必要なページを自動で取る場合も、規約で許されているかを先に確認します。
- アクセスは必要最小限にする:1日1回・1ファイルで足りるなら、それ以上取りに行きません。複数ファイルを連続で取るなら、1件ごとに数秒の
time.sleepを入れます。 - 失敗時に連打しない:再試行は回数と間隔を決めて行います。429(アクセス過多)への待ち方は method_whitelist削除と429再試行|26年10月 にまとめています。
- 制限を回避しない:ブロックされたときに送信元を変えたり認証を迂回したりする方向でAIに直させてはいけません。止められたら相手に連絡するのが正しい対応です。
毎朝止めずに動かすなら、手元PC・共有サーバーのcron・VPSのどれか
それぞれが止まる条件
置き場所 | 向いている条件 | 止まる条件 |
|---|---|---|
手元のPC(タスクスケジューラ・launchd等) | 取り込み先も手元のExcelやファイルで、PCを毎朝必ず起動している | スリープ・シャットダウン中、OS更新の再起動、ノートPCを閉じている日。時刻に寝ているとその日は実行されない |
共有レンタルサーバーのcron | 1日1回・数分で終わる処理で、pandasをインストールできるプラン | 実行時間やメモリの上限を超えたとき、 |
VPS(systemd timer) | Seleniumなど重い処理も一緒に動かしたい、取り込み先のDBも同じサーバーに置きたい | OS更新・ディスク満杯・契約更新忘れ。管理は自分の責任になる |
CSVを読んでSQLiteに追記するだけなら、共有サーバーのcronで足ります。取得にブラウザ操作が要る、他の自動化と一緒に常駐させたいならVPSです。手元PCは「その日PCを開かなかったら取り込みが無い」ことを許容できる場合に限ります。
cronで動かす場合
0 7 * * * cd /home/username/csvjob && ORDERS_API_KEY="$(cat /home/username/.orders_key)" /home/username/csvjob/.venv/bin/python orders_import.py >> cron.log 2>&1cronは対話シェルとは環境が違うため、venvのPythonをフルパスで指定します。キーはcrontabに直接書かず、別ファイル(chmod 600)から読みます。
VPSのsystemd timerで動かす場合
# /home/username/.config/systemd/user/orders-import.service
[Unit]
Description=取引先CSVの取り込み
[Service]
Type=oneshot
WorkingDirectory=/home/username/csvjob
EnvironmentFile=/home/username/.orders_env
ExecStart=/home/username/csvjob/.venv/bin/python orders_import.py
# /home/username/.config/systemd/user/orders-import.timer
[Unit]
Description=毎朝7時に取引先CSVを取り込む
[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true
[Install]
WantedBy=timers.targetsystemctl --user daemon-reload
systemctl --user enable --now orders-import.timer
systemctl --user list-timers
sudo loginctl enable-linger username # ログアウト後も動かすPersistent=true は、サーバーが止まっていた時間帯の分を起動時に実行し直す設定です。/home/username/.orders_env には ORDERS_API_KEY=... の1行だけを書き、chmod 600 にします。
止まったことに気づく仕組み
- 終了コード:上のコードは失敗時に1で終わるので、
systemctl --user status orders-import.serviceに失敗として残ります。 - 通知:失敗したらSlackやメールに送る仕組みを足します。終了コードが0以外のときだけ送る形にすると、通知が多すぎて見なくなるのを防げます。
- 成果物を外から見る:「成功したのに件数が増えていない」は終了コードでは分かりません。
orders.dbの件数やquarantine/に新しいファイルができたかを、別のスクリプトで毎日確認すると確実です。
依存パッケージとキーの扱い
2026年10月21日には、複数のAIモデルを扱うPythonライブラリ「LiteLLM」のバージョン1.107.0に、Pythonの起動だけで動いて認証情報を盗む不正プログラムが混入していたことが判明しました。環境変数に置いたキーも、同じ環境で動く悪意あるパッケージからは読めてしまいます。毎朝自動で動かすスクリプトでは、次の2点を守ります。
pip freeze > requirements.txtでバージョンを固定し、毎日の実行の中で自動アップグレードしない。更新は手動で、公開直後の版はすぐ入れない(Amazon Linuxでは公開から24時間以内の版を避けるクールダウン設定が紹介されています)。- このスクリプト用のvenvには、必要な
pandasとrequests以外を入れない。キーは取り込み専用のものを発行し、漏れたらすぐ無効化できるようにしておく。
関連する選択肢
毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。
※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。
まとめ:error_bad_lines は on_bad_lines の callable に置き換え、失敗を外に伝える
AIが今も出しがちな error_bad_lines=False はpandas 2.0で削除されており、今は on_bad_lines を使います。単純に "skip" へ置き換えると崩れた行が黙って消えるので、手元で調べるときは "warn"、毎朝の自動取り込みでは関数を渡して日付付きの別CSVへ退避するのが今の書き方です。AIに直させるときは、使わない引数名を名指しし、「キーは環境変数から」「timeoutとraise_for_status」「失敗したら終了コード1」まで指示文に書きます。出てきたコードでは、古い引数・except での握りつぶし・キーの直書き・通信の待ち時間の4箇所を確認してください。置き場所は、1日1回の軽い取り込みなら共有サーバーのcron、重い処理と一緒ならVPSのsystemd timer、PCを毎朝必ず起動するなら手元でも構いません。どこに置いても、止まったときに通知が届き、取り込み件数を外から確かめられる状態にしておくことが、毎日止めずに動かし続ける条件です。