你花三天写好一套商品抓取脚本,周二下午市场部上线了新版页面,所有 div.price__amount--v2 瞬间返回 null。然后你被迫启动无头浏览器,只为从 HTTP 响应里读一个本来就在那的数字。这种挫败感我经历过一遍又一遍,直到发现一个被很多人忽视的事实:产品数据早就是结构化数据了,安静躺在页面里,而你还在用选择器一棵一棵挖 DOM 树。

过去几年我为二十几家零售商搭过抓取器,一年最多用一次 CSS 选择器。不是我不信任选择器,是它们太像纸牌屋——看似挺稳,随便一阵风就塌。相比之下,直接从页面中的结构化代码块里提货,跨站点复用、跨改版存活的能力高出不止一个数量级。

下边这三种位置,是我每次打开一个商品页面时第一时间检查的。它们不是随机藏在哪里,而是零售网站自己主动塞进去的——有些是为了 SEO,有些是为了前端渲染,有些干脆就是后端接口的原样快照。顺序按可靠性和信息量排好了,直接看。

位置一:零售商亲手交给你的结构化标记

为了拿到搜索结果里带价格和库存状态的富文本展示,零售商会在页面里嵌入 schema.org/Product 标记。这段数据放在一个特殊的脚本块里,包含价格、货币、可用性,有时还有 GTIN 全球贸易项目代码。因为 schema.org 是公开标准,同一个解析器可以一字不改地用在鞋店、五金店、电子数码城——结构完全一致。

提取起来非常简单:拿到页面文本后,用正则匹配出所有符合格式的脚本块,解析成对象,在 @graph 数组或顶级节点里找到 @type 为 Product 的那个条目。name、offers.price、offers.priceCurrency、offers.availability,甚至 gtin13 或 gtin,一路取出来就行。全程不需要浏览器,不依赖任何选择器,一个轻量请求就能搞定。

这条路径的稳定性极高,因为零售商有动力保持 Schema 标签准确——丢了这个,他们就丢了 Google Shopping 的流量。但我要给你泼盆冷水:结构化标记是营销层面的总结,不是库存真相。我真实遇到过一种情况,标签里赫然写着 InStock,实际购买按钮却灰着,真正的库存检查在客户端 hydration 之后才通过另一个接口完成。因此这个位置永远是第一个被调用的,但绝不能是唯一的一个。

位置二:应用状态快照——驱动页面渲染的数据池

如果你的目标站点基于 Next.js、Nuxt.js 或同类框架构建,那么页面的 HTML 里几乎一定藏着一块数据,用来驱动服务端渲染到客户端的 hydration。Next 的是 __NEXT_DATA__,Nuxt 的是 __NUXT__,还有无数站点定义的 window.__INITIAL_STATE__。这块数据往往是干货所在:完整的 SKU 变体矩阵、每种尺寸的库存余量、所有角度的图片集、卖家信息——总之,一切页面渲染需要的东西,比前一种慷慨得多。

拿 Next.js 举例。在页面源码里匹配对应 id 的脚本标签,取出内容,然后顺着 props.pageProps 往下挖,通常就能撞见商品对象。路径每个站点都不一样,头一回确实让人头疼:你得把这个巨大的数据块格式化出来,挑几个疑似包含价格或 SKU 的字段做序列化,肉眼观察哪个节点里躺着你要的数字。但一旦摸清一家店的结构,它就不会再变——这是最丰富的信息源,仅次于直接调用内部 API。

不少开发者只盯着 DOM,甚至不知道还有这么一块数据池。把 __NEXT_DATA__ 里的商品对象展开看一眼你会发现,之前需要用三四个选择器拼凑的字段,这里全在一个树里待着,连属性名的拼写都跟后端一模一样。用这套办法抓取,维护成本从每次改版必崩变成了几个月回头看一次。

位置三:内部移动端接口——当其他位置靠不住时的王牌

有些页面刻意不埋结构化标记,也不预塞应用状态快照,或者故意把关键数据藏在异步加载里。这个时候我直接打开开发者工具,过滤 XHR 请求,找那些返回纯数据格式的移动端接口——它们往往是为了 App 或移动页准备的,返回的库存、价格信息最精确、最鲜活。

这类接口的地址有时藏在页面底部脚本里,有时是某个统一前缀加商品 ID。你需要额外绕一层网络拦截,但回报是即时且无渲染杂质的数据。和前面的位置组合使用,能让你在九成以上的零售站点上彻底告别选择器地狱。