← 全部文章

生鲜杂货价格抓取:哪家门店,哪类顾客?

生鲜杂货价格抓取常出现静默失败:门店 cookie 丢失会导致返回错误门店的真实价格。本文介绍如何验证门店,以及为什么单个顾客画像远远不够。

在华盛顿特区的一家 Safeway,一打 Lucerne 鸡蛋在 Instacart 上的标价分别为 3.99 美元、4.28 美元、4.59 美元、4.69 美元和 4.79 美元。同一家门店,同一时刻,不同的顾客。这五个数字中,哪一个应该写入你的价格看板?

核心挑战

生鲜日杂领域让价格情报不再只是“抓取商品页,解析价格”。杂货价格绑定到特定门店,通常还绑定到配送区域,且越来越取决于正在查看它的用户。

首先是门店维度。Scrapfly 的 生鲜日杂比价指南 指出,一加仑牛奶在一家 Walmart 售价 3.98 美元,而在 20 英里外的另一家则为 4.29 美元,并提醒没有门店位置的 session 会获取默认数据,这些数据可能与任何实体门店都不匹配。ScrapeInsight 的 2026 生鲜配送指南 在更细的粒度上发现了同样的情况:一个配送区域为 3.99 美元,几英里外则是 4.49 美元。Bright Data 的 ShopGrok 案例研究 描述了澳大利亚零售定价在该公司近四年的运营中逐渐演变为依赖邮政编码。

其次是顾客维度。2025 年 12 月,Groundwork Collaborative、Consumer Reports 和 More Perfect Union 在四个城市对 437 名顾客进行了实测,让他们在同一时间从同一家门店加购完全相同的 Instacart 购物车。74% 的商品出现了多种价格。在价格存在差异的情况下,最高价与最低价的平均差距为 13%,整个购物篮的差价约为 7%。

综合这些因素,就会遇到导致生鲜日杂数据成本高昂的失效模式。丢失门店上下文时,系统不会报错。网站不会返回 error。它会返回一个完全有效的价格,只是对应了你并未请求的门店,带有 SKU、数值和时间戳,能通过你配置的每一次 null 检查。

我们之前写过关于 拦截页面看起来像正常数据点 的情况。当前这种情形更难捕获,因为页面确实是一个价格页面。只是并非你所需的目标。

在 Response 中验证门店

大多数采集器会发送门店选择参数(cookie、query parameter 或 header)并直接信任它。更稳妥的做法是让 response 证明这一点。如果页面标明了渲染所针对的门店(例如嵌入数据中的 store ID 或自提横幅中的门店名称),该标记就会成为你的成功判定标准。在 FourA 上,这只需一次 Proxy Finder 调用:

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"]

该 request 中有三个细节很容易出错。

只要页面中出现 accept 中的任意一个字符串,验证就会通过。如果把价格标记和门店标记放在一起,错误门店的页面也会直接通过,因为页面中同样包含价格。请在 accept 中仅保留门店标记,并将验证码或质询字符串放入 fail。

自定义的 Cookie header 会改变重试机制的行为。当目标网站拒绝我们提供的浏览器时,Proxy Finder 通常会在下一次尝试时切换到另一个浏览器家族。但附带你的 cookie 时则不会。session 与获取它时所用的指纹绑定,因此携带自身 cookie 的 request 会保留初始使用的浏览器。如果某个连锁品牌的网站对浏览器要求苛刻,请通过 browser profiles 显式指定一个,而不是依赖自动轮换。

失败的任务会明确指出具体的失败类型。每个失败的 Proxy Finder response 都会携带一个 attemptReport。defense 统计识别到 Bot 拦截的响应数,noResponse 统计未能成功连接到网站的出口数,contentRejected 统计返回 HTTP 200 且无 Bot 拦截、仅因你的内容规则而被丢弃的页面数。对于生鲜数据采集器来说,最后一个计数有特定的含义:网站正常响应了,但返回的不是目标门店的数据。增加出口节点无法解决这个问题。attempt report guide 涵盖了其他计数字段的说明。

固定采集身份,或测量离散度

Instacart 的调研结果引入了第二个变量,有两种严谨的处理方式。

对于价格监控面板,将每个门店的采集任务绑定在单一身份上。使用相同的 cookie,通过 Single 重放成功请求的出口(r["proxy"],一个不透明 ID),并在每行数据中保存该 ID 以及 X-FourA-Request-Id response header。当价格发生跳变时,你可以区分出这是门店的真实调价还是 session 切换导致的变化。

对于定价机制研究,则应刻意反其道而行之。通过多个独立的 session 对同一门店内的同一 SKU 进行抽样,记录价格分布而非单个首次返回的结果。当消费者端存在五种不同报价时,仅报告单一数值的监控面板并不算精准,那只是运气好而已。

结果

以一家区域连锁企业为例,他们每天对 120 家竞争对手门店的 2,500 个 SKU 进行基准对比(基于行业基准的示意场景)。这意味着每天需要读取 300,000 个页面,而在任何给定的夜晚,总会有一部分请求返回错误的门店数据:cookie 格式变更、门店关闭,或者网站开始优先基于 IP 定位而非 cookie 定位。

  • 错误门店页面在采集阶段即告失败。 它们绝不会写入数据行,因此监控面板中的价格变动必定对应具体门店的真实变动。
  • 故障已分类归因。 高 contentRejected 分配给负责门店选择的团队,高 defense 属于访问权限问题,高 noResponse 则归属于出口代理。三方各司其职,对应三种修复方案,无人需要耗费半天排查原因。
  • 每行数据均可追溯。 每个观测记录都关联出口 ID 与 request ID,只需一次检索即可确认“该异常峰值是否真实”。
  • 价差分布转化为明确指标。 针对跨会话采样的 SKU,你可以直接报告消费者实际遇到的价格区间,而非单一采样值。

局限所在:会员价和忠诚度价格隐藏在账号体系之后,针对登录用户的对照实验绝不会出现在匿名会话中。采集这类数据首先是服务条款合规问题,其次才是工程问题。此外,门店标识符的有效性完全取决于提取策略。务必从页面源码提取,不要依赖缓存状态,并在连锁商超改版时重新核验。

Key Takeaway

过去,杂货价格是关于商品的确定事实。如今,它变成了关联商品、门店与消费者的综合事实。若采集器仅记录前者,产出的不过是结构整洁的噪声。

随着个性化定价持续普及,采购方对杂货数据的关注点正从“售价是多少?”转变为“此处的售价是多少,价差区间有多大?”因此,真正值得信赖的采集器并非拥有最多出口代理的系统,而是每次 request 都能明确指出目标门店并提供验证凭证的系统。