Python 爬虫解析 JSON-LD:多块 script、@graph 与坏数据的 9 个边界

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. 九个边界要在代码里写出来

  1. type 属性大小写和空白要容忍,但必须精确匹配 application/ld+json。
  2. 同一页面可能有多块 script,不能只调用一次选择器。
  3. 根节点可能是数组,不能只对字典调用 .get()。
  4. @graph 是节点集合,不是商品对象本身。
  5. @type 可能是字符串,也可能是数组,业务过滤要兼容两者。
  6. 价格、库存和货币可能在 offers、数组或自定义字段中,字段缺失不能补猜。
  7. HTML 转义、CDATA 和脚本中的换行都要交给 HTML/JSON 解析器处理,不要手工裁剪引号。
  8. 一块 JSON 解析失败时记录原文摘要和索引,继续处理其他块。
  9. 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。

相关推荐
要吃这碗饭1 小时前
YouTube 数据抓取出现 429/403 错误?2026 反爬机制拆解与爬虫选型指南
网络·爬虫·网络协议
jason.zeng@15022071 小时前
(八)现有架构上新增一个通用Excel导出工具
python·架构·langchain·excel·llama
袁袁袁袁满1 小时前
AI Agent 如何“看懂“互联网?
爬虫·python·自动化·爬虫实战·多线程爬虫
泡海椒1 小时前
JQuick-Excel 实战:用 VALIDATION 建立可维护的 Excel 导入校验
开发语言·python·excel
Duang007_1 小时前
生产可观测性:从“系统慢“到“根因“的完整链路(Go / TypeScript)
后端·python·golang·typescript·prometheus
jason.zeng@15022072 小时前
(九)多轮对话式新增维修记录实现方案
python·prompt·交互·llama
黑妹天下第一乖2 小时前
小智改造实战解读-首 token 延迟去哪了:云端与端侧大模型的分段对照
开发语言·人工智能·python·嵌入式硬件·自然语言处理·iot
坊钰2 小时前
【LangChain框架入门级】10. 文本向量与向量数据库(Embedding / Redis / Pinecone / MMR)
数据库·python·langchain·embedding
benchmark_cc2 小时前
本地已有一年历史 K 线,第二天更新别只拉“昨天一天”
python·数据分析·pandas·量化交易·股票数据·quantdash