Python 爬虫解析 JSON-LD:多块 script、@graph 与坏数据的 9 个边界
很多商品页、文章页把结构化数据放在 <script type="application/ld+json"> 中。它看起来像一个 JSON 对象,实际爬虫最容易在三个地方出错:只取第一块 script、把 @graph 当普通字段、遇到一块坏 JSON 就让整页失败。
本文用 Python 标准库做一个可运行解析器,回答四个问题:如何找出全部 JSON-LD、如何展开 @graph、怎样隔离坏数据、为什么解析成功也不能直接当成业务真值。
1. JSON-LD 是什么
W3C 的 JSON-LD 1.1 是 JSON 表示的 Linked Data 格式。页面只要把 JSON 放进 script 标签,浏览器不会把它当作可见文字;爬虫仍要先定位标签,再把文本交给 JSON 解析器。@context 定义词汇映射,@type 描述节点类型,@graph 可以在一份文档中放多个节点。
因此不要用一个贪婪正则去抓整段 HTML,也不要假定页面只有一个 Product。生产代码应当保留每个 script 的独立边界。
2. 一个只依赖标准库的解析器
下面的代码用 HTMLParser 收集全部 JSON-LD,再用 json.loads 逐块解析。每块单独捕获 JSONDecodeError,坏块不会吞掉其余有效数据;展开 @graph 时保留节点列表。
python
import json
from html.parser import HTMLParser
class JsonLdParser(HTMLParser):
def __init__(self):
super().__init__()
self.in_jsonld = False
self.parts = []
self.documents = []
def handle_starttag(self, tag, attrs):
attrs = {k.lower(): (v or "") for k, v in attrs}
if tag.lower() == "script" and attrs.get("type", "").lower() == "application/ld+json":
self.in_jsonld, self.parts = True, []
def handle_data(self, data):
if self.in_jsonld:
self.parts.append(data)
def handle_endtag(self, tag):
if tag.lower() == "script" and self.in_jsonld:
self.documents.append("".join(self.parts))
self.in_jsonld = False
def iter_nodes(value):
if isinstance(value, list):
for item in value:
yield from iter_nodes(item)
elif isinstance(value, dict):
graph = value.get("@graph")
if isinstance(graph, list):
yield from iter_nodes(graph)
else:
yield value
真实页面还可能把 JSON-LD 放在数组根节点,或者把 @graph 嵌套一层。上面的递归只负责结构展开,不负责猜测字段含义。
3. 可复现实验与输出
仓库中的 实验/jsonld_demo.py 构造了三块 script:一块 Product、一块包含 BreadcrumbList 和 Product 的 @graph,以及一块缺右花括号的坏 JSON。运行:
bash
python3 实验/jsonld_demo.py
本机 Python 3.13.13、macOS arm64 实测输出:
text
script_tags=3
valid_documents=2
expanded_nodes=3
invalid_documents=1
product_names=键盘,鼠标
这组数字说明"script 标签数""成功文档数"和"节点数"不是同一个概念。一个文档可以贡献多个节点,一块坏数据也不应让整个页面变成零结果。
4. 九个边界要在代码里写出来
type属性大小写和空白要容忍,但必须精确匹配application/ld+json。- 同一页面可能有多块 script,不能只调用一次选择器。
- 根节点可能是数组,不能只对字典调用
.get()。 @graph是节点集合,不是商品对象本身。@type可能是字符串,也可能是数组,业务过滤要兼容两者。- 价格、库存和货币可能在
offers、数组或自定义字段中,字段缺失不能补猜。 - HTML 转义、CDATA 和脚本中的换行都要交给 HTML/JSON 解析器处理,不要手工裁剪引号。
- 一块 JSON 解析失败时记录原文摘要和索引,继续处理其他块。
- JSON-LD 是页面声明,最后的价格与库存仍应以明确的业务接口、时间戳和权限边界为准。
5. 解析结果如何进入爬虫管道
建议把流程分成四层:HTML 定位层只返回脚本文本;JSON 层只负责语法校验;图展开层把 @graph 变成节点流;业务映射层再选择 Product、Article 等类型并做字段校验。每层输出计数和错误原因,抓取任务才知道是页面没有标记,还是标记损坏,还是字段不符合业务契约。
对于价格等数值,先保留原始字符串,再按货币和小数规则转换。对于 URL,记录页面 URL、JSON-LD 中的 url 与 @id,不要因为相似就覆盖。对于重复节点,用稳定的 @id 或页面 URL 去重,并保留来源 script 序号,方便排查模板问题。
6. 与正则方案的取舍
正则可以做快速探测,却难以正确处理嵌套对象、脚本内的 </script> 转义和多块内容。HTMLParser + json.loads 仍然轻量,且每个失败点都能给出异常。需要完整 JSON-LD 处理算法、远程 context 或 RDF 转换时,再选专用实现;不要把简单抓取和语义推理混成一个函数。
结语
JSON-LD 适合做"页面声明的结构化入口",不等于数据库快照。把多块 script、@graph、坏数据和字段缺失都当成正常边界,爬虫才不会因为一页模板变动就静默少抓数据。
参考:W3C JSON-LD 1.1,https://www.w3.org/TR/json-ld11/;实验文件见本文同目录 实验/jsonld_demo.py。