🎯 カスタマイズ可能で検出回避型のクラウドブラウザ。自社開発のChromiumを搭載し、ウェブクローラーAIエージェント向けに設計されています。👉今すぐ試す
ブログに戻ります

エージェントブラウザを使用してホームデポの商品データをスクレイピングする方法

Ava Wilson
Ava Wilson

Expert in Web Scraping Technologies

04-May-2026

主なポイント:

  • 1回のPDP取得で、完全な製品スキーマ。 Home DepotのPDP HTMLには、サーバーサイドでJSON-LDのProductブロックが埋め込まれている — 名称、ブランド、SKU、モデル、GTIN、画像、説明、オファー(価格、通貨、在庫)、集約評価、および上位10件のレビューが含まれています。これが早道です:水和待機なし、格闘するReactシェルなし。
  • 水和されたフィールドが残りを埋める。 セールフラグ、履行オプション(配送/ピックアップ/配達)、店舗ごとの在庫可用性テキスト、およびPDP上のレビューキャロセルは、JavaScriptが実行された後に到着します。Scrapeless Scraping Browser — このガイドが使用するエージェント対応のクラウドブラウザ — は、実際のクラウドブラウザでそれらをレンダリングするため、1回のCLI呼び出しで埋められたDOMが返されます。
  • 店舗ごとの在庫が差別化要因。 位置選択モーダルを介してHome Depotの店舗を設定すると、標準PDPフィールドに加えて、店舗特有の可用性とピックアップETAsが返されます — 一般的な検索APIでは公開されていない信号です。
  • スケールでの検索とカテゴリー。 /s/<query> および /b/<category> リスティングページは、Nao URLオフセットを介してページネートされます。PDPスクレイピングを支える同じDiscover → Extractパターンは、productId、タイトル、ブランド、価格、評価、レビュー数、画像、および標準PDP URLを持つページネートされたカードを返します。
  • クリーンなレビューのスキーマ。 各レビューの抽出器は、フラットでセマンティックな形状を放出します — idtitletextratingbadgesreviewer.nametimeoriginal_source.nameimages[].linktotal_positive_feedbacktotal_negative_feedback — これは、リネームなしで典型的なレビューインテリジェンスパイプラインに直接マッピングされます。
  • 米国プロキシの出口が必要 — 100%。 Home Depotは米国専用の小売サイトです。--proxy-country USはすべてのセッションで必須です。
  • 公式の製品URLを使用。 信頼できるPDPターゲットは /p/<slug>/<productId>(スラグ付き)です; /p/<productId> のような bare ID-only URL は、確認時にHome Depotの一般的なエラーページを返しました。レビュー ページのターゲットは /p/reviews/<slug>/<productId>/<page> です。
  • Discover → Extract。 最初にセマンティックアンカー(data-testidaria-label[itemprop]、セマンティックID)に対して get html でライブDOMを読み取り、その後、実際にレンダリングされたものに対して eval セレクタを書きます。クラス名はローテーションしますが、セマンティックアンカーは変更されません。

はじめに:エージェントブラウザを使用したHome Depot製品データのスクレイピング

Home Depotは、収益で米国最大のホームインプルーブメント小売業者であり、家電、工具、建築資材、サービスを含む公開カタログを提供しています。競争価格チーム、MAP遵守監視者、Home Depotを通じて販売するブランド所有者、在庫パイプライン、およびレビュー駆動の製品研究者にとって、PDP、検索/カテゴリー、および店舗ごとの在庫の表面がパイプラインを推進するデータとなります。

カタログはクライアント側で水和されます。典型的な製品詳細ページは、サーバーレンダリングされたJSON-LD Productブロックを持つ薄いHTMLシェルとして到着し、バンドルが読み込まれるとReactが残りを埋めます — セール価格、現在のプロモーション、履行オプション、店舗の可用性、評価ヒストグラム、およびPDP上のレビューキャロセルはすべてレンダリング後に populated されます。純粋なHTTPスクレイピングはシェルを返し、データはレンダーサイクルの1回後に存在します。

この記事では、JavaScriptレンダリング、住宅プロキシ出口、および店舗ごとの在庫チェックのためのセッションバウンド状態を処理するエージェント対応のクラウドブラウザである Scrapeless Scraping Browser を使用したターミナルファーストのワークフローを紹介します。以下のステップ1〜8では、完全なPDP抽出(JSON-LDファーストパス + 水和フィールド)、検索/カテゴリーのページネーション、店舗特有の可用性を解除する位置選択フロー、およびレビュー・パイプライン(JSON-LDからのトップ10およびレンダリングされたDOMのページネーション、ソート、フィルタ)がカバーされています。

すべてのステップは、skill-dev/SKILL.md に記載されている scrapeless-scraping-browser CLI を通過します。強調されるのは、エージェントブラウザの体験です:必要なプロダクトデータの形状を自然言語で説明し、スキルに基づいてCLIを動かさせます。

他の小売業者での同じDiscover → Extractパターンについては、Amazonスクレーパーの投稿を参照してください。


できること

  • 競争価格。 競合SKUの価格、セールフラグ、プロモーションテキストを継続的に追跡し、MAP違反をアラートします。
  • MAP遵守の監視。 ブランド所有者はカタログ全体の第三者売り手の価格を追跡し、MAPを下回るリスティングを監視します。
  • 在庫・履行インテリジェンス。 位置選択フローを介しての店舗ごとの在庫およびピックアップETA信号は、一般の検索APIでは示されない地域の可用性を明らかにします。
  • カタログ摂取。 検索およびカテゴリーリスティングは、標準化された製品スキーマ(productId、タイトル、ブランド、価格、評価、レビュー数、画像、公式URL)で下流パイプラインに供給されます。
  • レビューインテリジェンス。 感情分析、苦情クラスター、確認された購入者比率の追跡、写真証拠の収集 — フラットな各レビューのスキーマは、既存のレビュー・パイプラインにそのままスロットインします。
  • ブランド監視。 カテゴリ全体でファーストパーティおよび競合ブランドのレビューを追跡し、発売時のレビュー爆撃や持続的な品質の低下を検出します。
  • 製品開発。 レビューからの繰り返されるネガティブなテーマをロードマップの入力、サポートドキュメント、交換部品の変更、またはリスティングページの改善に変換します。

スクラペレス スクレイピングブラウザについて

スクラペレス スクレイピングブラウザは、ウェブクローラーとAIエージェント向けに設計されたカスタマイズ可能なアンチ検出クラウドブラウザです。特にホームデポ向けには、以下の機能があります:

  • 米国居住者プロキシ --proxy-country US による提供 — ホームデポに必要です。
  • クラウド側のJavaScriptレンダリングにより、価格、履行、店舗の可用性、レビューキャラセル、ソートドロップダウン、写真フィルター、およびページネーションバーが、Reactシェルではなく、コンテンツが埋め込まれた状態で届きます。
  • セッションの持続性 --session-id により、店舗探索、ソート/フィルタ、およびページネーションフローにおいて、スナップショット→クリック→入力のコレオグラフィーで、クッキーと適用された状態が一貫して保持されます。
  • アンチ検出のフィンガープリンティング が各セッションに適用されるため、PDPとリスティングページがオーガニックトラフィックと同様にレンダリングされます。
  • 単一のCLIインターフェース — 発見、抽出、およびページネーションステップに必要なすべての操作(open, wait, snapshot, eval, get, click, fill, cookies)が1つのCLIコールで実行可能です。

