ChatGPTやClaude Codeに求人の新着を集めるSeleniumスクリプトを書かせたのに、VPSのcronで動かすとChromeが起動しない。原因を探すと、AIのコードに options.headless = True と webdriver_manager の ChromeDriverManager().install() が入っている。どちらも今のSeleniumでは要らない、あるいは効かない書き方です。この記事では、AIに --headless=new と Selenium Manager 前提で書き直させる指示文を実物で載せます。そのうえで、Chromeやドライバが更新されてもcronが止まらない運用まで説明します。

AIが今も出してくる古いSeleniumの2行:options.headless と ChromeDriverManager

「求人サイトの新着をSeleniumで集めるPythonを書いて」とだけ頼むと、次のようなコードが返ってくることがあります。2026年9月22日のClaude Opus 5.5や9月29日のGPT-6.1 Solのように新しいモデルでも、こちらが使っているSeleniumのバージョンを伝えなければ、学習データに多い古い書き方が混ざる余地は残ります。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

options = Options()
options.headless = True   # ← 古い書き方
driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install()),   # ← もう要らない
    options=options,
)
driver.get("https://example.com/jobs")
for el in driver.find_elements("css selector", ".job-title"):
    print(el.text)
driver.quit()

options.headless = True は Selenium 4.13 で削除された

headless というプロパティ(設定用の窓口)は、Selenium 4.13で削除されました。怖いのは、Pythonではこの代入がエラーにならない場合があることです。存在しない名前に代入しても、ただの変数として受け付けられてしまいます。つまりヘッドレス(画面を出さない実行)の指定が効かないまま、処理が先へ進みます。

手元のPCでは、ブラウザの画面が開くだけで動いてしまいます。そのため問題に気づけません。ところが画面の無いVPSでは、Chromeを起動する段階で落ちます。

今の書き方は、起動オプションとして --headless=new を渡す形です。

webdriver_manager は Selenium Manager(4.6〜)で不要になった

ChromeDriver(SeleniumがChromeを操作するための仲介プログラム)は、Chromeのバージョンに合ったものが要ります。以前は外部ライブラリの webdriver_manager で取ってくるのが定番でした。

Selenium 4.6からは、Selenium ManagerがSelenium本体に入りました。webdriver.Chrome(options=options) と書くだけで、ドライバを自動で用意してくれます。

webdriver_manager を残しておくと、依存するライブラリが1つ増えます。さらに、そのライブラリのドライバ探しが失敗したときにも止まる原因を1つ抱えることになります。

項目

以前の書き方(AIが出しがち)

今の書き方

ヘッドレス

options.headless = True

options.add_argument("--headless=new")

ドライバの用意

Service(ChromeDriverManager().install())

書かない(Selenium Manager が用意する)

追加パッケージ

pip install selenium webdriver-manager

pip install selenium のみ

要素の指定

"css selector" の文字列直書き

By.CSS_SELECTOR

AIに --headless=new と Selenium Manager 前提で書き直させる指示文

Googleが2026年9月26日に公開したベンチマーク「Android Bench 2.0」では、数日〜1週間かかるような複雑な長期タスクで、AIの最高通過率が約91%から約28%に下がりました。AIは新しく書くのは得意でも、既存コードの改修は苦手ということです。

「直して」とだけ頼むのではなく、何をどう直すかを指示文に書き込むのが確実です。

ステップ1:最初の指示(ここでバージョンと条件を書く)

PythonとSeleniumで、求人一覧ページ https://example.com/jobs?sort=new から
新着の案件タイトルとURLを集めるスクリプトを書いてください。

前提:
- 実行環境は画面のないLinuxのVPS。cronで1日1回動かす
- selenium は 4.13 以上を使う(手元の pip show selenium の結果: Version: 4.xx)
- webdriver_manager は使わない。ドライバは Selenium Manager に任せる
- ヘッドレスは options.add_argument("--headless=new") で指定する
- 要素は By.CSS_SELECTOR で指定し、WebDriverWait で表示を待つ
- 一覧のカードは .job-card、タイトルのリンクは a.job-title
- 前回取得済みのURLは seen.json に保存し、新着だけを出力する

ポイントは、pip show selenium の実際の結果をそのまま貼ることです。バージョンを推測させないようにします。「4.xx」の部分には、自分の環境の値を入れてください。

ステップ2:出てきたコードで見つかった問題

最初の指示だけでは、次のような抜けが残ることがあります。

  • ページを連続で開くのに待ち時間が無い
  • 例外を except Exception: pass で握りつぶしている
  • 0件でも正常終了する
  • 失敗を知らせるWebhookのURLをコードに直書きしている

