Seleniumで書いたスクレイピングを、画面の無いLinuxサーバーのsystemd timerで毎日動かしていたのに、Chromeが更新された日から急に動かなくなった。そんな経験はないでしょうか。原因は、Chrome 132で旧headlessがChrome本体から削除され、--headless=oldが効かなくなったことです。旧方式は別バイナリのchrome-headless-shellに分離されました。この記事では、Selenium 4のChromeOptionsで--headless=newとchrome-headless-shellをどう指定するかを説明します。あわせて、systemd timerで止まらずに動き続ける構成までを、動くコードで示します。

Chrome 132で何が変わったか:--headless=oldは本体から消えた

以前の書き方

Chrome 131までは、次のように書けば旧headless(画面を描かない軽量なモード)で動いていました。ネット上の古い記事やAIが生成するコードには、今もこの形がよく出てきます。

options = webdriver.ChromeOptions()
options.add_argument("--headless=old")   # 旧headlessを明示(Chrome 132以降は本体に無い)
driver = webdriver.Chrome(options=options)

Chrome 132からは、旧headlessの実装がChrome本体に含まれていません。そのため、--headless=oldを渡しても旧方式では動きません。旧方式の実装は、chrome-headless-shellという別のバイナリとして配布されるようになりました。

今の選択肢は2つ

画面の無いサーバーで動かすなら、今は次のどちらかを選びます。

方式

指定方法

向いている場面

注意点

新headless

Chrome本体に--headless=newを渡す

通常のChromeと同じ描画・挙動が欲しいとき。aptでChromeを更新していく運用

本物のChromeと同じ処理をするぶん重い。Chromeが自動更新されると挙動が変わりうる

chrome-headless-shell

binary_locationにshellのパスを指定する(--headlessは不要)

旧headlessの挙動を前提に書いたスクリプト。軽さを優先したい小さなサーバー

バージョンを自分で管理する。ChromeDriverも同じバージョンを揃える必要がある

迷ったら、まず--headless=newで動かしてみてください。メモリが足りずに落ちる場合や、旧headlessの挙動に依存している場合はchrome-headless-shellに切り替えます。この判断の順序がいちばん手戻りが少ないです。

Selenium 4のChromeOptionsで両方を切り替えるコード

実行環境

  • Python 3.10以上(2026年10月9日リリースのSelenium 4.51は、Python 3.10以上をサポート)
  • pip install selenium。入ったバージョンはpip show seleniumで確認できます
  • Ubuntu/Debian系のLinuxサーバー(GUIなし)

現在のSeleniumはSelenium Managerがブラウザとドライバーを自動で用意します。以前のように、ChromeDriverを手で配置してパスを書く必要は基本的にありません。

スクリプト本体

import csv
import os
import sys
from datetime import datetime

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

TARGET_URL = "https://example.com/news"
OUT_DIR = "/home/username/scraper/data"


def build_driver():
    opts = Options()
    shell = os.environ.get("CHROME_HEADLESS_SHELL")
    driver_path = os.environ.get("CHROMEDRIVER_PATH")

    if shell:
        # chrome-headless-shell は常にheadlessなので --headless は渡さない
        opts.binary_location = shell
    else:
        # Chrome本体を使うときは新headlessを明示する
        opts.add_argument("--headless=new")

    opts.add_argument("--disable-dev-shm-usage")
    opts.add_argument("--window-size=1280,1024")

    if driver_path:
        return webdriver.Chrome(service=Service(driver_path), options=opts)
    return webdriver.Chrome(options=opts)


def scrape(driver):
    driver.set_page_load_timeout(60)
    driver.get(TARGET_URL)
    items = WebDriverWait(driver, 20).until(
        EC.presence_of_all_elements_located((By.CSS_SELECTOR, "article h2 a"))
    )
    return [(a.text.strip(), a.get_attribute("href")) for a in items]


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

    if not rows:
        print("0件でした。セレクタかページ構造を確認してください", file=sys.stderr)
        sys.exit(1)

    os.makedirs(OUT_DIR, exist_ok=True)
    path = os.path.join(OUT_DIR, f"{datetime.now():%Y%m%d}.csv")
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow(["title", "url"])
        writer.writerows(rows)
    print(f"{len(rows)}件を {path} に保存しました")


if __name__ == "__main__":
    main()

