商品价格不一致的六条报警,扣完之后一条不剩
本文目录
- 一件商品的价格,在你的站上到底存着几份?
- 按出现顺序比,还是按商品编号比?
- 配对要认哪一层的编号
- 先问它自己跟自己一不一样
- 报出来的那几条不一致,是怎么一条条被扣掉的?
- 一个商品页上摆着几十份商品声明,有几份在说这件商品?
- 比价格写错更常见的,是根本没写全
- 出口把价格发给了你,却不说那是什么货币
- 同一个价,一个出口写1799.99,另一个写179999
- 后台那个库存数量,是怎么跑到公网上去的?
- 那么真正该担心的是哪几件事?
- 三步把四份副本摆到同一屏上
- 常见问题解答
- 我不用Shopify,这篇跟我有关系吗?
- 审计工具报的价格不一致,是不是可以全部忽略?
- 为什么一定要按offers.sku而不是product.sku来配对?
- 页面上有推荐商品的结构化数据,算不算写错了?
- 变体太多,结构化数据要不要每个都写?
- 库存数量泄露怎么关掉?
- 货币这件事,最小的修法是什么?
- 这批数字能代表我的站吗?
- 做这套核对要花多久?
- 权威参考资料
摘要:从131个站里筛出181个能正常打开的商品页,把同一件商品的价格从四个地方各取一份,按报价层的SKU逐条配对,一共对上519条。相同513条,剩下6条差价全部出自地区定价,扣干净之后真正对不上的是0条,库存说法矛盾的只剩1条。真正该操心的在别处:数据出口把价格发给你,却有85.8%不说那是什么货币;一个商品页上摆着的Product声明有38.0%根本不在说这件商品;13个站把后台库存数量直接发到了公网,其中18条是负数。
这件事的起点,是一次没抓到的错。
做电商技术SEO的人,大多在某份审计报告里见过一行红字:商品页的结构化数据价格与页面价格不一致。工具给的证据看着无懈可击,一边是页面上那个数,一边是JSON-LD里那个数,明晃晃差着几十块。于是排期、开工单、找前端,折腾两周。保哥这回想把这行红字彻底查清楚,就从131个海外独立站里捞商品页,把同一件货的价格从四条互不相干的路径各取一份,摆到一起硬碰硬地比。
结果出乎意料:按商品编号严格配对之后,519条报价里真正对不上的是0条。六条看起来不一样的,扣到最后全是量的方式造成的。这一篇讲的就是那六条假警报怎么一条条被扣掉,以及扣完之后,剩在桌面上的真问题长什么样。
一件商品的价格,在你的站上到底存着几份?
先把牌摊开。一个跑在Shopify上的商品页,同一件货的价格不止一份,而是散在五个地方,来路各不相同。
| 副本 | 谁生成的 | 长什么样 | 谁在读 |
|---|---|---|---|
| 页面可见文字 | 主题模板 | $1,799.99 | 人 |
| og:price:amount | 主题的head片段 | 1,799.99 | 社交平台、部分抓取器 |
| JSON-LD的offers.price | 主题或第三方应用 | 1799.99 | 搜索引擎、AI助手 |
| 商品地址加.json后缀 | 平台自带,关不掉 | 字符串形式的元 | 任何人 |
| 商品地址加.js后缀 | 平台自带,关不掉 | 整数179999,单位是分 | 店铺自己的前端脚本 |
前三份是店主这边的产物,改主题、装应用都会动它们。后两份不一样,它们是平台送的,店主没开过,也关不掉——那个一次能拿走250条商品的数据出口,就是同一套东西的列表形态。这次要比的,正是这几份彼此之间对不对得上。
还得先说清楚两件与口径有关的事,不然后面的数字都站不住。第一,页面渲染层没有进严格统计。抽样用真实浏览器渲染了十个站之后发现,一个页面上可见的金额里混着分期月供、划线原价、配件单价、礼品卡面额,casper那一页最靠上的可见数字是46美元的月供,跟商品价599美元完全不是一回事。所以严格比对只在三份机器可读的副本之间做,渲染层只用来抽样核对。
第二,采集全程单线程、间隔2秒。131个站一轮跑下来,商品页能正常打开的181个,数据出口还开着的56个站。第二轮把请求顺序整个反过来重跑,顺便让同一个出口连着问两次——先确认它自己跟自己一样,再谈它跟别人一不一样。
按出现顺序比,还是按商品编号比?
第一版脚本差点让这篇稿子从头错到尾。
那天在jackery的储能电源页面上,脚本报了一个漂亮的数字:页面上写着3,149.00,JSON-LD里第一个Product节点写着12139,差3.86倍。这要是真的,就是一条能上标题的重大发现——给人看一个价,给谷歌报另一个价,差近四倍。
然后用浏览器打开亲眼看了一遍。那一页从上往下滚,3,149、4,639、5,479、7,909、12,139,五档套餐的价钱全都明明白白印在页面上;JSON-LD里那五个Product节点,一个不多一个不少,跟五档一一对应。它没有虚报任何东西,只是节点从贵往便宜排,而og:price取的是最便宜那一档。差3.86倍这个数,完全是按顺序取第一个取出来的。
这件事的教训比它本身值钱:当一页上有多个商品节点时,这一页的价格根本不是一个数。爬虫只会取走其中一个,取哪个由取法决定,跟这个站写得对不对没关系。于是整套比对推倒重来,改成按编号配对——把每个节点的SKU抓出来,去数据出口的变体清单里找同一个SKU,找到了才比价格、比库存、比货币。
顺手把这件事量了一下,好确认jackery不是孤例。在能同时拿到og:price和多档JSON-LD价格的页面里,og取到最低那一档的27个,取到最高那一档的1个,还有1个页面的og:price压根不落在JSON-LD的任何一档上。同一页上两个都是写给机器读的价格字段,本身就未必指向同一档货。另有13个页面的JSON-LD只写了一个价,却和og:price不相等,这里面混着地区定价和划线价两种情况,得逐个看才能断,不能一律算成错。
配对要认哪一层的编号
这里还有个更细的坑,是在baseus那台户外摄像头上撞见的。同一个Product节点里,商品层写着sku为S0003401,报价层的offers.sku却写着S0TW002130,两个是不同的变体,价格差30美元,库存一个有一个无。
价格和库存这两个字段,按schema.org对Offer类型的定义是挂在报价层的,不挂在商品层。所以配对必须以offers.sku为准;拿商品层的编号去配报价层的价,等于把一个节点自己的内部错位,算成了两份副本之间的不一致。这类错位实测中撞到两个站,数量不多,但每一个都会被工具报成价格不符。
先问它自己跟自己一不一样
还有一层不做就不敢下结论的功课。所有跨副本的差异,都建立在一个默认前提上:每份副本在两次请求之间是稳定的。这个前提得自己去验。
做法有两条。一条是让同一个数据出口连着问两次,中间隔着几秒和一次页面请求,比对两次返回的变体清单是否逐字一致;另一条是把整轮请求的先后顺序反过来重跑——第一轮是页面在前、出口在后,第二轮改成出口在前、页面在后。如果一个差异在正序里出现、反序里消失,那它量的就不是数据,是顺序。上一批做HTTP/2帧大小对照时,正是靠反序跑才发现下载窗口那个48%的差距根本不存在,这次把同一套纪律直接搬了过来。
两条都跑完了,结果比预想的干净。51个站做了连问两次这一项,两次返回的变体清单逐字一致的是51个,一个例外都没有。反序那一轮重新配上199条报价,相同197条、不同2条,而这2条正是正序里揪出来的那两个站,一条不多一条不少。再往细里对,49个商品在两轮之间的出口价格一个都没动过。
这一步花掉的时间不算少,一轮下来又是几百个请求。但它换来的是一句能站住的话:后面所有关于差异的结论,都不是抖动、不是缓存、也不是请求顺序变出来的。少了这一步,就只能写成量到了几条差异;有了这一步,才敢写成这几条差异是真的、而且只有这几条。
报出来的那几条不一致,是怎么一条条被扣掉的?
换成按报价层编号配对之后,519条报价对上了,其中513条一模一样,剩6条不同。接下来的活儿就是逐条问:这一条到底是它写错了,还是量错了。
| 扣除项 | 判据 | 扣掉多少 |
|---|---|---|
| 划线价 | 结构化数据里那个价等于出口的compare_at_price | 0条 |
| 地区定价 | 页面自报货币不是店铺基准货币,或规范地址带地区前缀 | 6条 |
| 层间错位 | 商品层SKU与报价层SKU指的不是同一个变体 | 配对阶段已规避 |
| 剩下的真差异 | — | 0条 |
划线价这一关本来是重点防的。商品页上那个被划掉的原价非常容易被结构化数据当成现价写出去,一写出去就是几十块的差额。这次一条都没撞上,算是个意外之喜,但检查还是得留着。
六条全部倒在了地区定价这一关,出自两个站,aloyoga三条、misen三条。aloyoga那条最典型:页面上、og:price里、JSON-LD里,三处齐刷刷写着110.0,货币是SGD;而数据出口给出的是78.00。两个数都没错——页面走了地区定价,数据出口不走。请求从一个被判成新加坡的出口IP发出去,站把它路由到带en-sg前缀的地址,页面那份价格跟着换算成新加坡元,而出口那份纹丝不动,还是店铺基准货币下的那个数。
库存那一侧同样干净:523条能配对的库存声明里,522条与出口完全一致,唯一那条矛盾出在beistravel,根子还是同一个——那个节点压根没写商品层的SKU。生鲜替换那一篇里提过一句缺货状态在页面、商品数据和抓取程序眼里是不是同一个,那时候只是提问,这次算是把它量出来了:在这批样本上是同一个。
所以那行红字,在这批样本里一次都没有真正成立过。上一次量爬虫和用户拿到的页面差异时也是这样,74%一路掉到2.2%;这一次更彻底,直接掉到了0。差别在于那一次剩了2.2%,这一次一条不剩。
一个商品页上摆着几十份商品声明,有几份在说这件商品?
既然价格本身没问题,那些让工具误报的节点又是从哪儿冒出来的?答案就在页面结构里。
153个能判定归属的商品页上,一共数出856个Product节点。按SKU回查出口的变体清单,属于本商品的只有531个,剩下325个、也就是38.0%,说的是页面上别的商品——推荐位、搭配购买、看了又看,那些卡片各自带着自己的结构化数据。45.1%的商品页不止一个Product节点,最多的一页有118个。
这里得交代一个口径,不然856这个数会被误读。采集时每页最多只留了前60个节点,181个页面里有3个因此被截断,其中taylorstitch那一页真实节点数是118个、brooklynbedding是84个、thirdlove是61个。所以856是个保守值,真实总量是1026个,而38.0%这个比例算的是留存下来的那部分。
被截断的那两页恰恰最极端。taylorstitch那件衬衫的页面上摆着118份商品声明,抽出来看的前60份里,属于本商品的是0个——一份都没有在描述你点进来看的那件货。allbirds那双跑鞋更好玩,53个节点里13个是本商品的十三个尺码,另外40个是别人的。工具要是不做归属判定,一头扎进去随便捞一个价出来,报错是必然的,不报才是运气。
比价格写错更常见的,是根本没写全
顺着这条线往下量,还有一个更值得管的数:153个商品页里,把自己所有变体都写进结构化数据的有82个,只写了一部分的43个,一个变体都没写的28个。
多尺码多颜色的商品只标一个价,搜索结果里露出的价格区间就是残缺的。这件事在变体商品的Schema与canonical三层治理里讲过原理,实测下来它比价格写错常见得多,偏偏几乎没有工具会报——因为工具只会比对已经写出来的字段,不会替你数少写了几个。
出口把价格发给了你,却不说那是什么货币
把注意力从页面挪到数据出口,第一个跳出来的问题跟价格对不对没关系,跟价格是什么有关系。
1085个变体记录里,标了货币的154条,没标的931条,八成半以上的价格数字是裸着发出来的。另一个出口更干脆:按这个接口的官方说明,它的价格一律是以分为单位的整数,从头到尾没有货币字段,规范里也不打算给。你拿到一个179999,它可能是1799.99美元,也可能是1799.99新加坡元,出口本身不负责告诉你。
这不是纸上谈兵的风险。同一次采集里,从同一个出口IP出发,页面自报的货币收到了五种:USD一百二十四次、SGD二十一次、EUR九次、DKK三次,还有三次是ANG,荷属安的列斯盾。九个商品页的规范地址直接带上了地区前缀,指向en-sg那一版。也就是说,同一个网址,谁来问、从哪儿问,拿到的定价上下文就不同,而那个不标货币的出口对此一无所知。比价工具、竞品监控、喂给购物代理的数据管道,全都在这个缺口上默认成美元。
这一层的判据其实很简单:跨境价格本地化只做在渲染层是不够的。凡是机器要读的地方,价格和货币必须成对出现,缺一个另一个就没有意义。多市场站还要往前一步,让每个地区版本的结构化数据各写各的货币,而不是全站共用一个基准币。
同一个价,一个出口写1799.99,另一个写179999
平台自带的两个出口,来路相同、指向同一件商品,价格的写法却是两套。
一个给的是字符串形式的元,另一个给的是整数形式的分。930条记录全都遵守各自的规矩,没有例外。规范上两边都没错,可它们放在一起,就是一道等着人踩的题。
踩法有两种,方向相反。一种是拿分当元,把179999当成十七万九千九百九十九,商品瞬间贵一百倍;另一种是拿元当分,1799.99当成十八块钱。前者会让着陆页价格与提交数据不一致这条政策直接生效,整批商品被判成价格异常;后者更危险,因为它看着像一次促销。字符串那一版还有第二层麻烦:它是字符串不是数字,直接参与算术运算的话,不同语言的隐式转换各有各的脾气。
保哥自己的判据是,任何跨系统传价格的地方都别传浮点数,也别传裸整数,传一个把金额和货币绑在一起的结构。这跟产品feed该谁管是同一个道理:数据一旦离开你的页面,接收方对你的单位约定一无所知。列表页那个每千克单价踩的也是这个坑,只不过它错在分母上。
后台那个库存数量,是怎么跑到公网上去的?
这一项是采集时顺手撞见的,撞见之后就没法当没看见。
那个AJAX形态的出口,在变体记录里附带一个字段,写的是这个变体当前还剩多少件。131个站里,13个站的304条变体记录把这个数字原样发到了公网上,中位数56件,最大的一条465件。不需要登录,不需要令牌,一条命令的事。
更有意思的是分布的两端:31条是0,18条是负数。负库存意味着卖出去的比记录里有的还多,也就是超卖已经发生了,正等着人工去收拾。这个状态平时只有运营在后台看得到,现在它挂在公网上,谁想看都能看。
要澄清一句:这个字段并不是所有站都发。同一次采集里626条记录没有它,说明它跟库存跟踪的开关以及主题的配置有关,不是平台强制。但反过来说,那13个站多半也不知道自己在发。库存数量本身不算机密,可它连起来能推的东西不少——上新节奏、动销速度、备货深度,竞品拿一个定时任务盯上几周,你的补货周期就摊开了。这类机读地址没有head标签,想拦只能在响应头和服务端做文章。
缺货的另一头也值得顺手看一眼。产品缺货下架之后的收尾一直是电商SEO的漏勺,而库存字段是判断该走哪条路的第一手依据——它是暂时缺货还是永久停售,页面该保留还是该410,都得先从这里读。
那么真正该担心的是哪几件事?
把这一轮的账结一下。价格一致性,在这批样本里是个伪问题;真正立得住的问题有四条,按轻重排下来是这样。
| 问题 | 实测量级 | 后果落在哪 |
|---|---|---|
| 变体没写全 | 153页里43页只写一部分,28页一个没写 | 搜索结果的价格区间残缺 |
| 出口不标货币 | 931条裸价格,占85.8% | 下游管道默认成美元 |
| 页上混着别人的商品声明 | 856个节点里325个不属于本商品 | 任何按顺序取值的工具都会误报 |
| 库存数量外泄 | 13个站304条,含18条负数 | 补货节奏和超卖状态被看光 |
这四条有个共同点:它们都不会被现成的审计工具标红。工具擅长比对两个数字一不一样,不擅长回答这两个数字是不是在说同一件事。结构化数据审计工具能把字段缺漏扒清楚,但归属判定、货币上下文、层间对齐这几件事,目前还得自己动手。
再往前看一步,这几条的分量还会变。购物代理读你的站时不会像人一样把页面从上到下看一遍,它多半直接奔着结构化数据和数据出口去。人还能靠常识识破一个标错一百倍的价格,管道不会——它只会照单全收,然后把这个数写进比价结果里。免费商品收录这条通道同样吃的是这份数据。
三步把四份副本摆到同一屏上
不用装任何东西,浏览器和一条命令就够。挑一个自己站上有多变体的商品页,照下面三步走一遍。
第一步,把两个出口的原文取回来,先看货币和单位:
curl -s "https://你的域名/products/某个handle.json" | head -c 2000
curl -s "https://你的域名/products/某个handle.js" | head -c 2000重点看三处:变体里的价格是带小数点的字符串还是整数分、有没有货币字段、有没有一个写着剩余件数的字段。第三处如果有值,先去后台把库存跟踪的显示关掉,再决定要不要在服务端拦。
第二步,把页面里的结构化数据抠出来,数一数有几个Product节点,再数一数其中几个的SKU出现在第一步那份变体清单里。差额就是页面替别人做的声明。这一步用JSON-LD校验工具过一遍语法,顺便能发现尾逗号之类会让整块失效的毛病;字段该填哪些、哪些是必填,对照商品摘要的字段要求过一遍就清楚了。
第三步,做一次货币对照:换一个地区的出口去请求同一个商品页,看规范地址会不会变、页面自报的货币会不会变、而两个数据出口那份会不会跟着变。如果页面变了、出口没变,你的下游管道就有一个默认成基准货币的缺口。这一条在只做单一市场的站上不成立,跨境多市场的站必查。页面渲染那一份价格从哪来,可以顺着主题模板拿到的product对象倒推回去。
最后提醒一句关于工具报告的读法。看到价格不一致这类告警,先问三个问题:它是按什么配对的、扣没扣掉划线价、知不知道这一页有地区定价。三个问题里有一个答不上来,那条告警就先别排期。按页面模板抽样做这套核对,比逐条网址跑一遍省得多。
常见问题解答
我不用Shopify,这篇跟我有关系吗?
有一半关系。两个数据出口是平台特有的,别的系统未必有同样的地址;但四份副本这个结构是通用的——页面渲染一份、og标签一份、结构化数据一份、给前端脚本用的接口一份,WooCommerce、Magento、自研前端全都是这个套路。归属判定、层间对齐、货币上下文这三件事,换个平台一样要做。
审计工具报的价格不一致,是不是可以全部忽略?
不能。这批样本只有181个商品页、519条报价,样本里为零不等于全网为零,尤其是装了大量第三方应用、结构化数据由多个来源拼出来的站,出错概率明显更高。正确的读法是把它降级成待核实:用文中那三个问题过一遍,能解释掉的关掉,解释不掉的再排期。
为什么一定要按offers.sku而不是product.sku来配对?
因为价格和库存这两个字段挂在报价层,不挂在商品层。一个Product节点可以描述一个商品系列,而它的offers指向其中某一个具体变体,这在规范里是允许的。实测中撞到两个站的两层编号指向不同变体,如果按商品层配对,这两条就会被算成价格不一致,而它们其实是节点自己的内部错位。
页面上有推荐商品的结构化数据,算不算写错了?
不算错,是常见做法,问题在于没有边界。规范上推荐位带自己的Product标记是合法的,但如果这些节点跟主商品混在同一层、又不通过主实体之类的字段声明主次,机器就只能靠猜。稳妥的做法是主商品那份单独放,并且让页面的主实体明确指向它。
变体太多,结构化数据要不要每个都写?
写不写取决于价格是否相同。价格一致的多变体,写一个带价格区间的报价聚合就够;价格不同的必须逐个写,否则搜索结果里露出的价格区间是残缺的。实测里28个商品页一个变体都没写,这些站的多规格商品在搜索结果里只能显示一个孤零零的价。
库存数量泄露怎么关掉?
先分清是哪一层的事。数量字段跟着库存跟踪设置走,后台关掉跟踪字段就没值了,但这会影响超卖控制,多数站不会这么干。更实际的做法是在服务端拦:那个出口是固定路径,可以在反向代理层对非同源请求做限制,或者用响应头把它挡在索引之外。要注意它本身是主题脚本在用的,一刀切会把加购功能弄坏,改之前先看自己主题调没调它。
货币这件事,最小的修法是什么?
在结构化数据里把货币字段写成这个页面实际渲染的那个货币,而不是店铺基准货币,这是成本最低也最容易被忽略的一步。做多市场的话,同一件商品在不同地区版本上的结构化数据必须各写各的货币,配合地区版本的规范地址一起提交。数据出口那一侧改不动,只能在下游接收端约定好按什么货币解释。
这批数字能代表我的站吗?
机制能,数值不能。四份副本各有来路、报价层与商品层分属两层、出口不做地区定价、单位一分一元,这几条是结构性的,换个样本一样成立。至于零条真差异、38.0%的节点不属于本商品、85.8%不标货币,只描述这131个站在2026年9月这一次采集里的样子,换一批站、换个时间、换个出口IP,绝对值一定会动。
做这套核对要花多久?
单个商品页十分钟,全站要看你有几套模板。商品页模板通常不超过五种,每种挑一个有多变体的样本走一遍三步,半天能收工。真正费时间的不是采集,是判断——每发现一处差异都得回头问一遍是它错了还是你量错了,这一步没法省,省了就会得到一堆假警报。
权威参考资料
本文标题:《商品价格不一致的六条报警,扣完之后一条不剩》
本文链接:https://zhangwenbao.com/product-price-copies-false-positive-audit.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0