ブログに戻ります

トレース可能なリクエストコンテキストでGoogle検索結果をページネートする

Michael Lee
Michael Lee

Expert Network Defense Engineer

15-Sep-2026

TL;DR:

  • Google検索APIのページネーションは収集されたスライスを変更します。 各レスポンスとともにstartと完全なリクエストを保持し、ページを相互に置き換え可能なバッチとして扱わないでください。
  • 定義された計画とレビューされたレスポンス契約内でのみ継続します。 この例では、オフセットを変更する前に返された次のリンクを確認し、あいまいな継続データで停止します。
  • 保留中の作業と重複した観察を可視化します。 各生のページレスポンスを保存します。重複排除は派生ビューに属し、HTTP 201は空のページではありません。

ページネーションは実験の変化を隠すことがあります。コレクターはオフセットを進めながら国設定を失ったり、保留中のレスポンスを完了したページと組み合わせて結果を完了と呼んだりする可能性があります。結果のURLリストは、これらの違いがどのようにコレクションに入ったかを明らかにしません。

Scrapeless Google Search APIstartを通じて検索オフセットをサポートしています。このGoogle検索APIページネーションガイドは、各リクエストの記録と停止理由を持つ意図的に制約されたコレクターを構築します。これは、すべての結果へのアクセスや中断のない歴史的ランキングシーケンスを約束することなく、アプリケーションのコントロールフローを示します。

前提条件と制約されたコレクション計画

このプログラムはPythonの標準ライブラリを使用し、実際のコレクションのためにSCRAPELESS_API_KEYにアカウントAPIキーが必要です。環境変数名はこの例の慣例です。キーはリクエストボディのログや生成されたキャプチャファイルの外に保管してください。

Google検索パラメータは、結果オフセットとしてstartを説明し、例として0、10、20を示しています。このプログラムはそれらのオフセットに計画を制限します。これはアプリケーションの制約であり、サービスの最大深さに関する声明や、各ページに固定数のオーガニックアイテムが含まれる保証ではありません。

基本クエリはcoffeegl=ushl=en、およびデスクトップ入力が含まれています。これらの設定は説明的なリクエスト構成です。観察された検索サンプルとして提示されているわけではありません。自分自身のコレクションの範囲を決定した後にのみ構成を変更してください。

この記事では、アカウントキーが提供されなかったため、認証されたリクエストは実行されませんでした。ローカルテストは合成レスポンスを用いて継続と状態のロジックを検証します。ライブアカウントの実行は、現在のレスポンス動作を検証するための前提条件です。

各生のキャプチャの横にページオフセットを保持する

Google検索リクエストワークフローscraper.google.searchPOST https://api.scrapeless.com/api/v1/scraper/requestに提出し、x-api-tokenで認証を行います。コレクターは送信された本文をレスポンスとは別に記録し、クライアントのタイムスタンプと独自の実行識別子を追加します。

HTTP 200にはタスクデータが含まれ、HTTP 201は処理中のタスクを表します。後者のレスポンスは停止する前に保存する必要があります。タスク識別子は生のレスポンスに保持され、別々に確認された完了ワークフローに使用されます。この例では、タスク取得エンドポイントを発明しません。

キャプチャエンベロープはアプリケーション所有です。そのrequestresponse、タイムスタンプ、およびエラーフィールドはネイティブAPIラッパーとして主張されていません。これらを区別して保持することは、JSONデータモデルに従い、後のマッパーのための根底にある証拠を保存します。

進む前に継続証拠を検査する

クイックスタートレスポンスの例にはpagination.nextが含まれていますが、サンプルURLは省略されています。その省略された値を実行しないでください。このコードは、オフセットを継続のヒントとして使用する前に、実際のトランケートされていないHTTPS Google検索URLを必要とします。

この例では、レビューされたGoogleホストのみを受け入れ、返されたリンクのクエリ、国、言語が基本構成と一致するかを確認します。すべての送信された基本設定を保持し、startのみを変更します。次のリンクに国フィールドが欠けている場合、サービスが意図したことに関する推測ではなく、レビュー停止を引き起こします。

これらのチェックは、URLコンポーネントパーシングを使用します。これらは、アプリケーションの狭い継続ポリシーを定義し、すべての有効なGoogle URLではありません。コレクターは返されたURLを直接追跡することはなく、レビューされたパラメータモードの本文を持って同じAPIエンドポイントに次のリクエストを送信します。

明示的な停止理由でコレクターを実行する

