ChatGPTに書かせた求人の新着収集スクリプトが find_element_by_xpath で止まり、どこを直せばいいか分からない。そう困っている人に向けた記事です。この書き方は Selenium 4.3 で削除されたので、AIに「直して」とだけ頼んでも同じ失敗を繰り返しがちです。この記事では、Claude Code に find_element(By.CSS_SELECTOR) と WebDriverWait を使う今の Selenium 4 の書き方へ直させる指示文を実物で示します。直ったコードの読み方と、手元PC・cron・VPSのどこで毎日動かすかまで扱います。

ChatGPTが今も出す find_element_by_xpath:Selenium 4.3で削除された書き方の見分け方

AIに最初に頼んだ指示と、よく返ってくるコード

コードを書き慣れていない人が最初に出す指示は、たいてい次のような一文です。

求人サイト https://example.com/jobs の新着をSeleniumで取ってCSVに保存するPythonを書いて。
結果は通知先にも送りたい。

これに対して生成AIは、次のような形のコードをよく返します。AIは学習時点の古い記事を大量に読んでいるため、すでに削除された書き方を今でも出します。

from selenium import webdriver
import time

NOTIFY_URL = "https://example.com/hook/XXXXXXXX"   # (4) 通知先を直書き

driver = webdriver.Chrome(executable_path="/usr/local/bin/chromedriver")  # (1)
driver.get("https://example.com/jobs")
time.sleep(10)                                       # (2) 表示を勘で待つ

jobs = driver.find_elements_by_xpath("/html/body/div[2]/main/div/ul/li")  # (1)
for job in jobs:
    try:
        title = job.find_element_by_xpath(".//h3").text
        url = job.find_element_by_xpath(".//a").get_attribute("href")
        with open("jobs.csv", "a") as f:
            f.write(f"{title},{url}\n")
    except:
        pass                                         # (3) 失敗を黙って捨てる

今の Selenium 4 でこれを実行すると、最初の find_elements_by_xpath の行で次のエラーになり、1件も取れません。

AttributeError: 'WebDriver' object has no attribute 'find_elements_by_xpath'

このエラーは「書き方が古い」というサインです。書き直しが必要な理由を示すためにここで1回だけ載せておきます。

AIが出したコードで自分で確認する4か所

Pythonを読み慣れていなくても、次の語をファイル内で検索すれば危うい箇所は見つかります。エディタの検索機能か、次のコマンドを使ってください。

grep -nE "_by_|executable_path|except:|sleep|https://" collect.py

見る箇所

検索する語

何が危ういか

今の書き方

(1) 古いAPI

_by_、executable_path

find_element_by_* は Selenium 4.3 で削除済みなので、その行で止まる。ドライバのパス指定も Selenium 4.6 以降は不要

find_element(By.CSS_SELECTOR, ...)。ドライバは Selenium Manager に任せる

(2) アクセス間隔・待機

sleep

表示待ちを time.sleep(10) の勘で済ませると、遅い日は空振りし、速い日は無駄に待つ。ページ間の間隔はそもそも書かれていないことが多い

表示待ちは WebDriverWait で行う。ページ間には間隔を明示する

(3) 例外で黙って止まる

except:

サイトの構造が変わって全件失敗しても、エラーを出さずに「0件」で正常終了する。毎日動かしていても止まったことに気づけない

予期しない例外はログに残し、終了コード1で終える

(4) キーの直書き

https://、KEY、TOKEN

通知先URLやAPIキーがコード内にあると、共有したりAIに貼ったりした時点で外に出る

環境変数から os.environ.get() で読む

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

以前(AIがよく出す)

今(Selenium 4)

driver.find_element_by_xpath("...")

driver.find_element(By.XPATH, "...") または By.CSS_SELECTOR

driver.find_elements_by_class_name("job")

driver.find_elements(By.CLASS_NAME, "job")

webdriver.Chrome(executable_path=...)

webdriver.Chrome(options=opts)(Selenium Manager がドライバを自動で用意する。4.6以降)

time.sleep(10) で表示を待つ

WebDriverWait(driver, 15).until(EC.presence_of_all_elements_located(...))

--headless

--headless=new(Chromeの新しいヘッドレスモード)

/html/body/div[2]/main/... のような絶対XPath(ページの先頭から階層をたどる指定)は、レイアウトが1段変わるだけで外れます。div.job-card のようにクラス名で指す CSSセレクタ(要素の種類と名前で場所を指す書き方)のほうが変更に強く、読んでも意味が分かります。

