Seleniumでページエラーに強くする:自動リロード+指数バックオフ+staleness検証【Python/コピペOK】

Seleniumでページエラーに強くする:自動リロード+指数バックオフ+staleness検証【Python/コピペOK】のアイキャッチ
  • URLをコピーしました!

「クリックはできたはずなのに画面が変わらない」「突然“読み込みに失敗”バナーが出る」
Web自動化では一時的不調が日常茶飯事。安定運用に大切なのは、

  1. エラーバナーの自動検知→自動リロード
  2. 指数バックオフ+ジッターで無理をしない
  3. クリック後はstaleness/URL変化で、本当に進んだか検証
  4. 入力・クリック前にpresence→visibility→scroll→clickableで整える

の4つです。

この記事は特定サイトに依存しない汎用テクニック集として、即コピペ運用できるように最小関数でまとめます。

目次

対象・前提

  • Python 3.9+ / Selenium 4.10+
  • pip install selenium webdriver-manager
  • Chrome想定(他ブラウザも同様の考え方で可)

共通インポート

import time, random, platform

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException

# サンプル実行用(create_driver で使用)
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager

設計から学ぶなら『実践 Selenium WebDriver』が近道です

¥3,250 (2025/10/06 14:03時点 | Amazon調べ)

1) エラーバナーの自動検知→自動リロード

読み込み失敗バナーサーバーエラーを見つけたら、決め打ち回数だけ refresh() で回復を試みます。
ポイントはWebDriverWait(..., lambda d: True)で待ったつもりにならないこと。

time.sleep()document.readyState確実に待機します。

ERROR_XPATHS = [
    '//*[@role="alert"]',
    '//*[contains(@class,"error") or contains(@class,"alert") or contains(@class,"warning")]',
    '//*[contains(text(),"読み込みに失敗") or contains(text(),"エラーが発生")]',
]
ERROR_KEYWORDS = ("失敗", "エラー", "再読み込み", "読み込みに失敗", "エラーが発生")

def wait_dom_ready(driver, timeout=10):
    WebDriverWait(driver, timeout, poll_frequency=0.2).until(
        lambda d: d.execute_script("return document.readyState") in ("interactive", "complete")
    )

def has_error_banner(driver) -> bool:
    try:
        for xp in ERROR_XPATHS:
            for e in driver.find_elements(By.XPATH, xp):
                txt = (e.text or "").strip()
                if any(k in txt for k in ERROR_KEYWORDS):
                    return True
        return False
    except Exception:
        return False

def reload_if_error_banner(driver, retry=3, wait_sec=2.0) -> bool:
    """バナーが出ていれば最大 retry 回 refresh。解消したら True。"""
    for _ in range(retry):
        if not has_error_banner(driver):
            return True
        driver.refresh()
        time.sleep(wait_sec)                # ← 疑似待機でなく確実に待つ
        try:
            wait_dom_ready(driver, timeout=max(3, int(wait_sec)))
        except Exception:
            pass
    return not has_error_banner(driver)

2) 指数バックオフ+ジッター(無理しない待機)

固定 sleep(1) 連打は効率も安定性も悪いです。
指数的に待ち時間を伸ばしつつ揺らぎ(ジッター)を入れ、衝突や連続失敗を避けます。

def exp_backoff(attempt: int, base: float = 0.8, cap: float = 12.0) -> float:
    """
    attempt: 0,1,2,... で増加。戻り値は秒。
    base * 2**attempt を上限 cap でクリップし、±15%のジッターを付与。
    """
    wait = min(cap, base * (2 ** attempt))
    return wait * random.uniform(0.85, 1.15)

使い方例:要素取得のリトライ

def wait_input_xpath(driver, xpath: str, tries: int = 5, timeout: int = 15):
    """
    presence→visibility→scroll→clickable を順に満たす。
    失敗時は指数バックオフで再試行。最後に“被り”も確認。
    """
    last_err = None
    for i in range(tries):
        try:
            w = WebDriverWait(driver, timeout, poll_frequency=0.35)
            el = w.until(EC.presence_of_element_located((By.XPATH, xpath)))
            el = w.until(EC.visibility_of_element_located((By.XPATH, xpath)))
            driver.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
            el = w.until(EC.element_to_be_clickable((By.XPATH, xpath)))
            # クリック可能でも“オーバーレイ被り”があると失敗するためチェック
            WebDriverWait(driver, 5, poll_frequency=0.2).until(lambda d: not_occluded(d, el))
            return el
        except Exception as e:
            last_err = e
            try:
                # バナーが残っていれば軽くリロード
                reload_if_error_banner(driver, retry=1, wait_sec=1.0)
            except Exception:
                pass
            wait_s = exp_backoff(i)
            print(f"[retry] prepare '{xpath}' attempt={i+1} wait={wait_s:.2f}s")
            time.sleep(wait_s)   # ← ここも“確実に”待つ
    raise TimeoutException(f"cannot prepare input: {xpath}") from last_err

def not_occluded(driver, el) -> bool:
    """
    要素中心点に対する elementFromPoint で、
    要素自身(または子孫)が最前面なら True(被りなし)とみなす。
    """
    try:
        x, y = driver.execute_script(
            "const r=arguments[0].getBoundingClientRect(); return [r.x+r.width/2, r.y+r.height/2];", el
        )
        top = driver.execute_script("return document.elementFromPoint(arguments[0], arguments[1]);", x, y)
        return driver.execute_script("return arguments[0]===arguments[1] || arguments[0].contains(arguments[1]);", el, top)
    except Exception:
        # 検査不能なら寛容に通す(実運用向け)
        return True

