スクレイピング

スクレイピング時に無限スクロールと動的ページネーションを処理する方法

隠しエンドポイントをリプレイできない場合は、スクロールチェーン、Load Moreクリック、仮想化リストのチェックを使ってレンダリング内部で無限スクロールを処理する。

Chris Collins

Chris Collins

2026年9月15日 · 2 分で読める

無限スクロールのフィードをスクレイピングする最良の方法は、たいていの場合まったくスクロールしないことである。ほとんどのフィードの裏側にはページネーション化されたリクエストがあり、多くはカーソルベースであり、そのリクエストを直接再現する方が、ブラウザを動かすよりも速く、安く、信頼性が高い。このアプローチについてはページネーションと無限スクロールを確実にスクレイピングする方法で詳しく扱っている。

本ガイドは、それがうまくいかないケースを対象にしている。リクエストが短命のトークンで署名されている。レスポンスがクライアント側でデコードされる不透明なブロブになっている。フィードは要素がビューポートに入ったときにのみロードされる。「次のページ」ボタンが、再構築できない状態に紐づいている。こうしたケースでは、実際のレンダリング内でスクロールとクリックを処理せざるを得ず、うまくいかなくなる箇所がいくつか存在する。

例では Shifter Web Scraping API を使用しており、これはヘッドレス Chrome でページを実行し、キャプチャの前にブラウザ操作の連鎖を受け付ける。

4種類の動的ページネーション

何も書き始める前に、自分が扱っているのがどのパターンかを見極める必要がある。それぞれ異なる技法が必要になるからだ。

パターン次のバッチを発生させるトリガー技法
スクロール駆動型フィード下部付近の要素がビューポートに入ることセンチネルまでスクロールし、待機し、繰り返す
もっと読み込むボタンアイテムを追加するボタンのクリッククリックし、新しいアイテムを待ち、繰り返す
URL状態ページネーション結果を移動するとページがURLを更新するそれらのURLを直接取得し、スクロールしない
仮想化リストスクロールするが、新しい行が現れると古い行がDOMから削除されるウィンドウ単位でキャプチャするか、裏側のリクエストを使う

3番目は最初にチェックする価値がある。対処コストが最も低いからだ。通常のブラウザでフィードをスクロールし、アドレスバーを観察する。page や offset のようなパラメータが変化するなら、そのサイトはすでにページネーション化されたURLを提供しており、各ページは通常のリクエストになる。

4番目は静かにデータを失うパターンであり、以下で独立したセクションを設けている。

正しいレンダリングと待機

すべての動的な技法は、同じ2つの制御から始まる。render_js=1 はページをヘッドレス Chrome で実行し、成功したリクエスト1回あたり静的フェッチと同じ1クレジットを消費する。wait_for_css は、セレクタがDOM内に存在するまでキャプチャを保留するため、まだ埋まっていないページから抽出することがなくなる。レンダリングの制御についてはJavaScriptのレンダリングに記載されている。

待機用セレクタは慎重に選ぶこと。リストコンテナを待つだけでは不十分である。多くのフレームワークは、まず空のコンテナをレンダリングし、後から中身を埋めるからだ。代わりにリスト内の最初のアイテムを待つこと。

スクロール駆動型フィード:センチネルまでスクロールする

ブラウザ操作は js_instructions に記述する。これは、キャプチャの前に順番に実行されるステップのJSON配列である。ドキュメント化されている操作は scrollToclickwait である。

スクロール駆動型フィードのパターンは、リストの後にある要素までスクロールし、次のバッチがロードされるのを待ち、これを繰り返すというものである。リストが成長するにつれてその要素はさらに下に移動していくため、再度そこへスクロールすることで次のロードがトリガーされる。

import json
import os

import requests

API = "https://scrape.shifter.io/v1"

instructions = [
    {"action": "click", "selector": "button.accept-cookies"},
    {"action": "scrollTo", "selector": "footer", "timeout": 5000, "block": "start"},
    {"action": "wait", "duration": 2000},
    {"action": "scrollTo", "selector": "footer", "timeout": 5000, "block": "start"},
    {"action": "wait", "duration": 2000},
    {"action": "scrollTo", "selector": "footer", "timeout": 5000, "block": "start"},
    {"action": "wait", "duration": 2000},
]

