無限スクロールのスクレイピング: 何回スクロールすればよいかを推測するのをやめよう
Advanced Data Extraction Specialist
TL;DR:
- 無限スクロールページは、HTMLにほとんど何も含まれていません。このデモで使用されるのは、0個の引用要素を含む2,671バイトです。すべてのアイテムはその後に到着します。
- 固定されたスクロールカウントは、誰もが出荷する失敗です。5回のスクロールで100個中60個のアイテムが返されました — エラーも警告もなく、データの40%が欠落したままきれいに終了します。
- 代わりに、アイテムの数が増え続ける間、スクロールし続けてください。それによってすべての100個が返されます。
- スクロールの数はサイトの特性ではありません。同じループで、ローカルでは11回のスクロールアクションが必要で、クラウドブラウザでは3回、どちらも100に達しました。
- ページは10回
/api/quotes?page=Nに呼び出しをしました。そのエンドポイントを直接読むと、同じ100個のアイテムが返され、has_nextが報告されるので、ループはいつ終了するかを知っています。 - 本ガイドにおけるクラウドブラウザの実行は、Scrapelessの無料プランでカバーされています。
無限スクロールは静かにスクレイパーを壊します。リクエストは成功し、セレクターは一致し、スクリプトは0で終了します — そしてデータセットは短いです。実行の中には、どれだけ見逃したかを教えるものはありません。なぜなら、ページはどれだけの情報があるかを約束していないからです。
このガイドは、ライブデモページでそれを測定します。ほとんどのチュートリアルが送信するループを構築し、正確に何を落としているかを示し、本当に終了条件のあるものと置き換え、そしてページがずっと行っていたリクエストを見つけます。
サーバーが実際に送信するもの
ブラウザなしでページを取得すると、解析するものは何もありません:
python
def served_html() -> tuple[int, int]:
request = urllib.request.Request(SCROLL_URL, headers={"User-Agent": UA})
with urllib.request.urlopen(request, timeout=45) as response:
html = response.read().decode("utf-8", "replace")
return len(html), html.count('class="quote"')
text
スクロールページのバイト数 : 2671
HTML内の引用要素数 : 0
2,671バイトでアイテムはゼロ。マークアップはシェルであり、コンテンツはページがロードされた後にスクリプトによって取得され、典型的には下部の近くにあるセンチネル要素が視界に入るときです — これはIntersection Observer仕様が定義しています。CSSセレクターは、非常にきれいには失敗できません — 何もそこにないので、一切一致しないのです。
インストール
bash
pip install playwright
playwright install chromium
2番目のコマンドはブラウザのバイナリをダウンロードします。pipパッケージだけでは起動しません。検証実行にはPlaywright 1.59.0を使用しました。
誰もが出荷するループ
標準的なレシピは、固定回数スクロールし、その後ページを読み取ることです。page.mouse.wheel()は、ページのリスナーが待機している種類の実際のホイールイベントを発火させます。これはUI Events仕様が定義しています:
python
def scroll_fixed(page, times: int, pause: float) -> int:
for _ in range(times):
page.mouse.wheel(0, 20000)
time.sleep(pause)
return page.locator("div.quote").count()
ライブページに対して5回スクロールします:
text
5回スクロール後の引用数 : 60
経過時間(秒) : 5.1
60アイテム。ページには100個あります。スクリプトは何も発生させず、セレクターは機能し、実行は外から見ると成功しているように見えます — これがこのバグの高価なバージョンにする理由です。5回は一度機能したので選び、以降の実行は静かに40%の不足を受け継いでしまいます。
数を増やすことも修正ではありません。それは未収集を無駄なスクロールと交換し、サイトが好きなときにバッチサイズを変更できるという推測を埋め込んでいます。
タイプ数が停滞するまでスクロールする
実際に欲しい条件は観察可能です:アイテム数がまだ増えている間スクロールし続け、安定したら止めます。
python
def scroll_until_stable(page, pause: float, no_growth_limit: int = 2) -> tuple[int, int]:
seen = page.locator("div.quote").count()
scrolls = 0
stable = 0
while stable < no_growth_limit:
page.mouse.wheel(0, 20000)
time.sleep(pause)
scrolls += 1
count = page.locator("div.quote").count()
if count == seen:
stable += 1
else:
stable = 0
seen = count
return seen, scrolls
no_growth_limitは、ページが終了したと信じるために必要な証拠の量を決定します。フラットな読み取り1回は証拠ではありません — 飛行中のバッチはリストの終わりと同じように見えます。2回連続のフラットな読み取りを要求することで、1回のスクロールが追加で必要となり、その曖昧さを取り除きます。
text
収集された引用数 : 100
実施されたスクロールアクション : 11
経過時間(秒) : 11.1
ページが行ったAPI呼び出し : 10
最初のAPI URL : https://quotes.toscrape.com/api/quotes?page=1
すべて100個、ループはカウントを発見し、推測するのではありません。経過時間の数値は、大部分がこのループが選択した待機時間です — 各1秒で11回のスクロール。これはループの特性であり、サイトの特性ではありません。
スクロールカウントはサイトの特性ではない
同じ関数をローカルのChromiumではなくクラウドブラウザで実行した結果:
text
収集された引用 : 100
実行されたスクロール動作 : 3
同じ関数、同じページ、同じ100項目 — 3 回のスクロール動作で、11回ではありません。ビューポートの高さと各バッチがどれだけ早く到着するかが、1回のホイールイベントでページがどれだけ進むかを決定し、どちらもあなたのコントロール下にはありません。スクロールが測定されるビューポートの寸法は、CSSOMビューモジュールが指定するものです。ハードコードされたスクロールカウントは、1つのマシンのウィンドウサイズに合わせて校正されています。
その実行では、Scrapeless Scraping Browserが使用されており、PlaywrightはChrome DevToolsプロトコルを通じて接続します。変更されるのは接続行のみです:
python
endpoint = (
"wss://browser.scrapeless.com/api/v2/browser"
f"?token={os.environ['SCRAPELESS_API_KEY']}&sessionTTL=180&proxyCountry=ANY"
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(endpoint, timeout=90000)
page = browser.new_page()
page.goto(SCROLL_URL, wait_until="domcontentloaded")
cloud_total, cloud_scrolls = scroll_until_stable(page, pause=1.0)
connect_over_cdpはchromium.launch()に置き換わり、ローカルプロセスにではなくソケットを介してChrome DevToolsプロトコルと通信しますので、スクロールループ、セレクタ、および抽出は正確にそのままで維持されます。環境にはSCRAPELESS_API_KEYというキーを保持してください;Scraping Browserの紹介には残りのセッションパラメータが文書化されています。
始めるのに1分かかります — 無料のScrapelessアカウントを作成すれば、無料プランでこの実行がカバーされます。
ページがリクエストしている内容を見る
スクロールループはページにデータを取得するように頼む一つの方法です。リクエストを聴取することで、何を取得しているかを示します:
python
calls: list[str] = []
context = browser.new_context(user_agent=UA)
context.on("request", lambda r: calls.append(r.url) if "/api/quotes" in r.url else None)
10回の呼び出しで、最初のものはhttps://quotes.toscrape.com/api/quotes?page=1です。ページはJSONエンドポイントをページネーションして結果をレンダリングしています;スクロールはそのトリガーに過ぎません。
そのエンドポイントは直接読み取ることができ、スクロールループが推測しなければならなかった質問に答えます:
python
def read_api() -> tuple[int, int]:
quotes = 0
page = 1
while True:
request = urllib.request.Request(API_URL.format(page=page), headers={"User-Agent": UA})
with urllib.request.urlopen(request, timeout=45) as response:
payload = json.loads(response.read().decode())
quotes += len(payload["quotes"])
if not payload["has_next"]:
return quotes, page
page += 1
text
収集された引用 : 100
リクエストされたAPIページ : 10
経過時間 : 3.8
同じ100項目で、has_nextが「カウントが変わらなくなった」という条件よりも明示的な終了条件として使用されています。レスポンスはすでに構造化されているため、フィールドの間にセレクタは存在せず — マークアップが再スタイルされたときに壊れるセレクタもありません。
スクロールループを書く前にこれを確認する価値があります。これは常に存在するわけではありません:多くのサイトは、スクロール時にサーバー側でレンダリングし、リクエストに署名したり、JSONの代わりにHTMLフラグメントを返したりします。それが存在する場合には、より耐久性のあるターゲットとなり、ブラウザはそれが存在することを示すものです。他のページネーションの形状 — 次のボタン、番号付きページ、もっと読み込む — に関しては、完全なページネーションガイドで各タイプをカバーしています。
実行する
bash
export SCRAPELESS_API_KEY="your-api-key"
python3 scroll_demo.py
検証実行からの完全な出力:
text
playwright 1.59.0
--- サーバーが実際に送信する内容 ---
スクロールページのバイト数 : 2671
HTML内の引用要素 : 0
--- 固定のスクロール数 ---
5回のスクロール後の引用数 : 60
経過時間 : 5.1
--- カウントが増えなくなるまでスクロール ---
収集された引用 : 100
実行されたスクロール動作 : 11
経過時間 : 11.1
ページが行ったAPI呼び出し数 : 10
最初のAPI URL : https://quotes.toscrape.com/api/quotes?page=1
--- Scraping Browserで同じループを実行 ---
収集された引用 : 100
実行されたスクロール動作 : 3
--- 同じAPIを直接読み取る ---
収集された引用 : 100
リクエストされたAPIページ : 10
経過時間 : 3.8
ブラウザ/API 時間比率 : 3倍
最後の行の比率は、ブラウザループの壁時計と直接の読み取りを比較しています。ブラウザ側のほとんどはスクロール間の意図的な1秒の遅延であるため、ページを条件のためにポーリングするコストとして読むべきです — ブラウザ自体のベンチマークとしては読みません。
## トラブルシューティング
**カウントが止まらない。** 一部のページはそのコンテンツをループさせます。安定性チェックとともに最大スクロールカウントでループを制限し、そのキャップに達した場合は、完了した実行としてではなくページを点検するシグナルとみなしてください。
**スクロール後もアイテムがゼロ。** コンテナがウィンドウの代わりにスクロールするかもしれません。`page.locator(...).hover()` の後に `page.mouse.wheel(...)` を使用して要素自体をスクロールするか、最後のアイテムに対して `scroll_into_view_if_needed()` を呼び出します。
**カウントが増え、その後ページが真っ白に。** 長いリストはしばしば仮想化されており、行はビューポートから離れるとDOMから削除されます。すべてを読み取るのではなく、進行中にアイテムを収集してください。
**ヘッドレスではなくヘッドで動作します。** ビューポートサイズは両者で異なるため、一回のホイールイベントがどれだけ移動するかやローダーが表示されるかが変わります。コンテキストで明示的なビューポートを設定してください。
**`playwright._impl._errors.Error: 実行可能ファイルが存在しません。`** ブラウザバイナリはダウンロードされていませんでした。`playwright install chromium` を実行してください。
## 結論
無限スクロールは「すべてを取得したのか?」を、スクレイパーが自分で答えなければならない質問に変え、固定スクロールカウントは推測して答えます。ここでの測定は、そのコストを示しています:5回のスクロールから100アイテム中60アイテムが得られ、別のブラウザに移動することでスクロールカウントが11から3に変わりました。
観測した条件 — 連続した読み取りの間でアイテムカウントが安定していること — に基づいてループしてください。ループを書く前に、ページのリクエストを観察してください:コンテンツが `has_next` フラグ付きのJSONとして到着すると、ページはすでに完了したかどうかを知る方法を教えてくれています。
試してみる準備はできましたか? <a href="https://app.scrapeless.com/passport/login?utm_source=website&utm_medium=blog&utm_campaign=scrapingbrowser&utm_term=infinite-scroll-web-scraping"><strong>Scrapelessの無料プランを開始</strong></a>し、<a href="https://www.scrapeless.com/ja/pricing?utm_source=website&utm_medium=blog&utm_campaign=scrapingbrowser&utm_term=infinite-scroll-web-scraping"><strong>現在の価格</strong></a>を確認してください。
## FAQ
**Q: 何回スクロールすべきですか?**
数を選ばないでください。上記の測定は2つのブラウザに対して1回のループを使用し、同じ100アイテムのために1つでは11回、もう1つでは3回のスクロールが必要でした。ビューポートの高さやバッチのタイミングが、各ホイールイベントが進む距離を決定します。アイテムカウントがまだ増えている間はループを続け、2回安定した後に停止してください。
**Q: ページのすべての読み込みが完了したかどうかはどうやってわかりますか?**
アイテムカウントが変わらない2回の連続読み取りが実用的なシグナルです。なぜなら、単一のフラットな読み取りは、まだ処理中のバッチと区別がつかないからです。ページの基礎となるリクエストが `has_next` のようなフラグを示す場合、それは推測ではなく明確な答えです。
**Q: 無限スクロールのためにブラウザは本当に必要ですか?**
しばしば必要ありません。このデモページは100アイテムすべてを直接読み取れるJSONエンドポイントからコンテンツをロードします。ブラウザは、エンドポイントを見つけるための手段です — リクエストリスナーをアタッチし、一度スクロールしてURLを読み取ります。スクロール時にサーバーサイドでレンダリングするサイトやリクエストに署名するサイトはブラウザが必要です。
**Q: スクレイパーがページに表示されるよりも少ないアイテムを返すのはなぜですか?**
ループが早く停止したか、リストが仮想化されていて行がスクロールアウトしてDOMから削除された可能性があります。各スクロール後にアイテムカウントを印刷してください:カウントが上昇し、その後減少する場合は仮想化を示し、ループが終了した時点でカウントがまだ上昇している場合はループが早すぎたことを示します。
**Q: `wait_until="networkidle"`で解決しますか?**
いいえ。最初の読み込みが落ち着くまで待ちますが、これはスクロールがフェッチを引き起こす前に起こります。バッチはスクロールイベントに応じて到着するため、ページはアイドル状態でほぼ空っぽであることがあります — まさに最初に測定された2,671バイトの状態です。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



