Seleniumで書いたスクレイピングが、ある朝から急に動かなくなった経験はありませんか。原因は、Chromeが4週間ごとに自動更新される一方で、webdriver_managerや手で置いたChromeDriverが古いまま残っていることです。Selenium 4.6以降はSelenium Managerが標準搭載され、webdriver.Chrome()だけでドライバーを解決できます。この記事では今の書き方と、systemd timerで毎日止めずに動かすためのSE_CACHE_PATHの固定方法をコード付きで解説します。

Selenium 4.6以降はwebdriver_managerを外して webdriver.Chrome() だけで書く

以前の書き方:webdriver_managerでドライバーを取得していた

Selenium 4.6より前は、Chromeに合うChromeDriver(Chromeを外から操作するための実行ファイル)を自分で用意する必要がありました。そのため、外部パッケージのwebdriver_managerにダウンロードさせて、そのパスをServiceに渡す書き方が定番でした。

# 以前の書き方(今は使わない)
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install())
)

# あるいはパスを決め打ちする書き方
# driver = webdriver.Chrome(service=Service("/home/username/bin/chromedriver"))

この書き方は「ドライバーの取得」を自分のコード側で抱えるため、取得の仕組みとChrome本体の更新タイミングがずれると、そのまま起動に失敗します。パスを決め打ちしている場合は、手で入れ替えるまで毎日止まり続けます。

今の書き方:Selenium Managerに任せる

Selenium 4.6以降は、Selenium本体にSelenium Managerが同梱されています。Serviceにパスを渡さずにwebdriver.Chrome()を呼ぶと、Selenium Managerが手元のChromeのバージョンを調べ、合うドライバーを取得・キャッシュしてから起動します。

# scraper.py(今の書き方)
import logging
import sys
import time

import pandas as pd
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

URL = "https://example.com/news"
OUT = "/home/username/scrape/news.csv"

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


def build_driver():
    opts = Options()
    opts.add_argument("--headless=new")
    opts.add_argument("--window-size=1280,1024")
    # Service を渡さない = ドライバーの解決は Selenium Manager に任せる
    return webdriver.Chrome(options=opts)


def log_versions(driver):
    caps = driver.capabilities
    browser = caps.get("browserVersion", "?")
    chromedriver = caps.get("chrome", {}).get("chromedriverVersion", "?").split(" ")[0]
    logging.info("Chrome %s / ChromeDriver %s", browser, chromedriver)


def scrape(driver):
    driver.get(URL)
    WebDriverWait(driver, 20).until(
        EC.presence_of_all_elements_located((By.CSS_SELECTOR, "article h2 a"))
    )
    rows = []
    for a in driver.find_elements(By.CSS_SELECTOR, "article h2 a"):
        rows.append({"title": a.text.strip(), "url": a.get_attribute("href")})
    return rows


def main():
    driver = build_driver()
    try:
        log_versions(driver)
        rows = scrape(driver)
    finally:
        driver.quit()

    if not rows:
        logging.error("0件でした。セレクタかページ構造を確認してください")
        return 1

    df = pd.DataFrame(rows).drop_duplicates(subset="url")
    df["fetched_at"] = pd.Timestamp.now().isoformat(timespec="seconds")
    df.to_csv(OUT, index=False, encoding="utf-8-sig")
    logging.info("%d件を保存しました: %s", len(df), OUT)
    return 0


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

ブロックごとの説明

  • build_driver():ヘッドレス(画面を出さない)でChromeを起動します。webdriver.Chrome(options=opts)とServiceを省くことが、Selenium Managerに解決を任せる合図です。webdriver_managerのimportは丸ごと不要になります。
  • log_versions():実際に使われたChromeとChromeDriverのバージョンを毎回ログに残します。定期実行で後から「いつChromeが上がったか」「ドライバーが追従したか」を追えるようにするためです。
  • scrape():WebDriverWaitで要素が現れるまで最大20秒待ってから取得します。time.sleep()で固定時間待つより、表示の遅れに強くなります(Explicit wait=条件を明示した待機)。
  • main():try/finallyで必ずdriver.quit()を呼び、Chromeのプロセスを残さないようにします。0件のときは終了コード1を返します。これが無いと、ページ構造が変わって何も取れなくても「成功」と記録され、気づけません。
  • pandasでの保存:URLの重複を落とし、取得時刻の列を足してCSVにします。utf-8-sigはExcelで開いたときの文字化けを防ぐためです。