rules = {
    "items": {
        "selector": "article.card",
        "type": "list",
        "item": {
            "link": {"selector": "a.card-link", "output": "@href"},
            "name": {"selector": "h3", "output": "text"},
            "price": {"selector": ".price", "output": "text"},
        },
    }
}

params = {
    "api_key": os.environ["SHIFTER_API_KEY"],
    "url": "https://shop.example.com/category/shoes",
    "render_js": 1,
    "wait_for_css": "article.card",
    "js_instructions": json.dumps(instructions),
    "extract_rules": json.dumps(rules),
}

resp = requests.get(API, params=params, timeout=120)
resp.raise_for_status()
items = resp.json()["items"]

ここにあるいくつかの詳細は意図的なものである。

クッキーバナーを最初に閉じるのは、オーバーレイがスクロールやクリックを妨げる可能性があるからだ。スクロール間の待機は、ネットワークリクエストが完了しDOMが更新される時間を与える。短すぎると、バッチが到着する前にキャプチャしてしまう。list タイプの extract_rules は各カードをJSONオブジェクトとして返すため、こちら側でパーサーを用意する必要はない。また requests はJSONパラメータを両方ともURLエンコードしてくれる。抽出構文については抽出ルールに記載されている。

大規模に実行する前に、サンプルページでこの連鎖をテストし、スクロールステップの数と待機時間を、そのサイトが実際にどのくらいの速さでロードするかに合わせて調整すること。

もっと読み込むボタン:クリックしてから成長を待つ

もっと読み込むボタンは、トリガーが異なるだけの同じループである。

[
  {"action": "click", "selector": "button.load-more", "timeout": 3000},
  {"action": "wait", "duration": 2000},
  {"action": "click", "selector": "button.load-more", "timeout": 3000},
  {"action": "wait", "duration": 2000}
]

つまずきやすい点が2つある。ボタンのセレクタはロード中に状態を変えることが多く、無効化クラスやスピナーが付くことがあるため、待機が十分でないと、ボタンが再びクリック可能になる前に2回目のクリックが発火してしまう。そして、読み込むものがなくなるとボタンは通常消える。これは完了を示す有用なシグナルだが、最後のクリックが何にも作用しない場合にも連鎖が耐えられるようにする必要があるということでもある。ここでも、実際のサイトに対してテストすること。

1回のレンダリングの時間予算

上記のすべてが、時間制限のある1つのブラウザセッション内で発生する。wait_for_css はデフォルトで30秒後にタイムアウトし、timeout はブラウザがそのページに費やせる時間の上限を決める。何千ものアイテムを持つフィードは、スクロールステップをいくつ連鎖させても、1回のレンダリング内では読み込みが終わらない。

したがって、連鎖を長くするのではなく、問題を分割すること。

結果セットを絞り込む。 フィルタ、並べ替え、カテゴリファセットは通常、より小さなフィードを生み出す。それぞれが完全にロードする20個の狭いフィードの方が、決して終わらない1つの巨大なフィードよりも信頼できる。

フィルタ済みURLをページ送りする。 多くのサイトはフィルタとURL状態を組み合わせており、これにより無限のスクロールが有限の通常リクエスト群に変わる。

フィードが本当に長く、絞り込めない場合は、裏側のリクエストにフォールバックする。 レンダリングせずに取得する。JSONを返すエンドポイントについては、auto_parser=1 がパース済みのボディを返す。

仮想化リスト:静かなデータ損失

一部のフィード、特に非常に長いものは、リスト仮想化を使用している。ビューポート付近の行だけがDOMに存在する。下にスクロールすると、上部の行は削除される。

仮想化されたリストを一番下までスクロールしてキャプチャすると、得られるのは最後の画面分のアイテムであり、スクロールして通り過ぎたすべてのアイテムではない。何もエラーにはならない。抽出結果は、クリーンで、もっともらしく、不完全なリストを返す。

出力を信用する前にこれを検出すること。スクロール連鎖を実行し、最初の画面のアイテムがキャプチャ結果にまだ存在するかを確認する。消えている場合、そのリストは仮想化されており、スクロールしてからキャプチャするという方法は機能しない。代わりにURL状態ページネーションか裏側のリクエストを使うこと。

複数リクエストにまたがる動的ページネーション

