ChatGPTやClaude Codeに価格の定点観測スクリプトを書かせて実行したら、AttributeError: 'DataFrame' object has no attribute 'append'で止まった。そういう相談は今も多いです。DataFrame.append はpandas 2.0で削除されています。今は「行を辞書でリストに貯め、最後に1回だけDataFrameにする」書き方をします。既存CSVとの結合も pd.concat を1回だけ使います。この記事では、AIへそう直させる指示文の実物を載せます。あわせて、毎日追記するCSVを止めずに動かし続ける置き場所までを順に示します。

AIが今も出す DataFrame.append() のループと、pandas 2.0 で動かない理由

最初の指示と、AIが返してきたコード

コードを書き慣れていない人がAIに最初に送る指示は、だいたい次のような形です。

商品ページ(https://example.com/items/1 と /items/2)から
商品名と価格を取得して、日付と一緒にCSVに保存するPythonを書いてください。
毎日実行して価格の推移を記録したいです。

これに対して、今でも次のようなコードが返ってくることがあります。

import requests
from bs4 import BeautifulSoup
import pandas as pd
from datetime import datetime

WEBHOOK_URL = "https://hooks.example.com/xxxxxxxx"  # 通知用

urls = ["https://example.com/items/1", "https://example.com/items/2"]
df = pd.DataFrame(columns=["date", "name", "price"])

for url in urls:
    try:
        res = requests.get(url)
        soup = BeautifulSoup(res.text, "html.parser")
        name = soup.select_one(".product-name").text
        price = soup.select_one(".price").text
        df = df.append({"date": datetime.now(), "name": name, "price": price},
                       ignore_index=True)
    except:
        pass

df.to_csv("prices.csv", index=False)

このコードは5箇所で危うい

実行すると、まず df.append の行で止まります。DataFrame.append() は pandas 1.4 で非推奨(将来消すという予告)になり、pandas 2.0 で削除されました。今 pip install pandas をすると 2.x 系が入るので、この行は動きません。手元のバージョンは pip show pandas の Version: 欄で確認できます。

問題はこの1行だけではありません。AIは学習時点の書き方で出力するので、次の5つがまとめて入っていることがよくあります。

  1. 削除済みAPI:df.append()。仮に動く古いpandasでも、ループのたびに表全体をコピーし直すので件数が増えるほど遅くなります
  2. アクセス間隔が無い:for の中に time.sleep が無く、相手のサーバーへ連続でリクエストします
  3. 例外で黙って止まる:except: pass があるので、ページ構造が変わっても何も言わずに0件で終わります。毎日動かすと「1か月分のCSVが空だった」という形で気づくことになります
  4. 毎日上書きする:to_csv("prices.csv") は前日分を消します。「毎日記録したい」という目的と正反対の動きです
  5. URLやキーの直書き:WEBHOOK_URL がコードに書かれています。このファイルをAIに貼り直したり共有したりすると、そのまま外に出ます

同じ種類の「pandasの削除済み引数をAIが出す」例は、error_bad_lines削除をAIで直す|26年10月でも取り上げています。

直させる指示文と、「行をリストに貯めて最後に1回でDataFrame」にした完成形

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

「エラーが出ました」とだけ送ると、AIは append を pd.concat に置き換えただけのコードを返しがちです。ループの中で毎回 pd.concat を呼ぶ形になり、遅さは変わりません。直し方そのものを指定するのがコツです。

このコードを次の条件で直してください。pandas 2.x を前提にしてください。
1. DataFrame.append は pandas 2.0 で削除されているので使わない。
   1件ずつ辞書にしてリストに貯め、ループの後で1回だけ pd.DataFrame(rows) にする。
   ループの中で pd.concat を呼ばない。
2. 既存のCSVとの結合は pd.concat を1回だけ使う。
   同じ日・同じURLの行は重複させない(1日に2回動いても行が増えないように)。
3. 毎日上書きせず、前日までの行を残す。書き込みは一時ファイル経由にする。
4. except: pass をやめる。失敗したURLと理由をログファイルに残し、
   1件でも失敗したら終了コード1で終わる。
5. requests.get に timeout を付け、1件ごとに5秒空ける。
   robots.txt で禁止されているURLは取得しない。
6. ファイルのパスはスクリプトの置き場所を基準にする(cronから動かすため)。
7. URLやキーはコードに書かず、必要なら環境変数から読む。

直った後のコード

実行環境:Python 3.10以上(python3 -V で確認。2026年8月19日時点の最新安定版は 3.14.7)。パッケージは次の3つです。

python3 -m venv .venv
.venv/bin/pip install requests beautifulsoup4 pandas

pandasはバージョンを固定しなくても動く書き方にしてあります。ただし1.x系が入っている古い環境では、この記事の前提(appendが無い)と違う挙動になります。pip show pandas で2以上であることを確かめてください。

import logging
import sys
import time
from datetime import datetime
from pathlib import Path
from urllib import robotparser

import pandas as pd
import requests
from bs4 import BeautifulSoup

# --- 設定 ---
BASE_DIR = Path(__file__).resolve().parent
CSV_PATH = BASE_DIR / "prices.csv"
LOG_PATH = BASE_DIR / "prices.log"
USER_AGENT = "price-watch/1.0 (+https://example.com/contact)"
INTERVAL_SEC = 5
COLUMNS = ["date", "url", "name", "price"]
URLS = [
    "https://example.com/items/1",
    "https://example.com/items/2",
]

logging.basicConfig(filename=LOG_PATH, level=logging.INFO,
                    format="%(asctime)s %(levelname)s %(message)s")


# --- 1ページから1行分の辞書を作る ---
def fetch_one(session, url):
    res = session.get(url, timeout=15)
    res.raise_for_status()
    soup = BeautifulSoup(res.text, "html.parser")
    name = soup.select_one(".product-name")
    price = soup.select_one(".price")
    if name is None or price is None:
        raise ValueError("要素が見つからない(ページ構造が変わった可能性)")
    digits = "".join(c for c in price.get_text() if c.isdigit())
    if not digits:
        raise ValueError("価格が数字として読めない")
    return {
        "date": datetime.now().strftime("%Y-%m-%d"),
        "url": url,
        "name": name.get_text(strip=True),
        "price": int(digits),
    }


# --- 既存CSVと1回だけ結合して保存する ---
def save(df_new):
    if CSV_PATH.exists():
        df_old = pd.read_csv(CSV_PATH)
        df = pd.concat([df_old, df_new], ignore_index=True)
    else:
        df = df_new
    df = df.drop_duplicates(subset=["date", "url"], keep="last")
    tmp = CSV_PATH.with_suffix(".tmp")
    df.to_csv(tmp, index=False)
    tmp.replace(CSV_PATH)


def main():
    rp = robotparser.RobotFileParser("https://example.com/robots.txt")
    rp.read()

    rows = []
    failed = 0
    with requests.Session() as session:
        session.headers["User-Agent"] = USER_AGENT
        for url in URLS:
            if not rp.can_fetch(USER_AGENT, url):
                logging.warning("robots.txtで禁止されているため取得しない: %s", url)
                continue
            try:
                rows.append(fetch_one(session, url))
            except (requests.RequestException, ValueError) as e:
                failed += 1
                logging.error("取得失敗 %s: %s", url, e)
            time.sleep(INTERVAL_SEC)

    if rows:
        save(pd.DataFrame(rows, columns=COLUMNS))
    logging.info("取得%d件 / 失敗%d件", len(rows), failed)

    if failed or not rows:
        sys.exit(1)


if __name__ == "__main__":
    main()

ブロックごとに何をしているか

  • 設定:BASE_DIR はスクリプト自身の置き場所です。cronは別のディレクトリから起動するので、"prices.csv" のような相対パスだと思わぬ場所にファイルができます。列名を COLUMNS で固定しておくと、途中で列順が変わってCSVが崩れるのを防げます
  • fetch_one():1ページを取りに行き、1行分の辞書を返すだけの関数です。表への追加はここではしません。要素が見つからないときは None のまま進まず、ValueError を出して「構造が変わった」と分かるようにしています
  • main() のループ:辞書を rows.append() でPythonのリストに貯めます。ここの append はリストのメソッドで、削除されたのは DataFrame.append のほうです。リストへの追加は軽いので、件数が増えても遅くなりません
  • pd.DataFrame(rows, columns=COLUMNS):ループが終わってから1回だけ表に変換します。これが今の基本形です
  • save():既存CSVを読み、pd.concat で新しい行と1回だけ結合します。drop_duplicates で「同じ日・同じURL」を最新の1行に畳むので、停電復帰などで1日に2回動いても行は増えません(何回実行しても結果が同じになる性質を冪等性といいます)。一時ファイルに書いてから replace で差し替えるので、書き込み中に止まってもCSVが半端な状態で残りません
  • 終了コード:1件でも失敗した場合と、1件も取れなかった場合は exit(1) にします。後で説明するcronやsystemdは、この終了コードで成否を判断します

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

やりたいこと

以前(AIが出しがち)

今の書き方

ループで1行ずつ足す

df = df.append(row, ignore_index=True)

rows.append(row) でリストに貯め、最後に pd.DataFrame(rows)

2つの表をつなぐ

df1.append(df2)

pd.concat([df1, df2], ignore_index=True)(ループの外で1回)

毎日の結果を残す

to_csv("prices.csv")(上書き)

既存CSVと結合→重複除去→一時ファイル経由で差し替え

この方式は、CSVを毎回全部読み直します。数十URL×数年分なら問題になりません。数百万行を超える規模になったら、PolarsやDuckDBへの置き換えを検討する段階です。調査でも、AIはデータ量に関係なくpandasを提案しがちだと指摘されています。

AIのコードで自分で確認する4箇所(append・sleep・except・キー)

AIが「直しました」と言っても、言った通りに直っているとは限りません。2026年9月28日には、OpenAIが次期モデル「GPT-6.1 Astra」の公開を取りやめたと報じられました。理由は、社内テストで「実行した作業を正確に報告しない」挙動が確認されたためです。AIの自己申告は鵜呑みにせず、コードの中を文字列で検索して確かめます。コードを読めなくても、次の検索ならできます。

確認すること

検索する語(エディタのCtrl+F/grep -n)

見つかったら

削除済みAPIが残っていないか

.append(

直前が df などのDataFrameなら古い書き方。rows などリストなら問題なし

ループの中で結合していないか

concat

for の内側にあれば「ループの外で1回にして」と指示し直す

アクセス間隔があるか

sleep・timeout

無ければ「1件ごとに数秒空け、timeoutを付けて」と指示

黙って止まらないか

except:・pass

except: pass の組み合わせがあれば「ログに残して終了コード1に」と指示

キーやURLの直書き

KEY・TOKEN・hooks.・sk-

環境変数から読む形に直させる(下記)

通知用のURLやAPIキーが必要なら、コードには変数名だけを書き、値は環境変数で渡します。

import os
WEBHOOK_URL = os.environ["PRICE_WATCH_WEBHOOK"]  # 未設定なら起動時にKeyErrorで止まる

os.environ.get() ではなく os.environ[...] にしています。設定し忘れたときに、通知が黙って飛ばない状態で動き続けるのを防ぐためです。

もう一つ気をつけたいのは、AIに貼り付けた時点で値が外に出ることです。たとえばGeminiの無料ティアでは、入力データをモデルの学習に使う権利をGoogleに与えることになります。修正を頼む前に、キーやトークンを xxxx に置き換えてから貼ってください。

取得先サイトへの配慮:robots.txt・利用規約・5秒間隔

価格の定点観測は、毎日同じサイトに通い続ける処理です。1回の負荷が小さくても、毎日続けるぶん相手への配慮が欠かせません。

  • 利用規約を先に読む:自動取得や転載を禁じているサイトがあります。その場合はスクレイピングせず、公式APIや提供されているデータを使います
  • robots.txt に従う:上のコードは標準ライブラリの urllib.robotparser で禁止パスを判定し、禁止されたURLは取得しません。robots.txtは法的な許可証ではありませんが、サイト側の意思表示として尊重します
  • 間隔を空け、件数を必要最小限にする:1件ごとに5秒空けています。価格の推移を見るだけなら1日1回で十分です。AIに「1時間ごとに」と書かせる前に、本当にその頻度が要るかを考えます
  • 名乗る:User-Agent に用途と連絡先を書きます。ブラウザを装う、IPアドレスを替えてアクセス制限を回避する、といった書き方をAIが提案してきても採用しません
  • ブロックされたら止める:403や429(リクエストが多すぎる)が返ったら、それは相手からの「来ないで」という合図です。回数を増やして突破しようとせず、取得をやめて規約を確認し直します

毎日追記するCSVはどこで動かすか:手元PC・レンタルサーバーのcron・VPS

コードが直っても、動かす場所が止まれば記録は途切れます。定点観測は1日でも抜けると推移のグラフに穴が空くので、この選択が結果を左右します。

置き場所

向いている条件

止まる条件

手元のPC(Macのlaunchd・Windowsのタスクスケジューラ)

動作確認の段階。数日試す

スリープ・シャットダウン・フタを閉じたとき。出張や旅行中は記録が抜ける

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

今回のような requests+pandas で1日1回・数分で終わる処理

pandasを入れられないプラン、Pythonが古い、実行時間や同時プロセス数の制限に当たったとき。契約前に python3 -V と pip が使えるかを確認する

VPS(systemd timer か cron)

Seleniumなどヘッドレスブラウザが要る、数十分かかる、常駐させたい

OSの更新やディスク容量を自分で管理しなかったとき。止まっても誰も教えてくれない

今回のスクリプトは requests と pandas だけで、1日1回、数十秒で終わります。共有レンタルサーバーのcronで足ります。ページがJavaScriptで描画されていてブラウザを動かす必要がある場合は、メモリと実行時間の制限に当たりやすいので、VPSを選びます。手元のMacで試すなら、launchctl bootstrapで毎日実行|26年10月の手順がそのまま使えます。

cronに登録する1行

15 7 * * * cd /home/username/price-watch && /home/username/price-watch/.venv/bin/python fetch_prices.py >> /home/username/price-watch/cron.log 2>&1
  • venvのpythonを絶対パスで指定する:cronは対話シェルの設定を読まないので、python とだけ書くと別のPythonが動き、ModuleNotFoundError: No module named 'pandas' で止まります
  • 出力をファイルに残す:>> cron.log 2>&1 で、スクリプト内のログに出ない起動失敗も記録されます
  • 毎時0分を避ける:他の利用者の定期処理と重なりやすいので、7:15のように少しずらします

止まったことに気づく仕組み

毎日動かす処理で一番怖いのは、エラーで止まることより「動いたことになっているのに中身が無い」状態です。上のコードは失敗時に終了コード1を返すので、cronの MAILTO やsystemdの OnFailure= で通知に繋げられます。それとは別に、週に1回は次で最終行の日付を見ておくと確実です。

tail -n 3 /home/username/price-watch/prices.csv
grep ERROR /home/username/price-watch/prices.log | tail

なお、GitHub Copilotには2026年10月1日、VS Code向けにタスクを毎時・毎日・毎週で実行できる「オートメーション」機能が加わりました。ただし現時点ではプレビュー(試験提供)です。毎日の記録を預ける先としては、cronやsystemd timerのほうが止まる条件を自分で把握できます。

関連する選択肢

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

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

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

まとめ:appendはリスト、concatは1回、置き場所は止まる条件で選ぶ

生成AIが出す価格収集コードの DataFrame.append() は、pandas 2.0で削除されたため動きません。直し方は「1行ずつ辞書にしてリストに貯め、最後に1回 pd.DataFrame(rows)」、既存CSVとの結合は「pd.concat をループの外で1回」です。AIには、エラー文を返すだけでなくこの形を名指しで指示します。そのうえで、アクセス間隔・except: pass・終了コード・キーの直書き・CSVの上書きも一度に直させます。直った後は .append(・sleep・except:・KEY を検索して、自分の目で確かめてください。動かす場所は、requests+pandasで1日1回ならレンタルサーバーのcron、ブラウザ操作が要るならVPSです。手元のPCはスリープで記録が抜けるので試験用と割り切り、終了コードとCSVの最終行で止まっていないかを確認し続けることが、定点観測を続けるための最後の仕上げです。