杂货价格抓取:大规模的商店和邮政编码级数据

杂货价格具有高度的本地化特性:同一商品在不同商店和邮政编码的价格不同。要捕捉这些价格,需要进行地理定位请求和UPC匹配。以下是如何构建一个经得起考验的商店级价格数据集。

杂货定价是网络上最具本地化的数据集之一。同一盒麦片在一个连锁店可能是$4.29,而在几英里外的另一家则是$5.19,配送平台根据您设置的商店和邮政编码报价不同的价格。这种变化正是数据的价值所在——对于追踪零售执行的CPG品牌、比较工具以及任何测量货架通胀的人来说。但这也意味着您不能仅仅抓取国家价格;您必须按商店和邮政编码捕捉价格,这涉及到地理定位和产品匹配问题。本指南涵盖了如何收集商店级杂货价格,通过UPC匹配产品,并构建一个经得起考验的购物篮指数。这是一般信息,不是法律建议——请遵守每个网站的条款。

为什么差价是机会

数据说明了一切。当《消费者报告》在六个代表性美国城市的连锁店中进行购物篮比较时,每个城市中最便宜和最贵的主流商店之间的差距超过33%——一旦包括仓储俱乐部和特色杂货店,差距更大。根据圣路易斯联邦储备银行分析的劳工统计局数据,2020年12月至2024年12月间食品价格上涨了25.5%,仅咖啡每年就上涨约20%。每周指数从超过150,000家商店中提取数据。一个捕捉谁最便宜、在哪里、以及每周如何变化的数据集,手动组装起来确实很难——这就是为什么一旦自动化,它就具有防御性。

位置控制价格,因此它控制请求

核心技术:要查看商店的本地价格,您必须以该商店区域的购物者身份出现。在大多数杂货和配送网站上,价格只有在您设置配送邮政编码或选择特定商店后才会显示,网站将其与您的会话和通常您的IP位置相关联。因此,请求有两个动态部分——您在负载中设置的商店/邮政编码,以及一个位于同一区域的出口IP,以便网站信任该位置。目标大都市的住宅IP可以解锁正确的本地定价;另一个州的数据中心IP可能会让您看到默认或被阻止的视图。

import httpx

# A residential exit in the target region unlocks that region's prices
def region_proxy(state):
    return f"http://USER:PASS-country-us-region-{state}@gate.quantumproxies.io:8000"

HEADERS = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
    "accept": "application/json",
}

def fetch_store_price(store_api_url, zip_code, state):
    # Many stores accept the ZIP as a cookie/param that scopes pricing
    r = httpx.get(
        store_api_url,
        params={"zipCode": zip_code},
        headers={**HEADERS, "cookie": f"store_zip={zip_code}"},
        proxy=region_proxy(state),
        timeout=30,
    )
    return r.json()
统计面板显示33%的购物篮价格差距,25.5%的食品通胀,以及每周指数中的150000多家商店
在同一城市的一个连锁店,购物篮的价格可能比另一个高出三分之一——捕捉这种差距就是关键所在。

读取水合JSON,而不是渲染的价格

现代杂货网站大量使用客户端渲染,并且——像大多数大型电子商务应用一样——它们将产品数据嵌入到页面中的JSON中,而不仅仅是呈现在屏幕上。寻找一个水合负载(通常在<script>标签中,如__NEXT_DATA__或商店特定的状态对象)携带价格、库存状态以及关键的UPC或GTIN。读取该JSON比抓取渲染的价格元素要稳健得多,并为您提供匹配所需的条形码。我们关于为什么抓取器返回空页面的说明涵盖了在可见HTML看起来空白时如何找到这些负载。

import re, json

def extract_hydration(html: str) -> dict:
    m = re.search(r'<script id="__NEXT_DATA__"[^>]*>(\{.+?\})</script>',
                  html, re.DOTALL)
    if not m:
        raise ValueError("hydration payload not found")
    return json.loads(m.group(1))

def parse_product(state_json: dict) -> dict:
    p = state_json["props"]["pageProps"]["product"]
    return {
        "upc": p.get("upc") or p.get("gtin"),
        "name": p.get("name"),
        "price": p["price"]["current"],
        "in_stock": p.get("availabilityStatus") == "IN_STOCK",
    }

