スクレイピング

ページネーションと無限スクロールを大規模かつ確実にスクレイピングする方法

ページネーションはスクレイパーが気づかぬうちにデータを失う箇所だ。ページの飛ばし、重複カウント、途中終了などが起こる。すべての項目を一度だけ取得し、完了したことを正しく判定する方法を解説する。

Chris Collins

Chris Collins

2026年8月6日 · 1 分で読める

ページネーションはスクレイピングの中でも些末な部分に見えます。page=2 をインクリメントし、次へボタンがなくなるまでループする、それで終わりだと。しかし実際には、ここがスクレイパーが最も静かにデータを失う場所でもあり、その損失は無音です。1ページから8ページまで収集し、サイトに上限をかけられて9ページから40ページを取りこぼしても、何もエラーにはなりません。あるいはリストが足元で動いてしまい、アイテムを二重にカウントすることもあります。あるいは読み込みが一時停止しただけなのに終端だと勘違いして早期に停止することもあります。最後のアイテムを、正確に一度だけ取得し、それを取得できたと確信すること、それが問題のすべてです。

この問題には2つの形があります。クラシックなページネーションと無限スクロールです。両者は1つの核心的な問いを共有しています。すべてを正確に一度だけ収集し、本当に完了したことをどう知るか、です。以下では両方についてその答え方を説明します。

クラシックなページネーション:自分がどの種類を扱っているか知る

ループを書く前に、ページネーションの仕組みを特定してください。よくある3種類のうち2つには、データを破損させる罠があるからです。

オフセットまたはページ番号によるページネーション?page=5?offset=100&limit=25)は最も一般的で、最も危険です。2つの問題が起こり得ます。1つ目はドリフトです。リクエストの間に基になるリストが変化すると(アクティブなサイトでは実際に起こります)、上部に押し込まれた新しいアイテムがすべてを下にずらすため、2ページ目が1ページ目と重複し、あるアイテムが隙間からすり抜けてしまいます。位置で重複排除しては絶対にいけません。安定したユニークIDで重複排除してください。2つ目は深いページネーションの上限です。多くのサイトは100ページ目や10,000オフセットを超えるとサービスを拒否するため、末尾はオフセットでは単純に到達不可能になります。この壁にぶつかったら、ページ送りでは末尾までたどり着けません。日付範囲、カテゴリ、価格帯など別の方法でデータを分割し、各スライス内でページネーションして、どのスライスも上限を超えないようにする必要があります。

カーソルまたはキーセットによるページネーション?after=<token>)は堅牢な種類です。レスポンスが次のバッチ用のカーソルを渡してくれるので、それに従い、カーソルがなくなったら停止します。カーソルはデータ内の安定した位置を指しており、ずれるオフセットではないため、ドリフトの影響を受けません。サイトがこれを提供している場合は常にこちらを優先し、カーソルを構築したり推測したりしようとせず、不透明なものとして扱い、与えられたものだけに従ってください。

cursor, seen = None, set()
while True:
resp = fetch(url, params={"after": cursor} if cursor else {})
for item in resp["items"]:
if item["id"] not in seen: # dedup by stable id, never by position
seen.add(item["id"]); yield item
cursor = resp.get("next_cursor")
if not cursor: # absent cursor is the real end signal
break

ここで最も有用な一手は、レンダリングされたHTMLではなくネットワーク層を見ることです。対象をブラウザで開き、XHR/フェッチのリクエストを観察してください。カーソルページネーションを備えたクリーンなJSONエンドポイントがページの裏に隠れていることが非常によくあります。それを直接呼び出す方が、ページごとにHTMLをスクレイピングするよりも高速で、帯域幅への負荷が少なく、はるかに信頼性が高くなります。

無限スクロール:たいていは変装したカーソルページネーション

無限スクロールは実際のブラウザをほとんど必要としません。内部的には、スクロールは単に同じページネーションされたフェッチをトリガーしているだけであり、ネットワークタブでその基になるリクエストを見つけられれば、APIとまったく同じようにそのカーソルで直接呼び出すことができ、ブラウザを完全にスキップできます。これは帯域幅と時間の面で劇的に安上がりです。

サイトが本当にレンダリングを必要とする場合は、PlaywrightPuppeteer で操作し、誰もがはまる3つの罠に注意してください。

第一に、何が読み込みをトリガーするかを把握してください。スクロール位置なのか、下部近くの IntersectionObserver のセンチネルなのか、「もっと見る」ボタンなのか。正しいものをトリガーしてください。

第二に、固定タイマーではなく、新しいバッチが実際に到着するのを待ってください。sleep(2) は競合状態です。コンテンツが読み込まれていることもあれば、そうでないこともあり、遅いプロキシ経由では読み込まれていないことがよくあります。アイテム数が増加するか、ネットワークがアイドル状態になるまで待ってください。

