← すべての記事

食料品価格スクレイピング: どの店舗、どの買い物客か?

食料品価格スクレイピングは静かに失敗します。店舗cookieが失われると、誤った店舗の実際の価格が返されます。店舗の一致を検証する方法と、単一の買い物客アカウントでは不十分な理由を解説します。

ワシントンD.C.にある同じSafewayのLucerne卵1パック(12個入り)が、Instacart上で3.99ドル、4.28ドル、4.59ドル、4.69ドル、4.79ドルで提示されていました。同じ店舗、同じ瞬間、異なる買い物客。これら5つの数値のうち、どれを価格パネルに採用すべきでしょうか。

課題

食料品分野は、価格インテリジェンスが「商品ページを取得して価格をパースするだけ」では通用しなくなる領域です。食料品の価格は店舗に紐づき、多くは配達ゾーンに依存し、さらには閲覧しているユーザー本人によっても変動します。

まずは店舗です。Scrapflyの食料品価格比較ガイドでは、あるWalmartで1ガロンの牛乳が3.98ドル、20マイル離れた別の店舗では4.29ドルとされており、店舗のロケーション情報がないセッションでは、実在するどの店舗とも一致しないデフォルトデータが返されると警告しています。ScrapeInsightの2026年食料品配達ガイドでも、さらに詳細なレベルで同様の現象が確認されています。ある配達ゾーンでは3.99ドル、数マイル離れたゾーンでは4.49ドルでした。Bright DataのShopGrokケーススタディでは、同社の約4年間の事業期間中にオーストラリアの小売価格が郵便番号依存へと移行していった経緯が説明されています。

次は買い物客です。2025年12月、Groundwork Collaborative、Consumer Reports、More Perfect Unionは4都市の買い物客437名を対象とした実証テストを実施し、同じ店舗から同じ日時に同一のInstacartカートを作成させました。その結果、商品の74%で複数の価格が表示されました。価格差が生じた場合、最安値と最高値の差は平均13%に達し、カート全体の合計額でも約7%の変動がありました。

これらが組み合わさることで、食料品データの収集コストを跳ね上げる失敗が生じます。店舗コンテキストが抜け落ちても、システムはエラーを出しません。サイトは何のエラーも返さず、要求していない店舗の正常な価格データを、SKU、数値、タイムスタンプとともに返し、すべてのnullチェックを通過してしまいます。

以前、ブロックがデータポイントのように見えるケースについて取り上げました。今回のケースは、ページ自体が正真正銘の価格ページであるため、検知がさらに困難です。ただし、求めている店舗のものではないという点を除けばです。

レスポンス内で店舗を検証する

多くのコレクターは店舗選択情報(cookie、クエリパラメータ、headerなど)を送信し、それを盲信しています。より堅牢なアプローチは、レスポンス自体にそれを証明させることです。ページ内にレンダリング対象となった店舗名、例えば埋め込みデータ内の店舗IDや受取バナー内の店舗名が含まれている場合、そのマーカーを成功の判定基準とします。FourAでは、Proxy Finderの呼び出し1回でこれが可能です。

import requests

r = requests.post(
    "https://api.foura.ai/api/proxy",
    headers={"X-API-Key": "pk_live_..."},
    json={
        "maxTries": 6,
        "request": {
            "method": "GET",
            "url": "https://grocer.example/product/0001234",
            "headers": [["Cookie", "store=1234"]],
            "validate": {
                "status": {"accept": [200]},
                "data": {
                    "accept": ["\"storeId\":\"1234\""],
                    "fail": ["Just a moment", "Access Denied"]
                }
            }
        }
    }
).json()

if "error" in r:
    report = r.get("attemptReport", {})
    print(report.get("summary", r["error"]))
    if report.get("contentRejected", 0) > report.get("defense", 0) + report.get("noResponse", 0):
        print("the site answered for another store: check the store selection")
else:
    page, exit_id = r["data"], r["proxy"]

そのリクエストにおいて見落としやすい3つの詳細があります。

accept は、指定した文字列のいずれか1つでもページ内に現れると成功と判定します。店舗マーカーの隣に価格マーカーを配置すると、価格が存在するだけで別の店舗のページであっても通過してしまいます。店舗マーカーのみを accept に残し、チャレンジ文字列は fail に配置してください。