実行環境

項目

内容

Python

3.10以上を推奨(Python 3.14系は2026年8月5日に3.14.7が出ており、アクティブサポート中)

パッケージ

pip install selenium pandas

Seleniumのバージョン

4.6以上が必須。入っている版はpip show seleniumで確認

削除するもの

pip uninstall webdriver-manager(requirements.txtからも外す)

Chrome本体

OS側にインストールしておく。Linuxならgoogle-chrome --versionで確認

Seleniumを4.6未満に固定しているrequirements.txtが残っていると、Selenium Managerが入らず以前の挙動のままです。バージョン指定は「4.6以上」になっているかを必ず確認してください。Python本体が古い場合は Python 3.10 EOL→uv移行 2026年10月版 も参考になります。

Chromeの4週間ごとの自動更新で、固定したChromeDriverが合わなくなる理由

ChromeとChromeDriverはメジャーバージョンが一致していないと起動できません。Chromeは約4週間ごとに新しいメジャーバージョンへ自動更新されるため、ドライバーを固定していると、更新された日から次のようなエラーで止まります。

selenium.common.exceptions.SessionNotCreatedException: Message: session not created:
This version of ChromeDriver only supports Chrome version XXX
Current browser version is YYY

このエラーを毎月手で直すのが、以前の運用の最大のコストでした。Selenium Managerに任せると、Chromeが更新された次の実行で、新しいバージョンに合うドライバーが自動で取得されます。

方式

Chrome更新後の挙動

手作業

パス決め打ち

更新日から毎回失敗

毎月ドライバーを差し替え

webdriver_manager

取得先・取得ロジックとChromeの更新がずれると失敗しうる

パッケージ更新の追従が必要

Selenium Manager(今の書き方)

次の実行で合うドライバーを取得

基本的に不要(ただし後述のキャッシュ先に注意)

ただし「自動で取得する」ということは、取得したドライバーをどこかに保存して使い回しているということです。手元で動かすだけなら意識しなくて済みますが、systemd timerなどで無人実行すると、この保存先が原因で止まることがあります。

systemd timerで毎日動かすなら SE_CACHE_PATH でキャッシュ先を固定する

なぜキャッシュ先を固定するのか

Selenium Managerは取得したドライバーをキャッシュに置きます。既定の置き場所は実行ユーザーのホーム配下(Linuxでは~/.cache/selenium)です。手元のターミナルで試したときと、systemdから起動したときで次の点が変わると、次のような止まり方をします。

  • ホームが無い・書けない:システムサービスとして別ユーザーで動かしたり、ProtectHome=のような保護設定を付けたりすると、キャッシュに書けず取得に失敗します。
  • 手動実行とタイマー実行でキャッシュが別になる:手で試したときはキャッシュ済みで動いたのに、タイマーからは初回扱いでダウンロードが走り、その時刻にネットワークが不安定だと失敗します。
  • キャッシュが壊れたときの消し場所が分からない:場所が実行条件で変わると、どこを消せば直るのかを調べるところから始まります。

環境変数SE_CACHE_PATHを指定すると、キャッシュ先を明示的に固定できます。手動実行とタイマー実行で同じ値を使えば、「手元では動くのにタイマーだと動かない」という差が消えます。

ユニットファイル(サービス)

一般ユーザーのuser unitとして置く例です。パスはすべて絶対パスで書きます(systemdはシェルの設定を読まないため、相対パスや~は使えません)。

# /home/username/.config/systemd/user/scrape.service
[Unit]
Description=Daily scrape with Selenium

