生鲜杂货价格是本地化的。一盒鸡蛋在郊区门店是一个价,在市中心是另一个价,在外卖配送平台上又是第三个价,因为平台会在同样的货架价格上加上自己的加价。如果你要对标当地竞争对手定价、检查某个品牌的促销活动,或按地区追踪食品通胀,你需要的是每个目标城市的价格,而不是一个全国平均数。这是价格情报的核心工作,而且对生鲜杂货来说比大多数零售品类都更难。
本文介绍生鲜杂货价格在哪些方面存在差异、外卖配送平台如何决定你看到哪家门店,以及如何搭建一套像当地购物者一样读取每个城市价格的采集方案。
生鲜杂货价格在哪些方面存在差异
超市很少只有一张价目表。常见的层级包括:
- 价格区域。 连锁超市会根据当地竞争和成本把门店划分为不同区域。同一连锁品牌相距几公里的两家门店,可能处于不同的价格区域。
- 门店级促销。 每周的促销传单和会员优惠往往因地区而异,有些优惠只在指定门店有效。
- 外卖配送平台加价。 第三方平台显示的价格可能高于门店货架价,另外还有随地区和时段变化的服务费和配送费。
- 商品供应。 一件商品可能在一家门店有货,在另一家缺货,这会改变一篮商品的实际成本。
- 包装规格和自有品牌。 同一产品线在不同地区可能有不同规格,如果不比较单位价格,价格变化就会被掩盖。
只做一次全国范围的抓取,会把这些因素混成一个误导性的数字。你记录的每个价格都需要附上门店、城市和渠道信息。
外卖配送平台和超市网站如何选择门店
大多数生鲜杂货网站在知道你的位置之前,不会展示完整的商品目录。它们大致按以下顺序判断:
- 已保存的门店或地址,来自 cookie 或账户。
- 你在门店选择器或配送范围检查中输入的地址或邮政编码。
- 你的 IP 位置,在你做出任何选择之前作为初步判断。
这带来一个实际影响。IP 能让你进入正确的城市,并让初步判断落在你想要的门店附近。而具体门店,精确到邮政编码,则由你提供给网站的地址决定。所以可靠的方案要两者兼顾:使用目标城市的住宅 IP,并在同一会话中输入门店或配送地址,让两者始终保持一致。我们关于何时需要城市级代理定向的文章解释了为什么 IP 与所选位置不一致会改变网站返回的内容。
确定商品篮
在开始采集之前,先确定你要比较的内容:
- 一篮固定商品,50 到 300 个:日常必需品、你自己的畅销品,以及竞争对手促销最多的商品。
- 每个商品的匹配键,例如网站提供时使用条形码(GTIN),否则按品牌、名称和规格匹配,并人工核对一次。
- 单位价格(每公斤、每升、每件)与货架价格并列记录,这样包装规格的变化就不会被误认为价格变化。
- 每个城市的门店清单:对你来说,哪些门店和哪些配送区域能代表这个城市。
保持商品篮稳定。如果每周都换,趋势就无法解读。
逐个城市采集
对于每个城市,通过定向到该城市的住宅代理开启一个会话,在网站中设置门店,然后读取商品篮。用粘性会话 ID 让整个会话保持同一个 IP,这样网站就不会看到购物者在中途切换位置。
customer-USERNAME-country-us-city-chicago-sid-chi-store-118-ttl-900:PASSWORD
会话 ID(sid)会在 ttl 指定的秒数内保持同一个出口。每家门店使用一个 ID,让每次门店访问看起来都像同一位购物者。对于大型市场,把工作分散到主要城市,例如美国页面上的芝加哥、达拉斯和亚特兰大,而不是从一个地点读取整个国家。
一个最简的 Python 循环如下:
import requests
GATEWAY = "p.shifter.io:443"
USER, PASS = "USERNAME", "PASSWORD"
def session_for(city, store_id):
s = requests.Session()
proxy = f"http://customer-{USER}-country-us-city-{city}-sid-{city}-{store_id}-ttl-900:{PASS}@{GATEWAY}"
s.proxies = {"http": proxy, "https": proxy}
return s
def read_basket(city, store_id, products):
s = session_for(city, store_id)
# 1. Set the store the way the site expects (store picker or address check).
s.post("https://grocer.example/api/store/select", json={"storeId": store_id}, timeout=30)
rows = []
for p in products:
r = s.get(f"https://grocer.example/api/product/{p['sku']}", timeout=30)
if r.status_code != 200:
rows.append({**p, "city": city, "store": store_id, "error": r.status_code})
continue
d = r.json()
rows.append({**p, "city": city, "store": store_id,
"price": d.get("price"), "unit_price": d.get("unitPrice"),
"promo": d.get("promotion"), "in_stock": d.get("available")})
return rows
这里的接口地址只是占位符。很多超市网站在设置门店后会从一个 JSON 接口加载价格;读取这个接口比解析页面更轻量、更稳定。
读取外卖配送平台数据
外卖配送平台多出两项内容,必须分开记录:平台内的商品价格和各项费用。需要采集:
- 平台内的商品价格,与超市自有网站上的货架价格并列,用来衡量加价幅度。
- 固定订单金额下的服务费和配送费,因为费用往往取决于订单总额。
- 采集时间,因为费用和商品供应在一天之内会发生变化。
同类比较:同一门店、同一篮商品、同一时段。
保持数据干净
- 每个价格都要记录城市、门店、渠道、时间和单位价格。 没有门店信息,你就无法解释价格变化。
- 标记促销,而不是把它们平均进去。为期两周的优惠并不是降价。
- 关注缺失商品。 价格为空往往意味着缺货,这本身就是一个信号。
- 检查个性化价格。 登录后的会员价可能与访客价不同。除非你研究的就是会员定价,否则以访客身份采集。我们的检测地域个性化定价和动态定价指南介绍了如何区分这两者。
- 只采集公开价格,控制合理的请求频率,并遵守每个网站的条款。
总结
生鲜杂货价格只有附上城市、门店和渠道才有意义。确定商品篮,决定哪些门店代表每个城市,通过当地的住宅 IP 进入每个城市,在同一会话中在网站内设置门店,并分别记录单位价格、促销和费用。日常必需品每周采集,竞争商品每天采集,你就能在本地价格变动发生时及时看到,而不是只得到一个模糊的全国数字。