ChatGPTに書かせた日次のCSV取り込みスクリプトが、ある朝から何も取り込まなくなった。原因を探すと、pandasのread_csv(date_parser=...)がpandas 3.0で削除され、TypeErrorになっていた。しかもAIのコードにはexcept Exception: passが入っていて、エラーすら表示されていなかった。この記事では、AIへの指示文を実物で見せながら、pandas date_formatへの移行とpd.to_datetime(format=...)への書き換えを進めます。そのうえで、毎朝止まらずに取り込み続けるための置き場所まで解説します。

pandas 3.0でread_csvのdate_parserは削除済み。AIは今も古い書き方を出す

以前の書き方と今の書き方

date_parserは、日付の列を1件ずつ関数に渡して変換させる引数です。pandas 2系では「将来削除する」という趣旨のFutureWarningが出るだけで、処理は動いていました。pandas 3系では引数そのものが無くなったため、渡した時点で止まります。

TypeError: read_csv() got an unexpected keyword argument 'date_parser'

このメッセージは「その名前の引数は存在しない」という意味です。書式を直すのではなく、引数の名前を差し替える必要があります。

やりたいこと

以前の書き方(AIがよく出す)

今の書き方

CSV読み込み時に日付列を変換

date_parser=lambda x: datetime.strptime(x, "%Y/%m/%d")

parse_dates=["date"], date_format="%Y/%m/%d"

書式を推測させる

infer_datetime_format=True

書式をdate_format=で明示する(推測が既定になったため引数自体が不要)

読み込み後に1列だけ変換

df["t"].apply(lambda x: datetime.strptime(...))

pd.to_datetime(df["t"], format="%Y/%m/%d %H:%M")

行を追記

df.append(row)

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

pandasではほかにもapplymapの改名やfillna(method=...)の非推奨化などが同じ時期に進んでいます。こちらはpandas applymap→map・ffill|26年10月にまとめています。

AIが古い書き方を出す理由

生成AIは、学習した時点のコードの書き方を再現します。ネット上にはdate_parserを使ったサンプルが大量に残っているので、「CSVを日付付きで読み込んで」と頼むと、今でもこの書き方が出てきます。

AI側の入れ替わりも速くなっています。GitHub Copilotでは2026年10月2日と10月19日に、Claude Opus 4.7・GPT-5.5・Gemini 3.5 Flashなど複数の旧モデルが提供終了になりました。モデルは数か月で替わり、出てくるコードの癖も変わります。だからこそ、出てきたコードを自分で点検する手順を持っておくほうが確実です。

ChatGPTへの最初の指示と、出てきたコードの危うい5箇所

最初の指示(よくある頼み方)

毎朝6時に https://example.com/export/daily.csv からCSVをダウンロードして、
master.csv に追記するPythonスクリプトを書いてください。
date列は 2026/10/17 の形式です。認証にはトークンが必要です。

AIが出したコード

import pandas as pd
import requests
from datetime import datetime

API_TOKEN = "xxxxxxxxxxxxxxxx"  # ここにトークンを入れてください
URL = "https://example.com/export/daily.csv"

def main():
    try:
        r = requests.get(URL, headers={"Authorization": f"Bearer {API_TOKEN}"}, verify=False)
        with open("today.csv", "wb") as f:
            f.write(r.content)
        df = pd.read_csv(
            "today.csv",
            parse_dates=["date"],
            date_parser=lambda x: datetime.strptime(x, "%Y/%m/%d"),
        )
        master = pd.read_csv("master.csv")
        for _, row in df.iterrows():
            master = master.append(row)
        master.to_csv("master.csv", index=False)
    except Exception:
        pass

main()

一見すると動きそうです。しかしpandas 3系ではdate_parserのTypeErrorがexcept Exception: passに飲み込まれます。その結果、何も表示されず、終了コード0で正常終了したように見えます。cronは「成功」と扱うので、取り込みが止まっていることに何日も気づけません。

自分で見つけられる確認ポイント

コードが読めなくても、エディタの検索で次の語を探せば危うい箇所はほぼ拾えます。

検索する語

何が起きるか

直し方

date_parser・infer_datetime_format

pandas 3系ではTypeErrorで止まる