どれも手元で1回動かしただけでは表に出ません。毎日動かし始めてから効いてきます。

ステップ3:直させる指示

次の点を直してください。直した箇所ごとに、何をなぜ変えたか一言添えてください。

1. ページを開く間に5秒以上の待機を入れる。並列実行はしない
2. 最初に robots.txt を urllib.robotparser で確認し、禁止されていれば取得せず終了する
3. except Exception: pass をやめる。失敗したらログに例外を残し、終了コード1で終わる
4. 取得件数が0件のときも異常として終了コード1にする(セレクタ変更の検知のため)
5. 通知先URLはコードに書かず、環境変数 NOTIFY_WEBHOOK_URL から読む。未設定なら通知しない
6. 起動時に、ChromeとChromeDriverのバージョンをログに出す
7. driver.quit() は finally で必ず呼ぶ

ステップ4:直った後のコード

実行環境は、画面の無いLinux(Ubuntu系のVPSを想定)にGoogle Chromeを入れたものです。Pythonは python3 -V で確かめたバージョンを、そのままAIへの指示にも書いてください。

パッケージは pip install "selenium>=4.13" だけです。下限を付けるのは、このコードが options.headless の無いSeleniumを前提にしているからです。

import json
import logging
import os
import sys
import time
import urllib.request
from pathlib import Path
from urllib import robotparser

from selenium import webdriver
from selenium.common.exceptions import TimeoutException, WebDriverException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

BASE = "https://example.com"
PAGES = [BASE + "/jobs?sort=new", BASE + "/jobs?sort=new&page=2"]
WAIT_BETWEEN_PAGES = 5          # 秒。相手サーバーへの配慮
SEEN_FILE = Path(__file__).with_name("seen.json")

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


def allowed_by_robots(url):
    rp = robotparser.RobotFileParser(BASE + "/robots.txt")
    rp.read()
    return rp.can_fetch("*", url)


def make_driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--window-size=1280,2000")
    return webdriver.Chrome(options=options)   # ドライバは Selenium Manager が用意する


def fetch_page(driver, url):
    driver.get(url)
    WebDriverWait(driver, 20).until(
        EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".job-card"))
    )
    rows = []
    for card in driver.find_elements(By.CSS_SELECTOR, ".job-card"):
        link = card.find_element(By.CSS_SELECTOR, "a.job-title")
        rows.append({"title": link.text.strip(), "url": link.get_attribute("href")})
    return rows


def notify(text):
    url = os.environ.get("NOTIFY_WEBHOOK_URL")
    if not url:
        return
    body = json.dumps({"text": text}).encode()
    req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req, timeout=10)


def main():
    if not all(allowed_by_robots(u) for u in PAGES):
        logging.error("robots.txt で禁止されているため取得しない")
        return 1

    seen = set(json.loads(SEEN_FILE.read_text())) if SEEN_FILE.exists() else set()
    rows = []
    driver = None
    try:
        driver = make_driver()
        caps = driver.capabilities
        logging.info("Chrome %s / ChromeDriver %s",
                     caps.get("browserVersion"),
                     caps.get("chrome", {}).get("chromedriverVersion", "?").split(" ")[0])
        for i, url in enumerate(PAGES):
            if i:
                time.sleep(WAIT_BETWEEN_PAGES)
            rows += fetch_page(driver, url)
    except (TimeoutException, WebDriverException):
        logging.exception("取得に失敗")
        notify("求人収集: 取得に失敗しました。ログを確認してください")
        return 1
    finally:
        if driver:
            driver.quit()

    if not rows:
        logging.error("0件。ページ構造が変わった可能性がある")
        notify("求人収集: 0件でした。セレクタを確認してください")
        return 1

    new = [r for r in rows if r["url"] not in seen]
    for r in new:
        print(f'{r["title"]}\t{r["url"]}')
    SEEN_FILE.write_text(json.dumps(sorted(seen | {r["url"] for r in rows}), ensure_ascii=False))
    logging.info("取得 %d 件 / 新着 %d 件", len(rows), len(new))
    return 0


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