Scrapelessでサインアップして無料プランでAPIキーを取得し、公式コミュニティに参加してください。フルCLIインターフェースはskill-dev/SKILL.mdに記載されており、プロキシソリューションページでは、クラウドブラウザをサポートする居住者プロキシプランが説明されています。
スクラペレス公式Discordコミュニティ
スクラペレス公式Telegramコミュニティ


前提条件

  • Node.js 18以上。
  • ScrapelessアカウントとAPIキー — app.scrapeless.comでサインアップ。
  • jq(オプション、シェルスクリプト内のJSON解析用 — 移植可能なgrepのフォールバックが以下に示されています)。
  • ターミナルの基本的な使い方に慣れていること。

インストール

以下のレシピはscrapeless-scraping-browser CLIで実行されます。セットアップは3ステップで、CLIユーザーとAIエージェントユーザーの両方が#1と#2が必要です。AIエージェントユーザーは#3も行います。

1. CLIパッケージをインストール

bash Copy
npm install -g scrapeless-scraping-browser

これにより、この投稿のすべてのステップで呼び出されるscrapeless-scraping-browserバイナリが提供されます。このスキルは独自のランタイムを持たず、AIエージェントにコマンドパターンを読み込むため、CLI自体は最初にインストールする必要があります。

2. APIキーを設定する

app.scrapeless.comからトークンを取得し、CLIが読み取れる場所に保存します:

bash Copy
scrapeless-scraping-browser config set apiKey your_api_token_here
scrapeless-scraping-browser config get apiKey   # 確認

AIエージェントを使用していますか? スキルの指示は、セッション呼び出しの前に認証が必要であることをエージェントに伝えます。エージェントが最初にCLIを取得する際にAPIキーが設定されていない場合、エージェントはあなたにプロンプトを出し、config set apiKey …コマンドを実行します。

設定ファイルは~/.scrapeless/config.jsonにあり、現在のユーザーのみにアクセスが制限されており、環境変数よりも優先され、エージェントやCIランナー間で移植可能です。CIパイプラインでは、次のようにすることをお勧めします:

bash Copy
export SCRAPELESS_API_KEY=your_api_token_here

3. AIエージェントにスクラペレススキルをインストール

これはステップ1とは別のステップです。ステップ1ではCLIバイナリがインストールされました — あなたのエージェントが呼び出すランタイムです。スキルは、エージェントがそれを正しく呼び出す方法(セレクタ、待機、リトライパターン、発見→抽出ワークフロー)を教えるものです。これらは異なるものであり、両方は必要です。

スキルは、SKILL.md + skill.json + references/を含むフォルダーです。正規のソースは、**scrapeless-ai/scrapeless-agent-browser → skills/scraping-browser-skill**リポジトリです。

Claude Code、Cursor、VS Code + GitHub Copilot、OpenAI Codex CLI、またはGemini CLIにインストールするには、**スクラペレスAIエージェントインストールガイド**に従ってください — 各エージェントのコピー&ペーストコマンド(bashおよびWindows PowerShell)が含まれています。インストール後にエージェントを再読み込みして、スキルを有効にします。

スキルが最初にエージェントの操作コンテキストに読み込むもの:

  • 認証~/.scrapeless/config.jsonまたはSCRAPELESS_API_KEYをチェックし、不足している場合は設定を促します。
  • Discover → Extract workflow — 最初に get html "<region>" でライブDOMを読み込み、安定したアンカー(data-testidaria-label、セマンティックID、[itemprop='review'])を特定し、その後、実際にレンダリングされたものに基づいて eval セレクタを書く。
  • Home Depotの待機に関する注意点open と任意のセレクタ待機の間に wait 1500 を挟んで、コールドセッションの chrome://new-tab-page/ 競合を避ける。全体的な networkidle の代わりに、レビューカード要素に対して wait '<review-anchor>' を使用する。なぜなら、Home Depotは決して収束しないレイジービーコンを発火し続けるから。
  • セレクタ構文 — CSSセレクタとアクセシビリティ参照(snapshot -i@e1)を使うタイミング。
  • 並列CLIワーカー — シングルシェルでの && チェーン、ユニークなセッション名、ホストごとに最大3つの同時ワーカー。
  • 一般的な落とし穴eval はJSONで引用された値を返す。open は成功したナビゲーションで非ゼロで終了する。セッションは接続が閉じると終了する。
  • 完全なコマンドリファレンスnew-sessionopenwaitevalgetclickfillsnapshotcookiesrecordingstop の各フラグ。

4. スキルが正しく接続されているか確認する

最初の本格的なHome Depotスクレイプの前に、安全なプロンプトでインストールをスモークテストする:

"Scrapelessスキルを使用して、https://example.com を開いてページタイトルを教えて。"

エージェントはScrapelessセッションを作成し、ナビゲートし、"Example Domain" と返答するはずです。この二語が戻ってくれば、スキルは読み込まれ、APIキーは設定されており、クラウドブラウザにアクセス可能です。

失敗した場合:

症状 考えられる原因 修正
"そのためのツール/スキルは持っていません" このエージェントセッションにスキルがロードされていない スキルインストールガイドに従って再インストールし、エージェントをリロード
Authentication failed / 401 APIキーが設定されていない scrapeless-scraping-browser config set apiKey <token> を再実行(インストールステップ2)
command not found CLIバイナリがPATHにない インストールステップ1を再実行
Home Depotで ERR_TUNNEL_CONNECTION_FAILED プロキシプールに割り当て時に利用可能な居住IPがなかった 新しいセッションを作成 — --proxy-country US を保持し、すぐに再試行
フリーズ / chrome://new-tab-page/ に落ちる コールドセッション待機競合 エージェントに再試行を依頼 — スキルは open と次の待機の間に wait 1500 を挿入することを知っている
Home Depotが一般的なエラーページを返す 非米国のエグレス、IDのみのURL、または一時的なセッションフィンガープリントフラグ --proxy-country US を確認し、標準的な /p/<slug>/<productId> URLを使用し、新しいセッションを作成して再試行
毎回の新しいセッション呼び出しで Scrapeless session has been terminated and cannot be reconnected ローカルデーモンが今は終了したセッションIDをキャッシュしており、それに再接続しようとしている デーモンを停止し、そのpidファイルをクリアした後、新しいセッションを作成: Stop-Process -Id (Get-Content "$env:USERPROFILE\.scrapeless-scraping-browser\default.pid") -Force; Remove-Item "$env:USERPROFILE\.scrapeless-scraping-browser\default.pid","$env:USERPROFILE\.scrapeless-scraping-browser\default.port" -Force (WindowsのPowerShell)。Linux/macOSではパスは ~/.scrapeless-scraping-browser/ です。

実際の使い方:エージェントにプロンプトを送る

インストール後、Home Depot製品データをエージェントに話しかけることでスクレイプします — bashをコピー&ペーストするのではなく。スキルはセレクタ、待機、再試行分類子、および発見 → 抽出パターンをエージェントのコンテキストにロードするため、1行の自然言語プロンプトで構造化されたJSONを取得できます。

貼り付けて使えるプロンプト