ブロックごとの説明

  • build_driver()の分岐:環境変数CHROME_HEADLESS_SHELLがあればshellを使い、無ければChrome本体を--headless=newで起動します。コードを書き換えずに、systemdのユニット側で方式を切り替えられるようにしてあります。
  • binary_location:起動するブラウザの実行ファイルを指定します。shellはそれ自体がheadless専用なので、--headless系のオプションは付けません。
  • --disable-dev-shm-usage:/dev/shm(共有メモリ領域)が小さい環境で、Chromeのタブがクラッシュするのを避けるための指定です。小さなVPSやコンテナでは付けておくと安全です。
  • --window-size:headlessでは画面サイズが小さく決まることがあります。サイズによってスマホ表示に切り替わるサイトもあるため、明示しておきます。
  • CHROMEDRIVER_PATH:shellを使うときは、同じバージョンのChromeDriverを明示的に渡します。Chrome本体を使うときはSelenium Managerに任せます。
  • WebDriverWait:time.sleep()で固定時間待つのではなく、要素が現れるまで待ちます。動的なページで取りこぼさないための今の定番の書き方です。
  • try/finallyでdriver.quit():例外で落ちた場合もChromeを終了させます。これが無いと、毎日の実行のたびにChromeのプロセスが残り、数日でメモリを食い尽くします。
  • 0件なら終了コード1:サイトの構造変更で何も取れなかった日を「成功」として扱わないためです。systemdのfailedとして記録されるので、後から気づけます。

Playwrightで書くほうが向いている場合もあります。待機処理まで含めた書き方はPlaywright locator移行と自動待機|26年10月にまとめています。

chrome-headless-shellをサーバーに置く手順

Chrome for Testingから取得する