ブロックごとに何をしているかを説明します。

  • allowed_by_robots():取得先の robots.txt を読み、巡回が許されているURLかを確かめます。禁止なら1ページも開かずに終わります。
  • make_driver():今の書き方の中心です。
    • --headless=new で画面を出さずに起動します。
    • webdriver.Chrome(options=options) には service= を渡しません。こうするとSelenium Managerがドライバを用意します。
    • --disable-dev-shm-usage は、共有メモリ領域が小さい環境でChromeが落ちにくくするための起動オプションです。
  • fetch_page():WebDriverWait でカードが表示されるまで最大20秒待ちます。そのうえでタイトルとURLを集めます。決め打ちの time.sleep で待つより、表示の遅い日にも強くなります。
  • バージョンのログ:driver.capabilities から、実際に使われたChromeとChromeDriverのバージョンを毎回記録します。ある日から止まったとき、ブラウザ側が更新されたのかをログだけで判別できます。
  • 例外と0件の扱い:失敗は終了コード1で返し、通知します。取得件数が0件のときも同じ扱いです。セレクタ(要素の住所)がずれると、エラーではなく「0件」として現れるためです。
  • notify():通知先は環境変数から読みます。コードを人に渡したりAIに貼ったりしても、URLが漏れません。

一覧が画面の描画前のデータ(JSON)で届いているサイトなら、Seleniumを使わずに済むこともあります。その見分け方は __NEXT_DATA__で0件解決|AIコード2026年9月版 にまとめています。

AIが出したSeleniumコードで自分で確認する4箇所と取得先への配慮

コードを読み慣れていなくても、次の語で検索(Ctrl+F)すれば危うい箇所は見つかります。

確認する箇所

検索する語

危ない理由

AIへの直させ方

古いAPI

headless = / webdriver_manager / find_element_by_

今のSeleniumで効かない、または不要

「selenium 4.13以上前提。--headless=new と Selenium Manager で書き直して」

アクセス間隔が無い

sleep が1つも無い、ThreadPool

相手サーバーに負荷をかけ、遮断の原因にもなる

「ページ間に5秒以上待機。並列にしない」

例外で黙って止まる

except Exception: pass / except: pass

失敗しても正常終了に見え、何日も気づけない

「失敗はログに残して終了コード1で終える」

APIキーの直書き

https://hooks. / api_key = / token =

コードを共有・AIに貼った時点で漏れる

「環境変数から読む。未設定なら処理を飛ばす」

IPA「情報セキュリティ10大脅威 2026」には、「AIの利用をめぐるサイバーリスク」が初めて入りました。APIキーやWebhookのURLを入れたままのコードをAIのチャットに貼るのも、同じリスクの一種です。直させる前に、秘密の値は XXXX に置き換えておきましょう。

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

  • robots.txt:上のコードのように、取得前に機械的に確認します。禁止されていれば取得しません。
  • 利用規約:求人サイトには「自動取得の禁止」や「掲載情報の転載禁止」を定めているところがあります。これはコードでは判定できないので、動かす前に自分で読んでください。公式APIやRSSが用意されていれば、そちらを優先します。
  • アクセス間隔:新着を見るだけなら、1日1〜2回・数ページで足ります。ページ間には待機を入れ、並列化はしません。
  • 書かないこと:ログインや画像認証、アクセス制限をすり抜ける指示はAIに出さないでください。制限がかかったら、それは「その取得方法は望まれていない」という相手の意思表示です。

VPSのcronで毎日動かす:Chrome・ChromeDriverの更新で止めない設定

VPSでよくある止まり方は、次の流れです。OSの自動更新でChromeの新しい版が入る。ところがドライバは古いまま残り、起動時に食い違って落ちる。

webdriver_manager やドライバの手置きをやめてSelenium Managerに任せると、実行のたびに、入っているChromeに合うドライバを探して用意する形になります。ただし、それが働くには次の条件が揃っている必要があります。

  • 外への通信ができること:ドライバを取りに行くためです。社内プロキシの内側などでは失敗します。
  • 実行ユーザーのホームディレクトリに書き込めること:取得したドライバはキャッシュとして保存されます。保存場所はSelenium公式ドキュメントの「Selenium Manager」の項で確認してください。
  • Selenium本体が古すぎないこと:Chromeだけが新しくなり続けると、いずれ組み合わせが合わなくなる可能性があります。月に1回程度、別の仮想環境で pip install -U selenium を試し、動いたら本番を上げます。

セットアップ手順

  1. rootではなく一般ユーザー(例:username)で作業します。Chromeをrootで動かすと、サンドボックス(安全のための隔離)を切る設定が必要になります。
  2. Google Chromeを、Google公式の配布パッケージから入れます。
  3. 仮想環境を作り、パッケージを入れて、その結果を固定します。
    cd /home/username/jobwatch
    python3 -m venv .venv
    .venv/bin/pip install "selenium>=4.13"
    .venv/bin/pip freeze > requirements.txt
    .venv/bin/pip show selenium        # 実際に入ったバージョンを確認
  4. 手で1回実行し、ログにChromeとChromeDriverのバージョンが出ることを確かめます。