プログラムを paginate_search.py として保存してください。環境にキーを設定した状態で、書き込み可能なディレクトリに python3 paginate_search.py を実行します。このスクリプトはユニークに名前付けされた出力ディレクトリを作成し、リクエストを順番に送信し、それぞれのページキャプチャと collection-summary.json を保持します。

注意: リアルタイムAPI収集は前提条件であり、この記事では行われていません。コードのローカル制御フローは合成応答を使用してテストされました。次のリンクポリシーに依存する前に、実際のアカウント出力を確認し、リクエストがHTTP 201を返す場合はタスク取得を別個に検証してください。

python Copy
import json
import os
import uuid
from datetime import datetime, timezone
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import parse_qs, urlsplit
from urllib.request import Request, urlopen

ENDPOINT = 'https://api.scrapeless.com/api/v1/scraper/request'
OFFSETS = (0, 10, 20)
BASE = {'q': 'coffee', 'gl': 'us', 'hl': 'en', 'device': 'desktop'}


def now():
    return datetime.now(timezone.utc).isoformat()


def fetch(body, key):
    request = Request(ENDPOINT, data=json.dumps(body).encode(), method='POST',
                      headers={'Content-Type': 'application/json', 'x-api-token': key})
    try:
        with urlopen(request, timeout=60) as response:
            status, raw = response.status, response.read().decode('utf-8')
    except HTTPError as error:
        status, raw = error.code, error.read().decode('utf-8', errors='replace')
    except (URLError, TimeoutError) as error:
        return None, None, type(error).__name__
    try:
        return status, json.loads(raw), None
    except json.JSONDecodeError:
        return status, {'unparsed_body': raw}, 'response_not_json'


def next_offset(payload, current):
    pagination = payload.get('pagination')
    link = pagination.get('next') if isinstance(pagination, dict) else None
    if not isinstance(link, str) or not link:
        return None, 'next_link_unavailable'
    try:
        parts = urlsplit(link)
        if (parts.scheme != 'https' or parts.hostname not in ('google.com', 'www.google.com')
                or parts.username or parts.password or parts.port or parts.path != '/search'
                or parts.fragment or '...' in link or '…' in link):
            return None, 'next_link_needs_review'
        query = parse_qs(parts.query, keep_blank_values=True)
        if any(query.get(k) != [BASE[k]] for k in ('q', 'gl', 'hl')):
            return None, 'next_context_needs_review'
        values = query.get('start', [])
        if len(values) != 1 or not values[0].isascii() or not values[0].isdigit():
            return None, 'next_offset_needs_review'
        offset = int(values[0])
        if offset <= current or offset not in OFFSETS:
            return None, 'next_offset_outside_plan'
        return offset, None
    except ValueError:
        return None, 'next_link_needs_review'


def collect(directory, key):
    directory = Path(directory)
    directory.mkdir(parents=True, exist_ok=False)
    seen, visited, pages = set(), set(), []
    offset, stop = 0, None
    while offset in OFFSETS and offset not in visited:
        visited.add(offset)
        body = {'actor': 'scraper.google.search', 'input': dict(BASE, start=offset)}
        started = now()
        status, payload, error = fetch(body, key)
        run_id = uuid.uuid4().hex
        capture = {'run_id': run_id, 'requested_at': started, 'received_at': now(),
                   'request': body, 'http_status': status, 'response': payload, 'error': error}
        filename = f'{offset}-{run_id}.json'
        (directory / filename).write_text(json.dumps(capture, ensure_ascii=False, indent=2), encoding='utf-8')
        page = {'start': offset, 'capture': filename, 'new_exact_urls': None}
        pages.append(page)
        if status == 201:
            stop = 'pending'
            break
        if status != 200 or error:
            stop = 'collection_error'
            break
        rows = payload.get('organic_results') if isinstance(payload, dict) else None
        if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows):
            stop = 'unmapped_organic_results'
            break
        links = {row['link'] for row in rows if isinstance(row.get('link'), str) and row['link']}
        page['new_exact_urls'] = len(links - seen)
        seen.update(links)
        if not rows:
            stop = 'empty_organic_slice'
            break
        if not links:
            stop = 'no_usable_link_strings'
            break
        if page['new_exact_urls'] == 0:
            stop = 'no_new_exact_urls'
            break
        if len(visited) == len(OFFSETS):
            stop = 'planned_page_limit'
            break
        offset, stop = next_offset(payload, offset)
        if stop:
            break
    summary = {'pages': pages, 'stop_reason': stop, 'unique_exact_url_strings': len(seen)}
    (directory / 'collection-summary.json').write_text(json.dumps(summary, indent=2), encoding='utf-8')
    return summary