独自の Cookie ヘッダーを使用すると、リトライ時の動作が変化します。サイト側が提示されたブラウザを拒否した場合、Proxy Finder は通常、次の試行で別のブラウザファミリーに移行します。ただし、独自の cookie が付加されている場合は異なります。セッションはそれを取得したシグネチャに紐づくため、自身の cookie を保持するリクエストは開始時のブラウザを維持します。特定のチェーンのサイトがブラウザに厳格な場合は、ローテーションに頼るのではなく、ブラウザプロファイル で明示的に指定してください。

また、失敗したタスクからは失敗の種類を特定できます。失敗したすべての Proxy Finder レスポンスには attemptReport が含まれます。defense は bot 検知が確認された応答をカウントし、noResponse はサイトに到達しなかった出口をカウントし、contentRejected は bot 検知なしの HTTP 200 で返されたものの、設定したコンテンツルールによってのみ破棄されたページをカウントします。食料品コレクターにとって、この最後のカウントは特定の意味を持ちます。サイトからの応答はあるものの、対象の店舗ではないということです。出口を増やしてもこの問題は解決しません。その他のカウントについては 試行レポートガイド で説明しています。

ショッパーを固定するか、ばらつきを測定するか

Instacart の調査結果は2つ目の変数を提示しており、対処法は2通りあります。

価格パネル用途では、各店舗の収集を単一のアイデンティティで保持します。正常に取得できた出口(r["proxy"]、不透明な ID)を同一の cookie とともに Single 経由で再実行し、その ID と X-FourA-Request-Id レスポンスヘッダーを各レコードに保存します。価格が変動した際、店舗での変更なのかセッションの変更によるものかを判別できます。

価格調査用途では、意図的に逆のアプローチを取ります。同一店舗内の同一 SKU を複数の独立したセッションを通じてサンプリングし、最初の回答ではなく分布データを保持します。購入者が5種類の価格を目にしている状況で1つの数値しか報告しないパネルは、正確なのではなく単に運が良いだけです。

結果

競合120店舗の2,500 SKU を毎日更新してベンチマークしている地域チェーンを例に挙げます(業界ベンチマークに基づく想定シナリオ)。これは1日あたり30万回のページ読み取りに相当し、どの夜であっても一部は誤った店舗のデータとして返されます。cookie フォーマットの変更、店舗の閉店、あるいはサイトが cookie よりも IP ロケーションを優先し始めたことなどが原因です。

  • 誤った店舗のページは収集時に失敗する。 行として保存されないため、パネル内の価格変動は確実にその店舗での変動となる。
  • 障害が分類されて届く。 高い contentRejected は店舗選択の担当者へ、高い defense はアクセスの問題、高い noResponse は exit の問題となる。3つの担当区分と3つの修正方針があり、原因の推測に午前中を費やすことはなくなる。
  • 全行が追跡可能になる。 観測ごとの exit ID と request ID により、「このスパイクは本物か?」という疑問が単なる検索操作で解決する。
  • 価格差が手元にある数値になる。 複数セッションにわたってサンプリングする SKU について、単一の値ではなく、購入者が実際に目にする価格帯を報告できる。

この手法の限界: 会員価格やロイヤルティ価格はアカウントの背後にあり、ログイン済み購入者を対象とした実験は匿名セッションには表示されない。それらの収集は、エンジニアリング以前に利用規約の問題となる。また、店舗マーカーの信頼性は選択の正確さに依存する。メモリからではなくページソースから取得し、チェーンがサイトをリニューアルするたびに再確認すること。

Key Takeaway

食料品の価格は、かつては商品に関する静的な事実だった。現在では、商品、店舗、そして購入者に関する事実であり、最初の商品情報のみを記録するコレクターは、整ったスキーマを持つノイズを生成しているに過ぎない。

購入者ごとの価格設定が広がり続けるにつれ、バイヤーが食料品データに求める問いは「いくらか?」から「ここではいくらか、そして価格差の幅はどの程度か?」へと移行する。したがって、信頼に値するコレクターとは exit の数が最も多いものではない。すべての request がどの店舗向けのものであるかを提示し、それを証明できるものとなる。