「クリックはできたはずなのに画面が変わらない」「突然“読み込みに失敗”バナーが出る」
Web自動化では一時的不調が日常茶飯事。安定運用に大切なのは、
- エラーバナーの自動検知→自動リロード
- 指数バックオフ+ジッターで無理をしない
- クリック後はstaleness/URL変化で、本当に進んだか検証
- 入力・クリック前に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』が近道です /
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 True3) クリック後の“本当に進んだか”を検証(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 how4) “人間らしい”小ワザを使う(任意)
急な操作は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)