crontab の書き方

PATH=/usr/local/bin:/usr/bin:/bin
NOTIFY_WEBHOOK_URL=(自分の通知先。ここはcrontabにだけ書く)
15 7 * * * cd /home/username/jobwatch && /usr/bin/flock -n /tmp/jobwatch.lock .venv/bin/python collect_jobs.py >> logs/run.log 2>&1
  • PATH を書く:cronは最低限の環境変数で動きます。手で打つと動くのにcronだと動かない、という典型的な原因はこれです。
  • venvのpythonを直接指定する:source activate に頼りません。
  • flock -n:前回の実行が長引いていたら、今回は起動しません。Chromeが2つ同時に立ち上がると、メモリの小さいVPSでは両方とも落ちます。
  • >> logs/run.log 2>&1:標準出力とエラーの両方を残します。先に mkdir logs しておいてください。

Chromeの自動更新を止めるかどうか

sudo apt-mark hold google-chrome-stable で、Chromeの更新を止めることはできます。ただしセキュリティ修正も入らなくなります。

おすすめは、更新は止めずに次の2つを組み合わせる形です。

  • ログのバージョン行を見て、Chromeが上がった日に失敗していないかを確かめる
  • 失敗したら通知で気づく

更新を止めるのは、どうしても合わない版が出たときの一時的な手段に留めます。

書いたスクリプトをどこで動かし続けるか:手元PC・共有サーバーのcron・VPSが止まる条件

置き場所

向いている条件

止まる条件

手元のPC

試作段階、週に数回で足りる

スリープ・電源オフ・蓋を閉じる・OS更新の再起動。実行時刻にPCが起きていなければ、その日は飛ぶ

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

Seleniumを使わず、requests等で取れるサイト

Chromeを入れられないことが多く、Seleniumは基本的に動かない。実行時間やプロセス数に制限がある場合は途中で切られる(契約プランの仕様で確認)

VPS

Chromeが必要、毎日決まった時刻に動かしたい

メモリ不足でChromeが落ちる、Chrome更新とSeleniumの組み合わせずれ、外部通信の遮断、取得先のページ構造変更、契約の更新忘れ

判断の目安は次のとおりです。

  • Seleniumが必要(ページがJavaScriptで組み立てられる)なら、VPSです。
  • HTMLに最初から中身が入っているなら、requestsとBeautifulSoupに書き直せます。そうすれば共有サーバーのcronでも足ります。
  • どちらか迷ったら、次のように確かめてからAIに書き直させます。
    • ブラウザで「ページのソースを表示」する
    • 欲しい文字列が入っているかを確認する

Seleniumで価格の監視を組んだ例は、Selenium 4で価格監視をAIに書かせる2026年9月版 にあります。

2026年9月には、クラウドで常時動く仕組みの発表も相次ぎました。OpenAIの「Codex」のクラウド実行や常時稼働型エージェント「dots」(9月29日のDevDay 2026)、Microsoftの「Autopilot」(9月25日発表・限定プレビュー)です。

ただし、決まった時刻に決まったページを数枚見るだけの処理なら、VPSとcronで十分です。しかも、どこで何が起きたかをログで追える形が一番確実です。

関連する選択肢

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

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

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

Selenium Manager前提に直したら、毎日動かす前に確かめる3つのこと

AIが出す options.headless = True は、Selenium 4.13で削除された書き方です。エラーにならないまま効かないことがあり、画面の無いVPSで初めて落ちます。webdriver_manager も、Selenium 4.6からのSelenium Managerがあれば不要です。

AIには「pip show selenium の実際の結果」と「--headless=new・Selenium Manager・By.CSS_SELECTOR・WebDriverWait」を最初の指示に書きます。出てきたコードは、次の4点を検索して確かめてください。

  • 待機が無い
  • except: pass で握りつぶしている
  • 0件でも正常終了する
  • キーを直書きしている

見つかったら、番号付きで直させます。

毎日動かすなら、Chromeが要る時点で置き場所はVPSです。cronには次の4つを入れます。

  • PATH
  • venvのpythonのフルパス
  • flock
  • ログの出力先

スクリプト側では、ChromeとChromeDriverのバージョンを毎回ログに残し、失敗と0件を通知します。これでChromeの更新やサイトの改修があっても、止まった日のうちに気づけます。