通过UPC匹配,而不是通过名称

杂货价格数据中最大的错误是通过名称匹配产品。“Cheerios家庭装”、“Cheerios家庭装18盎司”和“通用磨坊Cheerios(家庭装)”是同一商品,在三家商店中有三个标签——通过名称匹配,您的比较就是噪音。相反,通过UPC/GTIN条形码匹配,无论每个商店如何命名产品,它都是相同的。每个严肃的杂货比较工具都这样做;这使得跨商店的购物篮有意义。

from collections import defaultdict

# Group scraped rows by barcode so each item lines up across stores
def compare_by_upc(rows: list[dict]) -> dict:
    by_upc = defaultdict(dict)
    for r in rows:                      # r = {store, upc, price, ...}
        if r.get("upc"):
            by_upc[r["upc"]][r["store"]] = r["price"]
    # cheapest store per item
    return {upc: min(prices, key=prices.get) for upc, prices in by_upc.items()}

从条形码匹配的行中,您可以构建重要的指标:一个购物篮指数。定义一个固定的常用商品篮,在每个商店按邮政编码定价,并总计——这个单一数字,随着时间的推移进行跟踪,揭示了33%的差距和每周的通胀。因为您在多个位置和商店运行相同的收集,通过住宅代理池进行每次请求轮换,保持任何一个出口看起来不正常,同时保留您设置的地理位置。

在地理定位的IP上收集本地杂货价格

扩展到真实数据集

生产级的杂货价格源是一个矩阵:商店×邮政编码×产品×时间。这需要大量请求,也是DIY抓取变得繁重的地方——您在处理地理绑定的会话、在部署之间变化的水合负载,以及大连锁店的反机器人层。一个Scraper API在需要时渲染,轮换地理定位的IP并返回干净的JSON,将大部分内容简化为一个调用,因此您可以将精力放在购物篮逻辑上,而不是管道上。有关同一数据的定价策略方面,请参阅我们的竞争对手价格监控构建价格比较数据层指南。

杂货价格收集的管道图:设置商店和邮政编码,获取水合JSON,通过UPC匹配,构建购物篮指数
设置位置,读取页面传递的JSON,通过条形码匹配,然后随着时间的推移按邮政编码总计购物篮。

常见问题解答

如何按位置抓取杂货价格?

在请求中设置商店或配送邮政编码(通常是参数或cookie),并通过出口IP位于该地区的住宅代理路由,这样网站就会信任该位置并返回本地定价。然后从页面的水合JSON中读取价格和UPC,而不是渲染的元素。按邮政编码重复以构建地理矩阵。

为什么通过UPC而不是名称匹配杂货产品?

因为商店对同一商品的命名不同——尺寸、品牌排序和缩写都不同——所以名称匹配会产生错误匹配和噪音比较。UPC或GTIN条形码在零售商之间是相同的,因此通过它匹配可以让您对比每个商店抓取的完全相同商品的价格。

杂货配送价格真的因邮政编码而异吗?

是的。杂货和配送平台将定价、促销和可用性范围限定在商店内,而商店与您的配送邮政编码相关联,因此同一产品在相邻地区的价格可能不同。《消费者报告》发现,在同一城市中,最便宜和最贵的连锁店之间的购物篮差距超过33%,这就是为什么位置特定的收集很重要。

抓取杂货价格合法吗?

收集公开可见的价格很常见——新闻媒体和价格指数每周都这样做——但这取决于网站的条款和您的司法管辖区。坚持公共产品页面,避免个人数据和登录区域,控制请求频率,并为商业用途获得法律建议。这是一般信息,不是法律建议。

杂货价格数据的生死取决于位置和身份。设置商店和邮政编码,从该地区的住宅IP呈现,读取水合JSON,并通过UPC匹配——然后总计购物篮并随着时间跟踪。这样做跨越足够多的商店和邮政编码,您就拥有了一个手动比较无法企及的数据集。

使用Scraper API构建您的杂货价格源