ChatGPTやClaude Codeに求人の新着収集スクリプトを書かせたら、Retry(method_whitelist=...) のところで止まった。あるいは動いたものの、朝の定期実行がいつまでも終わらず、翌日分と重なっていた。どちらも生成AIのコードでよく起きます。原因は、urllib3 2.x で削除済みの method_whitelist と、timeout の無い requests.get() です。この記事では、allowed_methods+HTTPAdapter+timeout=(接続, 読み取り) の今の書き方へAIに直させる指示文を、実物で載せます。応答の無いサイトに当たっても毎日決まった時刻に終わらせる置き場所も、あわせて示します。

AIが今も出す method_whitelist と timeout 無しの requests.get()

最初の指示と、出てきたコード

コードを書き慣れていない人が出しがちな指示は、次のようなものです。

求人サイトの新着一覧を5ページ分取得するPythonスクリプトを書いてください。
通信が失敗したらリトライしてください。毎朝自動で動かしたいです。

この指示に対して、生成AIは次のようなコードを返すことがあります(要点だけ抜き出しています)。

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

API_KEY = "abcd1234"   # 通知用のキー

retry = Retry(total=5, backoff_factor=1,
              status_forcelist=[500, 502, 503, 504],
              method_whitelist=["GET"])
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))

for page in range(1, 6):
    try:
        r = requests.get(f"https://example.com/jobs?page={page}")
        print(r.text[:100])
    except:
        pass

一見すると、リトライも例外処理も入っているように見えます。ところが、毎日動かす前提で読むと危うい箇所が5つあります。

AIのコードで自分で確認すべき5箇所

コードが読めなくても、次の語を検索(Ctrl+F)すれば見つけられます。

探す語

何が問題か

直した後の形

method_whitelist

urllib3 2.x で削除された引数。Retry() を作る時点で落ちる

allowed_methods

