用统一Schema抽取多站点商品数据
文章用 Python 展示先定义 JSON Schema,再让模型抽取网页内容并验证输出的流程,以减少逐站维护解析器的负担。还比较自建流程与托管 Actor,并讨论异常 JSON、成本和可信度问题。
AI 深度解读
把不同电商页面里的商品名、价格和库存统一成 JSON,难点往往不是第一次抓取,而是接下来不断增加的网站和改版。作者提出的办法是先定义数据结构,再让 LLM 从页面文字中提取,减少逐站维护解析器的工作。
这个思路有用,但要分清两件事:统一输出结构,不等于保证数据正确。原文既展示了 Python 接入方式和一次实际结果,也给出了失败状态、收费规则与适用边界。作者同时是所介绍工具的开发者,阅读时需要带上这层背景。
先定结构,能省掉哪部分维护
传统抓取通常先研究页面,再写 CSS 选择器。网站一多,就会出现多套解析逻辑,最后却都只输出 title、price、availability。
Schema-first 把顺序倒过来:先约定字段、类型和必填要求,再从不同页面提取相同结构。下游保存、比较和告警的代码因此可以复用。
自己实现也不复杂:用 requests.get(url, timeout=30) 获取 HTML,经 html_to_text 去掉标签、脚本和导航,再用 call_your_llm(prompt(text, SCHEMA)) 请求模型,最后通过 json.loads 和 jsonschema.validate 检查结果。原文示例将三个字段设为字符串,只要求 title、price 必填。
真正麻烦的是外围处理:模型可能返回解释文字、尾逗号或错误类型,需要有次数上限的修复重试;登录墙、验证码和 HTTP 429 可能被误当成正文;长页面截断后,底部字段可能消失。
还有一个容易忽略的契约问题。作者主张页面没写出的字段应返回 null 并标记,但示例 Schema 只允许字符串。如果自己照着实现,需要同步设计空值类型和缺失状态,否则正确表达「不知道」也可能校验失败。
托管工具交付的不只是三个字段
作者开发的 AI Page to JSON: URL + Schema Extractor,把获取页面、调用模型、结构校验和状态返回封装成 Apify Actor。它只通过普通 HTTP 获取公开页面,不启动浏览器。
输入必须有 urls,并提供 fields 和/或完整的 jsonSchema。简单需求可以传三个字段;字段也能附带提示,例如 price: Product price in USD。严格流程可以传入完整 Schema,缺少必填字段的结果会标为 partial,并列出 missingFields。不过,提示是否会触发币种换算,原文没有说清,不能据此认定工具具备可靠换汇能力。
下面是官方 Python 客户端的调用方式,解决提交任务和遍历结果的问题:
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("steady-scrape/ai-page-to-json").call(run_input={
"urls": [
"https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html",
],
"fields": ["title", "price", "availability"],
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
print(row["status"], row["url"], row["data"])
这里要留意 APIFY_TOKEN 从环境变量读取,以及结果通过 defaultDatasetId 获取;示例最后直接打印,正式流程还需要按 status 分流。
作者提供的一次已验证运行,返回了以下裁剪结果:
{
"url": "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html",
"status": "ok",
"data": {
"title": "Tipping the Velvet",
"price": "£53.74",
"availability": "In stock (20 available)"
},
"missingFields": null,
"nullFields": null,
"truncated": false,
"pageTitle": "Tipping the Velvet | Books to Scrape - Sandbox",
"chargeEvent": "page-extracted"
}
要关注的不只是 data:missingFields、nullFields 和 truncated 也应进入后续处理。这个样例证明该页面跑通过,不能代表不同网站上的整体准确率。
每周比价,要先分清失败和缺货
原文给出的业务流程很具体:把不同商店的商品 URL 放在 CSV 中,每周用相同字段运行一次,只保留 status 为 ok 的结果,将 URL、商品名、价格、库存和运行日期追加到表格,再与上周比较。
状态区分决定这张表能不能信。refused 表示登录或付费墙,blocked 对应验证码或 429,js_required 表示需要浏览器渲染,另有 failed。这些结果应单独检查,不能当成商品缺货或价格为空。页面改版后也仍需抽查,因为模型的提取质量可能变化。
按原文列出的事件计费,ok 和 partial 都是每页 $0.010;只要至少成功提取一页,每次运行再收 $0.001;每次启动还有 Apify 的 $0.00005 Actor start event。因此,100 页全部提取的每周任务费用为:
100 × $0.010 + $0.001 + $0.00005 = $1.00105
失败、拒绝、拦截、仅靠 JavaScript 渲染以及空页面不收页面提取费。全部失败时也不收 $0.001 运行费,但启动费仍在。原文还提到可以在运行选项中设置单次费用上限。
我的判断
结构校验通过,只能证明字段形状合规,不能证明价格是真的。
作者把这个边界讲清楚了,这是方案里最值得保留的提醒。一个格式正确的字符串,仍可能是原价、分期金额,或者页面上另一件商品的价格。原文没有提供跨站准确率、重复运行一致性,也没有与 CSS 选择器做对照测试,所以「改版后通常不必重写解析器」仍是经验之谈。
我的取舍是:对于结构各异、数量有限的公开静态页面,这种方式值得试;对于布局稳定、采集频繁、价格直接触发业务动作的页面,确定性解析仍值得优先比较。托管 Actor 省下的是重试、校验、状态处理等工程工作,代价是逐页费用和外部服务依赖。
当前版本不支持浏览器渲染,不绕过登录、付费墙或验证码,也没有个人数据采集功能。长页可能受 maxInputChars 限制,并返回 truncated: true。这些都是选型条件,不能靠调整提示词补齐。
能拿来做什么
- 如果你在维护跨站商品采集脚本,可以先固定
title、price、availability,用一批代表性公开页面验证效果,再决定是否替换现有解析器。 - 在 Node 或 Python 的下游流程中,先检查
status、缺失字段和截断标记,再写入价格表;让访问失败与业务上的无价格分别处理。 - 做每周价格或库存监控时,可以先用 Apify scheduler 定时运行,人工抽查价格和日期,再逐步接入告警,并设置单次费用上限。
- 需要接入自动化流程时,原文给出的入口包括 Apify API、webhooks、Make、Zapier、n8n 和 Apify MCP server;先确认目标页面能通过普通 HTTP 提供内容。