Claude Codeへの指示文:find_element(By.CSS_SELECTOR)とWebDriverWaitに直させる

「直して」だけでは同じ古いコードが戻ってくる

エラーを貼って「直して」とだけ頼むと、AIは find_element_by_xpath を find_element(By.XPATH, ...) に置き換えるだけで終わることがあります。そうなると、(2)〜(4) の問題はそのまま残ります。また、AIは取得先サイトのHTMLを知りません。要素のクラス名は自分でブラウザの開発者ツール(右クリック→「検証」)で確かめて伝える必要があります。

実際に使う指示文

Claude Code ならファイルを読み、コマンドを実行して確かめるところまで任せられます。次の文をそのまま使い、括弧の中だけ自分の値に置き換えてください。

collect.py を実行すると次のエラーで止まります。
AttributeError: 'WebDriver' object has no attribute 'find_elements_by_xpath'
pip show selenium を実行して、表示された Version を確認してから直してください。

次の条件で書き直してください。
1. find_element_by_* は使わず、find_element(By.CSS_SELECTOR, ...) を使う。
   /html/body/... のような絶対XPathは使わない
2. 一覧の1件は <div class="job-card">、タイトルのリンクは <a class="job-title">
   (ブラウザの開発者ツールで確認した値)
3. time.sleep で表示を待たず、WebDriverWait と expected_conditions で要素の出現を待つ
4. ドライバのパスは指定しない(Selenium Manager に任せる)。ヘッドレスは --headless=new
5. ページを開く前に robots.txt を確認し、禁止なら開かない。複数ページの間は5秒以上空ける
6. except: pass は使わない。予期しない例外はスタックトレースごとログに残し、終了コード1で終える
7. 通知先URLはコードに書かず、環境変数 NOTIFY_WEBHOOK_URL から読む。requests には timeout を付ける
8. 既に見たURLは seen.json に記録し、新着だけを CSV に追記する
9. cron / systemd timer から実行するので、ファイルの場所はスクリプトのある場所を基準にする

直したら一度実行し、エラーが出ないことを確かめてから結果を見せてください。

ポイントは、「直す」ではなく「どう直すか」を条件として列挙することです。もう1つは、「実行して確かめる」まで指示することです。AIの出力は下書きなので、最後に動かして確かめるのは人間の役目です。この前提は外さないでください。

実行環境

  • Python 3.12 以降。Python 3.10 は 2026年10月2日でサポートが終わりました。2026年9月30日には 3.14.8・3.13.16・3.12.15 がセキュリティ修正版として出ています(SSLContext の欠陥 CVE-2026-19445 が Critical と評価されたもの)。python3 -V で確認し、各系列の最新版を使ってください
  • Google Chrome または Chromium が入っていること(Selenium はブラウザを実際に動かします)
  • パッケージ:pip install "selenium>=4.6,<5" requests
    • 下限の 4.6:ドライバを自動で用意する Selenium Manager がこの版から使えるため
    • 上限の <5:Selenium 5.0 では CDP(Chrome DevTools Protocol)ベースの API が削除される予定と報じられています。毎日動かすスクリプトが知らないうちにメジャー更新で壊れないよう、上限を付けておきます
  • 入った版は pip show selenium で確認します。ここで具体的な最新版の番号は書きません。自分の環境で確かめてください

書き直した求人新着収集スクリプト(Selenium 4.6以降)をブロックごとに読む

指示どおりに直ると、次のような形になります。取得先は example.com、セレクタは例です。

設定とログの準備

import csv
import json
import logging
import os
import sys
import time
from pathlib import Path
from urllib import robotparser

import requests
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
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"
LIST_URLS = [f"{BASE}/jobs?page={n}" for n in (1, 2)]
WAIT_SEC = 5  # ページとページの間に空ける秒数

DATA_DIR = Path(__file__).resolve().parent
SEEN_FILE = DATA_DIR / "seen.json"
OUT_CSV = DATA_DIR / "new_jobs.csv"