あなたがエージェントに言うこと あなたが受け取るもの
"Home Depot商品204279858の完全な製品スキーマを取得して(価格、ブランド、モデル、画像、可用性)。" ステップ2のJSON-LD製品スキーマ + ステップ3の水分補給された価格/履行ペイロード
"このHome Depot PDPの価格 + セールフラグを追跡してください。" pricewasPriceonSalepromotioncurrency
"ホームデポで『コードレスドリル』を検索し、結果の最初の5ページを返してください。" ページネーションされたリストカード:productId、title、brand、price、rating、reviewCount、image、productUrl
"ZIP 90015で商品204279858の在庫を確認してください。" storeavailabilityTextpickupEtastockCount(店舗ごとのペイロード)
"これらの20商品IDについて、価格 + 店舗ごとの可用性をZIP 33101で取得してください。" 各IDごとの1つの製品データJSONと店舗セットの履行ペイロード
"Home Depot商品ID326716329をその標準的なレビューURLに解決し、レビューJSONを返してください。" 標準URLと製品レベルのサマリーおよびレビュー配列
"この /p/reviews/... URLから最初の100件のHome Depotレビューを取得し、新しい順にソートしてください。" ページネーションされたレビューとページおよびページインデックスのメタデータ
"このHome Depot製品の写真レビューのみを取得してください。" images.length > 0 でフィルタリングされたレビュー
"商品204279858について、確認済み購入者のレビューのみをリストしてください。" 確認済み購入者バッジでフィルタリングされたレビュー
"最新のレビューを取得し、役立ち度でソートして、上位30件を返してください。" UIソートフローの後のレビュー、役立ち票でランク付け
"商品ページを開き、店舗のZIPコードを90015に設定し、その後、店舗のコンテキストレビューをスクレイピングします。" 店舗設定されたブラウザセッションから収集されたレビュー(手順5を参照)

実例:商品204279858(DEWALTドリル)のレビューを取得する

あなたが入力した内容:

"Home Depotの商品204279858の上位レビューについて、タイトル、本文、評価、およびレビュアー名を取得します。JSON形式で返してください。"

エージェントの計画(簡単な英語で):

  1. 204279858を標準的なPDP URL /p/<slug>/204279858に解決します。
  2. 米国の出口セッションを作成します(--proxy-country US);一時的なos error 10054 / 503に対して1回再試行します。
  3. PDP URLを開き、1500ミリ秒待機してから、コールドセッションのレースを避けるために'h1'を待機します。
  4. script[type="application/ld+json"]に対してevalを行い、埋め込まれたProductスキーマを解析し、上位10件のレビューと集計を返します。
  5. 10件以上のレビューが必要な場合は、/p/reviews/<slug>/204279858/<page>に移動し、ページごとにレンダリングされたDOMエクストラクター(手順6)を実行します。

返ってくる内容(説明的な出力、本文抜粋は長さのために省略):

json Copy
{
  "productId": "204279858",
  "productUrl": "https://www.homedepot.com/p/DEWALT-20V-MAX-Cordless-1-2-in-Drill-Driver-2-20V-1-3Ah-Batteries-Charger-and-Bag-DCD771C2/204279858",
  "productName": "DEWALT 20V MAX Cordless 1/2 in. Drill/Driver, (2) 20V 1.3Ah Batteries, Charger and Bag DCD771C2",
  "brand": "DEWALT",
  "sku": "1000014677",
  "modelNumber": "DCD771C2",
  "overallRating": 4.7,
  "totalReviews": 11168,
  "reviewsReturned": 10,
  "reviews": [
    {
      "title": "素晴らしいツール!",
      "text": "こちらを2週間使用していますが、値段以上の価値があります。品質は非常に良いです...",
      "rating": 5,
      "reviewer": { "name": "kevein" },
      "time": null,
      "original_source": { "name": "homedepot.com" }
    },
    {
      "title": null,
      "text": "古いCraftsmanのコードレスドリルを交換するために、Dewalt 20v max Drill/Driverを購入しました...",
      "rating": 5,
      "reviewer": { "name": "DIYer_Bill" },
      "time": null,
      "original_source": { "name": "homedepot.com" }
    }
    // ... 同じ形式のレビューがさらに8件
  ]
}

timenullで返されます。というのも、Home DepotはJSON-LDレビューオブジェクトにdatePublishedを含めていないためです — タイムスタンプはレンダリングされたレビューページのDOMに存在し、レビューのタイムスタンプが必要な場合は手順6のパスを通じて取得します。

これがこのスクレイピングのためのユーザー向けのすべての表面です。以下の手順1–8にあるbash、セレクタ、待機時間は、スキルによってエージェントが実行するものです — あなたはそれらを入力する必要がありません。

プロンプトの整形: 返ってくる内容を制御する方法

フレーズ 効果
"…JSON形式で返す" / "…CSV形式で" 出力形式
"…フィールド: タイトル、テキスト、評価、時間のみ" エージェントが抽出するフィールドを制限
"…上位25件" / "…ページ1〜10で" ページネーションの深さ
"…最新のものを先に" / "…低評価のものを先に" UIの並べ替えフローをトリガー
"…写真レビューのみ" 写真レビューのフィルターをトリガー
"…確認済の購入者のみ" バッジによって抽出されたレビューをフィルター
"…reviews.jsonlに保存する" 各レビューを1行ごとにファイルに書き込む
"…上位5件の苦情を要約する" 抽出後の分析をチェーン
"…最初に店舗のZIPを90015に設定する" スクレイピング前に店舗ロケーターのフローをトリガー

これがワークフローです。以下の手順1–8は、内部参照です — 一度読んで、発見→抽出パターンがどのように構成されるかを見てください。その後、エージェントがそれを適用することを信頼してください。


ステップ1 — Scrapeless Scraping Browserに接続する

米国の出口セッションを作成します。

bash Copy
SESSION=$(scrapeless-scraping-browser new-session \
  --name "homedepot-product-data" \
  --ttl 1800 \
  --recording true \
  --proxy-country US \
  --json | jq -r '.data.taskId')

echo "Session: $SESSION"

jqなしのポータブルフォールバック:

bash Copy
SESSION=$(scrapeless-scraping-browser new-session \
  --name "homedepot-product-data" --ttl 1800 --recording true \
  --proxy-country US --json \
  | grep -oE '"taskId":"[^"]*"' | cut -d'"' -f4)

住宅用プロキシの割り当ては、時折最初の試行で一時的な503を返すことがあります — 1回再試行します。ERR_TUNNEL_CONNECTION_FAILEDが表示された場合、プロキシプールに割り当て時に利用可能なIPがないことを示しており、セッションを再作成してしばらく待機してください。


ステップ2 — 高速経路: JSON-LDから商品スキーマと上位10件のレビューを抽出

Home DepotのPDP HTMLは、サーバー側のJSON-LD Productブロックを埋め込んでいます — 名前、ブランド、SKU、モデル、GTIN、画像、説明、オファー(価格、通貨、在庫)、総合評価、上位10件のレビュー — すべて水分補給を待つことなく。商品が出荷しないSchema.orgに準拠するフィールド(例: availability, seller, priceValidUntil, mpn, color)はnullで返され、nullableとして扱われるべきです。

標準のPDP(スラグを含む — bare IDのみのURLは一般的なエラーページを返します)を開き、JSON-LDパーサーをevalします:

bash Copy
PRODUCT_ID="204279858"
Copy
PRODUCT_SLUG="DEWALT-20V-MAX-コードレス-1-2-インチ-ドリル-ドライバー-2-20V-1-3Ah-バッテリー-充電器-および-バッグ-DCD771C2"
PDP_URL="https://www.homedepot.com/p/$PRODUCT_SLUG/$PRODUCT_ID"

scrapeless-scraping-browser --session-id $SESSION open "$PDP_URL"
# 次の待機がナビゲーションが確定する前にプリウォームの
# chrome://new-tab-page/に解決しないように、短いポーズをとります。
scrapeless-scraping-browser --session-id $SESSION wait 1500
# H1プロダクトタイトルを待つ — JSON-LDブロックはサーバーでレンダリングされ、
# 同じ静的HTMLに統合され、H1が見えるときには必ず存在します。
# (`script[type="application/ld+json"]` で直接待たないでください — `wait`は
# 「可視」状態をデフォルトとしており、`<script>`要素は決して可視になりません。)
scrapeless-scraping-browser --session-id $SESSION wait 'h1'

