こんにちは、Heywaです。
PythonとSeleniumを使ったブラウザ自動化は、日々のルーティンワークを撲滅する強力な武器です。しかし、実際に運用を始めると必ず直面する壁があります。それが「予期せぬエラーによるスクリプトの停止」です。
要素の読み込み遅延、サイトのレイアウト変更、ネットワークの瞬断…。これらに適切に対処(例外処理)しなければ、自動化システムはすぐに使い物にならなくなってしまいます。
私は現在、ボートレース予測のデータ収集やブログの自動投稿など、複数のSeleniumスクリプトをAWS上で24時間稼働させていますが、適切なエラー処理と設定ファイル分離を実装したことで、ここ2年間ほぼ無停止での運用を実現しています。
今回は、私が実践しているSeleniumエラー処理のベストプラクティスを、Claude Codeで生成した実例コードとともに詳細に解説します。
Selenium自動化で頻発するエラー(例外)とその原因
Python Selenium自動化において、エラー(Exception)を理解することは安定稼働への第一歩です。まずは、よく遭遇する代表的なエラーを見ていきましょう。
| 例外クラス名 | 発生原因 | 主な対策 |
|---|---|---|
| NoSuchElementException | 指定した要素がDOMに存在しない | WebDriverWaitで読み込みを待機する |
| TimeoutException | 指定時間内に要素が現れなかった | リトライ処理・ページリロードを実装する |
| StaleElementReferenceException | 取得済み要素のDOMが再描画された | 操作直前に要素を再取得する |
| ElementClickInterceptedException | 要素の上に別の要素が重なっている | 重なり要素の消滅を待つ or JS強制クリック |
1. NoSuchElementException 対策
最も頻繁に遭遇するのが NoSuchElementException です。これは「指定した要素(IDやクラス名など)がページ上に存在しない」場合に発生します。多くの場合、ページの読み込みが完了する前に要素を探そうとしていることが原因です。time.sleep() で固定の秒数を待つのは非効率かつ不安定なため、後述する WebDriverWait を使った明示的な待機(Explicit Wait)を実装して解決します。
2. TimeoutException 解決
TimeoutException は、WebDriverWait で指定した時間内に要素が現れなかった場合に発生します。ネットワークが極端に遅い、あるいはサイト側で障害が起きている可能性があります。このエラーをキャッチした場合は、ページをリロードしてリトライする処理を組み込むのが効果的です。
3. StaleElementReferenceException
要素を取得した後に、JavaScriptなどによってページの一部が再描画(DOMが更新)されると、取得済みの要素への参照が古くなり StaleElementReferenceException が発生します。SPA(Single Page Application)でよく起こります。要素を操作する直前に、再度 find_element で要素を取得し直すことで回避できます。
4. ElementClickInterceptedException
クリックしようとした要素の上に、別の要素(ポップアップ広告やローディングスピナーなど)が重なっていると ElementClickInterceptedException が発生します。重なっている要素が消えるまで待機するか、JavaScriptを使って強制的にクリックさせる(driver.execute_script("arguments[0].click();", element))ことで突破できます。
Claude Codeで生成する堅牢なSeleniumスクリプト実例
これらのエラー処理を最初からすべて手書きするのは骨が折れます。そこで私は、Claude Codeを使ってベースとなるスクリプトを生成させています。ターミナルでClaude Codeを起動し、以下のようなプロンプトを投げます。
「PythonとSeleniumを使って指定URLからデータを取得するスクリプトを書いて。以下の要件を満たすこと。1. NoSuchElementExceptionやTimeoutExceptionなどの例外処理を適切に行うこと。2. WebDriverWaitを使った明示的な待機を実装すること。3. エラー発生時はスクリーンショットを保存すること。4. 設定(URLや認証情報)はconfig.yamlから読み込む設計にすること。」
設定ファイル分離(config.yaml)
まずは設定ファイルです。環境依存の変数をコードから分離することで、保守性が劇的に向上します。
# config.yaml
target_url: "https://example.com/data"
timeout_seconds: 15
max_retries: 3
headless_mode: true
screenshot_dir: "./errors"
エラー処理を網羅したPythonスクリプト(main.py)
import yaml, os, time
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import (
NoSuchElementException,
TimeoutException,
StaleElementReferenceException,
ElementClickInterceptedException
)
with open('config.yaml', 'r') as f:
config = yaml.safe_load(f)
def setup_driver():
options = Options()
if config['headless_mode']:
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
# Mac M1環境でもSelenium Managerが自動でChromeDriverを管理
return webdriver.Chrome(options=options)
def take_error_screenshot(driver, error_name):
os.makedirs(config['screenshot_dir'], exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filepath = f"{config['screenshot_dir']}/{error_name}_{timestamp}.png"
driver.save_screenshot(filepath)
print(f"スクリーンショット保存: {filepath}")
# ここにSlack/LINE/メール通知処理を追加するとさらに強力
def fetch_data_with_retry():
driver = setup_driver()
wait = WebDriverWait(driver, config['timeout_seconds'])
for attempt in range(config['max_retries']):
try:
print(f"試行 {attempt + 1}/{config['max_retries']}...")
driver.get(config['target_url'])
target_element = wait.until(
EC.presence_of_element_located((By.ID, "target-data"))
)
data = target_element.text
print("データ取得成功:", data)
driver.quit()
return data
except TimeoutException:
print("TimeoutException: タイムアウトしました。")
take_error_screenshot(driver, "timeout")
except NoSuchElementException:
print("NoSuchElementException: 要素が見つかりません。")
take_error_screenshot(driver, "nosuch")
except StaleElementReferenceException:
print("StaleElementReferenceException: DOMが更新されました。")
except ElementClickInterceptedException:
print("ElementClickInterceptedException: 要素がブロックされています。")
take_error_screenshot(driver, "intercepted")
except Exception as e:
print(f"予期せぬエラー: {e}")
take_error_screenshot(driver, "unexpected")
# 指数バックオフ: 1秒 → 2秒 → 4秒
time.sleep(2 ** attempt)
driver.quit()
raise Exception("最大リトライ回数に達しました。")
if __name__ == "__main__":
try:
fetch_data_with_retry()
except Exception as e:
print(f"処理が失敗しました: {e}")
高度なエラー対策テクニック
1. WebDriverWaitとリトライ(指数バックオフ)
上記のコードでは、エラーが発生した際に time.sleep(2 ** attempt) という処理を入れています。これは指数バックオフ(Exponential Backoff)と呼ばれる手法の簡易版です。1回目の失敗後は1秒、2回目は2秒、3回目は4秒と、待機時間を指数関数的に増やすことで、サーバー側の一時的な過負荷が原因だった場合に、負荷をかけずに回復を待つことができます。
2. エラー時のスクリーンショットとメール通知
ヘッドレスモード(画面非表示)で運用していると、エラーが起きた時に「画面がどうなっていたか」が分かりません。そのため、driver.save_screenshot() を使ってエラー発生直後の画面を保存する処理は必須です。さらに、この画像を添付してSlackやLINE、メールに自動通知する仕組みを組み込めば、外出先からでも即座に異常を検知できます。
3. Mac M1環境でのChromeDriverエラー対策(webdriver_manager 自動管理)
Mac M1(Apple Silicon)環境でSeleniumを動かす際、以前はアーキテクチャの違いによるChromeDriverのエラーが頻発していました。しかし現在は、Selenium 4.6以降に内蔵された webdriver_manager(Selenium Manager)のおかげで、ドライバのバージョン管理が完全に自動化されています。手動でドライバをダウンロードしてパスを通す古い手法は捨て、最新のSeleniumに任せるのが一番のエラー対策です。
4. ヘッドレスモード エラー対策
ヘッドレスモードでは、通常のブラウザと微妙に挙動が異なる場合があります。特に注意すべきは、画面サイズの違いによるレイアウト崩れです。options.add_argument('--window-size=1920,1080') のように明示的にウィンドウサイズを指定することで、多くのヘッドレス特有のエラーを回避できます。
まとめ:エラー処理こそが自動化の「要」
「動くスクリプト」を書くのは簡単ですが、「止まらないスクリプト」を書くにはシステム思考に基づいた設計が必要です。今回紹介したポイントを整理すると以下のようになります。
| 対策 | 効果 |
|---|---|
| WebDriverWait(明示的待機) | NoSuchElement / Timeout の大半を防ぐ |
| 例外ごとのキャッチ処理 | エラー原因の特定と適切なリカバリが可能 |
| 指数バックオフによるリトライ | 一時的な障害からの自動回復 |
| スクリーンショット保存 | ヘッドレス環境でのデバッグを可能にする |
| config.yaml による設定分離 | コードを変更せず設定変更・環境移行が可能 |
| Selenium Manager(webdriver_manager) | Mac M1含む全環境でのドライバ管理を自動化 |
Claude CodeのようなAIを活用すれば、これらの複雑な例外処理やリトライロジックを、誰でも簡単に実装できるようになりました。皆さんもぜひ、この記事のサンプルコードを参考に、エラーに強い「Awesomeな自動化パイプライン」を構築してみてください。
