使用Python进行JSON-LD网络抓取:提取结构化数据
Advanced Data Extraction Specialist
TL;DR:
- JSON-LD 通常是从网页到类型化记录的最短路径。 在编写标题、作者、出版日期、图像或产品字段的选择器之前,查找
<script type="application/ld+json">。 - 阅读每个 JSON-LD 块。 一页可能会将组织、面包屑、文章、产品和 FAQ 数据分散在多个脚本中。
- 规范化三个顶层结构。 一个 JSON-LD 块可以是一个对象、一组对象,或一个其
@graph包含有用节点的对象。 - Schema.org 字段在实践中是可选的。 通过
@type选择节点,将缺失的字段保持为None,仅验证管道真正需要的字段。 - Beautiful Soup 不执行 JavaScript。 如果页面在加载后注入 JSON-LD,首先获取呈现的 HTML,然后在该响应上运行相同的解析器。
- 您可以在没有云模型的情况下测试解析器。 以下完整的 Python 示例读取一个真实的
Article节点,将其标题与可见的 H1 进行比较,并验证输出。 - 从公共、有限的页面开始。 创建一个免费的 Scrapeless 账户,当您的目标需要呈现的 HTML 时。
JSON-LD 通常包含抓取器即将从分散的页面元素中重建的字段。在一篇实时的 Scrapeless 文章中,一个单独的 Article 对象包含标题、作者、出版商、日期、规范 URL、主图像、描述和关键词。可见页面仍然很重要,但结构化元数据为提取管道提供了一个类型化的起点。
JSON-LD 遵循 JSON-LD 1.1 规范,而 Article、Product 和 BreadcrumbList 等词汇源自 Schema.org 的结构化数据模型。这两个标准都无法保证每个出版商填写每个属性。您的解析器必须保留这种不确定性,而不是发明值。
JSON-LD 网页抓取的优势
当页面发布机器可读的信息而不是仅仅人类可读的布局时,JSON-LD 是有用的。常见节点包括:
Article和NewsArticle用于标题、日期、作者、图像和出版商;Product用于名称、品牌、优惠、评分和标识符;BreadcrumbList用于层级和规范类别路径;Organization、Person和LocalBusiness用于实体元数据;VideoObject、Recipe、Event及其他特定领域类型。
脚本块不是私有端点。它是页面响应的一部分,旨在供机器使用,比如搜索爬虫。这使得它比生成的 CSS 类更具耐用性,但并不自动完整或正确。将其视为一个源来进行验证,而不是一个神谕。
安装 Beautiful Soup
本指南使用 Python 3.10 或更高版本、requests 和 beautifulsoup4 4.15.0:
bash
python -m pip install "requests>=2.32,<3" "beautifulsoup4==4.15.0"
Beautiful Soup 的 树搜索 API 可以根据属性筛选标签,这足以收集每个匹配的脚本。JSON 解码保持在 Python 的标准库中。
获取源 HTML
从普通的 HTTP 请求开始。这里使用的目标在初始响应中发布其 JSON-LD,因此 JavaScript 渲染会增加成本而不改变结果:
python
import requests
URL = "https://www.scrapeless.com/zh/blog/what-is-web-scraping?utm_source=website&utm_medium=blog&utm_campaign=universalscrapingapi&utm_term=json-ld-structured-data-web-scraping"
response = requests.get(
URL,
headers={"User-Agent": "Mozilla/5.0"},
timeout=30,
)
response.raise_for_status()
print("HTML 字节:", len(response.content))
text
HTML 字节: 439487
字节计数在页面包更改时可能会变化。重要的不变因素是响应至少包含一个可解码的 application/ld+json 脚本。
解析每个 JSON-LD 块
除非页面合同明确承诺一个块,否则不要使用 soup.find(...)。find_all 保留了文章、面包屑和出版商可能存在于不同脚本中的可能性:
python
import json
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, "html.parser")
scripts = soup.find_all("script", type="application/ld+json")
parsed_blocks = []
for script in scripts:
raw = script.get_text(strip=True)
try:
parsed_blocks.append(json.loads(raw))
except json.JSONDecodeError:
continue
print("JSON-LD 块数:", len(scripts))
print("解码块数:", len(parsed_blocks))
跳过格式错误的块只有在您同时记录了跳过的数量时才是安全的。一个无声的 except 可能会将元数据回归变成一个看似成功的空数据集。
规范化对象、数组和 @graph
JSON-LD 的作者有几种有效的方式来分组节点。这个生成器压平了抓取器最常遇到的三种形状:
python
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):
for item in graph:
yield from iter_nodes(item)
else:
yield value
nodes = [node for block in parsed_blocks for node in iter_nodes(block)]
article = next(node for node in nodes if node.get("@type") == "Article")
print("规范化的节点数量:", len(nodes))
print("选定的类型:", article["@type"])
@type 也可以是一个数组。如果您的语料库包括发布者发出的 "@type": ["Article", "NewsArticle"],请在测试成员资格之前规范化该字段。
构建可空记录
嵌套对象需要与顶层字段一样的关注。作者可能是一个字典、一个列表、一串字符串,或者不存在。这个目标使用字典,因此示例以防御性方式读取它,并将缺失的可选字段保留为 None:
python
visible_h1 = soup.find("h1").get_text(" ", strip=True)
author = article.get("author") or {}
publisher = article.get("publisher") or {}
record = {
"type": article.get("@type"),
"headline": article.get("headline"),
"visible_h1": visible_h1,
"author": author.get("name") if isinstance(author, dict) else None,
"publisher": publisher.get("name") if isinstance(publisher, dict) else None,
"published": article.get("datePublished"),
"modified": article.get("dateModified"),
"image": article.get("image"),
"description": article.get("description"),
"keywords": article.get("keywords"),
"source_url": URL,
}
在每一行中保留 source_url 使得后续审计成为可能。没有来源,已修正的解析器无法判断哪些记录需要重建。
验证您的管道所需的字段
验证应反映下游合同,而不是 Schema.org 允许的每个属性:
python
required = ("headline", "author", "published", "source_url")
missing = [field for field in required if not record.get(field)]
if missing:
raise ValueError(f"缺失的必填字段: {missing}")
print("标题:", record["headline"])
print("可见 H1:", record["visible_h1"])
print("标题匹配:", record["headline"] == record["visible_h1"])
print("作者:", record["author"])
print("发布者:", record["publisher"])
print("发布日期:", record["published"])
text
标题: 什么是网页抓取?权威指南 2025
可见 H1: 什么是网页抓取?权威指南 2025
标题匹配: 真
作者: Emily Chen
发布者: Scrapeless
发布日期: 2025-09-17T08:35:31.224Z
该页面的标题匹配。不要对此结果进行概括:编辑有时会更新可见 H1,而不更新结构化元数据,或者在 JSON-LD 中使用更短的搜索标题。比较两个表面的数据是一个有用的质量检查。
准备在渲染响应上运行相同的解析器吗?打开一个 Scrapeless 账户,并保持解析代码不变。
完整可运行的提取器
完整的脚本结合了发现、规范化、选择、可空映射和验证:
python
import json
import requests
from bs4 import BeautifulSoup
URL = "https://www.scrapeless.com/zh/blog/what-is-web-scraping?utm_source=website&utm_medium=blog&utm_campaign=universalscrapingapi&utm_term=json-ld-structured-data-web-scraping"
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):
for item in graph:
yield from iter_nodes(item)
else:
yield value
response = requests.get(
URL,
headers={"User-Agent": "Mozilla/5.0"},
timeout=30,
)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
nodes = []
invalid_blocks = 0
scripts = soup.find_all("script", type="application/ld+json")
for script in scripts:
try:
nodes.extend(iter_nodes(json.loads(script.get_text(strip=True))))
except json.JSONDecodeError:
invalid_blocks += 1
article = next(node for node in nodes if node.get("@type") == "Article")
author = article.get("author") or {}
publisher = article.get("publisher") or {}
visible_h1 = soup.find("h1").get_text(" ", strip=True)
record = {
"type": article.get("@type"),
"headline": article.get("headline"),
"visible_h1": visible_h1,
"author": author.get("name") if isinstance(author, dict) else None,
"publisher": publisher.get("name") if isinstance(publisher, dict) else None,
"published": article.get("datePublished"),
"image": article.get("image"),
"keywords": article.get("keywords"),
"source_url": URL,
}
required = ("headline", "author", "published", "source_url")
missing = [field for field in required if not record.get(field)]
if missing:
raise ValueError(f"缺少必需的字段: {missing}")
print(f"HTML 字节数: {len(response.content)}")
print(f"JSON-LD 块数: {len(scripts)}")
print(f"标准化节点数: {len(nodes)}")
print(f"无效块数: {invalid_blocks}")
print(f"类型: {record['type']}")
print(f"标题: {record['headline']}")
print(f"可见 H1: {record['visible_h1']}")
print(f"标题匹配: {record['headline'] == record['visible_h1']}")
print(f"作者: {record['author']}")
print(f"出版社: {record['publisher']}")
print(f"发布日期: {record['published']}")
print(f"关键词字符数: {len(record['keywords'] or '')}")
实时运行返回了一个有效块,一个标准化的 Article 节点,没有格式错误的块,标题匹配,作者为 Emily Chen,出版社为 Scrapeless,关键词字符串中有 140 个字符。
当 JSON-LD 只在渲染后出现时
Beautiful Soup 解析它所接收的字节;它不会运行页面的 JavaScript。快速诊断方法是将原始响应与浏览器 DOM 进行比较。如果浏览器显示 application/ld+json 脚本但 requests 没有找到,则在解析之前获取渲染的 HTML。
注意:下面的请求需要一个已资助的 Scrapeless 帐号。验证帐户在最终检查期间返回了余额不足的响应,因此这个 HTTP 调用是一个先决条件;上述解析器完全针对真实的公共页面运行。
python
import os
import requests
rendered = requests.post(
"https://api.scrapeless.com/api/v2/unlocker/request",
headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"]},
json={
"actor": "unlocker.webunlocker",
"input": {
"url": URL,
"method": "GET",
"js_render": True,
},
},
timeout=90,
)
rendered.raise_for_status()
html = rendered.json()["data"]
将 html 传入 BeautifulSoup 并重用相同的标准化器。通用抓取 API 提供渲染的响应;它不会改变 JSON-LD 合同。
常见的 JSON-LD 数据问题
页面有多个匹配的节点
同时按类型和身份进行选择。对于产品变体,使用 @id、URL、SKU 或其他稳定字段,而不仅仅是选择第一个 Product 节点。
@type 是一个数组
在检查成员资格之前,将其转换为集合。对于字符串的严格相等测试将会错过有效的多类型节点。
脚本包含 HTML 实体或注释
JSON-LD 应该是有效的 JSON 文本。如果出版商将其包裹在无效语法中,请将该块记录为格式错误,并修复该来源的解析器;不要应用广泛的字符串替换,这可能会破坏合法值。
结构化元数据与可见文本不一致
存储这两个值并为您的用例定义优先级。搜索审核可能更喜欢 JSON-LD 标题;内容监控可能更喜欢可见的 H1。不匹配是一种数据,而不仅仅是错误。
字段从对象变为列表
在边界处进行标准化。作者和图像通常在出版商的 CMS 发展过程中在一个对象和数组之间切换。
结论
一个可靠的 JSON-LD 抓取器有四个功能:收集每个匹配的脚本,解码而不隐藏无效块,标准化字典、列表和 @graph,然后验证一个小的下游合同。这个路径比从页面布局选择器重建相同记录更短,更稳定。保持可见的 DOM 作为交叉检查,保留来源来源,只有当初始 HTML 证明这是必要的时才引入渲染。
从 Scrapeless 免费计划开始,查看 开发文档,并检查 Scrapeless 定价,然后再将渲染的语料库投入生产。
常见问题
问:JSON-LD 比可见的 HTML 更容易抓取吗?
是的,当发布者包含您需要的字段时。JSON-LD 给您提供了命名属性和类型,而可见的 HTML 通常需要选择器和文本清理;您仍然应将关键字段与呈现的页面进行比较。
问:为什么我应该解析每个 application/ld+json 脚本?
一个页面可以将不同的实体放置在不同的块中。只读取第一个脚本可能返回组织或面包屑,而错过您想要的文章或产品。
问:@graph 对提取有什么意义?
@graph 将多个 JSON-LD 节点组合在一个对象内。扁平化图形,然后按 @type、@id、URL 或其他稳定标识符选择节点。
问:如果缺少 JSON-LD 属性怎么办?
将可选属性保持为 None,仅在您自己下游合同所需的字段缺失时才失败。Schema.org 描述了可能的属性;它并不强迫发布者填入所有属性。
问:JSON-LD 可能与可见页面不同吗?
是的。元数据和可见内容可以在不同的时间表上更新,或针对不同的表面进行优化。当差异重要时,存储这两个值并明确优先级。
问:我需要浏览器来提取 JSON-LD 吗?
当脚本存在于初始 HTML 中时,不需要。只有当客户端 JavaScript 在原始响应到达后插入或修改结构化数据时,您才需要渲染。
问:提取公共 JSON-LD 总是被允许吗?
没有任何普遍规则使每个集合都合法或被允许。请审查网站的条款和robots 指令,保持请求量在合理范围内,仅收集您需要的字段,并在敏感或商业用途上寻求法律建议。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



