無限スクロールは「スクロールした」で満足してはいけない
無限スクロール型のページをSeleniumで巡回していると、window.scrollToを1回呼んだだけで満足する実装にしてしまいがちです。しかし実際には、ネットワークが少し遅い、遅延読み込みの発火条件がスクロール量やビューポートの交差判定に依存している、といった理由で、スクロール自体は正常に実行できているのに新しいアイテムが1件も増えていない、という状態が普通に発生します。
これに気づかず「スクロールを実行した回数」をそのまま「読み込みが進んだ回数」として扱っていると、巡回ログ上ではページ末尾まで到達したように見えるのに、実際に取得できたアイテム数を確認すると途中で止まっている、という食い違いが起きます。原因はスクロール操作の失敗ではなく、スクロール後の遅延読み込みが発火していないのに、こちらがそれを検知していないことにあります。
要素数の変化を「読み込み成功の証拠」とする
対象にしているページの多くは、遅延読み込みが実際に発火すると、一覧に並ぶカードなどの要素数がその分だけ増えるようにできています。この増加が確認できて初めて「本当に新着が読み込まれた」とみなすようにしました。
まず、対象要素の個数を数える関数を用意します。
def count_items(driver, selector)
driver.find_elements(:css, selector).count
end
この関数を「スクロール前」と「スクロール後」で呼び比べるのが、以降の処理全体の土台になります。単に例外が起きなかったことではなく、この個数が実際に増えたかどうかで成否を判定します。
段階的スクロール→待機→キー操作という三段構え
いきなり最下部までジャンプするより、少しずつスクロールしてから最後に下端まで到達させる方が、遅延読み込みのトリガーが安定して発火します。それでも要素数が増えなければ、PAGE_DOWNキー送信という別の手段にフォールバックし、それでもダメならリトライ上限で諦める、という三段構えにしました。
def scroll_to_load_more(driver, item_selector, max_retries: 3)
before_count = count_items(driver, item_selector)
retry_count = 0
loop do
# 1. 段階的にスクロールしてから最下部まで到達させる
begin
total_height = driver.execute_script('return document.body.scrollHeight')
step = [total_height / 3, 500].max.to_i
3.times do
driver.execute_script("window.scrollBy(0, #{step});")
sleep 0.5
end
driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
rescue => e
warn "スクロール実行でエラー(継続): #{e.message}"
end
# 2. 新規アイテムの出現を短いタイムアウトで待つ
begin
wait = Selenium::WebDriver::Wait.new(timeout: 8)
wait.until { count_items(driver, item_selector) > before_count }
return true
rescue Selenium::WebDriver::Error::TimeoutError
retry_count += 1
end
break if retry_count >= max_retries
# 3. スクロールで反応しない場合はキー操作でフォールバック
5.times do
driver.action.send_keys(:page_down).perform rescue nil
sleep 0.3
end
end
false
end
stepを全体の高さの3分の1(最低500px)に決めているのは、一気に飛ばすと途中にある交差判定のしきい値を素通りしてしまい、遅延読み込みが発火しないページがあるためです。3回に分けて段階的に進め、最後にもう一段document.body.scrollHeightまで到達させることで、途中のしきい値と最下部の両方を確実に通過させています。
呼び出し側では戻り値がfalseだった場合を「これ以上読み込めない」とみなし、ページ末尾に到達したか異常な状態にあると判断してリロードや次の処理に切り替えます。
if scroll_to_load_more(driver, '.item', max_retries: 3)
puts '新しいアイテムを読み込みました'
else
puts '新規アイテムが読み込まれませんでした。末尾到達とみなします'
driver.navigate.refresh
end
なぜ三段構えにする価値があるのか
段階スクロールだけだと、レイアウトが特殊なページ(仮想スクロールやIntersectionObserver依存の遅延読み込みなど)で反応しないケースが残ります。キー操作によるスクロールはブラウザのネイティブなスクロールイベントを発生させるため、JavaScriptのscrollToでは拾われない実装にも効くことがあります。逆にキー操作だけだと、フォーカスの当たっている要素によって効かないことがあるため、まずJavaScriptでの段階スクロールを試し、ダメならキー操作、という順序で組み合わせるのが経験上いちばん歩留まりが良い構成です。
そして最後に必ずリトライ上限を設けてfalseを返すようにしておくこと。「増えるまで待ち続ける」実装は、ページ末尾に到達した正常なケースと、何らかの理由で本当に固まっているケースを区別できず、バッチ全体が無限に停止するリスクを抱えます。リトライ回数とタイムアウト秒数は対象サービスの応答速度に依存するため、まずはログを見ながら「何回・何秒待てば大半のページで末尾到達と判断できるか」を実測し、そこに余裕を持たせた値に調整するのがおすすめです。
まとめ
- 無限スクロールの成否は「スクロールを実行したか」ではなく「対象要素の個数が実際に増えたか」で判定する
- 段階的スクロール→短いタイムアウトでの待機→キー操作フォールバック、の三段構えにすると遅延読み込みの実装差に強くなる
- 一気に最下部までジャンプせず、全体の高さの3分の1程度に分けて段階的に進めることで、途中にある交差判定のしきい値を素通りしにくくなる
- リトライ上限を必ず設け、達したら
falseを返して呼び出し側に「末尾到達」または「異常」の判断を委ねる - リトライ回数やタイムアウト秒数は対象ページの応答速度を実測しながら調整する


コメント