bigywater.tech
爬虫工程 · 50

页面里的内嵌 JSON:script 标签里的数据怎么提取

现代网页的数据常常不在可见的 HTML 里,而是塞在 <script> 标签中:JSON-LD、__NEXT_DATA__、window.__STATE__ = …。怎么稳妥地取出来并解析?这篇用 9 种页面写法对比正则、lxml、raw_decode 和 json5 几种做法各自在哪里失效。

N1 对 N2
数据里含 "};" 时,非贪婪正则解析失败,raw_decode 成功
undefined
json5 解析不了的 JS 关键字——要先替换成 null
3 个实体
2 个 JSON-LD 块(其中一个是 @graph)展开后的实体数

type 为 json 的 <script>

实测 · 本机执行输出
  页面类型                                                          M1              M2              M3
  M1 正则取 <script> 再 json.loads / M2 lxml 取 script 文本/属性 再 json.loads / M3 lxml 取 script + json5
  JSON-LD(一个)                                                ✓               ✓               ✓
  __NEXT_DATA__                                              ✓               ✓               ✓
  JSON 里含 <\/script>(合法转义)                                   ✓               ✓               ✓
  数据在属性里(&quot; 实体)                                          空               ✓               空
  两个 JSON-LD(其中一个是 @graph)                                ✓(2)            ✓(2)            ✓(2)
  JSON-LD 里有注释和末尾逗号(不是合法 JSON)                ✗JSONDecodeError✗JSONDecodeError               ✓
  JSON 字符串里含 </script>(没有转义)                  ✗JSONDecodeError✗JSONDecodeError     ✗ValueError

最简单的情形:application/ld+json、__NEXT_DATA__ 等,用 lxml 取 script 文本再 json.loads,或者用正则,都能成功;数据在属性里(data-product="{&quot;…}")时必须由 HTML 解析器解码实体。不合法的 JSON(注释、末尾逗号)要用 json5。如果 JSON 字符串里有没转义的 </script>,HTML 解析器在那里就结束了脚本,没有补救办法。

JS 赋值语句

实测 · 本机执行输出
  页面类型                                                      N1          N2          N3          N4
  N1 正则 {.*?}; 再 json.loads / N2 从 { 起 raw_decode / N3 按花括号配对取出再 json5 / N4 同 N3,先把 undefined 换成 null
  window.__STATE__ = {…};后面还有 JS              ✗JSONDecodeError           ✓           ✓           ✓
  JS 对象字面量(单引号、无引号键、末尾逗号、undefined)           ✗JSONDecodeError✗JSONDecodeError ✗ValueError           ✓

window.__STATE__ = {…}; 后面常跟着别的代码,数据里也可能有 };。可靠的做法是从第一个 { 起用 json.JSONDecoder().raw_decode(只读一个完整 JSON 值),或自己按花括号配对(跳过字符串)取出子串。真正的 JS 对象字面量(单引号、无引号键、末尾逗号、undefined)要用 json5,而 json5 不认识 undefined,要先替换。

细节与陷阱

实测 · 本机执行输出
  非贪婪正则 {.*?}; 遇到字符串里的 "};" :json.loads 报 JSONDecodeError: Unterminated string starting at: line 1 column 94 (在 desc 字段里的 "};" 处就被截断了)
  raw_decode 从第一个 { 开始只读一个完整的 JSON 值,忽略后面的 JS:取到 ['name', 'offers', 'tags', 'desc', 'big']
  JS 对象字面量交给 json.loads/raw_decode:JSONDecodeError: Expecting property name enclosed in double quotes:
  json5 直接解析含 undefined 的字面量:ValueError: <string>:1 Unexpected "u" at column 59(json5 不认识 undefined)
  先把 undefined 换成 null 再用 json5:{'name': 'Widget', 'price': 12.5, 'tags': ['a', 'b'], 'missing': None}
  字符串里含没转义的 </script>:HTML 解析器在第一个 </script> 就结束了脚本。lxml 取到的 script 文本是 '{"html": "'
  转义成 <\/script> 的合法 JSON:正则取到 [{'html': '</script><b>x</b>', 'n': 1}],lxml 取到 [{'html': '</script><b>x</b>', 'n': 1}]
  属性里的 JSON:lxml 自动解码实体;正则直接取到的原始文本是 '{&quot;name&quot;: &quot;Widget&quot;, &'
  整数精度:JSON 里的 9007199254740993 在 Python 里是 9007199254740993(精确);页面里的 JS 如果当 Number 处理,会变成 9007199254740992

合法转义的 <\/script> 没问题;超过 2^53 的整数在 Python 里精确,在浏览器的 JS 里不精确,两边看到的值不同;提取出来的字符串解码后才是真实内容。

JSON-LD 的 @graph

实测 · 本机执行输出
  共 2 个块,展开 @graph 之后有 3 个实体:[('Product', 'A'), ('Org', 'O'), ('WebSite', 'W')]

一个页面可以有多个 JSON-LD 块,块里可以用 @graph 放一个实体列表,遍历时要展开。

分步演示

内嵌 JSON
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。9 种页面由本机脚本生成(JSON-LD、__NEXT_DATA__、JS 赋值、JS 对象字面量、合法转义的 <\/script>、属性里的 JSON、两个 JSON-LD、带注释的伪 JSON、没转义的 </script>);lxml 6.1.1、标准库 json 与 json.JSONDecoder.raw_decode、json5(pip 装进临时目录,实验后删除);所有解析都在字符串上完成,不发网络请求
    • 没有验证:真实站点的内嵌 JSON(大小、压缩、混淆、分块加载);用 Node 执行 JS 对象字面量得到数据的做法(执行不可信的 JS 有安全风险,没有测);内嵌数据与可见 HTML 不一致时以谁为准;JSON-LD 的各种 @context、@id 引用的解析;script 标签 type="module" 或带 nonce 的情形;webpack/rsc 流式数据格式(如 self.__next_f.push)
    京ICP备2026067502号-1京公网安备11010502063487号