杂货价格抓取:大规模的商店和邮政编码级数据
杂货价格具有高度的本地化特性:同一商品在不同商店和邮政编码的价格不同。要捕捉这些价格,需要进行地理定位请求和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()

读取水合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%的差距和每周的通胀。因为您在多个位置和商店运行相同的收集,通过住宅代理池进行每次请求轮换,保持任何一个出口看起来不正常,同时保留您设置的地理位置。
扩展到真实数据集
生产级的杂货价格源是一个矩阵:商店×邮政编码×产品×时间。这需要大量请求,也是DIY抓取变得繁重的地方——您在处理地理绑定的会话、在部署之间变化的水合负载,以及大连锁店的反机器人层。一个Scraper API在需要时渲染,轮换地理定位的IP并返回干净的JSON,将大部分内容简化为一个调用,因此您可以将精力放在购物篮逻辑上,而不是管道上。有关同一数据的定价策略方面,请参阅我们的竞争对手价格监控和构建价格比较数据层指南。

常见问题解答
如何按位置抓取杂货价格?
在请求中设置商店或配送邮政编码(通常是参数或cookie),并通过出口IP位于该地区的住宅代理路由,这样网站就会信任该位置并返回本地定价。然后从页面的水合JSON中读取价格和UPC,而不是渲染的元素。按邮政编码重复以构建地理矩阵。
为什么通过UPC而不是名称匹配杂货产品?
因为商店对同一商品的命名不同——尺寸、品牌排序和缩写都不同——所以名称匹配会产生错误匹配和噪音比较。UPC或GTIN条形码在零售商之间是相同的,因此通过它匹配可以让您对比每个商店抓取的完全相同商品的价格。
杂货配送价格真的因邮政编码而异吗?
是的。杂货和配送平台将定价、促销和可用性范围限定在商店内,而商店与您的配送邮政编码相关联,因此同一产品在相邻地区的价格可能不同。《消费者报告》发现,在同一城市中,最便宜和最贵的连锁店之间的购物篮差距超过33%,这就是为什么位置特定的收集很重要。
抓取杂货价格合法吗?
收集公开可见的价格很常见——新闻媒体和价格指数每周都这样做——但这取决于网站的条款和您的司法管辖区。坚持公共产品页面,避免个人数据和登录区域,控制请求频率,并为商业用途获得法律建议。这是一般信息,不是法律建议。
杂货价格数据的生死取决于位置和身份。设置商店和邮政编码,从该地区的住宅IP呈现,读取水合JSON,并通过UPC匹配——然后总计购物篮并随着时间跟踪。这样做跨越足够多的商店和邮政编码,您就拥有了一个手动比较无法企及的数据集。