ChatGPTやClaude Codeに「求人サイトの新着を毎日CSVに保存するPythonを書いて」と頼んだのに、実行すると to_csv で TypeError が出る。直してもExcelで開くと日本語が文字化けする。この記事では、AIが今も出しがちな pandasの line_terminator 削除とCSVのExcel文字化けを、lineterminator= と encoding='utf-8-sig' で解決する書き方を示します。AIに渡す指示文もそのまま載せます。最後に、その収集スクリプトを毎日止めずに動かす置き場所まで扱います。
ChatGPTが出した求人CSVスクリプトが pandas 2.x で止まる2つの理由
最初にAIへ出した指示
コードを書き慣れていない人が最初に出す指示は、たいてい次のような形です。
求人サイト https://example.com/jobs の新着求人を1〜50ページ取得して、
タイトル・勤務地・URLを今日の日付のCSVに保存するPythonスクリプトを書いてください。
完了したら通知用のWebhookに知らせてください。AIが返してきたコード(問題あり)
import requests
from bs4 import BeautifulSoup
import pandas as pd
from datetime import date
WEBHOOK_URL = "https://hooks.example.com/T000/xxxxxxxx" # ← 直書き
rows = []
for i in range(1, 51):
try:
res = requests.get(f"https://example.com/jobs?page={i}")
soup = BeautifulSoup(res.text, "html.parser")
for card in soup.select(".job-card"):
rows.append({
"title": card.select_one(".title").text,
"place": card.select_one(".place").text,
"url": card.select_one("a")["href"],
})
except Exception:
pass # ← 失敗を黙って捨てる
df = pd.DataFrame(rows)
df.to_csv(f"jobs_{date.today()}.csv", index=False,
encoding="utf-8", line_terminator="\n")
requests.post(WEBHOOK_URL, json={"text": "完了"})このコードは一見それらしく見えますが、今の環境では2か所で期待どおりに動きません。
line_terminator=は pandas 2.0 で削除された引数です。1.x 系でlineterminator=に改名されて旧名は非推奨になり、2.0 で消えました。そのため実行するとTypeError(unexpected keyword argument 'line_terminator')で止まります。AIは学習データに古い書き方が多く残っているため、今もこの名前を出してきます。encoding="utf-8"のCSVは、日本語版Excelでダブルクリックして開くと文字化けします。BOM(ファイル先頭に付く「これはUTF-8です」という目印)が無いと、Excelは日本語の旧来の文字コードとして読もうとするためです。
調査では、AIが生成するコードの正解率は約60%にとどまり、約4割にバグや考慮漏れが含まれるとされています。2026年の調査では、開発者がAI生成コードのレビューに使う時間が週11.4時間となり、自分で書く時間の9.8時間を上回りました。書かせたコードを確認する作業は、もう省けない工程になっています。
以前の書き方と今の書き方
項目 | 以前(AIが出しがち) | 今の書き方 |
|---|---|---|
改行コードの指定 |
|
|
Excelで開くCSVの文字コード |
|
|
行の追加 |
|
|
3行目の append も pandas 2.0 で消えた書き方です。AIが追記処理で出してきたときの直し方は append削除→concat追記|AIコード2026年10月 にまとめています。
encoding="cp932"(Shift_JIS系)に変えても文字化けは消えます。ただし求人タイトルに含まれる絵文字や一部の異体字はcp932で表せず、UnicodeEncodeError で止まる原因になります。Excel向けには utf-8-sig を選ぶ方が安全です。
AIのコードで自分で見つけるべき4か所(line_terminator・sleep・except・キー直書き)
コードを全部読めなくても、次の語をエディタの検索(Ctrl+F / Cmd+F)で探せば、危ない箇所は見つかります。
確認箇所 | 検索する語 | 何が問題か | あるべき姿 |
|---|---|---|---|
古いAPI |
| 今のライブラリでは削除済みで、実行時に止まる |
|
アクセス間隔 |
| 見つからなければ、50ページを間隔なしで連続取得している | 1リクエストごとに数秒空ける。ページ数も必要最小限にする |
例外の握りつぶし |
| 全ページ失敗しても空のCSVができ、「成功」として終わる | ログに残す。1件も取れなければ終了コード1で異常終了する |
秘密情報の直書き |
| ファイルを共有・公開した時点で漏れる | 環境変数から読む |
Seleniumの find_element_by_xpath などが出てきた場合の書き換えは find_element_by_xpath削除|2026年10月版 を参照してください。
また、エラーを相談するときにWebhookのURLやAPIキーが入ったままのコードをChatGPTなどに貼らないでください。入力内容はサービス側のサーバーに送られます。デフォルト設定では学習に使われる可能性もあります。貼る前に xxxxxxxx などに置き換えてください。
「pandas 2.x前提で lineterminator= と utf-8-sig を使え」と直させる指示文
直させる指示(そのままコピーして使える)
「エラーが出ました」とだけ伝えると、AIは同じ古い書き方のまま別の箇所をいじりがちです。何を前提にするかを明示します。
次の条件で、さきほどのスクリプトを書き直してください。
1. pandas 2.x 前提で書くこと。to_csv の line_terminator は削除済みなので
lineterminator= を使うこと。DataFrame.append も使わないこと。
2. CSVは日本語版Excelでダブルクリックして文字化けしないよう
encoding="utf-8-sig" で保存すること。
3. 取得前に robots.txt を確認し、禁止されているURLは取得しないこと。
4. 1リクエストごとに3秒空けること。ページ数は最大5にすること。
5. requests には timeout を付け、失敗したら間隔を広げて最大3回まで再試行すること。
6. except で例外を握りつぶさず logging でエラー内容を残すこと。
1件も取得できなかったら終了コード1で終わること。
7. Webhook URL はコードに書かず、環境変数 NOTIFY_WEBHOOK_URL から読むこと。
8. 各ブロックが何をしているか、日本語のコメントを付けること。ポイントは、削除された名前(line_terminator)と正しい名前(lineterminator)を両方とも指示に書くことです。正しい名前だけを伝えても、AIが古い名前に戻すことがあります。
直った後のコード
import logging
import os
import sys
import time
from datetime import date
from pathlib import Path
from urllib.parse import urljoin
from urllib.robotparser import RobotFileParser
import pandas as pd
import requests
from bs4 import BeautifulSoup
# --- 設定 ---
BASE = "https://example.com"
LIST_URL = BASE + "/jobs?page={page}"
MAX_PAGES = 5
INTERVAL_SEC = 3
USER_AGENT = "jobs-daily-collector/1.0"
OUT_DIR = Path(os.environ.get("JOBS_OUT_DIR", "/home/username/jobs_csv"))
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("jobs")
# --- robots.txt を一度だけ読む ---
def load_robots():
rp = RobotFileParser(BASE + "/robots.txt")
rp.read()
return rp
# --- 1ページ取得(タイムアウト・再試行つき) ---
def fetch(session, url):
for attempt in range(3):
try:
res = session.get(url, timeout=(10, 30))
res.raise_for_status()
return res.text
except requests.RequestException as e:
wait = 5 * (2 ** attempt)
log.warning("取得失敗 %s (%s) %d秒後に再試行", url, e, wait)
time.sleep(wait)
log.error("3回失敗したので諦めます: %s", url)
return None
# --- HTMLから求人を抜き出す ---
def parse(html):
soup = BeautifulSoup(html, "html.parser")
rows = []
for card in soup.select(".job-card"):
title = card.select_one(".title")
link = card.select_one("a[href]")
if not title or not link:
continue
place = card.select_one(".place")
rows.append({
"title": title.get_text(strip=True),
"place": place.get_text(strip=True) if place else "",
"url": urljoin(BASE, link["href"]),
})
return rows
# --- 終了を通知(URLは環境変数から。未設定なら送らない) ---
def notify(text):
url = os.environ.get("NOTIFY_WEBHOOK_URL")
if not url:
return
try:
requests.post(url, json={"text": text}, timeout=10)
except requests.RequestException as e:
log.warning("通知に失敗: %s", e)
def main():
rp = load_robots()
session = requests.Session()
session.headers["User-Agent"] = USER_AGENT
rows = []
for page in range(1, MAX_PAGES + 1):
url = LIST_URL.format(page=page)
if not rp.can_fetch(USER_AGENT, url):
log.error("robots.txt で禁止されています: %s", url)
return 1
html = fetch(session, url)
if html:
rows.extend(parse(html))
time.sleep(INTERVAL_SEC)
if not rows:
log.error("1件も取得できませんでした(サイト構造の変更を疑う)")
notify("求人収集: 0件で終了しました")
return 1
# --- CSV保存:pandas 2.x の書き方 ---
df = pd.DataFrame(rows).drop_duplicates(subset="url")
OUT_DIR.mkdir(parents=True, exist_ok=True)
path = OUT_DIR / f"jobs_{date.today():%Y%m%d}.csv"
df.to_csv(path, index=False, encoding="utf-8-sig", lineterminator="\n")
log.info("%d件を保存: %s", len(df), path)
notify(f"求人収集: {len(df)}件を保存しました")
return 0
if __name__ == "__main__":
sys.exit(main())ブロックごとの説明
- 設定:取得先・ページ数・間隔・保存先を冒頭にまとめています。保存先は環境変数
JOBS_OUT_DIRで変えられます。.job-cardなどのセレクタ(HTMLのどこを抜くかの指定)は例です。実際のサイトのHTMLを見て合わせてください。 load_robots():robots.txtをページごとに読み直すと、それ自体が余計なアクセスになります。そのため最初に一度だけ読みます。fetch():timeout=(10, 30)は「接続に10秒・応答に30秒まで待つ」という意味です。これが無いと、相手が応答しないときに永遠に待ち続けます。失敗したら5秒→10秒→20秒と間隔を広げて再試行します(指数バックオフ)。parse():要素が欠けたカードは飛ばします。urljoinで相対URLを完全なURLに直します。notify():Webhook URLは環境変数から読みます。コードにはURLを一切書きません。main()の0件判定:サイトのHTMLが変わると、エラーは出ずに「0件」になるのが典型です。0件を異常として終了コード1を返すので、後述のsystemdやcronで失敗として検知できます。- CSV保存:今回の主役です。
lineterminator="\n"で改行をOSに依存せず統一し、encoding="utf-8-sig"でExcelでも文字化けしないCSVにしています。
実行環境:pandas 2.x と Python 3.12.15 以降を pip で入れる
- 仮想環境を作って有効にします:
python3 -m venv .venv→source .venv/bin/activate - パッケージを入れます:
pip install "pandas>=2" requests beautifulsoup4pandas>=2と下限を付けるのは、lineterminator=を前提にしたコードを古い1.x系で動かさないためです。 - 入ったバージョンを確認します:
pip show pandas・python3 -V
Python本体も確認してください。2026年9月30日に Python 3.14.8・3.13.16・3.12.15 が公開され、SSLContext の解放後メモリ利用の欠陥 CVE-2026-19445(CVSS v4.0 基本値 9.2、Critical)が修正されました。HTTPSで外部サイトに毎日アクセスするスクリプトなので、python3 -V の結果がそれぞれの系列の修正版より古ければ更新しておきましょう。
取得先サイトへの配慮(robots.txt・利用規約・アクセス間隔)
- robots.txt:上のコードは禁止されたURLを取りに行きません。ただし robots.txt はあくまで最低限の目安です。
- 利用規約:求人サイトには自動取得やデータの再配布を禁じているところがあります。公式APIやRSSが提供されていれば、そちらを優先してください。
- アクセス間隔と量:AIが最初に出した「50ページを間隔なし」は、相手のサーバーに負荷をかける書き方です。新着だけを見るなら先頭の数ページで足ります。実行も1日1回で十分です。
- ブロックされたときに、ヘッダーの偽装やIPの切り替えで回避する方法はAIに頼まないでください。止められたら取得をやめ、規約と公式の提供手段を確認するのが正しい対応です。
毎日止めずに回す置き場所:自宅PC・レンタルサーバーのcron・VPSのsystemd timer
スクリプトが動いても、置き場所を間違えると「気づいたら1週間止まっていた」ことになります。それぞれがどういう条件で止まるかで選んでください。
置き場所 | 向いている条件 | 止まる条件 |
|---|---|---|
手元のPC(タスクスケジューラ・launchd) | 試運転中。数日分だけ取れればよい | スリープ・電源オフ・ノートPCを閉じた日。予定時刻に寝ていると、その回は実行されないか、開いた時刻にずれる |
共有レンタルサーバーのcron | 今回のように requests + BeautifulSoup だけで完結する | 事業者の実行時間・プロセス数の制限に当たったとき。Chromeを動かすSeleniumは使えないことが多い。上限値は事業者ごとに違うので公式マニュアルで確認する |
VPS(systemd timer) | Seleniumなどのブラウザ操作が要る。失敗時に通知や再実行の仕組みを組みたい | OS・Pythonの更新を怠って壊れたとき。ディスクが一杯になったとき。月額の支払いが止まったとき |
共有レンタルサーバーのcronで動かす
0 7 * * * cd /home/username/jobs && NOTIFY_WEBHOOK_URL="$(cat /home/username/.notify_url)" /home/username/jobs/.venv/bin/python collect_jobs.py >> /home/username/jobs/cron.log 2>&1cronは .bashrc を読まないので、仮想環境のPythonをフルパスで書き、環境変数もこの行で渡します。.notify_url は chmod 600 にして、自分以外が読めないようにしておきます。cron.log に「0件」のエラーが残っていないかを週に1度は確認してください。cronには、失敗したときに知らせる仕組みがありません。
VPSのsystemd timerで動かす
# /home/username/.config/systemd/user/jobs.service
[Unit]
Description=求人新着の収集
[Service]
Type=oneshot
WorkingDirectory=/home/username/jobs
EnvironmentFile=/home/username/jobs/.env
ExecStart=/home/username/jobs/.venv/bin/python collect_jobs.py
# /home/username/.config/systemd/user/jobs.timer
[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true
[Install]
WantedBy=timers.targetPersistent=true を付けると、サーバーが止まっていた間に逃した回を、起動後に実行し直します。.env には NOTIFY_WEBHOOK_URL=... を書いてGit管理から外し、パーミッションを600にします。有効化は systemctl --user enable --now jobs.timer です。ログアウト後も動かすには loginctl enable-linger username も必要です。
このスクリプトは0件のときに終了コード1を返すので、systemdはそれを「失敗」として記録します。失敗したらSlackなどに知らせる設定は systemd OnFailureでSlack通知|26年10月 の手順がそのまま使えます。
関連する選択肢
毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。
※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。
まとめ:lineterminator・utf-8-sig・0件で異常終了の3点をAIに守らせる
生成AIが書く求人収集スクリプトは、pandas 2.0 で削除された to_csv(line_terminator=) で TypeError になります。encoding="utf-8" のままだとExcelで文字化けします。今の書き方は lineterminator="\n" と encoding="utf-8-sig" です。AIに直させるときは、削除された名前と正しい名前の両方を指示に書いてください。あわせて sleep・except: pass・Webhook URLの直書きの3か所を検索で点検し、robots.txt の確認とアクセス間隔を入れさせます。1件も取れない日は異常終了させれば、サイト構造の変更にも気づけます。置き場所は、試運転なら手元のPC、requests だけで済むならレンタルサーバーのcron、ブラウザ操作や失敗通知が要るならVPSのsystemd timer です。それぞれ止まる条件が違うので、条件で選んでください。