1. 結論(この記事で得られること)
スクレイピングの要素取得が不安定になる原因は、セレクタの書き方ではなく、タイミング・DOM構造・実行環境の差異にあります。
この記事を読むと以下が手に入ります:
- 要素が取れたり取れなかったりする原因を5分で切り分ける方法
- Playwright / Selenium / Puppeteer における待機戦略の実装パターン
- AI(Claude / GPT)を使った最速デバッグフロー
- 本番環境で動き続けるための監視設計とリトライロジック
私自身、3年前に EC サイトの価格監視システムで「ローカルでは動くのに本番で週2回落ちる」という地獄を経験しました。原因は「time.sleep(3)」という雑な待機処理。それ以来、unstable なスクレイピングは「待機設計の失敗」だと考えるようになりました。
2. 前提(環境・読者層)
想定読者
- Python / JavaScript でスクレイピングを書いている
- 「たまに動かない」レベルの不安定さに困っている
- Selenium / Playwright / Puppeteer などのブラウザ自動化ツールを使用
検証環境
- Python 3.10 + Playwright 1.40
- Node.js 18 + Puppeteer 21
- 対象:SPA(React / Vue)および従来型のサーバーサイドレンダリングサイト
3. Before:よくあるつまずきポイント
3-1. 固定時間sleep に頼る設計
# ❌ 悪い例
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://example.com")
time.sleep(3) # 「だいたい3秒で読み込む」前提
element = driver.find_element(By.CSS_SELECTOR, ".price")
何が問題か?
- ネットワーク速度や サーバー負荷で遅延が変動
- 3秒で足りないときもあれば、無駄に待つときもある
- CI/CD 環境やコンテナでは特に不安定
私も昔これで「本番だけたまに落ちる」を経験しました。ローカルは光回線、本番は AWS の Lambda@Edge 経由で遅延が倍違ったんですね。
3-2. 存在チェックなしでいきなりアクセス
// ❌ 悪い例
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// 要素が存在する前提で取得
const text = await page.$eval('.dynamic-content', el => el.textContent);
console.log(text);
})();
何が起きるか?
- SPA では React の hydration 完了前に要素がない
- 「$eval」 が 「null」 で落ちる
- エラーログに 「Cannot read property 'textContent' of null」
3-3. セレクタの脆弱性を無視
# ❌ 脆弱なセレクタ例
driver.find_element(By.CSS_SELECTOR, "div > div > div:nth-child(3) > span")
問題点
- サイトの HTML 構造変更で即死
- A/B テストで DOM 順序が変わると壊れる
- 広告挿入で nth-child がズレる
4. After:基本的な解決パターン
4-1. 明示的な待機(Explicit Wait)を使う
# ✅ 良い例:Selenium + WebDriverWait
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get("https://example.com")
try:
# 最大10秒待機、要素が出現したら即次へ
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".price"))
)
print(element.text)
except TimeoutException:
print("要素が見つかりませんでした")
ポイント
- 「presence_of_element_located」:DOM に存在すればOK
- 「visibility_of_element_located」:visible(display:none でない)が必要なら
- タイムアウトは「最大待ち時間」で、早く見つかれば即座に次へ進む
4-2. Playwright の auto-waiting を活かす
# ✅ Playwright は自動で待機してくれる
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
# デフォルトで要素が actionable になるまで待機
price = page.locator(".price").text_content()
print(price)
browser.close()
Playwright の強み
- 「locator()」 は自動で要素の出現 + 可視化 + 安定化を待つ
- リトライロジックが組み込み済み
- タイムアウトは 「timeout=10000」(ミリ秒)でカスタマイズ可能
4-3. 堅牢なセレクタ戦略
# ✅ 優先順位の高い順
# 1. data属性(一番安定)
page.locator('[data-testid="product-price"]')
# 2. 意味のあるクラス名
page.locator('.product-price')
# 3. テキスト内容(多言語対応注意)
page.locator('text="Price:"')
# 4. XPath(最終手段)
page.locator('xpath=//div[@class="price-container"]/span')
実務での判断基準
- 自社サイト → data-testid を開発段階で仕込む
- 他社サイト → class 名 + 構造の組み合わせ、定期監視必須