prev = 0
while True:
page.mouse.wheel(0, 20000) # trigger the next batch
page.wait_for_function( # wait for real arrival, not a timer
"n => document.querySelectorAll('.item').length > n", arg=prev)
items = page.query_selector_all('.item')
if len(items) == prev: # no growth after a real wait
break # ...but see termination below
prev = len(items)

第三に、最も静かに多くのデータを切り捨てる原因、それは仮想化されたリストです。react-windowのようなライブラリは、高速性を保つために画面外の行をDOMから削除します。そのため、下までスクロールしてからDOMをスクレイピングすると、最後に表示されていたウィンドウしか得られず、それ以外は何も得られません。スクロール中に表示された時点でアイテムを抽出する必要があり、最後にまとめて行ってはいけません。

本当に完了したことを知る

終了判定は最も難しい部分です。なぜなら「読み込みが止まった」には、見た目が同じでもまったく異なる2つの原因があるからです。本当に末尾に到達したのか、あるいはサイトが途中でレート制限をかけ、静かにそれ以上の配信を停止したのか。後者を前者として扱うことこそ、末尾が欠落したデータセットを出荷してしまう典型的な原因です。

3つの防御策があります。停止した読み込みは終端だと判断する前に何度か再試行し、遅いバッチを完了と誤認しないようにしてください。サイトが総数を公開している場合はそれと照合してください。「1,240件の結果」というヘッダーはチェックサムです。900件しか収集していなければ、完了したのではなく途中で切り捨てられたのです。そして、リクエストのバーストの直後に停止した読み込みは、終端ではなくソフトブロックの疑いがあるものとして扱い、不完全なデータを受け入れる代わりに、新しいIDでその末尾を再試行してください。これは、パイプライン監視における充足率と期待件数のチェックが、実行全体を通じて捕捉しようとしているのと同じ、静かな失敗の問題です。

重複排除と完全性は常に

完全なデータセットと、もっともらしく見える不完全なデータセットの違いを生むのは2つの習慣です。ページ番号、順序、あるいは行全体のコンテンツハッシュではなく、安定したユニークIDで重複排除すること。なぜなら順序はずれ、行は軽微に編集されることがあるからです。そして、サイトが総数を提供している箇所では、収集数と期待数を常に照合し、切り捨てが誰にも気づかれないまま後になってから発覚するギャップではなく、辻褄の合わない数値として表面化するようにしてください。

プロキシの役割

ページネーションされた一連の処理は通常1つの論理的なセッションであり、そう見えるべきです。単一クエリのすべてのページが同じIPを通って出ていくように、スティッキーセッションを使ってください。シーケンスの途中でローテーションすると、1人の訪問者が一貫してページ送りすることを期待するアンチボットシステムを引っかける可能性があり、パーソナライズされたサイトや地域によって内容が変わるサイトでは、ページ間で矛盾した結果が返ってくることさえあります。各クエリに専用のスティッキーセッションを割り当て、1つのクエリ内ではなくクエリ間でローテーションしてください。

深いページネーションはまた、短時間で単一のホストへ多数のリクエストを送ることを意味し、これはまさにレート制限やブロックを受けるところです。シーケンスにペースを設け、429を叩き続けるのではなくバックオフし、クリーンなプールに頼ることで、そもそもチャレンジを受ける頻度を減らしてください。シーケンス途中のブロックは終端を装うため、良好なIPレピュテーションはここで二重の役割を果たします。切り捨てそのものを減らすと同時に、期待件数チェックと組み合わせることで、発生してしまった切り捨てを無音のままにせず可視化します。

結論

ページネーションは些末な部分ではなく、完全性が勝ち取られるか失われるかの分かれ目です。実際の仕組みを特定し、オフセットとHTMLよりもカーソルページネーションや基になるJSONエンドポイントを優先してください。位置ではなく安定したIDで重複排除してください。無限スクロールについては、可能な限り基になるフェッチを直接呼び出し、レンダリングが必要な場合はタイマーではなく実際の読み込みを待ち、仮想化されたリストがデータを食いつぶさないよう、表示された時点でアイテムを抽出してください。何よりも、「読み込みが止まった」ことは、期待件数チェックや再試行した末尾によって本当に終端だったと証明されるまで疑わしいものとして扱い、シーケンスが一貫性を保つよう各ページネーションクエリを専用のスティッキーセッションで実行してください。

そうすればリスト全体を正確に一度だけ収集でき、それができたと確信できます。シーケンスをクリーンなレジデンシャルゲートウェイに向け、末尾がブロックの壁にならないようにしてください。そしてGB単位の料金体系により、リクエストごとの計量に足を引っ張られることなく、深いページネーションをクロールできます。

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

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

始める