[Service]
Type=oneshot
WorkingDirectory=/home/username/scrape
Environment=SE_CACHE_PATH=/home/username/scrape/.selenium-cache
ExecStart=/home/username/scrape/.venv/bin/python /home/username/scrape/scraper.py
TimeoutStartSec=15min
  • Type=oneshot:1回実行して終わる処理であることを示します。
  • Environment=SE_CACHE_PATH=...:キャッシュ先を、スクリプトと同じディレクトリの下に固定しています。書き込み権限が確実にある場所を選ぶのがポイントです。
  • ExecStart:仮想環境(venv)のPythonを絶対パスで指定します。システムのPythonを使うと、Seleniumが入っていない・版が違う、という事故になります。
  • TimeoutStartSec:ページが応答しないままChromeが固まったときに、いつまでも終わらない状態を防ぎます。

ユニットファイル(タイマー)

# /home/username/.config/systemd/user/scrape.timer
[Unit]
Description=Run scrape.service every morning

[Timer]
OnCalendar=*-*-* 06:30:00
RandomizedDelaySec=300
Persistent=true

[Install]
WantedBy=timers.target
  • OnCalendar:毎日6:30に起動します。
  • RandomizedDelaySec=300:0〜5分の範囲でずらします。毎日ぴったり同じ時刻にアクセスが集中するのを避けられます。
  • Persistent=true:電源が落ちていて実行時刻を逃した場合、次の起動時に1回だけ実行します。

有効化と、初回の「キャッシュの温め」

# タイマー実行と同じ SE_CACHE_PATH で1回手動実行し、ドライバーを取得しておく
SE_CACHE_PATH=/home/username/scrape/.selenium-cache \
  /home/username/scrape/.venv/bin/python /home/username/scrape/scraper.py

# ログアウト後もuser timerを動かす
loginctl enable-linger username

systemctl --user daemon-reload
systemctl --user enable --now scrape.timer

# 次回の実行予定と、実行ログの確認
systemctl --user list-timers scrape.timer
journalctl --user -u scrape.service -n 50

最初にタイマーと同じSE_CACHE_PATHで手動実行しておくと、キャッシュにドライバーが入った状態から無人運転を始められます。ログにはlog_versions()が出したChromeとChromeDriverのバージョンが並ぶので、Chromeの更新日にドライバーが追従したかをjournalctlで確かめられます。

キャッシュが壊れたと思われるときは、/home/username/scrape/.selenium-cacheを丸ごと消して手動実行し直せば、取り直されます。場所を固定しておくと、この復旧手順が1行で済みます。

同じ考え方でPlaywrightを動かす場合は Playwright定期実行をsystemdで 2026年10月 にまとめています。

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

日本の法律にスクレイピングそのものを禁止する規定はありませんが、どう行い、何に使ったかで責任を問われます。毎日自動で動かすなら、次の3点はコードと運用の両方で守ってください。

robots.txtをコードで確認する

from urllib import robotparser

USER_AGENT = "Mozilla/5.0 (compatible; my-daily-check)"

def allowed(url):
    rp = robotparser.RobotFileParser("https://example.com/robots.txt")
    rp.read()
    return rp.can_fetch(USER_AGENT, url)

# main() の先頭で
# if not allowed(URL):
#     logging.error("robots.txt で拒否されています: %s", URL)
#     return 1

robots.txtを守る義務を直接定めた法律はありませんが、運営者の拒否の意思表示として扱われます。拒否されているページを取得すると、著作権法30条の4(情報解析のための利用)の例外が使えなくなったり、不法行為の判断材料にされたりする可能性があります。