3) クリック後の“本当に進んだか”を検証(staleness_of / url_contains / 事後出現要素)

elem.click()無視されることは珍しくありません。クリック直後は、

  • 元要素の staleness(古いDOMになったか)
  • URL変化
  • 特定要素の事後出現クリック前からあった要素では即Trueになるので要注意

いずれかEC.any_of(...) で待ち、画面が動いた事実を検証します。

def click_and_wait(driver, btn, url_contains=None, appear_xpath=None, timeout=15):
    # (重要)appear_xpath が「クリック前から存在していたか」を先に判定
    pre_exist = False
    if appear_xpath:
        try:
            pre_exist = bool(driver.find_elements(By.XPATH, appear_xpath))
        except Exception:
            pre_exist = False

    # クリック:ネイティブ→ActionChains→JS の順でフォールバック
    try:
        btn.click()
        how = "native"
    except Exception:
        from selenium.webdriver.common.action_chains import ActionChains
        try:
            ActionChains(driver).move_to_element(btn).pause(0.2).click().perform()
            how = "actions"
        except Exception:
            driver.execute_script("arguments[0].click();", btn)
            how = "js"

    # “動いたか”の検証条件を組み立て
    conds = []
    try:
        conds.append(EC.staleness_of(btn))  # DOM更新の有力シグナル
    except Exception:
        pass
    if url_contains:
        conds.append(EC.url_contains(url_contains))
    if appear_xpath and not pre_exist:
        # クリック前には無かった要素の「事後出現」を監視(presenceよりvisibilityを推奨)
        conds.append(EC.visibility_of_element_located((By.XPATH, appear_xpath)))

    if conds:
        WebDriverWait(driver, timeout, poll_frequency=0.3).until(EC.any_of(*conds))
    return how

4) “人間らしい”小ワザを使う(任意)

急な操作はUI側の遅延とぶつかりがち。
少しの間や視線移動を挟むだけで、成功率が上がることがあります。
macOSの全選択COMMAND、Windows/Linuxは CONTROL にも対応させます。

def human_pause(a=0.35, b=0.85):
    time.sleep(random.uniform(a, b))

def hover(driver, elem, pause=(0.15, 0.35)):
    try:
        ActionChains(driver).move_to_element(elem).pause(random.uniform(*pause)).perform()
    except Exception:
        pass

def type_like_human(el, text: str, speed="normal"):
    mod = Keys.COMMAND if platform.system() == "Darwin" else Keys.CONTROL
    el.send_keys(mod, 'a'); el.send_keys(Keys.DELETE)
    head = (0.7, 1.4) if speed == "slow" else (0.10, 0.25)
    per  = (0.12, 0.22) if speed == "slow" else (0.03, 0.08)
    time.sleep(random.uniform(*head))
    for ch in str(text):
        el.send_keys(ch)
        time.sleep(random.uniform(*per))

5) まとめ:“押す前に整え、押した後は確かめる”

  • presence→visibility→scroll→clickable の順で整える
  • オーバーレイ被りelementFromPoint)も確認
  • エラーバナーを自動検知→refresh実待機で)
  • リトライは指数バックオフ+ジッター
  • クリック後は staleness/URL/事後出現 のいずれかで進捗検証

クリックそのものを安定させる実装(ネイティブ→ActionChains→JSのフォールバック、スクロール位置合わせ等)も合わせると効果が倍増します ▼

スクレイピング寄りの実装はこの1冊が鉄板です

コピペ用:最小サンプル(骨格)

# ここまでの関数(wait_dom_ready / reload_if_error_banner / exp_backoff /
# wait_input_xpath / not_occluded / click_and_wait / hover / human_pause / type_like_human)
# を上に定義しておく

def create_driver(headless=False, user_agent=None, for_ci=False):
    opts = Options()
    if headless:
        opts.add_argument("--headless=new")
    if user_agent:
        opts.add_argument(f"--user-agent={user_agent}")
    opts.add_experimental_option('excludeSwitches', ['enable-automation'])
    opts.add_experimental_option("detach", True)
    if for_ci:
        opts.add_argument("--no-sandbox")
        opts.add_argument("--disable-dev-shm-usage")
        opts.add_argument("--window-size=1280,800")

    service = Service(ChromeDriverManager().install())  # Selenium ManagerでもOK
    driver = webdriver.Chrome(service=service, options=opts)
    driver.implicitly_wait(0)
    return driver

if __name__ == "__main__":
    driver = create_driver()
    driver.get("https://example.com")
    wait_dom_ready(driver)

    # 1) 必要ならエラーバナー解消
    reload_if_error_banner(driver, retry=2, wait_sec=1.5)

    # 2) 入力欄を“整える” → 入力
    input_el = wait_input_xpath(driver, '//input[@name="q"]')
    hover(driver, input_el); human_pause()
    type_like_human(input_el, "selenium tips", speed="normal")

    # 3) 送信ボタンをクリック → “進んだ事実”を待つ
    btn = wait_input_xpath(driver, '//button[@type="submit"]')
    how = click_and_wait(driver, btn, url_contains="/search", appear_xpath='//*[@id="results"]')
    print("clicked via:", how)
よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

当ブログの運営者であり全ての記事を書いた陰の者。
なんでも好きなものだけ書きます。
X(旧Twitter)でも更新情報とたまにくだらないことを発信中。
コメント解放しておりませんので、Xの方で感想いただけると嬉しくて更新が早くなる傾向にあります。調子乗りです。

目次