各ページが、セッションに対して保存されたカーソルなど、サーバー側の状態に依存する別々のリクエストである場合は、session_id を使ってその状態を巡回の間、一定に保つこと。セッションはリクエスト間でクッキー、ブラウザ状態、上流のIPを保持し、10分間アイドル状態が続くと期限切れになる。セッションの寿命が続く間は country を同じに保つこと。途中で切り替えると、ロケールに紐づいたクッキーが無効になる可能性があるからだ。詳細はセッションとプロキシにある。

params.update({
    "session_id": "shoes-walk-07",
    "country": "de",
})

巡回を止めないこと。ページ間でコードが何か遅い処理をしている間にセッションがアイドル状態になると、期限切れになりカーソルが壊れる。

すべて取得できたことを確認する

途中で止まったスクレイパーは、完了したスクレイパーとまったく同じに見える。すべての実行に完全性チェックを組み込むこと。

  • 表示されている合計と比較する。 多くのフィードは結果件数を表示する。ページが1,284件と表示していて、960件しか抽出できていないなら、途中で止まっている。
  • 安定したキーで重複排除する。 アイテムのリンクやIDなど、位置ではなく。スクロールフィードはアイテムを頻繁に再レンダリングし、コンテンツが挿入されるにつれて位置がずれる。
  • 終了マーカーを確認する。 もっと読み込むボタンが消えることや、結果終了メッセージは肯定的な確認になる。最後のステップの後にそれがないなら、連鎖が短すぎたということである。
  • 時間経過での完全性を追跡する。 昨日1,200件だったカテゴリが今日400件になっているのは、たいていの場合、在庫が変わったのではなく、マークアップやロード挙動が変わったということである。

クレジットとレイテンシ:サイトごとにアプローチを選ぶ

アプローチクレジットレイテンシ信頼性リスク
裏側のリクエストを再現する直接送信すればなし。APIを通す場合はページごとに1低い署名済みまたは不透明なリクエスト
URL状態ページネーションページごとに1低から中程度ページネーション化されたURLが必要
1回のレンダリング内でのスクロールまたはクリック連鎖連鎖全体で1高い時間予算、仮想化

複数のバッチをスクロールするレンダリングは1クレジットで済むのに対し、同じバッチを個別のリクエストとして再現するとリクエストごとに1クレジットかかる。トレードオフはレイテンシと時間予算である。長い連鎖は遅くなり、タイムアウトしやすくなる。裏側のリクエストが現実的でない中規模のフィードにはスクロール連鎖を使い、それ以外にはほかの2つの手法を使うこと。

FAQ

無限スクロールに対しては常にJavaScriptをレンダリングすべきか?

いいえ。まずURL状態ページネーションと、再現可能な裏側のリクエストの有無を確認すること。レンダリングはフォールバックであり、デフォルトではない。

連鎖にはいくつのスクロールステップを持たせるべきか?

時間予算内で欲しいバッチをそのサイトがロードするのに必要な分だけである。サンプルページで測定し、予算が許す以上に必要な場合は、代わりにフィードを絞り込むこと。

なぜ抽出結果がスクロールして通り過ぎたアイテムより少なくなるのか?

ほぼ常にリスト仮想化が原因であり、スクロールするにつれて行がDOMから離脱している。最初の画面のアイテムがキャプチャまで残っているかを確認すること。

各スクロールステップはクレジットを消費するのか?

いいえ。連鎖全体が1つのリクエスト内で実行され、成功したリクエスト1回につき1クレジットである。

結論

無限スクロールとは、前面にブラウザが付いたカーソルページネーションであり、カーソルに直接到達できるならそうすべきである。それができない場合は、レンダリング内で処理すること。コンテナではなく最初のアイテムを待つ、センチネルまでスクロールするかもっと読み込むをクリックしてステップ間に十分な待機を入れる、フィードを絞り込んで時間予算を尊重する、キャプチャを信用する前に仮想化をテストする、そしてサイト自身が示す合計に対して完全性を検証すること。

そもそもレンダリング済みのAPIリクエストが適切なツールかどうかを判断する方法については、JavaScriptの多いサイトにWeb Scraping APIが必要な場合を参照のこと。製品についてはJSレンダリング対応のWeb Scraping APIページにあり、プランは料金ページにある。

始める準備はできていますか?

Shifterのレジデンシャルプロキシをお試しください。IP 205M+件、195+カ国、$0.10/GBから。

始める