logging.basicConfig(
    filename=DATA_DIR / "collect.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)

取得先・ページ数・間隔をファイルの先頭にまとめています。DATA_DIR は「このスクリプトが置いてあるフォルダ」です。cron はホームディレクトリなど別の場所から起動するので、"jobs.csv" のような相対パスのままだと、予想外の場所にファイルができたり既読記録を見失ったりします。ログは collect.log に日時つきで残ります。

robots.txt の確認とブラウザの起動

def load_robots():
    res = requests.get(f"{BASE}/robots.txt", timeout=10)
    rp = robotparser.RobotFileParser()
    rp.parse(res.text.splitlines())
    return rp


def make_driver():
    opts = Options()
    opts.add_argument("--headless=new")
    opts.add_argument("--window-size=1280,2000")
    return webdriver.Chrome(options=opts)

load_robots() は取得先の robots.txt(サイトが「どこを機械的に取得してよいか」を示すファイル)を読みます。timeout=10 は、相手が応答しないときに10秒で諦めるための指定です。これが無いとスクリプトが永遠に待ち続け、翌日の実行とも重なります(詳しくは requests timeout忘れ|AIコード2026年10月)。make_driver() には executable_path がありません。ドライバの用意は Selenium Manager に任せています。

一覧ページから求人を取り出す

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

ここが find_element_by_xpath を置き換えた本体です。WebDriverWait(driver, 15).until(...) は「div.job-card が1つ以上現れるまで最大15秒待つ」という意味です。表示が速ければすぐ次へ進み、15秒経っても出なければ TimeoutException を出して止まります。

各カードの中では find_element(By.CSS_SELECTOR, "a.job-title") でリンクを探します。見つからなければ NoSuchElementException が出ます。ここでわざと例外を握りつぶしていません。サイトの構造が変わったときに、「0件で正常終了」ではなく「失敗」として表に出すためです。

新着だけ保存して通知し、失敗は終了コード1で返す

def notify(text):
    hook = os.environ.get("NOTIFY_WEBHOOK_URL")
    if not hook:
        logging.warning("NOTIFY_WEBHOOK_URL が未設定のため通知を省略")
        return
    requests.post(hook, json={"text": text}, timeout=10)


def main():
    seen = set(json.loads(SEEN_FILE.read_text())) if SEEN_FILE.exists() else set()
    rp = load_robots()
    driver = make_driver()
    new_jobs = []
    try:
        for i, url in enumerate(LIST_URLS):
            if not rp.can_fetch("*", url):
                logging.warning("robots.txt で禁止されているため取得しない: %s", url)
                continue
            if i > 0:
                time.sleep(WAIT_SEC)
            for job in collect(driver, url):
                if job["url"] not in seen:
                    new_jobs.append(job)
                    seen.add(job["url"])
    finally:
        driver.quit()

    if new_jobs:
        is_first = not OUT_CSV.exists()
        with OUT_CSV.open("a", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=["title", "url"])
            if is_first:
                writer.writeheader()
            writer.writerows(new_jobs)
        notify(f"求人の新着 {len(new_jobs)} 件")

    SEEN_FILE.write_text(json.dumps(sorted(seen), ensure_ascii=False))
    logging.info("新着 %d 件", len(new_jobs))


if __name__ == "__main__":
    try:
        main()
    except Exception:
        logging.exception("収集に失敗")
        notify("求人の収集が失敗しました。collect.log を確認してください")
        sys.exit(1)
  • 通知先は環境変数から読む:コードを人に見せても、AIに貼っても、通知先URLは漏れません
  • finally: driver.quit():途中で失敗しても Chrome を必ず閉じます。これが無いと、失敗のたびにブラウザが裏に残ってメモリを食いつぶし、数日後に何も動かなくなります
  • 既読の記録は最後に書く:途中で失敗した日は seen.json を更新しません。翌日の実行で同じ求人をもう一度「新着」として拾い直せます
  • 最後の except Exception:AIのコードにあった except: pass とは役割が逆です。握りつぶさずに、logging.exception() でスタックトレース(どの行で何が起きたかの記録)を残します。そのうえで通知し、sys.exit(1) で「失敗した」と実行元に伝えます。cron や systemd はこの終了コードで成否を判断します

robots.txt・利用規約・5秒間隔:取得先に負荷をかけない設定

  • robots.txt:上のコードは、禁止されたURLを開かずにログへ残します。AIのコードには、この確認がまず入っていません
  • 利用規約:機械的な取得を禁じているサイトでは使わないでください。公式のAPIや、新着をメールで受け取る機能があるなら、そちらを優先します
  • アクセス間隔と回数:開くのは一覧の数ページだけにし、ページ間に5秒空けます。実行は1日1回で十分です。求人の詳細ページを全件開くような書き方は、相手のサーバーの負担が大きく増えるので避けます
  • 集めるデータを絞る:保存するのはタイトルとURLだけです。担当者名などの個人に関わる情報は取りません
  • ログインや制限を回避しない:ブロックされたら、それが相手の答えです。AIに「ブロックされないようにして」と頼むと回避の手口を書いてくることがあります。そういうコードは使わないでください

手元PC・共有サーバーのcron・VPSのsystemd timer:毎日止めずに動かす置き場所

置き場所ごとの「止まる条件」

置き場所

向いている条件

止まる条件

手元のPC

まず動作を確かめる段階。毎日決まった時刻にPCを開いている人

スリープ中・電源オフ・フタを閉じた状態では動かない。OSの更新による再起動でも止まる

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

ブラウザを使わず requests だけで取れるサイト

Chrome を入れられないことが多く、Selenium のスクリプトはそもそも起動できない。長時間・高負荷のプロセスはサーバー側で打ち切られることがある。which google-chrome chromium で有無を確認し、規約で常駐や重い処理の扱いを確かめる

VPS(systemd timer)

Selenium のようにブラウザを動かす処理を毎日回す人

メモリ不足(free -h で確認)。Chrome の更新に追従するドライバの取得にネットワークが要るので、外に出られないと失敗しうる。月額料金を止めれば当然止まる

今回のようにブラウザを動かすスクリプトなら、現実的な置き場所は手元PCで確かめてからVPSです。

VPSでの設定(systemd timer)

通知先URLは /home/username/jobcheck/.env に NOTIFY_WEBHOOK_URL=... と1行で書き、chmod 600 .env で自分だけが読めるようにします。続けて次の2ファイルを ~/.config/systemd/user/ に置きます。

# collect.service
[Unit]
Description=求人新着の収集

[Service]
Type=oneshot
WorkingDirectory=/home/username/jobcheck
EnvironmentFile=/home/username/jobcheck/.env
ExecStart=/home/username/jobcheck/.venv/bin/python collect.py

# collect.timer
[Unit]
Description=求人新着の収集を毎朝7時に実行

[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true

[Install]
WantedBy=timers.target
systemctl --user daemon-reload
systemctl --user enable --now collect.timer
loginctl enable-linger username      # ログアウト後もタイマーを動かす
systemctl --user list-timers         # 次回の実行予定を確認
journalctl --user -u collect.service # 実行結果を確認
  • Persistent=true:サーバーが止まっていた時間の分を、起動時に1回実行します
  • loginctl enable-linger:これが無いと、ログアウトした時点でユーザーのタイマーが止まります
  • スクリプトが sys.exit(1) で終わると、systemctl --user status collect.service に failed と出ます。通知と合わせて二重に気づけます

共有サーバーやcronを使う場合

0 7 * * * cd /home/username/jobcheck && . ./.env && .venv/bin/python collect.py >> cron.log 2>&1

cron はログイン時と違って環境変数も PATH もほぼ空の状態で動きます。そのため、Python は .venv/bin/python のように仮想環境の中を指定します。.env は NOTIFY_WEBHOOK_URL=... の形で書いておき、実行のたびに読み込みます。標準出力とエラーは cron.log に残します。

毎日動かすのは「AI」ではなく「直したスクリプト」

Claude Code には 2026年9月の v2.1.257〜v2.1.263 で、無人実行向けの --permission-prompts none が加わったと報じられています。cron から AI そのものを毎日起動する使い方もできるようになりました。ただ、求人の新着収集は毎日同じ処理です。AIは書き直しと点検に使い、毎日動かすのは確かめ済みの Python スクリプトにするほうが、結果が日によってぶれず、止まった原因もログで追えます。

関連する選択肢

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

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

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

find_element_by_xpathからWebDriverWait・systemd timerまでの要点

AttributeError: 'WebDriver' object has no attribute 'find_element_by_xpath' は、AIが Selenium 4.3 で削除された書き方を出したというサインです。直すときは「直して」で済ませず、条件を列挙して指示します。主な条件は、find_element(By.CSS_SELECTOR) を使うこと、WebDriverWait で待つこと、ドライバ指定をやめること、robots.txt を確認して5秒間隔を空けること、except: pass の禁止、通知先を環境変数から読むことです。セレクタは自分で開発者ツールで確かめて伝え、「実行して確かめる」まで頼みます。AIのコードは _by_・except:・sleep・https:// を検索すれば自分でも点検できます。毎日動かすなら、ブラウザを使う処理は共有サーバーでは動かないことが多いので、手元で確かめてからVPSの systemd timer(Persistent=true と linger)に置きます。失敗は終了コード1と通知で気づける形にしておきます。