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。

相关推荐
Angeldream12313 分钟前
第 10 章 医学影像伦理与安全
图像处理·人工智能·python·安全·机器学习·医学影像
勤奋的大熊猫28 分钟前
Python 双层 enumerate 循环选取最后一层循环的优雅写法
python
JYCJ_34 分钟前
星愿池·子项清单
python
Wang's Blog36 分钟前
Java 项目实战: 交友项目 - 动态查询开工:昨日回顾与今日任务
java·python·交友
2601_9679791239 分钟前
0 到 1 学爬虫:用 Python 爬取网页数据指南一(附实战案例)
爬虫·python·实战案例·html解析·网页数据
云和数据.ChenGuang1 小时前
光哥说Python 异常深度解析:从异常模型到工程化实践
java·前端·人工智能·python·langchain
SelectDB1 小时前
云数仓月度账单总是超预算:Apache Doris 与 Snowflake 五类成本项拆解与对账脚本
python
迪菲赫尔曼1 小时前
【基础篇 12】把环境与源码接起来:检查 GPU,再安装服务器上的项目
运维·服务器·开发语言·人工智能·python·yolo·目标检测
2601_962885721 小时前
2026 Python 股票数据接口选型指南(横评 + 决策树)
开发语言·python·区块链
龙腾AI白云2 小时前
大模型检索增强生成(RAG)的技术原理与架构设计
数据库·python·机器学习·知识图谱