date_format=・pd.to_datetime(format=...)

.append((DataFrameに対して)

pandas 2.0で削除済み。AttributeErrorになる

pd.concatでまとめて結合

exceptの直後のpass

失敗が見えなくなる。最も危険

ログに残し、終了コード1で終える

"で囲まれたトークン・キー

コードを共有した時点で漏れる

環境変数から読む

verify=False

証明書の検証を切る。中間者攻撃に弱くなる

外す

requests.get(にtimeoutが無い

相手が応答しないと永久に待ち続ける

timeout=30などを付ける

open("today.csv"のような相対パス

cronは別のディレクトリから起動するので、ファイルが見つからない

スクリプトの場所を基準にしたパスにする

今回のコードはdate_parser・.append(・except: pass・トークンの直書き・verify=Falseの5つに該当します。タイムアウトと相対パスも問題です。

直させる指示文と、pandas 3.0で動く日次CSV取り込みコード

直させる指示(そのまま使える形)

「エラーが出たので直して」だけだと、AIはdate_parserだけを別の古い書き方に置き換えることがあります。実行環境のバージョンと、禁止する書き方を明示するのがコツです。

次のスクリプトを直してください。条件は以下のとおりです。

【環境】pip show pandas の結果を貼ります(ここに実際の出力を貼る)。pandas 3系です。

【必須の修正】
- read_csv の date_parser は削除されているので使わない。date_format= で書式を指定する
- 時刻を含む updated_at 列は pd.to_datetime(format="%Y/%m/%d %H:%M") で変換し、
  読めない行が1行でもあれば例外にして止める
- 日付列が日付型になっていなければ例外にする(書式違いで文字列のまま残るのを防ぐ)
- DataFrame.append は使わず pd.concat。id と date が同じ行は最新で上書き
- トークンはコードに書かず環境変数 DATA_API_TOKEN から読む。未設定なら終了コード1
- verify=False を外し、timeout=30 を付ける。失敗時は60秒空けて最大3回まで
- except Exception: pass は禁止。失敗は logging でスタックトレースごと残し、終了コード1を返す
- cron から実行するので、パスはスクリプトの置き場所を基準にする
- master.csv は一時ファイルに書いてから置き換える(途中で落ちても壊れないように)

最後に、修正した箇所と理由を箇条書きで添えてください。

実行環境

  • Python 3系(バージョンはpython3 -Vで確認する。pandas 3系が入るかどうかはpipが判断する)
  • pip install "pandas>=3,<4" requests
  • 上限の<4を付ける理由は、次のメジャー版で今回のように引数が削除されても、ある朝突然止まらないようにするためです。版を上げるときは手元で試してから上限を外します
  • 入った版はpip show pandasで確認し、pip freeze > requirements.txtで記録しておく

取り込むCSVはid,date,updated_at,valueの4列で、dateは2026/10/17、updated_atは2026/10/17 05:40の形式とします。

直った後のコード

import logging
import os
import sys
import time
from pathlib import Path

import pandas as pd
import requests

BASE_DIR = Path(__file__).resolve().parent
RAW_DIR = BASE_DIR / "raw"
MASTER = BASE_DIR / "master.csv"
URL = "https://example.com/export/daily.csv"

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)
log = logging.getLogger("daily_import")


def download(token: str) -> Path:
    RAW_DIR.mkdir(exist_ok=True)
    headers = {
        "Authorization": f"Bearer {token}",
        "User-Agent": "daily-csv-import/1.0 (+https://example.com/contact)",
    }
    for attempt in range(1, 4):
        try:
            r = requests.get(URL, headers=headers, timeout=30)
            r.raise_for_status()
            break
        except requests.RequestException as e:
            log.warning("取得失敗 %d/3: %s", attempt, e)
            if attempt == 3:
                raise
            time.sleep(60)
    path = RAW_DIR / f"{pd.Timestamp.now():%Y%m%d}.csv"
    path.write_bytes(r.content)
    return path


def load(path: Path) -> pd.DataFrame:
    df = pd.read_csv(
        path,
        parse_dates=["date"],
        date_format="%Y/%m/%d",
        encoding="utf-8",
    )
    if not pd.api.types.is_datetime64_any_dtype(df["date"]):
        raise ValueError("date 列を日付として読めませんでした")

    df["updated_at"] = pd.to_datetime(
        df["updated_at"], format="%Y/%m/%d %H:%M", errors="coerce"
    )
    bad = int(df["updated_at"].isna().sum())
    if bad:
        raise ValueError(f"updated_at を読めない行が {bad} 行あります")
    return df


def merge(df: pd.DataFrame) -> int:
    if MASTER.exists():
        master = pd.read_csv(
            MASTER, parse_dates=["date", "updated_at"], date_format="ISO8601"
        )
        before = len(master)
        merged = pd.concat([master, df], ignore_index=True)
    else:
        before = 0
        merged = df
    merged = merged.drop_duplicates(subset=["id", "date"], keep="last")
    merged = merged.sort_values(["date", "id"])
    tmp = MASTER.with_suffix(".tmp")
    merged.to_csv(tmp, index=False)
    tmp.replace(MASTER)
    return len(merged) - before


def main() -> int:
    token = os.environ.get("DATA_API_TOKEN")
    if not token:
        log.error("環境変数 DATA_API_TOKEN が未設定です")
        return 1
    try:
        path = download(token)
        df = load(path)
        added = merge(df)
    except Exception:
        log.exception("取り込みに失敗しました")
        return 1
    log.info("取り込み完了: %d 行読込 / %d 行追加", len(df), added)
    return 0


if __name__ == "__main__":
    sys.exit(main())

ブロックごとの説明

  • 冒頭の定数:BASE_DIRはスクリプト自身の置き場所です。cronがどのディレクトリから起動しても、raw/とmaster.csvを同じ場所に読み書きできます。
  • download():トークンは引数で受け取り、コードには書きません。timeout=30で応答しない相手を待ち続けないようにしています。raise_for_status()は、403や500のときにエラーページをCSVとして保存しないための処理です。失敗したら60秒空けて最大3回まで試し、それでも駄目なら例外を上に投げます。取得した生データは日付名でraw/に残すので、後で取り込み直せます。
  • load():ここが今回の主役です。parse_dates=["date"]で変換対象の列を指定し、date_format="%Y/%m/%d"で書式を渡します。date_parserのように関数を1件ずつ呼ばないので、速くもなります。注意点は、書式が合わないとread_csvはエラーを出さず、列を文字列のまま残すことです。そこで日付型になったかをis_datetime64_any_dtypeで確かめ、なっていなければ止めます。
  • pd.to_datetime(format=...):時刻付きの列は読み込み後に変換しています。errors="coerce"は、読めない値をNaT(日時の欠損値)にする指定です。そのままでは黙って欠損するので、NaTの件数を数えて1件でもあれば止めます。元データの空欄もNaTになるため、空欄が正常にありうる列ではこの判定を外してください。
  • merge():appendの代わりにpd.concatで一度に結合します。同じidとdateの行は最新を残すので、同じ日に2回実行しても行が増えません。保存済みのmaster.csvはto_csvがISO形式(2026-10-17)で書き出すため、読み戻すときはdate_format="ISO8601"を指定します。書き込みは一時ファイルに出してから置き換えるので、途中で落ちてもmaster.csvは壊れません。
  • main():失敗したらlog.exceptionでスタックトレースごと記録し、終了コード1を返します。これでcronやsystemdが「失敗」と認識できます。元のコードのexcept: passとの最大の違いはここです。

date_format と pd.to_datetime の使い分け

場面

使うもの

読み込みと同時に、書式が1種類の列を変換する

read_csv(parse_dates=[...], date_format="...")。列ごとに書式が違うならdate_format={"列名": "書式"}の辞書で渡せる

変換に失敗した行を数えたい・止めたい

pd.to_datetime(..., format="...", errors="coerce")のあとでNaTを数える

1列に複数の書式が混ざっている

pd.to_datetime(..., format="mixed")。ただし誤読しても気づきにくいので、できれば取得元で書式を揃える

取得元サイトへの配慮:robots.txt・利用規約・アクセス間隔

  • まず公式の手段を使う:CSVのエクスポート機能やAPIが用意されているなら、それを使います。画面をスクレイピングするより相手の負荷が小さく、仕様変更にも強くなります。
  • 利用規約を読む:自動取得を禁止しているサービスは対象にしません。トークンを発行するサービスなら、利用目的や回数の上限が書かれていることが多いです。
  • robots.txtを確認する:urllib.robotparserで取得先のパスが許可されているかを確かめられます。
  • 1日1回で足りるものは1日1回:再試行は60秒空けて3回までにしています。間隔を詰めたり、失敗のたびに即座に叩き直したりはしません。User-Agentには、身元が分かる文字列を入れておきます。
  • 認証や回数制限をすり抜ける書き方は、AIが提案してきても採用しません。

毎朝止めずに取り込み続ける場所:手元PC・レンタルサーバーのcron・VPS

止まる条件で選ぶ

置き場所

向いている条件

止まる条件

手元のPC

試運転。毎朝決まった時刻にPCが起きている

スリープ中・電源オフ中は発火しない。ノートPCを閉じた日は丸ごと飛ぶ

共有レンタルサーバーのcron

1日1回、数十秒で終わる取り込み

Pythonの版を自分で選べず、pandas 3系が入らないことがある。実行時間やメモリの制限で強制終了される。失敗通知を設定しないと気づけない

VPS

取り込み後に集計・通知まで続けて回す。版を自分で固定したい

管理は自分の責任。ディスクの空き不足、OS更新後の再起動忘れ、契約更新の失念で止まる

サーバーを持たない方法としてはGitHub Actionsの定期実行もあります。こちらはGitHub Actions cronスクレイピング|26年10月で詳しく書いています。

共有サーバーのcronに登録する例

先にpython3 -Vとpip show pandasをサーバー上で実行し、pandas 3系が入ることを確かめます。入らない場合は、pandas 2系の範囲でdate_format=を使えば同じコードが動きます。date_formatはpandas 2.0で追加された引数です。

DATA_API_TOKEN=ここに実際のトークン
MAILTO=you@example.com
0 6 * * * /home/username/csvjob/.venv/bin/python /home/username/csvjob/daily_import.py >> /home/username/csvjob/import.log 2>&1
  • Pythonは仮想環境のフルパスで指定します。python3とだけ書くと、サーバー既定の古い版で動くことがあります。
  • 終了コード1で終わったときにメールが届くかは、サーバーのcron設定によります。届かない環境ではimport.logを見るか、失敗時にSlackなどへ通知する処理をAIに追加させます。

VPSならsystemd timerで「止まっていた分」も拾う

# /home/username/.config/systemd/user/csvjob.service
[Service]
Type=oneshot
WorkingDirectory=/home/username/csvjob
EnvironmentFile=/home/username/csvjob/.env
ExecStart=/home/username/csvjob/.venv/bin/python daily_import.py

# /home/username/.config/systemd/user/csvjob.timer
[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true

[Install]
WantedBy=timers.target

Persistent=trueを付けると、サーバーが止まっていて6時を逃した日も、起動した時点で1回実行されます。トークンは権限600の.envに置き、コードにもリポジトリにも入れません。ログはjournalctl --user -u csvjob.serviceで見られます。終了コード1はfailedとして記録されます。

ユーザー単位のtimerは、ログアウトすると止まります。loginctl enable-linger usernameを実行しておくと、ログアウト後も動き続けます。

関連する選択肢

毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。

レンタルサーバー エックスサーバー

※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。

毎朝の取り込みを止めないための3つの確認

AIに書かせたCSV取り込みが止まる原因は、多くの場合ひとつに絞れます。pandas 3.0で削除されたread_csv(date_parser=...)のような古い書き方を、AIが今も出してくることです。直すときはdate_format=とpd.to_datetime(format=...)に置き換えます。加えて、日付型になったかを確かめ、読めない行があれば止める処理を入れます。指示文にはpip show pandasの結果と、禁止する書き方(date_parser・append・except: pass・直書きのトークン)を明記します。受け取ったコードは、確認ポイントの語で検索して点検してください。置き場所は止まる条件で選びます。試運転は手元のPC、短い日次処理は共有サーバーのcron、版を固定して確実に回したいならVPSのsystemd timerです。どこで動かす場合も、失敗を終了コード1で返し、それに気づける経路を用意して初めて「毎朝止まらない」状態になります。