chrome-headless-shellは、Googleの「Chrome for Testing」の配布ページ(https://googlechromelabs.github.io/chrome-for-testing/)から入手できます。同じページで、同じバージョン番号のchromedriverも配布されています。shellとドライバーは必ず同じバージョンで揃えてください。

# 配布ページで linux64 の chrome-headless-shell と chromedriver のURLを確認して取得する
mkdir -p /home/username/chrome && cd /home/username/chrome
curl -LO "(配布ページに載っている chrome-headless-shell-linux64.zip のURL)"
curl -LO "(同じバージョンの chromedriver-linux64.zip のURL)"
unzip chrome-headless-shell-linux64.zip
unzip chromedriver-linux64.zip

# バージョンが一致しているか確認する
./chrome-headless-shell-linux64/chrome-headless-shell --version
./chromedriver-linux64/chromedriver --version

この記事では具体的なバージョン番号やURLを書いていません。配布ページの値は更新されていくため、取得するたびにページで確認してください。

足りない共有ライブラリを確認する

最小構成のサーバーでは、Chromeが使う共有ライブラリが入っていないことがあります。次のコマンドで、不足しているものだけが表示されます。

ldd /home/username/chrome/chrome-headless-shell-linux64/chrome-headless-shell | grep "not found"

何も表示されなければ揃っています。表示されたライブラリはaptで入れます。どのパッケージに含まれているかはapt-file search <ライブラリ名>で調べられます。

systemd timerで毎日止まらず動かす設定

サービスとタイマーのユニット

ユーザー単位のsystemdで動かします。~/.config/systemd/user/に2つのファイルを置きます。

# ~/.config/systemd/user/scrape.service
[Unit]
Description=Daily scraping job

[Service]
Type=oneshot
WorkingDirectory=/home/username/scraper
Environment=CHROME_HEADLESS_SHELL=/home/username/chrome/chrome-headless-shell-linux64/chrome-headless-shell
Environment=CHROMEDRIVER_PATH=/home/username/chrome/chromedriver-linux64/chromedriver
ExecStart=/home/username/scraper/.venv/bin/python scrape.py
TimeoutStartSec=15min
# ~/.config/systemd/user/scrape.timer
[Unit]
Description=Run scraping job every morning

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

[Install]
WantedBy=timers.target

Chrome本体の--headless=newで動かす場合は、Environment=の2行を消すだけで切り替わります。

各設定の意味

  • Type=oneshot:1回実行して終わる処理であることを示します。常駐させないので、終わればメモリは解放されます。
  • Environment=:systemdから起動するとログイン時のシェル設定は読まれません。そのため、必要な値はここに書きます。
  • ExecStartは仮想環境のpythonを絶対パスで指定:PATHに頼ると、別のPythonで起動されてseleniumが見つからなくなります。
  • TimeoutStartSec=15min:ページが応答せず固まった場合に、15分で打ち切ります。打ち切るとサービス配下のプロセスがまとめて停止されるので、Chromeが残り続けることもありません。
  • Persistent=true:サーバーが止まっていて実行時刻を逃した場合、次に起動したときに1回分を実行します。

有効化と確認

systemctl --user daemon-reload
systemctl --user enable --now scrape.timer
loginctl enable-linger username        # ログアウトしても user timer を動かし続ける

systemctl --user list-timers           # 次回の実行時刻を確認
systemctl --user start scrape.service  # 今すぐ1回試す
journalctl --user -u scrape.service -n 50

enable-lingerを忘れると、SSHから抜けた時点でユーザーのsystemdが止まり、タイマーも発火しません。ログを見てもエラーが残らないため、気づきにくい落とし穴です。

Selenium Managerのキャッシュに注意

Chrome本体で動かす場合、Selenium Managerが取得したドライバーはユーザーのホームディレクトリ配下にキャッシュされます。そのため、手動で試したユーザーとsystemdで動かすユーザーを同じにするのが確実です。rootで試してから一般ユーザーのtimerに載せると、キャッシュも権限も別になり、初回に失敗する原因になります。

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

毎日自動で動かすということは、相手のサーバーへ毎日アクセスするということです。実装の前に、次の点を確認してください。

  • robots.txt:https://example.com/robots.txtで、取得したいパスがDisallowされていないかを確認します。
  • 利用規約:自動取得の禁止や、取得したデータの利用範囲が書かれていないかを確認します。ログインが必要なページや、個人情報を含むページは特に慎重に扱ってください。
  • 公式APIの有無:APIが提供されているなら、そちらを使うのが基本です。
  • アクセス間隔:複数ページを巡回するときは、1ページごとに数秒以上の間隔を空けます。並列で一気に開くような書き方はしません。1日1回で足りるデータなら、1日1回にとどめます。
  • 制限は回避しない:CAPTCHAやアクセス制限が出たら、それは相手の「来ないでほしい」という意思表示です。突破する方法を探すのではなく、取得自体をやめるか、運営者に問い合わせてください。

関連する選択肢

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

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

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

毎日動かし続ける場所:手元のPC・共有サーバーのcron・VPSが止まる条件

書いたスクリプトが正しくても、動かす場所が止まれば結果は出ません。それぞれの止まる条件は次のとおりです。

実行場所

止まる条件

向いている場合

手元のPC(launchd・cron)

スリープ中、フタを閉じたとき、電源を切ったとき。実行時刻にPCが動いていなければその日は飛ぶ

取得が週に数回で、多少の取りこぼしを許せる

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

多くのプランで、ブラウザの起動やメモリ使用量・実行時間に制限がある。Chromeが起動できないか、途中で強制終了されることが多い

requests + BeautifulSoupで足りる静的ページの取得

VPS・自宅の常時稼働サーバー(systemd timer)

メモリ不足でChromeが落ちる、ディスクが満杯になる、OSやChromeの更新で挙動が変わる

Selenium・Playwrightでブラウザを動かす取得を毎日確実に回したい

Seleniumでブラウザを起動する処理を毎日回すなら、現実的な選択肢はVPSか常時稼働の自宅サーバーです。共有サーバーのcronは、ブラウザを使わずに済む取得に向いています。

まとめ

Chrome 132で旧headlessがChrome本体から削除され、--headless=oldは効かなくなりました。今の選択肢は2つです。Chrome本体に--headless=newを渡す方法と、別バイナリのchrome-headless-shellをbinary_locationで指定する方法です。まずは--headless=newで動かし、重さや旧挙動への依存が問題になったらshellに切り替えるのが手戻りの少ない順序です。shellを使う場合は、Chrome for Testingから同じバージョンのChromeDriverを揃えます。systemd timerで動かす際の要点は、try/finallyでのdriver.quit()、TimeoutStartSecによる打ち切り、Persistent=true、enable-lingerの4点です。0件の日を異常終了にしておけば、サイトの構造変更にも気づけます。動かす場所は、ブラウザを毎日起動するならVPSか常時稼働のサーバーが前提になります。