现代网页的数据常常不在可见的 HTML 里,而是塞在 <script> 标签中:JSON-LD、__NEXT_DATA__、window.__STATE__ = …。怎么稳妥地取出来并解析?这篇用 9 种页面写法对比正则、lxml、raw_decode 和 json5 几种做法各自在哪里失效。
页面类型 M1 M2 M3 M1 正则取 <script> 再 json.loads / M2 lxml 取 script 文本/属性 再 json.loads / M3 lxml 取 script + json5 JSON-LD(一个) ✓ ✓ ✓ __NEXT_DATA__ ✓ ✓ ✓ JSON 里含 <\/script>(合法转义) ✓ ✓ ✓ 数据在属性里(" 实体) 空 ✓ 空 两个 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="{"…}")时必须由 HTML 解析器解码实体。不合法的 JSON(注释、末尾逗号)要用 json5。如果 JSON 字符串里有没转义的 </script>,HTML 解析器在那里就结束了脚本,没有补救办法。
页面类型 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 自动解码实体;正则直接取到的原始文本是 '{"name": "Widget", &'
整数精度:JSON 里的 9007199254740993 在 Python 里是 9007199254740993(精确);页面里的 JS 如果当 Number 处理,会变成 9007199254740992合法转义的 <\/script> 没问题;超过 2^53 的整数在 Python 里精确,在浏览器的 JS 里不精确,两边看到的值不同;提取出来的字符串解码后才是真实内容。
共 2 个块,展开 @graph 之后有 3 个实体:[('Product', 'A'), ('Org', 'O'), ('WebSite', 'W')]一个页面可以有多个 JSON-LD 块,块里可以用 @graph 放一个实体列表,遍历时要展开。