🎯 一款可定制、具备反检测功能的云浏览器,由自主研发的 Chromium驱动,专为网页爬虫AI 代理设计。👉立即试用
返回博客

使用Python进行JSON-LD网络抓取:提取结构化数据

Emily Chen
Emily Chen

Advanced Data Extraction Specialist

30-Jul-2026

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 规范,而 ArticleProductBreadcrumbList 等词汇源自 Schema.org 的结构化数据模型。这两个标准都无法保证每个出版商填写每个属性。您的解析器必须保留这种不确定性,而不是发明值。

JSON-LD 网页抓取的优势

当页面发布机器可读的信息而不是仅仅人类可读的布局时,JSON-LD 是有用的。常见节点包括:

  • ArticleNewsArticle 用于标题、日期、作者、图像和出版商;
  • Product 用于名称、品牌、优惠、评分和标识符;
  • BreadcrumbList 用于层级和规范类别路径;
  • OrganizationPersonLocalBusiness 用于实体元数据;
  • VideoObjectRecipeEvent 及其他特定领域类型。

脚本块不是私有端点。它是页面响应的一部分,旨在供机器使用,比如搜索爬虫。这使得它比生成的 CSS 类更具耐用性,但并不自动完整或正确。将其视为一个源来进行验证,而不是一个神谕。

安装 Beautiful Soup

本指南使用 Python 3.10 或更高版本、requestsbeautifulsoup4 4.15.0:

bash Copy
python -m pip install "requests>=2.32,<3" "beautifulsoup4==4.15.0"

Beautiful Soup 的 树搜索 API 可以根据属性筛选标签,这足以收集每个匹配的脚本。JSON 解码保持在 Python 的标准库中。

获取源 HTML

从普通的 HTTP 请求开始。这里使用的目标在初始响应中发布其 JSON-LD,因此 JavaScript 渲染会增加成本而不改变结果:

python Copy
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 Copy
HTML 字节: 439487

字节计数在页面包更改时可能会变化。重要的不变因素是响应至少包含一个可解码的 application/ld+json 脚本。

解析每个 JSON-LD 块

除非页面合同明确承诺一个块,否则不要使用 soup.find(...)find_all 保留了文章、面包屑和出版商可能存在于不同脚本中的可能性:

python Copy
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 Copy
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 Copy
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 Copy
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 Copy
标题: 什么是网页抓取?权威指南 2025
可见 H1: 什么是网页抓取?权威指南 2025
标题匹配: 真
作者: Emily Chen
发布者: Scrapeless
发布日期: 2025-09-17T08:35:31.224Z

该页面的标题匹配。不要对此结果进行概括:编辑有时会更新可见 H1,而不更新结构化元数据,或者在 JSON-LD 中使用更短的搜索标题。比较两个表面的数据是一个有用的质量检查。

准备在渲染响应上运行相同的解析器吗?打开一个 Scrapeless 账户,并保持解析代码不变。

完整可运行的提取器

完整的脚本结合了发现、规范化、选择、可空映射和验证:

python Copy
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 Copy
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,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录