アカウントデータのスクレイピング
概要
ユーザーがPlaywrightブラウザ上で自らログインします。あなたはサイトのページが使用するデータソースを発見し、それをページ内で再生(セッションCookie+実際のヘッダー)し、生のレスポンスをディスクに保存します。資格情報は決して扱いません。
使用するタイミング
- 「<サービス>の履歴を取得して」「注文/旅行/予約をスクレイピングして」「このスクレイパーリポジトリを使って」
- 古いスクレイパーリポジトリ(requests + パスワード/OTPログイン): GitHubでエンドポイント名とセレクタのみを読むために使用します。決して実行しないでください。ボットチェック(HTTP 202空ボディ、キャプチャ)によって壊れ、パスワードを要求します。
最初に公式エクスポートを確認してください。 Google Takeout、「データをダウンロード」、GDPR/DPDPリクエスト、公開API、メールの領収書。遅いエクスポートは数日かかります。ユーザーに最初にリクエストしてもらい、今すぐスクレイピングし、到着したらIDで相互確認してください。
ワークフロー
- 何かを書き込む前に出力ディレクトリをgitignoreしてください(
data/<service>/またはプロジェクトの同等のもの)。git check-ignoreで確認してください。生データには住所、電話番号、GPSが含まれます。 - 移動、ログインをハンドオフします。 アカウントページを開き、ユーザーにブラウザウィンドウで電話番号/パスワード/OTPを入力し、完了したら返信するよう伝えてください。チャットでコードを要求しないでください。
- データソースを発見します(
browser-snippets.md§1)。リスナーを設定してリロードし、分類します:- JSON XHR/fetch(最良)— リストエンドポイント、ページネーションの形状(
next_url/カーソル、page_number、オフセット=最後のID、endOfList)、および詳細エンドポイント(1つのアイテムを開く)をメモします。 - GraphQL — 1つの
POST /graphql。完全なリクエストボディ(operationName、query または persisted-query hash、variables)を記録します。 - サーバーレンダリングHTML — データは
documentレスポンスに含まれます。リストおよび詳細ページをページ内で取得し、DOMParserでパースします。後で再パースできるように生のHTMLも保存します。 - 分割リスト — リストがフィルター(年ドロップダウン、日付範囲、アーカイブ済み/キャンセル済み/デジタルなどのタブ)で分割されている場合、すべてのフィルター値を読み、それぞれをページネーションします。
- JSON XHR/fetch(最良)— リストエンドポイント、ページネーションの形状(
- 認証。 まず
fetch(url, {credentials: 'include'})を試します。401/403/空が返ってきた場合は、ライブリクエストから非ブラウザヘッダーをコピーします(§2)。コピーするだけで、値を推測しないでください。ヘッダー名のみをログに記録し、値は決して表示または保存しないでください。 - ページ内でバックグラウンドページネーション(§2):
window.__x = {running, pages, error}、リクエスト間隔1.5秒、リトライ、短い評価でポーリングします。各レスポンス: ログインページ/キャプチャページの場合(URLにsignin/captchaが含まれる、または期待されるJSONの代わりにHTML)、errorを設定して停止します。これらはしばしば200として返されます。 - 移動する前に保存します(§3)。
goto/リロードはwindowをクリアします。絶対filenameパスでbrowser_evaluateを使用し、オブジェクトを返します(JSON.stringifyは二重エンコードされます)。ファイルあたり約5MBは許容されます。それを超える場合はバッチごとに分割します。 - 署名付き/再生不可能なリクエスト(リクエスト署名ヘッダー、nonce ボディ、編集を拒否する永続化クエリ): 各詳細ページにアクセスし、受動的にレスポンスをキャプチャします(§4)。
- ビルド+検証(以下参照)データディレクトリ外のパーサースクリプトで行い、次にインサイトノートを作成します。既存のリポジトリレイアウトがあればそれに従います(例:
tools/<service>/build_db.py、データの横にノート)。そうでなければ質問します。
読み取り専用。 ページがデータを表示するためにリクエストするものだけを再生します。GraphQL: 操作で判断します(query/Get*/List*)、決して mutation ではありません。決して並べ替え、キャンセル、評価、アーカイブ、削除しないでください。
検証 — すべて実行してください
| チェック | 理由 |
|---|---|
| ページネーションがエラーやログインリダイレクトではなく、カーソル/リストの終了によって終了した | サイレントトランケーション |
| 最後に「アプリで古い注文を見る」のようなテキストがないか確認 | Webビューは履歴を制限(Swiggy: 約11ヶ月) |
| サイトのカウントエンドポイントやヘッダーだけを信頼しない | Blinkitのカウントは40と言ったが、リストには83あった |
| コンポーネントが行ごとの合計と一致する(アイテム+料金−割引=支払額) | パーサーのずれを検出。サイトが古いレコードの明細を省略している場合は、そう言うこと。無理に合わせない |
| アイテム価格がアイテム合計と一致する | 単価とライン合計の混同 |
| 一意のID。完全な重複を除去 | エクスポートと領収書で二重リストされることがある |
| 別のソース(エクスポート、領収書)でIDを相互確認 | カバレッジを証明 |
| すべてのリスト/タブ/フィルターがカバーされたか(アーカイブ、キャンセル、他の年、サブサービス) | 「すべての注文」はしばしば複数のリストにまたがる |
パースに関する注意
- サーバー駆動UI API(
snippets、widgets、widget_type、layout): ウィジェットを順に処理し、各ウィジェットのtextフィールドを収集し、既知のラベルでマッピングします。1テキストウィジェットは多くの場合、次のウィジェットのラベルです。ラベル名をホワイトリストに登録するか、すべてのフィールドがずれます。 - 金額はパイサ/セントの場合があります。「FREE」=0。先頭の「-」=割引。通貨記号とインド式カンマ区切りを削除します。通貨は別々に保持します。
- タイムスタンプ: 購入 → ユーザーのタイムゾーン。旅行/移動 → レコードの現地時間。
- 生ファイルに日付を付けて保存します(
orders-raw-YYYY-MM-DD.json)。ビルド済みのSQLiteの隣に置きます。
Git
以下だけをコミットします: gitignore行、パーサースクリプト、README/インデックス行。生ファイルとSQLiteはローカルに保持します。明示的なパスをステージングします(ダーティツリーで決して git add -A しない)。求められたときにのみコミットします。
よくある間違い
- 1つの長い同期的なevaluateループ — タイムアウトします。バックグラウンド化してポーリングします。
- チャットに一括データを返してしまう —
filenameを返すこと。 - スニペットリスナーをアタッチしたままにする —
page.offで解除するか、繰り返し実行でハンドラがスタックします。 - HTMLレンダリングサイトでxhr/fetchのみに絞って発見し、「データソースがない」と結論づけること。