requests.get( に timeout が無い

相手が応答を返さないと、いつまでも待ち続ける

timeout=(5, 30) のように接続と読み取りを分けて指定

except: の直後に pass

失敗しても何も記録されず、終了コードも0のまま。止まっていても気づけない

ログに残し、全滅なら0以外で終了

API_KEY = "

キーの直書き。ファイルを共有・公開した時点で漏れる

環境変数から読む

sleep が無い

相手のサーバーへ連続でアクセスする

1リクエストごとに数秒空ける

上のコードには、見落としやすい点がもう1つあります。せっかく作った session を使わず、ループ内で requests.get() を直接呼んでいます。そのためリトライ設定がどこにも効いていません。「リトライして」と頼んだのに、実際にはリトライしないコードです。

method_whitelist で落ちるときの表示

urllib3 が 2.x の環境では、次のように Retry を作る行で止まります。

TypeError: Retry.__init__() got an unexpected keyword argument 'method_whitelist'

手元の urllib3 のバージョンは pip show urllib3 で確認できます。生成AIは、学習データに残っている古い書き方をそのまま出すことがあります。調査結果にも「AIは学習データに基づいたコードを生成するため、最新のライブラリの仕様変更や非推奨化されたAPIに対応できていないことがある」とあります。これはモデルが新しくなっても起きます。2026年9月にはClaude Opus 5.5が出て、Claude Code内ではClaude Fable 5.1が100万トークンのコンテキストで使えるようになりました。ChatGPTもGPT-6系へ移っています。それでも、古いAPIを出さないとは限りません。

allowed_methods+HTTPAdapter+timeout=(接続, 読み取り) へ直させる指示文

直させる指示(そのまま貼ってよい)

「動かないので直して」だけでは、AIは method_whitelist を書き換えるだけで終わりがちです。毎日動かす前提を伝え、確認してほしい点を列挙します。

このスクリプトを毎朝1回、無人で動かします。次の条件で書き直してください。

1. Python 3.12 以降、requests と urllib3 2.x で動くこと。
   Retry は method_whitelist ではなく allowed_methods を使うこと。
2. Retry は HTTPAdapter に渡して Session にマウントし、
   すべてのリクエストをその Session 経由で送ること。
3. すべてのリクエストに timeout=(接続秒, 読み取り秒) をタプルで付けること。
4. リトライ対象は GET と HEAD、ステータス 429/500/502/503/504。
   Retry-After ヘッダーがあれば従うこと。
5. スクリプト全体に締め切り時間を設け、超えたら打ち切って終了コード1で終わること。
6. except: pass は使わない。失敗は logging でページ番号つきで記録し、
   1件も取れなかったら終了コード1で終わること。
7. 1リクエストごとに3秒以上空けること。
8. キーやトークンはコードに書かず、環境変数から読むこと。無ければエラーで止めること。
9. 変更点を、変更前と変更後が分かる形で箇条書きにしてから、コード全体を出すこと。

最後の9番が大事です。変更点を先に列挙させると、AIが黙って省いた項目に気づけます。返ってきたら、前の節の5箇所をもう一度検索して確かめてください。

直った後のコード

import logging
import os
import sys
import time

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

BASE_URL = "https://example.com/jobs"
PAGES = 5
INTERVAL_SEC = 3          # 1リクエストごとの間隔
TIMEOUT = (5, 30)         # (接続, 読み取り) 秒
DEADLINE_SEC = 15 * 60    # スクリプト全体の締め切り

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


def make_session():
    retry = Retry(
        total=3,
        connect=3,
        read=2,
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET", "HEAD"],
        backoff_factor=2,
        respect_retry_after_header=True,
        raise_on_status=False,
    )
    adapter = HTTPAdapter(max_retries=retry)
    s = requests.Session()
    s.mount("https://", adapter)
    s.mount("http://", adapter)
    s.headers["User-Agent"] = "job-collector/1.0 (+https://example.com/contact)"
    return s


def main():
    token = os.environ.get("NOTIFY_TOKEN")   # 通知に使うトークン
    if not token:
        log.error("環境変数 NOTIFY_TOKEN がありません")
        return 2

    started = time.monotonic()
    session = make_session()
    results = []
    failed = 0

    for page in range(1, PAGES + 1):
        if time.monotonic() - started > DEADLINE_SEC:
            log.error("締め切りを超えたので打ち切ります(%dページ目)", page)
            return 1
        try:
            r = session.get(BASE_URL, params={"page": page}, timeout=TIMEOUT)
            r.raise_for_status()
        except requests.RequestException as e:
            failed += 1
            log.warning("page=%d 取得失敗: %s", page, e)
            continue
        results.append(r.text)
        time.sleep(INTERVAL_SEC)

    log.info("取得 %d / 失敗 %d", len(results), failed)
    if not results:
        return 1
    # ここで解析・保存・通知を行う
    return 0


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

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

  • import:Retry は urllib3.util.retry から直接読み込みます。古いコードに多い requests.packages.urllib3 経由の書き方はやめておきます。
  • 冒頭の定数:間隔・タイムアウト・締め切りを1か所にまとめています。あとでAIに「間隔を5秒に」と頼むときも、ここだけ直せば済みます。
  • make_session():
    • allowed_methods は、リトライしてよいHTTPメソッドの指定です。旧 method_whitelist と同じ役割で、名前だけが変わりました。
    • status_forcelist に入れたステータスが返ったときに、再試行します。
    • backoff_factor を設定すると、再試行のたびに待ち時間が伸びます。
    • respect_retry_after_header=True にすると、相手から「何秒後に来て」と指示されたときにそれに従います。
    • raise_on_status=False は、再試行を使い切ったらレスポンスをそのまま返す設定です。判定は後段の raise_for_status() に任せます。
    • この Retry を HTTPAdapter に渡し、Session に mount します。これで初めて、その Session から送るリクエストにリトライが効きます。
  • timeout=(5, 30):前が接続までの上限、後ろが読み取りの上限(秒)です。1つの数値だけを渡すと、両方に同じ値が使われます。
  • 締め切りの確認:time.monotonic() で開始からの経過時間を測り、上限を超えたら打ち切ります。monotonic はPCの時計合わせの影響を受けない時計です。
  • 例外処理:通信系のエラーだけを requests.RequestException で受けます。何ページ目で何が起きたかをログに残し、次のページへ進みます。
  • 終了コード:取得が1件も無ければ1、キーが無ければ2を返します。sys.exit(main()) がこの値をOSに渡すので、cronやsystemdが「失敗」と判定できます。

timeout を付けても「必ず終わる」わけではない

timeout の読み取り秒は、データが届く間隔の上限です。応答全体にかかる時間の上限ではありません。相手が少しずつデータを返し続けると、30秒を超えても切れないことがあります。また、相手が長い Retry-After を返せば、その分だけ待ちます。

そこでこのコードは、締め切りを3段で重ねています。

  1. 1回のリクエストを timeout で切る
  2. スクリプト内の DEADLINE_SEC で打ち切る(判定はページの合間だけ)
  3. 外側(cronの timeout コマンドや systemd の TimeoutStartSec)で強制終了する

3段目はスクリプトの外にあるので、どんな固まり方をしても確実に止まります。置き場所は最後の節で扱います。

実行環境:Python 3.12〜3.14 と requests・urllib3 の確認

  • Python:2026年9月30日時点の最新版は 3.14.8・3.13.16・3.12.15 です。3.10はサポート終了(EOL)になったので、新しく環境を作るなら3.12以降にします。
  • パッケージ:pip install requests。urllib3 は requests の依存として一緒に入ります。
  • バージョン確認:python3 -V、pip show requests urllib3。
    • urllib3 が 2.x なら method_whitelist は使えません。
    • 逆に、かなり古い urllib3 が残っている環境では allowed_methods が通らないことがあります。その場合はバージョンを固定せず、requests ごと更新するのが手早い対処です。

AIに書かせるときは、最初の指示に「Python 3.12、urllib3 2.x」と書き添えます。調査結果でも、使用するライブラリとバージョンを指示に明記するのが有効とされています。そうすると、古い書き方が出る確率を下げられます。

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

リトライを入れると、障害中のサイトへ繰り返しアクセスするコードにもなりえます。次の点は、AIが書いたコードでも自分で確認してください。

  • 利用規約:自動取得を禁止しているサイトや、公式のAPI・RSSを用意しているサイトがあります。APIやRSSがあれば、そちらを使います。
  • robots.txt:https://example.com/robots.txt を開き、取得したいパスが Disallow になっていないかを見ます。Pythonでは標準ライブラリの urllib.robotparser で判定できます。
  • アクセス間隔:1リクエストごとに数秒空け、ページ数にも上限を設けます。上のコードの INTERVAL_SEC と PAGES がこれに当たります。
  • リトライは控えめに:回数を少なくし、待ち時間が伸びる設定にして、Retry-After に従います。429(リクエストが多すぎる)が返ったら、相手からの「来すぎ」の合図です。
  • User-Agent:何のツールかが分かる名前にします。

AIに「もっと速く」「ブロックされないように」と頼むと、並列化や制限を回避する書き方が返ってくることがあります。相手に負荷をかけ、規約違反にもなりうるので採用しないでください。

毎日決まった時刻に終わらせる置き場所:手元のPC・cron・systemd timer の TimeoutStartSec

3つの置き場所と、それぞれが止まる条件

置き場所

向いている条件

止まる条件

外側の締め切り

手元のPC

試運転。取りこぼしても困らない収集

スリープ中・電源オフ・ログアウト。指定時刻に閉じていれば実行されない

スケジューラ側の設定次第

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

数分で終わる処理を1日数回

サーバー会社が決めた実行時間や頻度の上限を超える。長時間動くプロセスを止められる場合がある(規約を確認)

timeout コマンド

VPSのsystemd timer

ブラウザを使う重い処理や、取りこぼしたくない処理

VPS自体の停止。契約・料金の未払い

TimeoutStartSec

手元のMacで動かす場合は、launchd定期実行はbootstrapで|2026年10月に登録手順をまとめています。

共有サーバーのcronで動かす

0 7 * * * cd /home/username/jobs && NOTIFY_TOKEN=$(cat /home/username/.notify_token) timeout 20m .venv/bin/python collect.py >> collect.log 2>&1
  • timeout 20m は、20分経っても終わらなければ強制終了するコマンドです。スクリプト内の締め切り(15分)より少し長くしておくと、通常はスクリプト側が先に打ち切ってログを残します。
  • トークンはファイルに置いて読み込みます。ファイルは chmod 600 で自分だけが読めるようにします。
  • 出力をログファイルに追記しておけば、止まった日の原因を後から追えます。

VPSのsystemd timerで動かす(TimeoutStartSec)

# /home/username/.config/systemd/user/jobs.service
[Service]
Type=oneshot
WorkingDirectory=/home/username/jobs
EnvironmentFile=/home/username/.config/jobs.env
ExecStart=/home/username/jobs/.venv/bin/python collect.py
TimeoutStartSec=20min

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

[Install]
WantedBy=timers.target
  • Type=oneshot は「起動して、終わったら完了」という種類のサービスです。この種類では、systemdの既定で開始の時間制限が効かないことがあります。そのため TimeoutStartSec=20min を明示し、20分を超えたら強制終了させます。
  • EnvironmentFile に NOTIFY_TOKEN=... を書いておけば、コードに直書きせずにキーを渡せます。このファイルも chmod 600 にします。
  • Persistent=true を付けると、サーバーが止まっていた間の回を起動後に実行します。
  • 有効化は systemctl --user enable --now jobs.timer です。結果は journalctl --user -u jobs.service で確認できます。スクリプトが終了コード1を返した日は、ここに失敗として残ります。
  • ログアウト後もユーザーのタイマーを動かし続けるには、loginctl enable-linger username が必要です。

Playwrightのようにブラウザを起動する収集を同じ形で動かす方法は、Playwright定期実行をsystemdで 2026年10月で扱っています。

新しい「定期実行するAI」との関係

2026年9月から10月にかけて、AI側にも定期実行の機能が出てきました。

  • GitHub Copilot in VS Code:タスクを時間・日・週単位でスケジュールする自動化機能(プレビュー)
  • OpenAIの「dots」:専用のクラウドコンピューターで24時間動く常時稼働エージェント
  • Microsoft Copilotの「Autopilot」:作業の定期実行などを自動で行うエージェント

ただし、どこで動かすにしても、timeout の無いリクエストが固まる問題は残ります。失敗が黙って捨てられる問題も同じです。置き場所を変える前に、スクリプト自体が「必ず終わり、失敗を終了コードで伝える」形になっているかを確かめてください。

関連する選択肢

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

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

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

method_whitelist と timeout を直して毎日動かすための要点

生成AIが書いた収集スクリプトは、urllib3 2.x で削除された method_whitelist で落ちることがあります。timeout の無い requests.get() で固まることもあります。直させるときは「無人で毎日動かす」前提を伝え、次を列挙して指示します。allowed_methods+HTTPAdapter を Session にマウントすること、timeout=(接続, 読み取り) を付けること、全体の締め切りを設けること、except: pass をやめること、キーを環境変数から読むこと、アクセス間隔を空けること。返ってきたコードは、5つの語を検索して自分で確認します。最後に外側の締め切りを置けば、毎日決まった時刻に終わるようになります。共有サーバーなら timeout コマンド、VPSなら systemd の TimeoutStartSec です。手元のPCは、スリープ中は動かない前提で使ってください。