利用規約とアクセス間隔

  • 利用規約:「スクレイピング」「クローリング」「自動」「ボット」「ロボット」「機械的」「プログラム」といった語で検索し、禁止されていないかを確認します。禁止されているサイトでは実施しません。公式APIがあるならAPIを優先します。
  • アクセス間隔:複数ページを巡回するなら、1ページごとに数秒以上の間隔を空けます(例:time.sleep(5))。過去には1秒に1回程度のアクセスで逮捕された事例(後に起訴猶予)もあり、サーバーに過度な負荷をかけると偽計業務妨害罪などに問われる可能性があります。
  • ログインが必要なページ:他人のID・パスワードを使ったり、認証や制限をすり抜けたりする行為は不正アクセス禁止法違反になります。この記事のコードはログイン不要の公開ページを前提にしています。
  • 取得したデータの使い方:著作物をそのまま公開・提供すると30条の4の対象外になります。個人データを本人の同意なく販売するのも原則違法です。

毎日1回・数ページ程度に留め、取得結果に変化が無ければ追加のアクセスをしない作りにするのが、相手にも自分にも安全です。

毎日動かし続けるのは手元のPC・共有レンタルサーバーのcron・VPSのどれか

コードが正しくても、動かす場所が止まれば結果は出ません。SeleniumはChrome本体が動かせる環境であることが前提なので、ここで選択肢が絞られます。

動かす場所

向いている条件

止まる条件

手元のPC(launchd・systemd timer)

毎日PCを開く人。まず試すとき

スリープ中・フタを閉じている間は動かない。Macのlaunchdは開いたときにまとめて動くため、時刻がずれる

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

requests+BeautifulSoupで取れる静的なページだけのとき

Chromeをインストールできない・常駐プロセスや実行時間に制限があることが多く、Seleniumは基本的に動かせない。cronは環境変数をほとんど引き継がないのでSE_CACHE_PATH等を明示しないと失敗する

VPS・自宅の常時稼働PC(systemd timer)

Seleniumで動的なページを毎日取るとき

メモリ不足(Chromeは1プロセスでも重い)、OS更新後の再起動忘れ、自宅なら停電・回線断。enable-lingerを忘れるとログアウトで止まる

  • 手元のMacで動かす場合は、systemdの代わりにlaunchdを使います。登録手順は launchd定期実行はbootstrapで|2026年10月 を参照してください。launchdでもEnvironmentVariablesにSE_CACHE_PATHを書いて固定する考え方は同じです。
  • 共有サーバーしか無い場合は、まず対象ページがJavaScriptなしで取れるかを確かめます。取れるならSeleniumをやめてrequests(最新安定版は2026年5月14日リリースのrequests-2.34.2)で書き直す方が、環境の制約を受けずに済みます。
  • Seleniumが必須なら、systemd timerが使えるVPSか常時稼働のPCが現実的です。journalctlでログが残り、Persistent=trueで取りこぼしも拾えます。

どの環境でも共通して必要なのは、(1) 絶対パス、(2) 環境変数の明示(SE_CACHE_PATHを含む)、(3) 失敗時に0以外の終了コードを返すこと、(4) ログを残すこと、の4つです。この4つが揃っていれば、Chromeが更新された日も、ページ構造が変わった日も、「止まったこと」に気づけます。

関連する選択肢

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

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

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

まとめ:webdriver_managerを外し、SE_CACHE_PATHを固定して無人運転する

Selenium 4.6以降は、webdriver_managerや決め打ちのChromeDriverパスを捨て、webdriver.Chrome(options=opts)だけを書くのが今の書き方です。Chromeが約4週間ごとに自動更新されても、Selenium Managerが次の実行で合うドライバーを取得します。ただし無人で毎日動かすなら、ドライバーのキャッシュ先が実行条件で変わらないよう、systemdのユニットにEnvironment=SE_CACHE_PATH=...を書いて固定し、同じ値で一度手動実行してキャッシュを温めておきます。あわせて、使われたChrome・ChromeDriverのバージョンをログに残し、0件なら失敗として終了コードを返すことで、止まったことに確実に気づけます。動かす場所は、Seleniumが必要ならVPSか常時稼働PCのsystemd timer、静的ページだけなら共有サーバーのcronでもrequestsで足ります。robots.txt・利用規約・アクセス間隔への配慮も、コードの一部として組み込んでください。