scrapeless-scraping-browser --session-id $SESSION eval '
  (() => {
    const ld = [...document.querySelectorAll("script[type=\"application/ld+json\"]")]
      .map((s) => { try { return JSON.parse(s.textContent); } catch { return null; } })
      .filter(Boolean);
    const product = ld.find((o) => o["@type"] === "Product");
    if (!product) return JSON.stringify({ error: "このページにProduct JSON-LDがありません" });

    const productId =
      product.productID || product.sku ||
      location.pathname.match(/\/(\d+)(?:\/\d+)?(?:[/?#]|$)/)?.[1] || null;

    const offers = Array.isArray(product.offers) ? product.offers[0] : product.offers || null;
    const offerPrice = offers
      ? (offers.price ?? offers.priceSpecification?.price ?? offers.lowPrice ?? null)
      : null;
    const availability = offers?.availability
      ? String(offers.availability).replace(/^https?:\/\/schema\.org\//, "")
      : null;

    const images = []
      .concat(product.image || [])
      .flat()
      .filter(Boolean);

    const reviews = (Array.isArray(product.review) ? product.review : product.review ? [product.review] : [])
      .map((r) => ({
        title: r.headline || null,
        text: r.reviewBody || null,
        rating: Number(r.reviewRating?.ratingValue) || null,
        reviewer: { name: r.author?.name || null },
        time: r.datePublished || null,
        original_source: { name: "homedepot.com" },
      }));

    return JSON.stringify({
      productId,
      productUrl: location.href,
      productName: product.name,
      brand: product.brand?.name || product.brand || null,
      sku: product.sku || null,
      modelNumber: product.model || null,
      gtin: product.gtin13 || product.gtin14 || product.gtin12 || product.gtin8 || product.gtin || null,
      mpn: product.mpn || null,
      category: product.category || null,
      description: product.description || null,
      color: product.color || null,
      image: images[0] || null,
      images,
      offers: {
        price: offerPrice != null ? Number(offerPrice) : null,
        currency: offers?.priceCurrency || null,
        availability,
        priceValidUntil: offers?.priceValidUntil || null,
        itemCondition: offers?.itemCondition
          ? String(offers.itemCondition).replace(/^https?:\/\/schema\.org\//, "")
          : null,
        seller: offers?.seller?.name || null,
      },
      overallRating: Number(product.aggregateRating?.ratingValue) || null,
      totalReviews: Number(product.aggregateRating?.reviewCount) || null,
      reviewsReturned: reviews.length,
      reviews,
    });
  })()
'

この単一のPDP取得は、サーバーでレンダリングされた完全な製品スキーマを返します — productId、name、brand、sku、model、gtin、image、description、offers(price、currency、availability)、集計評価 — さらに、上位10件のレビューをフラットなセマンティック形状で返します( idtitletextratingreviewer.nametimeoriginal_source.nameimages[].linktotal_positive_feedbacktotal_negative_feedback)。これにより、Reactシェルに依存せずに確実に返されます。

いくつかのフィールドは条件付きです。JSON-LDは、schema.orgが製品にマークを付けているものを出荷します — gtinmpncolorpriceValidUntilsellerは、ほとんどのカタログ項目に存在しますが、すべての項目にはありません(null可能として扱ってください)。販売フラグの検出、現在のプロモーションテキスト、店舗ごとの在庫状況は、JSON-LDではなくレンダリングされたDOMにあります — ステップ3でその表面をカバーします。星ごとの評価ヒストグラム(5★/4★/3★/2★/1★カウント)については、ステップ6でカバーされるレビューページにインラインでレンダリングされます。


ステップ3 — PDPハイドレートフィールド(価格、履行、画像、仕様)

ステップ2のJSON-LDパスは、静的な製品スキーマを返します。販売価格のオーバーライド、現在のプロモーションテキスト、履行オプション(自宅への配送、店舗でのピックアップ、予定された配達)、在庫ありバッジ、ヒーローイメージギャラリー、および仕様/機能テーブルはすべてレンダリングされたDOMに存在し、Reactがハイドレートした後に届きます。同じPDP上で、クラス名のローテーションを生き残るセマンティックアンカーに対して、発見->抽出フローを駆動します。
最初にPDPの価格領域に対してget htmlディスカバーパスを実行し、アクティブなアンカー名を確認してから、evalセレクタをページが実際に送信する内容に合わせて絞り込みます。クラス名は回転するため、data-testidおよびaria-labelパターンが安定した表面です。

bash Copy
# セッションはまだステップ2からPDP上にあります。価格領域がレンダリングされるのを待ち、
# 周囲のHTMLを覗いてアンカーを確認します。
scrapeless-scraping-browser --session-id $SESSION wait '[data-testid*="price" i], [class*="price" i], [data-component*="Price"]'
scrapeless-scraping-browser --session-id $SESSION get html '[data-testid*="price" i], [data-testid*="fulfillment" i]'

発見されたHTMLから、最も長く存続するアンカーは[data-testid*="price"][data-testid*="fulfillment"][data-testid*="availability"]、及びフルフィルメントボタンのaria-labelaria-label*="Ship to Home"aria-label*="Store Pickup"aria-label*="Scheduled Delivery")です。それからevalでエクストラクタを実行します:

bash Copy
scrapeless-scraping-browser --session-id $SESSION eval '
  (() => {
    const text = (el) => (el ? el.textContent.replace(/\s+/g, " ").trim() : null);
    const number = (s) => {
      if (!s) return null;
      const m = String(s).replace(/,/g, "").match(/-?\d+(?:\.\d+)?/);
      return m ? Number(m[0]) : null;
    };

    const priceText =
      text(document.querySelector("[data-testid*=\"price\" i]")) ||
      text(document.querySelector("[class*=\"price\" i]"));
    const wasPriceText = text(document.querySelector(
      "[data-testid*=\"was-price\" i], [class*=\"was-price\" i], [data-testid*=\"strike\" i]"
    ));
    const onSale = !!wasPriceText && priceText !== wasPriceText;

    const promotion = text(document.querySelector("[data-testid*=\"promo\" i], [data-testid*=\"saving\" i]"));

    const fulfillment = [...document.querySelectorAll(
      "[data-testid*=\"fulfillment\" i] button, [aria-label*=\"Ship\" i], [aria-label*=\"Pickup\" i], [aria-label*=\"Delivery\" i]"
    )].map((btn) => {
      const label = btn.getAttribute("aria-label") || text(btn);
      return label ? { option: label, available: !btn.matches("[disabled], [aria-disabled=\"true\"]") } : null;
    }).filter(Boolean);

    const availabilityText = text(document.querySelector(
      "[data-testid*=\"availability\" i], [data-testid*=\"in-stock\" i]"
    ));

    const heroImg = document.querySelector(
      "img[data-testid*=\"main-image\" i], [data-testid*=\"image-gallery\" i] img, picture img"
    );

    const features = [...document.querySelectorAll(
      "[data-testid*=\"feature\" i] li, [class*=\"feature\" i] li, [data-testid*=\"highlights\" i] li"
    )].map(text).filter(Boolean).slice(0, 20);

    const specs = Object.fromEntries(
      [...document.querySelectorAll("[data-testid*=\"specs\" i] tr, [class*=\"specs\" i] tr")]
        .map((row) => {
          const cells = [...row.querySelectorAll("th, td")].map(text);
          return cells.length >= 2 ? [cells[0], cells.slice(1).join(" ")] : null;
        })
        .filter(Boolean)
    );

    return JSON.stringify({
      productUrl: location.href,
      price: number(priceText),
      priceText,
      wasPrice: number(wasPriceText),
      onSale,
      promotion,
      currency: "USD",
      fulfillment,
      availabilityText,
      image: heroImg?.currentSrc || heroImg?.src || null,
      features,
      specs,
    });
  })()
'

ほとんどの価格情報パイプラインでは、ステップ2とステップ3を同じセッションで実行します:ステップ2は標準的なJSON-LDスキーマ(productId、brand、sku、model、gtin、image、offers)をキャプチャし、ステップ3は水和されたオーバーライド(セールフラグ、プロモーション、フルフィルメントオプション、在庫バッジ、仕様/機能)をキャプチャします。2つのペイロードはproductUrlでクリーンにマージされます。


ステップ4 — 検索およびカテゴリリスト

ホームデポは/s/<query>で検索を公開し、/b/<category-slug>でカテゴリのランディングページを公開しています。両方ともNaoURLオフセット(?Nao=24?Nao=48、…)を介してページネーションします。各ページは約24の製品カードを表示します。

bash Copy
QUERY="cordless drill"
# クエリ内のスペースをエンコードします(ホームデポは標準的なURLエンコーディングを使用)
SEARCH_URL="https://www.homedepot.com/s/${QUERY// /%20}"

scrapeless-scraping-browser --session-id $SESSION open "$SEARCH_URL"
# 検索ページは二段階のレンダリングを行います:最初に
# 単一カードを持つReactプレースホルダースケルトンがマウントされ、その後本物の約24カードのグリッドが
# 構築されます。単純な`wait '[data-testid*="product-pod" i]'`はプレースホルダーで解決できるため、
# エクストラクタは空のグリッドに対して実行されます。実際のグリッド(≥ 5カード)を待機し、
# 単一の一致を待たないようにします。
scrapeless-scraping-browser --session-id $SESSION wait 3000
scrapeless-scraping-browser --session-id $SESSION eval \
  'document.querySelectorAll("[data-testid*=\"product-pod\" i], [data-pod-position]").length >= 5'

scrapeless-scraping-browser --session-id $SESSION eval '
  (() => {
    const text = (el) => (el ? el.textContent.replace(/\s+/g, " ").trim() : null);
    const number = (s) => {
      if (!s) return null;
const m = String(s).replace(/,/g, "").match(/-?\d+(?:\.\d+)?/);
return m ? Number(m[0]) : null;
};
const cards = [...document.querySelectorAll("[data-testid*=\"product-pod\" i], [data-pod-position]")];
const results = cards.map((c) => {
const link = c.querySelector("a[href*=\"/p/\"]");
const href = link?.getAttribute("href");
const productId = href?.match(/\/(\d{6,})(?:[/?#]|$)/)?.[1] || null;
const titleEl = c.querySelector("[data-testid*=\"product-title\" i], [class*=\"product-title\" i], h2, h3");
const ratingEl = c.querySelector("[aria-label*=\"out of\" i]");
const reviewCountEl = c.querySelector("[data-testid*=\"review-count\" i], [class*=\"review-count\" i]");
const priceEl = c.querySelector("[data-testid*=\"price\" i], [class*=\"price\" i]");
const brandEl = c.querySelector("[data-testid*=\"brand\" i], [class*=\"brand\" i]");
const img = c.querySelector("img");
return {
productId,
productUrl: href ? new URL(href, location.origin).href : null,
title: text(titleEl),
brand: text(brandEl),
price: number(text(priceEl)),
priceText: text(priceEl),
rating: number(ratingEl?.getAttribute("aria-label")),
reviewCount: number(text(reviewCountEl)),
image: img?.currentSrc || img?.src || null,
};
}).filter((r) => r.productId || r.productUrl);
const offset = Number(new URLSearchParams(location.search).get("Nao") || 0);
return JSON.stringify({
query: location.href,
page: Math.floor(offset / 24) + 1,
pageSize: results.length,
results,
});
})()

**ページネーションループ** 最初の5ページのために:

```bash
for offset in 0 24 48 72 96; do
scrapeless-scraping-browser --session-id $SESSION open "$SEARCH_URL?Nao=$offset"
scrapeless-scraping-browser --session-id $SESSION wait 1500
scrapeless-scraping-browser --session-id $SESSION wait '[data-testid*="product-pod" i], [data-pod-position]'
scrapeless-scraping-browser --session-id $SESSION eval '/* 同じリスティング抽出器 */' \
> "search-page-$((offset / 24 + 1)).json"
done

多数のクエリにわたるファンアウトを高めるために、各クエリごとに新しいセッションを生成し、ホストあたりの同時実行を ≤3 ワーカーに制限してください(ステップ8の parallel-workers ノートを参照)。カテゴリーページ (/b/<category-slug>) は同じ Nao オフセットと同じ抽出器を受け入れます。


ステップ5 — 店舗ごとの在庫確認(ZIPコード)

ホームデポの独特な特徴は 店舗ごとの在庫状況 です:ロケーションセレクターモーダルを介してホームデポの店舗を設定することで、同じPDP上で店舗ごとの在庫とピックアップETAテキストが表示され、全国的な fulfillment オプションが並びます。 同じフローは、「最も役立つ」レビューの順位付けを地域の役立ち票にシフトさせることもできるため、単一の店舗設定セッションで両方の在庫とレビューコンテキストのユースケースをカバーします。

スニペット内の @e15 / @e22 / @e25 アクセシビリティ参照はプレースホルダです — Scrapeless snapshot -i はページレンダリングごとに参照を動的に割り当てます。スナップショットをキャプチャし、「変更ストア」、「ZIP入力」、「検索/確認ボタン」とラベル付けされた行を見つけ、クリックシーケンスを実行する前に実際の @e<n> 数字に置き換えます。 [data-testid*="store-locator" i] / [data-testid*="store-name" i] セレクタを get html 発見パスでライブDOMに対して確認してください — これらはホームデポの一般的な data-testid 規則に一致しますが、ページがレンダリングするものに合わせて厳密にする必要があります。

bash Copy
ZIP="90015"

# 1. PDPを開いて、ロケーションセレクタのアクセシビリティ参照を表示します。
scrapeless-scraping-browser --session-id $SESSION open \
"https://www.homedepot.com/p/$PRODUCT_SLUG/$PRODUCT_ID"
scrapeless-scraping-browser --session-id $SESSION wait 1500
scrapeless-scraping-browser --session-id $SESSION wait '[data-testid*="store-locator" i], [aria-label*="store" i]'
scrapeless-scraping-browser --session-id $SESSION snapshot -i > /tmp/pdp-refs.txt

# 2. 「ストアを変更」 / 「私のストア」をクリック → ZIPを入力 → 確認します。
#    スナップショットが返すものに応じて、以下の @e<n> 参照を調整してください。
scrapeless-scraping-browser --session-id $SESSION click @e15 # ストアを変更
scrapeless-scraping-browser --session-id $SESSION wait 800
scrapeless-scraping-browser --session-id $SESSION fill @e22 "$ZIP" # ZIP入力
scrapeless-scraping-browser --session-id $SESSION click @e25 # 検索 / 確認
scrapeless-scraping-browser --session-id $SESSION wait 1500
scrapeless-scraping-browser --session-id $SESSION wait '[data-testid*="fulfillment" i], [data-testid*="availability" i]'

# 3. 店舗ごとの在庫 + ストアバッジを抽出します。
scrapeless-scraping-browser --session-id $SESSION eval '
(() => {
const text = (el) => (el ? el.textContent.replace(/\s+/g, " ").trim() : null);
return JSON.stringify({
productUrl: location.href,
store: text(document.querySelector("[data-testid*=\"store-name\" i], [data-testid*=\"my-store\" i]")),
zip: "'$ZIP'",
```json
{
  "availabilityText": "テキスト(document.querySelector(\"[data-testid*='availability' i], [data-testid*='in-stock' i]\"))",
  "pickupEta": "テキスト(document.querySelector(\"[data-testid*='pickup' i], [aria-label*='Pickup' i]\"))",
  "stockCount": "テキスト(document.querySelector(\"[data-testid*='stock-count' i], [class*='stock-count' i]\"))"
}

ストア設定状態は、セッションの残りの間にクッキーに保存されます - その後の呼び出し(ステップ3のハイドレートされたフィールドの評価、ステップ6のレンダリングされたレビューエクストラクター、ステップ7のソートフロー)は、モーダルを再駆動することなく地域のビューを返します。

クッキーセットのフォールバック。 位置選択モーダルがアクセスできない場合(ポップアップブロッカー、A/Bバリアント、一時的なWAF)、同じ結果がクッキーを介して利用可能です — THD_LOCSTORE / THD_PERSIST がストアIDとZIPを保持します。scrapeless-scraping-browser cookies set を介して設定し、次の open がクリックフローなしでストアコンテキストPDPを返します。


ステップ6 — レビュー11+レンダリングされたカルーセル経由

ワークフローがJSON-LD上位10以上を必要とする場合(完全なレビューコーパス、適用されたソート/フィルタ、カスタム日付範囲)、レンダリングされたレビュウィジェットが表面になります。レビューは/p/reviews/<slug>/<productId>/<page> にあり、オンPDPカルーセル内にあります。両方は、同じページネートされた商品レビューのマイクロフロントエンドを介してレンダリングされます。

発見 → 抽出フローを駆動します:

bash Copy
REVIEWS_URL="https://www.homedepot.com/p/reviews/$PRODUCT_SLUG/$PRODUCT_ID/1"

scrapeless-scraping-browser --session-id $SESSION open "$REVIEWS_URL"
scrapeless-scraping-browser --session-id $SESSION wait 1500
# networkidleはホームデポでは安定しないため、レビューカードアンカーを待機します。
scrapeless-scraping-browser --session-id $SESSION wait "#reviews, [itemprop='review'], [data-testid*='review' i]"

# アンカーを確認するためにレビュー区域のHTMLを覗き見します
scrapeless-scraping-browser --session-id $SESSION get html "#reviews, [data-component*='Review' i]"

返されたHTMLから、安定したアンカーを特定します:レビューカードは通常[itemprop='review'][data-testid*='review' i]、または繰り返された<article>/<li>構造の下にあります。星ウィジェットは"5 out of 5"のようなaria-labelを持っています。レビュアー名は繰り返された見出しパターンの近くに位置しています。確認された購入者バッジは安定したテキストコンテンツを公開します。ページネーションボタンはアクセシブルなラベルを持っています。

次に抽出をevalします:

bash Copy
scrapeless-scraping-browser --session-id $SESSION eval '
  (() => {
    const text = (el) => (el ? el.textContent.replace(/\s+/g, " ").trim() : null);
    const number = (v) => {
      if (v == null) return null;
      const m = String(v).replace(/,/g, "").match(/-?\d+(?:\.\d+)?/);
      return m ? Number(m[0]) : null;
    };
    const unique = (xs) => [...new Set(xs.filter(Boolean))];

    const productId =
      location.pathname.match(/\/(\d+)(?:\/\d+)?(?:[/?#]|$)/)?.[1] || null;

    const root =
      document.querySelector("#reviews") ||
      document.querySelector("[data-component*=\"Review\" i]") ||
      document.querySelector("[data-testid*=\"review\" i]") ||
      document.body;

    const cards = [...root.querySelectorAll(
      "[itemprop=\"review\"], [data-testid*=\"review\" i], article, li"
    )].filter((c) => {
      const bodyStr = text(c) || "";
      const hasRating = !!c.querySelector("[aria-label*=\"out of\" i]");
      const looksReviewish = /(verified|recommend|helpful|review|purchased)/i.test(bodyStr);
      return bodyStr.length > 40 && (hasRating || looksReviewish);
    });

    const reviews = cards.map((c) => {
      const bodyStr = text(c) || "";
      const ratingLabel =
        c.querySelector("[aria-label*=\"out of\" i]")?.getAttribute("aria-label") ||
        text(c.querySelector("[data-testid*=\"rating\" i]"));
      const dateEl = c.querySelector("time, [datetime], [data-testid*=\"date\" i]");
      const helpfulText = unique(
        [...c.querySelectorAll("button, [aria-label*=\"helpful\" i]")]
          .map((el) => el.getAttribute("aria-label") || text(el))
      ).join(" ");
      return {
        id: c.getAttribute("id") || c.getAttribute("data-review-id") || null,
        title: text(c.querySelector("[data-testid*=\"title\" i], h3, h4")),
        text: text(c.querySelector("[data-testid*=\"body\" i], p")),
        rating: number(ratingLabel),
        isRecommended: /recommend/i.test(bodyStr)
          ? !/not recommend|would not recommend/i.test(bodyStr) : null,
        badges: unique(
          [...c.querySelectorAll("[data-testid*=\"badge\" i], [class*=\"badge\" i]")].map(text)
            .concat(bodyStr.match(/Verified Purchaser|Verified Buyer|Pro|Staff Pick|Incentivized/gi) || [])
        ),
        reviewer: { name: text(c.querySelector("[data-testid*=\"author\" i], [class*=\"author\" i]")) },
        time: dateEl?.getAttribute("datetime") || text(dateEl),
        original_source: { name: "homedepot.com" },

images: ユニーク([...c.querySelectorAll("img")].map((i) => i.currentSrc || i.src)).map((link) => ({ link })),
total_positive_feedback: number(helpfulText.match(/(\d+)\s*(?:人\s*)?(?:が\s*)?(?:役立った|はい|はい)/i)?.[1]),
total_negative_feedback: number(helpfulText.match(/(\d+)\s*(?:役に立たなかった|いいえ|下)/i)?.[1]),
verified: /verified/i.test(bodyStr),
};
}).filter((r) => r.text || r.title);

Copy
return JSON.stringify({ productId, productUrl: location.href, reviewsReturned: reviews.length, reviews });

})()
'

Copy
レンダードDOMエクストラクタはレビューのページングのパス(ステップ8)、適用されたソート/フィルター(ステップ7)、およびワークフローがレビュー11+を必要とするたびに適用される。欠落したフィールドは、キーを削除するのではなく、`null` または `[]` として扱い、Home Depotがレビューカードテンプレートを回転させたときにダウンストリームパイプラインの安定性を保つ。

> **レビューごとのスキーマ:** `id`, `title`, `text`, `rating`, `badges`, `reviewer.name`(ネスト)、`time`, `original_source.name`(ネスト)、`images[].link`(オブジェクトの配列)、`total_positive_feedback`, `total_negative_feedback`、さらにScrapelessの追加情報 `isRecommended`(レビューのテキストから派生したブール値)と `verified`(バッジのテキストから派生したブール値)。欠落したフィールドは `null` または `[]` として保持し、ダウンストリームの消費者がDOMの回転にわたって安定するようにする。

もしレンドレビューのパスが待機後にゼロカードを返す場合、マイクロフロントエンドが完全に水和していない。待機を再試行し、`Access Denied` が返された場合は新しいセッションで再試行するか、ステップ2のJSON-LDトップ10とGraphQLページングパス(`/federation-gateway/graphql?opname=reviewSentiments`)にフォールバックして詳細なクエリを行う。

---

## ステップ7 — UIを介してレビューをソートおよびフィルタリング

Home Depotはレビューページに(最新、評価最高、評価最低、最も役立つ、写真レビューのみ)ソートオプションを公開している。それらのコントロールを同じ持続的セッション内で操作する — スナップショット → クリックの調整によりクッキーと適用された状態が保持されます。

> スニペット内の `@e34` / `@e35` / `@e36` / `@e41` リファレンスはプレースホルダー — Scrapeless `snapshot -i` はページレンダリングごとにリファレンスを動的に割り当てます。最初にスナップショットをキャプチャし、ソートドロップダウンとフィルターボタンの行を見つけ、実際の数字に置き換えてからクリックシーケンスを実行します。

```bash
# インタラクティブコントロールのアクセシビリティリファレンスを表示する
scrapeless-scraping-browser --session-id $SESSION snapshot -i > /tmp/reviews-refs.txt

# スナップショットからソートドロップダウン/フィルターボタンのリファレンスを特定し、クリックします。
# 通常のHome Depotレビューページでは、これらは次のように示されます:
#   @e34 [button] "ソート基準: 最も役立つ"
#   @e35 [button] "写真のみ"
#   @e36 [button] "確認済み購入者"
# 下の@refナンバーはスナップショットによって返されたものに調整してください。

scrapeless-scraping-browser --session-id $SESSION click @e34   # ソートドロップダウンを開く
scrapeless-scraping-browser --session-id $SESSION wait 800
scrapeless-scraping-browser --session-id $SESSION snapshot -i > /tmp/sort-options.txt
scrapeless-scraping-browser --session-id $SESSION click @e41   # "最新"オプション
scrapeless-scraping-browser --session-id $SESSION wait 1500
scrapeless-scraping-browser --session-id $SESSION wait "#reviews, [data-component*='Review' i]"

# レビュー領域が再描画された後、抽出を再実行する
scrapeless-scraping-browser --session-id $SESSION eval '/* ステップ6と同じレビュー抽出本体 */'

写真のみのパスの場合は、表示される写真レビューのフィルターをクリックします。レビュー領域はフィルタされたセットで再描画され、ステップ6の同じエクストラクターは写真レビューのみを返します。ページが特定の商品に対して写真フィルターを公開していない場合は、すべての表示レビューを抽出し、images.length > 0 でポストフィルタリングを行います。


ステップ8 — レビューをページングする

Home Depotは/p/reviews/<slug>/<productId>/<page>を介してレビューをページングします。2つのパターンが機能します:

パターンA — 表示されている「次へ」コントロールを操作する同じセッション内で、ステップ7を介してソート/フィルタの状態が適用されている場合に便利です。

bash Copy
for page in $(seq 1 5); do
  scrapeless-scraping-browser --session-id $SESSION eval '/* レビュースキーマを抽出 */' \
    > "reviews-page-$page.json"

  # 表示されている「次へ」ページネーションコントロールをクリック
  scrapeless-scraping-browser --session-id $SESSION eval '
    (() => {
      const next = [...document.querySelectorAll("a, button")]
        .find((el) => /next/i.test(el.getAttribute("aria-label") || el.textContent || ""));
      if (!next) return false;
      next.scrollIntoView({ block: "center" });
      next.click();
      return true;
    })()
  '
  scrapeless-scraping-browser --session-id $SESSION wait 1500
  scrapeless-scraping-browser --session-id $SESSION wait "#reviews, [data-component*='Review' i]"
done

パターンB — ページごとの新しいセッションで直接URLでページングする、スケールでの並列抽出に便利です:

bash Copy
PRODUCT_ID="326716329"
SLUG="NextWall-31-35-sq-ft-Off-White-Faux-Shiplap-Vinyl-Paintable-Peel-and-Stick-Wallpaper-Roll-PP10000"

for page in $(seq 1 10); do
ja Copy
SID=$(scrapeless-scraping-browser new-session \
    --name "hd-reviews-p$page" --ttl 300 --proxy-country US --json \
    | jq -r '.data.taskId')

  URL="https://www.homedepot.com/p/reviews/$SLUG/$PRODUCT_ID/$page"
  scrapeless-scraping-browser --session-id $SID open "$URL"
  scrapeless-scraping-browser --session-id $SID wait 1500
  scrapeless-scraping-browser --session-id $SID wait "#reviews, [data-component*='Review' i]"
  scrapeless-scraping-browser --session-id $SID eval '/* review-extraction body */' \
    > "reviews-page-$page.json"
  scrapeless-scraping-browser stop $SID
done

各ページごとにセッションを新規作成します。長いセッションを使用するのではなく、新しいセッションを作成します。 再利用された --session-id は理論的にはより効率的ですが、実際には Scrapeless セッションは空の chrome://new-tab-page/ を返したり、約 3 回のハイドレートページリクエスト後に終了したりします。ページごとの短い有効期限付きセッションは、より信頼性が高く、並列化が簡単で、理解も容易です。

並列ワーカーを実行していますか? デフォルトの ~/.scrapeless-scraping-browser/ デーモンは同じホスト上のプロセス間で共有されます - 複数のワーカーがそれぞれの --session-id を渡していても、他のワーカーのセッションを乗っ取る可能性があります。効果的な方法: 1つのジョブに対するすべての CLI 呼び出しを単一シェルの && 呼び出しとして連鎖させること(デーモンの視点からは原子的です - 他のワーカーはあなたのステップ間に割り込むことができません)、ユニークなセッション名を使用すること(ポートは名前からハッシュ化されます)、およびホストごとに約 3 ワーカーに並行性を制限します。より高いファンアウトが必要な場合は、ホスト間でシャードします。「Parallel CLI Agents」については skill-dev/SKILL.md を参照してください。

ページ間のデュープは、ID が存在する場合は id で、そうでない場合は reviewer.name + time + title + text にフォールバックします。ピン留めされたレビューやスポンサーウィジェットは、ページ間で繰り返されることがあります。


取得する内容

Scraping Browser はライブ DOM を返します - 抽出スキーマは呼び出し側が eval ステップに書き込むものです。ステップ 6 の discover → extract テンプレートを使用した単一のレビュー ページパスでは、今日戻ってくるフィールドは次のようになります:

json Copy
// スキーマは実際にステップ 6 の eval が発行する内容を反映しています。
// フィールド値は説明用のサンプルであり、どの製品の固定スナップショットではありません。
{
  "productId": "326716329",
  "productUrl": "https://www.homedepot.com/p/reviews/<slug>/326716329/1",
  "reviewsReturned": 1,
  "reviews": [
    {
      "id": "abc123",
      "title": "簡単な取り付けと堅実な品質",
      "text": "製品はクリーンに取り付けられ、期待通りに動作しました。",
      "rating": 5,
      "isRecommended": true,
      "badges": ["認証購入者"],
      "reviewer": { "name": "HomeDepotCustomer" },
      "time": "2024-04-04",
      "original_source": { "name": "homedepot.com" },
      "images": [],
      "total_positive_feedback": 8,
      "total_negative_feedback": 0,
      "verified": true
    }
  ]
}

ステップ 6 の eval は productIdproductUrlreviewsReturned、および reviews[] を発行します。評価履歴 (overallRating、星ごとの ratings[])、集計 totalReviews、および製品レベルのメタデータは ステップ 2 の JSON-LD eval によって発行されます - ワークフローが集計 + コーパスを一緒に必要とする場合は、ステップ 2 とステップ 6 の両方を同じセッションに引き込んでください。ステップ 6 の抽出器は、[data-testid*="histogram" i] / [aria-label*="stars" i] 行に対するヒストグラムパスで拡張できますが、上記の eval 本体には含まれていません - 必要に応じて明示的に追加してください。

この出力についてのいくつかの率直な観察は、スケールで実行する前に知っておく価値があります:

  • 水分補給のタイミング。 assets.thdstatic.com/experiences/paginated-product-reviews/* のレビュー マイクロフロントエンドは波のように描画されます:ページクロームと H1 が最初に描画され、その後ヒストグラムとレビューカードが描画されます。レビューカードアンカーに対する wait(ステップ 6)は抽出のゲートです。discover パスがページ クロームのみを返した場合は、もう少し待ってから get html を再実行し、セレクタを絞り込む前に確認してください - カードは通常、1 回のレンダー サイクルの距離にあります。
  • セレクタの安定性。 [itemprop='review'][data-testid*='review' i]、および aria-label*='out of' 星ウィジェットは最も長生きするアンカーです。ハッシュ接頭辞で始まるクラス名はデプロイ時にローテーションします。
  • ヒストグラムの存在。 評価ヒストグラムはレビュー ページにインラインでレンダリングされますが、製品カテゴリーごとに異なる DOM 形状を使用します。その不在を再試行するのではなく、nullable として扱ってください - 非常に少数のレビューがある製品では、まったくレンダリングされない場合があります。
  • 認証購入者バッジ。 二つの安定した表面: 「認証購入者」という文字が記載されたバッジスタイルのスパンと、バッジラッパーにある data-testid。どちらかと一致させます。
  • 写真。 添付された写真のあるレビューは、レビューカード内の <img> 要素を公開します。非写真レビューは単に <img> 子要素を持たない - images.length > 0 はクリーンなフィルターです。
Copy
- **WAFインタースティシャル。** 一部のアロケーションはHome Depotの`アクセス拒否`ページにたどり着きます(`bodyLen` ≪ 1000、レビューマーカーなし)。ステップ6のスクリプトはそのページを検出する必要があります(例:`if (/Access Denied|Error Page/i.test(document.title)) throw`)ので、呼び出し元は新しいセッションで再試行します。

## 無料プランを取得してスクレイプを始める:
Scrapelessの活気あるコミュニティに参加して、$5-10の**無料プラン**を取得し、仲間の革新者たちとつながりましょう。

[Scrapeless公式Discordコミュニティ](https://discord.gg/stFPK2xKHY)
[Scrapeless公式Telegramコミュニティ](https://t.me/+uELlyZh2JGw1M2Ux)

---

## よくある質問

**Q1: Home Depotをスクレイピングするのにプロキシは必要ですか?**  
はい — 100%。Home Depotはアメリカの小売サイトであり、アメリカ以外の出入口には一般的なエラーページを表示します。`--proxy-country US`はすべてのセッションで必要です。

**Q2: 価格と履行オプションはどこにありますか — JSON-LDそれともレンダリングされたDOMですか?**  
両方です。JSON-LDは標準的な`offers.price`、`offers.priceCurrency`、および`offers.availability`(ステップ2)を提供します。セール価格のオーバーライド、現在のプロモーションテキスト、各店舗の在庫バッジ、履行ボタン(自宅配送/受け取り/配達)は、Reactがハイドレートした後にポピュレートされ、レンダリングされたDOMから抽出されます(ステップ3)。

**Q3: 店舗ごとの在庫をどうやってスクレイピングしますか?**  
ロケーションセレクターモーダルを操作します:PDPを開き、「店舗を変更」をクリックし、ZIPを入力し、確認します。同じセッション内の後続のeval呼び出しは地域のビューを返します。完全なスナップショット→クリック→入力の振り付けについてはステップ5を参照してください。モーダルにアクセスできない環境のために、クッキーセットフォールバック(`THD_LOCSTORE` / `THD_PERSIST`)がステップ5の最後に文書化されています。

**Q4: なぜ私のセッションは時々`ERR_TUNNEL_CONNECTION_FAILED`を返しますか?**  
プロキシプールにはアロケーション時に利用可能な住宅IPがありませんでした。新しいセッションを発行し、短時間再試行してください。

**Q5: なぜページは時々`アクセス拒否`を返しますか?**  
Home DepotのWAFは時々新しいアロケーションに挑戦します。新しいセッションを発行し、再試行してください。ステップ6のエクストラクターはエラーページのタイトルを検出し、呼び出し元が新しいセッションで再試行できるようにスローする必要があります。

**Q6: 検索およびカテゴリーのページはどのようにページネーションされますか?**  
`Nao` URLオフセット(`?Nao=24`、`?Nao=48`、…)を介して、ページあたり24枚のカードです。ステップ4ではリスティングエクストラクターとページネーションループを扱っています。カテゴリーランディングページ(`/b/<slug>`)は同じオフセットを受け入れます。

**Q7: 写真レビューのみをスクレイピングできますか?**  
はい。まずは表示可能な写真レビューのフィルターを試してください(ステップ7)。特定の製品にそのコントロールが存在しない場合、すべての表示可能なレビューを抽出し、`images.length > 0`でポストフィルターしてください。

**Q8: 最新または最低評価のレビューをどうやって取得しますか?**  
同じ永続セッション内のUIソートコントロールを使用します — スナップショット→クリックの振り付けはステップ7にあります。関連するコントロールをクリックし、レビュー領域が再レンダリングされるのを待ってから、抽出`eval`を再実行します。

**Q09: Home DepotがDOMを変更した場合はどうなりますか?**  
ディスカバーパスを再実行します:`get html "<region>"`、現在の安定したアンカー(`data-testid`、`aria-label`、`[itemprop]`、意味的ID)を特定し、`eval`セレクターを調整します。ハッシュ化されたクラス名を永続的なセレクターとして出荷しないでください。

**Q10: 1つのセッションで何製品を収集できますか?**  
信頼性のために、1つのScrapelessセッションを小さな論理的スクレイプ(いくつかのPDPまたはいくつかの検索ページ)に保ちなさい。大規模な作業には、タスクごとに新しいセッションを発行し、並行処理をホストごとに≤ 3に保ちます(ステップ8の並行ワーカーの注意)。ファンアウトを高くするためにホスト間でシャーディングします。

**Q11: AIエージェントなしで実行できますか?**  
はい。ステップ1〜8のCLIコマンドは、エンドツーエンドでbashとして動作します。エージェント駆動のワークフロー(スキル + 自然言語プロンプト)は推奨される道です。なぜなら、スキルは発見→抽出パターン、待機の落とし穴、並行ワーカーのルールを持たせるため、プロンプトは1行に保つことができるからです。

**Q12: なぜ単純な製品IDではなく標準の`/p/<slug>/<productId>` URLを使用するのですか?**  
単独のIDのみのURL(例:`/p/<productId>`)は、検証時にHome Depotの一般的なエラーページを返しました。標準のPDP URL `/p/<slug>/<productId>`および標準のレビューURL `/p/reviews/<slug>/<productId>/<page>`は信頼できるブラウザターゲットです。ページを開く前に製品IDをそれらの形状に解決してください。

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

最も人気のある記事

カタログ