はじめに
Webスクレイピングの現場では、静的なHTMLなら比較的簡単に処理できる。
しかし、JavaScriptで動的に構築されるDOMを相手にするとなると、話は一変する。
思ったように要素が取得できず、エラーが頻発する。
まるで、掴んだと思った瞬間に消える蜃気楼だ。
本記事では、Yahoo!知恵袋を対象に実施したスクレイピング中に発生した実例と、その回避策を具体的に解説する。
同様の課題を抱える開発者にとって、少しでも手がかりとなれば幸いだ。
前提としての注意点
Webスクレイピングを行う際には、技術的な工夫だけでなく、倫理的・法的な配慮も不可欠となる。
以下に代表的な注意事項を挙げる。
- 利用規約の確認
対象サイトにスクレイピングの禁止条項が含まれているケースは珍しくない。事前確認はマスト。 -
リクエスト過多の回避
一気にアクセスを仕掛けると、サーバーに負荷をかけるばかりか、自身のIPやアカウントがブロックされる危険もある。 -
個人情報・著作権の取り扱い
収集したデータが個人情報や著作物に該当する場合、公開・二次利用には慎重な判断が求められる。
エラーが出る、その理由
Yahoo!知恵袋の「プログラミング」カテゴリに絞ってスクレイピングを行う過程で、以下のようなスタックトレースを伴うエラーが発生した。
GetHandleVerifier [...]
Microsoft::Applications::Events::EventProperty::empty [...]
(No symbol) [...]
一見して意味が取りづらいが、これは「対象要素が見つからず、処理が進められなかった」ことを示していた。
原因は以下の三点に集約される。
(1) JavaScriptによるDOM構築が未完了の段階で要素検索
HTMLの構造が、JavaScriptによって遅延的に形成されていたため、まだ表示されていない要素を探そうとしてしまった。
(2) 明示的な読み込み完了待機の欠如
画面の読み込み完了を待たずに処理を進めたことで、未レンダリング状態の要素に対して検索を行い、エラーとなっていた。
(3) クラス名検索の完全一致への依存
BeautifulSoupでの要素抽出において、クラス名の完全一致を前提としていた。クラス名が動的に変化する設計の場合、これでは対応しきれない。
解決策とその実装
以上の課題を踏まえ、以下のような具体的対処を施した。
ページロードの完了を確実に待機
Seleniumのexecute_script
メソッドを活用し、document.readyState
がcomplete
となるまで処理を中断するように変更した。
def wait_for_page_load(driver, timeout=20):
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script('return document.readyState') == 'complete'
)
クラス名の部分一致検索に切り替え
動的クラス名に対応するため、lambda
を使った部分一致での検索を採用。XPathのcontains()
も駆使して、頑強なセレクタに仕上げた。
question_elem = soup.find("p", class_=lambda x: x and "ClapLv1TextBlock_Chie-TextBlock__Text" in x)
WebDriverWait(driver, 20).until(
EC.presence_of_element_located(
(By.XPATH, "//p[contains(@class, 'ClapLv1TextBlock_Chie-TextBlock__Text')]")
)
)
質問リンクの取得方法をBeautifulSoupからSeleniumに移行
SeleniumでのXPath検索を用い、動的に描画されるリンクにも追随できるように改善。
question_links = [
elem.get_attribute("href")
for elem in driver.find_elements(By.XPATH, "//a[contains(@href, '/qa/question_detail')]")
]
例外処理の強化とデフォルト値の導入
取得に失敗した場合にも処理を止めず、既定値でスキップする形に変更。
question_text = question_elem.get_text(strip=True) if question_elem else "質問が取得できませんでした"
ログ出力による進行可視化
ループ処理中の進行状況を出力することで、スクレイピングの進捗を把握しやすくした。
for i, link in enumerate(question_links, 1):
print(f"Processing {i}/{len(question_links)}: {link}")
最終的なスクリプト(一部抜粋)
以下のスニペットは、改善を加えたあとの動作安定版の一部となる。
driver.get("https://chiebukuro.yahoo.co.jp/search?p=プログラミング")
wait_for_page_load(driver)
question_links = [
elem.get_attribute("href")
for elem in driver.find_elements(By.XPATH, "//a[contains(@href, '/qa/question_detail')]")
]
for link in question_links:
driver.get(link)
wait_for_page_load(driver)
WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.XPATH, "//p[contains(@class, 'ClapLv1TextBlock_Chie-TextBlock__Text')]"))
)
soup = BeautifulSoup(driver.page_source, "html.parser")
question_elem = soup.find("p", class_=lambda x: x and "ClapLv1TextBlock_Chie-TextBlock__Text" in x)
question_text = question_elem.get_text(strip=True) if question_elem else "取得失敗"
print(question_text)
まとめ
動的コンテンツを含むページに対して安定したスクレイピングを実現するには、「待つこと」「柔軟に探すこと」「失敗を許容すること」が鍵となる。
今回取り上げたYahoo!知恵袋の事例では、これらを押さえることで多数の要素取得エラーを解消できた。
動的なHTMLに頭を悩ませている方は、本記事のアプローチを一度試してみてほしい。
そして、スクレイピングにおける“見えない壁”を突破してほしい。
コメント