if __name__ == '__main__':
    key = os.environ['SCRAPELESS_API_KEY']
    output = 'search-pages-' + uuid.uuid4().hex
    print(json.dumps(collect(output, key), indent=2))

タイムアウトはローカルクライアント設定であり、サービスのパフォーマンス主張ではありません。トランスポート例外や非JSON応答はエラーレコードを生成し、収集を停止します。プログラムは部分的な実行を全体と見なすのではなく、以前のキャプチャを保持します。

Scrapelessでスクレイピングを開始する

Scrapelessでウェブスクレイピングと自動化ワークフローを強化しましょう!
今日サインアップして $5の無料クレジット を獲得しましょう — クレジットカードは不要です

今すぐ Scrapelessダッシュボード で無料クレジットを請求しましょう。

検索証拠からの重複排除を分離する

要約は、各ページ上の新しい正確なURL文字列をカウントし、すべての元の応答を保持します。繰り返されたURLは、それらのキャプチャのページコンテキストとともに残ります。これにより、レビューアはエクスポート中に重複を失うことなく重複を検査できます。

正確な文字列の重複排除は意図的に狭いです。トラッキング変種、フラグメント、正規URL、または1つのドメインからの異なるページをマージしません。より広いグルーピングポリシーは、別バージョンの変換に存在し、元のリンクを保持する必要があります。

プログラムは、使えるページが新しい正確なURL文字列を提供しないときに停止します。これは収集予算の決定であり、他に関連するページが存在しない証拠ではありません。同様に、現在の空のオーガニック配列は、徹底的な検索境界を確立することなくこの実行を停止します。

部分的なコレクションを正直に解釈する

要約の停止理由を読み、そのURLカウントを使用する前に確認してください。 planned_page_limit はローカルの制約に達したことを意味します。 next_link_unavailable は継続が確立されなかったことを意味します。保留中、マッピングされていない状態、収集エラー状態は未完成または使用できない証拠を示し、成功した結果の検出ではありません。

各リクエストには独自のタイムスタンプがあります。順次ページは同時に収集されず、プログラムは呼び出し間での共有サーバーセッションを主張しません。この実行を別の実行と比較する際には、収集ウィンドウを保持してください。

プロヴェナンスモデルは、生ページ観察、収集活動、派生した重複排除リストを分離するのに役立ちます。要約が各キャプチャを参照する限り、小さなファイルシステムレイアウトでもそれらの関係を保持できます。

結論

制限されたコレクションを計画し、提出したすべてのリクエストを保持し、応答がアプリケーションの継続ルールをサポートする場合にのみ進めます。明示的な停止理由と保持された重複により、部分的なデータセットを理解可能にし、それにすべてのGoogle結果が含まれていると主張しません。

SERPスナップショットデータセットのアプローチは、収集後に保存した観察を整理するのに役立ちます。どのレコードが比較可能であるかを決定するときにページオフセットを表示可能に保ってください。

次の検索観察を構築する

このワークフローの検索データには Scrapeless Google Search API を使用してください。収集を計画するときに Scrapelessの価格 を確認し、構成に Google検索パラメータ を保持してください。

Discord または Telegram でコミュニティと実装について議論してください。

よくある質問

Q: start=10 は10のオーガニック行を保証しますか?

いいえ。 start はオフセットを指定します。要求されたオフセットからその長さを導出するのではなく、実際のオーガニック配列を検査してください。

Q: このスクリプトはすべてのGoogle結果を収集しますか?

いいえ。小さなアプリケーションプランによって制約されており、継続または使用可能なデータがない場合に停止します。

Q: HTTP 201には何が起こりますか?
応答は保存され、収集は保留中で停止します。最終検索データを解釈する前に、検証されたタスク完了ワークフローが必要です。

Q: コンテキストフィールドを省略した次のリンクでなぜ停止するのですか?

この例では、明示的なクエリ、国、および言語の合意が必要です。証拠が不足している場合は、継続についての黙示的な仮定ではなく、レビューがトリガーされます。

Q: 生のキャプチャから繰り返されたURLは削除されますか?

いいえ。要約の正確な文字列カウントのみが重複排除されます。元の観察は検査のために利用可能なままです。

Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。

最も人気のある記事

カタログ