跳至内容
免费开始
返回博客

如何在 Python 中解析 JSON:从 API 响应到网页中隐藏的 JSON

Suciu Dan最后更新于 4 min read
如何在 Python 中解析 JSON:从 API 响应到网页中隐藏的 JSON
简而言之:在 Python 中解析 JSON,请使用内置的 json 模块:字符串或字节用 json.loads(),文件用 json.load(),HTTP 响应用 Requests 的 response.json()。请分别检查 HTTP 状态码和 JSON 有效性,因为 200 响应也可能是 HTML。本指南还介绍解码错误、嵌套访问、校验、大文件以及隐藏在网页中的 JSON。

Python 的内置 json 模块用于解析 JSON。对于 json.loads() 处理字符串或字节数组, json.load() 用于字符串或字节数组,以及 Requests 的 response.json() 用于处理 HTTP 响应。本指南涵盖了解码错误、安全的嵌套访问、有效性验证、大文件处理、数值精度以及网页中嵌入的 JSON。

关键要点

  • 建议优先使用 json.loads 用于字符串/字节, json.load ;打开文件时应使用 UTF-8(或 utf-8-sig 当存在 BOM 时)。
  • 将 HTTP 状态和 JSON 有效性视为独立的检查:200 响应仍可能是 HTML。
  • 使用 .get() 链或一个小型 dig() 辅助函数来处理杂乱的嵌套数据;解析后使用 Pydantic 或 JSON Schema 进行验证。
  • 使用 ijson 流式处理大型数组,将抓取结果作为 JSON Lines 追加,并在自己的有效负载上测量 orjson 的性能。
  • 当 HTML 布局频繁变化时,优先使用页面中嵌入的 JSON(JSON-LD、 __NEXT_DATA__) 或爬取 API 的结构化字段,而非易受 HTML 布局频繁变化影响的脆弱 CSS 选择器。

如何在 Python 中解析 JSON 字符串?

调用 json.loads() 对 str 或 bytes 值上调用`parse()`。它会返回一个 Python 对象——通常是 dict 或 list——该对象反映了 JSON 结构。

import json

raw = """
{
  "sku": "WSA-BOTTLE-750",
  "title": "Insulated Steel Water Bottle 750ml",
  "price": 19.99,
  "currency": "USD",
  "in_stock": true,
  "variants": [
    {"color": "matte black", "capacity_ml": 750},
    {"color": "ocean blue", "capacity_ml": 750}
  ],
  "seller": {
    "name": "TrailGear Co",
    "rating": 4.8,
    "country": "US"
  }
}
"""

product = json.loads(raw)
print(product["sku"], product["variants"][0]["color"])
# WSA-BOTTLE-750 matte black

# bytes work the same way
product_from_bytes = json.loads(raw.encode("utf-8"))
assert product_from_bytes["title"] == product["title"]

整个示例中始终显示的是同一条产品列表,因此每个示例都是基于一个“抓取式”记录构建的。

如何在 Python 中读取 JSON 文件?

以 UTF-8 编码打开文件,并将文件对象传递给 json.load().

import json
from pathlib import Path

path = Path("product.json")
with path.open(encoding="utf-8") as f:
    product = json.load(f)

print(product["currency"])  # USD

当 JSON 文件以 BOM 开头时该怎么办

某些编辑器在保存 UTF-8 文件时会添加字节顺序标记(EF BB BF)。该不可见字符位于第一个 { 之前,会导致正常解码失败。请使用 encoding="utf-8-sig" ,这样 Python 会在 json.load 运行前将其移除。若已获取字节数组,请使用 utf-8-sig 进行解码,然后调用 json.loads.

import json
from pathlib import Path

bom_path = Path("product_bom.json")
with bom_path.open(encoding="utf-8-sig") as f:
    product = json.load(f)

# Or from bytes:
product = json.loads(bom_path.read_bytes().decode("utf-8-sig"))

JSON 类型与 Python 类型的映射

JSON 类型与 Python 类型的映射关系由 json.org 上的 JSON 语法和标准库定义:

JSON

Python (json.loads)

Python → JSON (json.dumps)

object

dict

dict

数组

list

list / tuple

字符串

str

str

数字 (整数)

int

int

数字(实数)

float

float / int

true / false

True / False

bool

null

None

None

为什么经过一次往返后你的键和元组会发生变化

JSON 对象名称始终是字符串。整数字典键在序列化后会变成字符串 dumps → loads。元组会转换为列表,因为 JSON 只支持数组。集合无法序列化,并会引发 TypeError.

import json

print(json.loads(json.dumps({1: "a", 2: "b"})))
# {'1': 'a', '2': 'b'}

print(json.loads(json.dumps({"coords": (10, 20)})))
# {'coords': [10, 20]}

try:
    json.dumps({"tags": {"refillable", "bpa-free"}})
except TypeError as e:
    print(e)
# Object of type set is not JSON serializable

如何解析来自 API 的 JSON 响应?

调用 response.raise_for_status() 来处理 HTTP 错误,然后 response.json() 来解码正文。状态码与 JSON 有效性是分开的:成功状态并不保证 JSON 正文有效。

import requests

resp = requests.get("https://httpbin.org/json", timeout=15)
resp.raise_for_status()
data = resp.json()
print(type(data), list(data)[:3])

当 200 响应并非 JSON 时

拦截和挑战页面通常会返回带有 HTTP 200 状态码的 HTML。请检查 Content-Type,捕获解码失败,并展示正文的简短、安全的预览:

import json
from typing import Any

import requests


def get_json(url: str, timeout: float = 15.0) -> Any:
    resp = requests.get(url, timeout=timeout)
    resp.raise_for_status()
    content_type = resp.headers.get("Content-Type", "")
    if "json" not in content_type.lower() and "javascript" not in content_type.lower():
        preview = resp.text[:120].replace("\n", " ")
        raise ValueError(
            f"Expected JSON Content-Type, got {content_type!r}. "
            f"Body starts: {preview!r}"
        )
    try:
        return resp.json()
    except json.JSONDecodeError as e:
        preview = resp.text[:120].replace("\n", " ")
        raise ValueError(
            f"JSON decode failed at pos {e.pos}: {e.msg}. "
            f"Body starts: {preview!r}"
        ) from e


product_feed = get_json("https://httpbin.org/json")
# get_json("https://httpbin.org/html")  # raises ValueError

状态码为 200 的 HTML 页面通常是屏蔽页面,我们的《避免被屏蔽的爬虫指南》中解释了如何规避此类情况。

如何修复 JSONDecodeError?

JSONDecodeError 表示输入不是有效的 JSON:正文为空、有效负载被截断、包含 HTML、单引号、尾随逗号或类似情况。请阅读 e.msg, e.lineno, e.colno,并 e.pos,然后打印 pos ,以便查明断点位置。完整的属性和钩子详情请参阅 Python json 模块的文档。

import json


def locate_decode_error(text: str) -> None:
    try:
        json.loads(text)
    except json.JSONDecodeError as e:
        start = max(0, e.pos - 20)
        end = min(len(text), e.pos + 20)
        print(f"{e.msg} at line {e.lineno}, col {e.colno}, pos {e.pos}")
        print(f"Around error: {text[start:end]!r}")


locate_decode_error('{"sku": "WSA-BOTTLE-750", "price":')
# Expecting value at line 1, col 36, pos 35

常见原因及其表现形式

症状(错误信息片段)

可能原因

解决方法

Expecting value 在第 0 位

正文为空或HTML/undefined

检查传输过程;确认您获取的是JSON

Expecting value 字符串中间

响应被截断

重试、延长超时时间,或使用带长度检查的流处理

Expecting property name enclosed in double quotes

单引号键或尾随逗号

修复数据源;不要使用 eval

Extra data

一个字符串中包含两个 JSON 值

拆分 JSON 行,或一次解析一个值

预览中的 HTML 标签

软阻塞或登录页面

修复请求头、Cookie 或渲染问题;请参阅上文的阻塞指南

如何安全地访问嵌套的 JSON?

硬索引(data["seller"]["rating"]) 会引发 KeyError 。 .get() 链会返回默认值,但在深层路径中会产生冗余信息。一个小型类型化 dig() 辅助函数会遍历字典键和列表索引,并且它对缺失路径的处理方式与显式 null.

from typing import Any


def dig(data: Any, *path: str | int, default: Any = None) -> Any:
    cur: Any = data
    for key in path:
        if isinstance(cur, dict):
            if key not in cur:
                return default
            cur = cur[key]
        elif isinstance(cur, list) and isinstance(key, int):
            if key < 0 or key >= len(cur):
                return default
            cur = cur[key]
        else:
            return default
    return cur


product = {
    "variants": [{"color": "matte black", "capacity_ml": 750}],
    "seller": {"name": "TrailGear Co", "rating": 4.8},
}

print(dig(product, "seller", "rating"))                 # 4.8
print(dig(product, "variants", 0, "color"))             # matte black
print(dig(product, "variants", 9, "color", default="?"))  # ?
print(dig({"seller": {"rating": None}}, "seller", "rating", default="missing"))
# None  — key exists, value is null
print(dig({"seller": {}}, "seller", "rating", default="missing"))
# missing — key absent

如何在 Python 中验证 JSON 数据?

解析仅能验证语法。在 loads,请使用 Pydantic 或 JSON Schema 检查类型和必填字段。

from decimal import Decimal

from pydantic import BaseModel, ValidationError


class Variant(BaseModel):
    color: str
    capacity_ml: int


class Seller(BaseModel):
    name: str
    rating: float
    country: str


class Product(BaseModel):
    sku: str
    title: str
    price: Decimal
    currency: str
    in_stock: bool
    variants: list[Variant]
    seller: Seller
    notes: str | None = None


product = Product.model_validate(
    {
        "sku": "WSA-BOTTLE-750",
        "title": "Insulated Steel Water Bottle 750ml",
        "price": 19.99,
        "currency": "USD",
        "in_stock": True,
        "variants": [{"color": "matte black", "capacity_ml": 750}],
        "seller": {"name": "TrailGear Co", "rating": 4.8, "country": "US"},
    }
)
print(product.price)  # 19.99

try:
    Product.model_validate(
        {
            "sku": "WSA-BOTTLE-750",
            "title": "Insulated Steel Water Bottle 750ml",
            "price": "cheap",
            "currency": "USD",
            "in_stock": True,
            "variants": [],
            "seller": {"name": "TrailGear Co", "rating": 4.8, "country": "US"},
        }
    )
except ValidationError as e:
    print(e.error_count(), "validation error(s)")

与 JSON Schema 具有相同的结构,可通过 jsonschema (另请参阅《理解 JSON Schema》):

import jsonschema

schema = {
    "type": "object",
    "required": ["sku", "title", "price", "currency", "in_stock", "variants", "seller"],
    "properties": {
        "sku": {"type": "string"},
        "title": {"type": "string"},
        "price": {"type": "number"},
        "currency": {"type": "string", "minLength": 3, "maxLength": 3},
        "in_stock": {"type": "boolean"},
        "variants": {
            "type": "array",
            "items": {
                "type": "object",
                "required": ["color", "capacity_ml"],
                "properties": {
                    "color": {"type": "string"},
                    "capacity_ml": {"type": "integer"},
                },
            },
        },
        "seller": {
            "type": "object",
            "required": ["name", "rating", "country"],
            "properties": {
                "name": {"type": "string"},
                "rating": {"type": "number"},
                "country": {"type": "string"},
            },
        },
    },
}

jsonschema.validate(
    {
        "sku": "WSA-BOTTLE-750",
        "title": "Insulated Steel Water Bottle 750ml",
        "price": 19.99,
        "currency": "USD",
        "in_stock": True,
        "variants": [{"color": "matte black", "capacity_ml": 750}],
        "seller": {"name": "TrailGear Co", "rating": 4.8, "country": "US"},
    },
    schema,
)

如何将 JSON 写入字符串或文件?

使用 json.dumps 将数据写入字符串,使用 json.dump 用于文件。 indent 美化格式, sort_keys=True 稳定差异,并 separators=(",", ":") 压缩数据。

import json
from pathlib import Path

product = {
    "sku": "WSA-BOTTLE-750",
    "title": "Insulated Steel Water Bottle 750ml",
    "price": 19.99,
    "currency": "USD",
    "in_stock": True,
}

print(json.dumps(product, indent=2, sort_keys=True))
print(json.dumps(product, separators=(",", ":")))

with Path("product_out.json").open("w", encoding="utf-8") as f:
    json.dump(product, f, ensure_ascii=False, indent=2)

在不使用转义序列的情况下书写非英语文本

默认情况下 dumps 会将非ASCII字符转义为 \uXXXX。当 ensure_ascii=False 。

import json

title = {"title": "Bouteille d'eau 750ml — acier"}
print(json.dumps(title))
# {"title": "Bouteille d'eau 750ml \u2014 acier"}
print(json.dumps(title, ensure_ascii=False))
# {"title": "Bouteille d'eau 750ml — acier"}

序列化 datetime、Decimal 和 dataclasses

对于编码器不认识的类型,请传入 default= 函数。若需可重用的编码器类,也可继承 json.JSONEncoder 。

import json
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from decimal import Decimal
from typing import Any


def default_encoder(obj: Any) -> Any:
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return str(obj)
    if hasattr(obj, "__dataclass_fields__"):
        return asdict(obj)
    raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")


@dataclass
class Offer:
    sku: str
    price: Decimal
    listed_at: datetime


offer = Offer(
    sku="WSA-BOTTLE-750",
    price=Decimal("19.99"),
    listed_at=datetime(2026, 10, 6, 12, 0, tzinfo=timezone.utc),
)
print(json.dumps(offer, default=default_encoder))
# {"sku": "WSA-BOTTLE-750", "price": "19.99", "listed_at": "2026-10-06T12:00:00+00:00"}

如何确保价格和大型数字的精度?

使用 parse_float=Decimal ,因此诸如 19.99 等值能以十进制小数的形式保持精确,而非二进制浮点数。RFC 8259(JSON 标准)不允许使用 NaN 或 Infinity。解析后,Python的decimal模块通常是存储货币安全值的地方。

import json
from decimal import Decimal

data = json.loads('{"price": 19.99}', parse_float=Decimal)
print(data["price"], type(data["price"]))
# 19.99 <class 'decimal.Decimal'>

# Reject NaN / Infinity when encoding
try:
    json.dumps({"price": float("nan")}, allow_nan=False)
except ValueError as e:
    print(e)

Python 的解码器支持 NaN 和 Infinity 作为扩展格式。若需在输入时拒绝这些格式,请传递一个 parse_constant 会触发异常,用于在输入时拒绝这些扩展名。

如何检测 JSON 中的重复键?

JSON 标准 RFC 8259 规定对象名称应唯一,但 json.loads 会默认保留最后一个值。 object_pairs_hook 可以抛出异常代替。

import json
from typing import Any


def detect_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
    seen: set[str] = set()
    out: dict[str, Any] = {}
    for key, value in pairs:
        if key in seen:
            raise ValueError(f"Duplicate key: {key!r}")
        seen.add(key)
        out[key] = value
    return out


text = '{"sku": "A", "sku": "B", "price": 1}'
print(json.loads(text)["sku"])  # B — last value wins

try:
    json.loads(text, object_pairs_hook=detect_duplicates)
except ValueError as e:
    print(e)  # Duplicate key: 'sku'

如何处理 JSON Lines 文件?

JSON Lines 格式每行存储一个 JSON 值。每次抓取后将一条记录追加到文件末尾,然后逐行读取,跳过空行,并在记录解码失败时报告行号。

import json
from pathlib import Path

path = Path("products.jsonl")

# Append during a run
with path.open("a", encoding="utf-8") as f:
    for rec in (
        {"sku": "WSA-BOTTLE-750", "price": 19.99},
        {"sku": "WSA-LID-01", "price": 4.5},
    ):
        f.write(json.dumps(rec, ensure_ascii=False) + "\n")

# Read with line numbers
records = []
with path.open(encoding="utf-8") as f:
    for lineno, line in enumerate(f, start=1):
        line = line.strip()
        if not line:
            continue
        try:
            records.append(json.loads(line))
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid JSON on line {lineno}: {e.msg}") from e

print(len(records), "records")

如何解析一个大到超出内存容量的 JSON 文件?

PyPI 上的 ijson 流处理:迭代路径下的项目,而非加载整个文档。仅在 use_float=True 仅在二进制浮点精度可接受的情况下使用。

import json
from decimal import Decimal
from pathlib import Path

import ijson

# Build a sample file so the example runs anywhere
large = Path("large_products.json")
items = [{"sku": f"SKU-{i}", "price": i * 1.5} for i in range(5_000)]
large.write_text(json.dumps({"items": items}), encoding="utf-8")

count = 0
total = Decimal("0")
with large.open("rb") as f:
    for item in ijson.items(f, "items.item"):
        count += 1
        total += Decimal(str(item["price"]))

print(count, "items; sum", total)

orjson 比 json 模块更快吗?

通常是,但请根据您的实际数据进行测试。PyPI 上 orjson 的文档指出了实践中重要的 API 差异: dumps 返回 bytes,选项采用标志形式而非关键字参数,且缩进使用 OPT_INDENT_2 而非 indent 整数。

import json
import timeit

import orjson

payload = {
    "sku": "WSA-BOTTLE-750",
    "title": "Insulated Steel Water Bottle 750ml",
    "price": 19.99,
    "variants": [{"color": "matte black", "capacity_ml": 750}],
}

encoded = orjson.dumps(payload)
assert isinstance(encoded, bytes)
decoded = orjson.loads(encoded)
pretty = orjson.dumps(payload, option=orjson.OPT_INDENT_2)

def harness() -> None:
    orjson.dumps(payload)
    json.dumps(payload)

print(timeit.timeit(harness, number=5_000))
# Run locally and compare; numbers vary by machine and payload.

如何提取隐藏在网页中的 JSON 数据?

许多网页会将数据以 JSON 格式封装在 <script> 标签中,这通常比渲染后HTML中的CSS选择器更稳定。解析该JSON后,使用与处理API响应相同的工具对其进行遍历。

读取 JSON-LD 结构化数据

JSON-LD 位于 script[type="application/ld+json"] 块中。一个页面可能会输出单个对象、列表或一个 @graph 包装器。根据 @type == "Product" (或您所需的任何类型)进行过滤:

import json
from typing import Any

from bs4 import BeautifulSoup

html = """
<!DOCTYPE html>
<html><head>
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Insulated Steel Water Bottle 750ml",
  "sku": "WSA-BOTTLE-750",
  "offers": {"@type": "Offer", "price": "19.99", "priceCurrency": "USD"}
}
</script>
<script type="application/ld+json">
[
  {"@type": "BreadcrumbList", "itemListElement": []},
  {"@type": "Product", "name": "Spare Lid", "sku": "WSA-LID-01"}
]
</script>
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@graph": [
    {"@type": "Organization", "name": "TrailGear Co"},
    {"@type": "Product", "name": "Bottle Bundle", "sku": "WSA-BUNDLE"}
  ]
}
</script>
</head><body></body></html>
"""


def extract_json_ld_products(page_html: str) -> list[dict[str, Any]]:
    soup = BeautifulSoup(page_html, "html.parser")
    products: list[dict[str, Any]] = []

    def collect(node: Any) -> None:
        if isinstance(node, list):
            for item in node:
                collect(item)
            return
        if not isinstance(node, dict):
            return
        if node.get("@type") == "Product":
            products.append(node)
        graph = node.get("@graph")
        if isinstance(graph, list):
            collect(graph)

    for tag in soup.select('script[type="application/ld+json"]'):
        if not tag.string:
            continue
        collect(json.loads(tag.string))
    return products


for product in extract_json_ld_products(html):
    print(product["sku"], product["name"])

当页面通过 JavaScript 注入 JSON-LD 时,会在返回页面之前由托管浏览器进行渲染,从而生成最终的 HTML 代码。

阅读 __NEXT_DATA__ 及其他框架状态

__NEXT_DATA__ 及其他框架状态

Next.js 及类似框架会将页面属性嵌入到 script#__NEXT_DATA__ 标签中。解析该标签的文本内容,然后使用 dig()进行遍历。如需完整的零售行业操作指南,请参阅我们的沃尔玛数据抓取指南,该指南演示了如何从 __NEXT_DATA__ 解析产品记录。

import json
from typing import Any

from bs4 import BeautifulSoup


def dig(data: Any, *path: str | int, default: Any = None) -> Any:
    cur: Any = data
    for key in path:
        if isinstance(cur, dict):
            if key not in cur:
                return default
            cur = cur[key]
        elif isinstance(cur, list) and isinstance(key, int):
            if key < 0 or key >= len(cur):
                return default
            cur = cur[key]
        else:
            return default
    return cur


html = """
<!DOCTYPE html>
<html><body>
<script id="__NEXT_DATA__" type="application/json">
{
  "props": {
    "pageProps": {
      "product": {
        "sku": "WSA-BOTTLE-750",
        "title": "Insulated Steel Water Bottle 750ml",
        "price": 19.99
      }
    }
  }
}
</script>
</body></html>
"""

soup = BeautifulSoup(html, "html.parser")
tag = soup.find("script", id="__NEXT_DATA__")
next_data = json.loads(tag.string)
print(dig(next_data, "props", "pageProps", "product", "sku"))
# WSA-BOTTLE-750

调用页面加载时后台调用的 JSON 端点

打开开发者工具 → 网络,刷新页面,并按 Fetch/XHR 过滤。许多产品和商品列表界面会加载一个浏览器已调用的 JSON URL。如果该端点在网站条款允许下可公开访问,您可以使用与页面相同的请求头直接发送请求,并使用 response.json()。如有已文档化或明确公开的 API,请优先选用。在调用内部端点之前,请查阅该网站的条款以及我们关于负责任的网络数据的原则。

如何解析来自 WebScrapingAPI 的 JSON?

Scraper API 可以返回选定的字段作为 JSON,而不是原始 HTML。使用 extract_rules 解析命名字段, json_response=1 获取完整封装(包括 JSON-LD 元数据),或从 envelope["body"]。请将键保留在 WSA_API_KEY.

使用 extract_rules 获取命名字段

将转换为 JSON 字符串的 extract_rules 对象传递给 /v2。响应中每条规则键对应一个属性。嵌套规则会遍历重复的卡片; table_json 将 HTML 表格转换为行对象。

import json
import os

import requests

api_key = os.environ["WSA_API_KEY"]

extract_rules = {
    "title": {"selector": "h1", "output": "text"},
    "results": {
        "selector": "article.product_pod",
        "all": 1,
        "output": {
            "name": {"selector": "h3 a", "output": "@title"},
            "url": {"selector": "h3 a", "output": "@href"},
            "price": {"selector": "p.price_color", "output": "text"},
        },
    },
}

resp = requests.get(
    "https://api.webscrapingapi.com/v2",
    params={
        "api_key": api_key,
        "url": "https://books.toscrape.com/",
        "extract_rules": json.dumps(extract_rules),
    },
    timeout=60,
)
resp.raise_for_status()
fields = resp.json()
print(fields["title"])
# All products
print(fields["results"][:2])
# [{'name': 'A Light in the Attic', 'url': 'catalogue/a-light-in-the-attic_1000/index.html', 'price': '£51.77'}, {'name': 'Tipping the Velvet', 'url': 'catalogue/tipping-the-velvet_999/index.html', 'price': '£53.74'}]

提取规则参考列表列出了所有输出类型,包括 table_json.

读取完整的 JSON 响应封装

设置 json_response=1 将抓取结果封装为 JSON 对象,并使用 headers, cost, initial-status-code, body, type,并将刮取结果封装在 JSON 对象中, metadata。该 metadata 该对象包含提取的微数据和 JSON-LD,因此 metadata["json-ld"] 当您仅需页面中的结构化数据时,这便是一个快捷方式。

import os

import requests

api_key = os.environ["WSA_API_KEY"]

resp = requests.get(
    "https://api.webscrapingapi.com/v2",
    params={
        "api_key": api_key,
        "url": "https://example.com",
        "json_response": 1,
    },
    timeout=60,
)
resp.raise_for_status()
envelope = resp.json()

print(envelope["initial-status-code"], envelope["type"], envelope["cost"])
body = envelope["body"]          # page content as a string
json_ld = envelope.get("metadata", {}).get("json-ld", [])
print(type(body), len(json_ld))

完整的 JSON 响应文档中详细描述了每个信封字段。

解析信封内的 JSON 正文

当目标 URL 本身返回 JSON 时,封装器仍会将该有效载荷 body 中作为字符串。请参阅 type (例如 "json")并使用 json.loads(envelope["body"]) 进行解码,

import json
import os

import requests

api_key = os.environ["WSA_API_KEY"]

resp = requests.get(
    "https://api.webscrapingapi.com/v2",
    params={
        "api_key": api_key,
        "url": "https://httpbin.org/json",
        "json_response": 1,
    },
    timeout=60,
)
resp.raise_for_status()
envelope = resp.json()

if envelope.get("type") == "json":
    payload = json.loads(envelope["body"])
else:
    # HTML or other types still arrive as a string in body
    payload = envelope["body"]

print(type(payload))

对于搜索结果,如果 SERP API 以结构化 JSON 格式返回结果,则完全跳过 HTML 解析。

应该使用哪种 JSON 解析方法?

根据输入内容选择相应的工具:对于字符串和文件,使用标准库;对于大型数组,使用 ijson;对于嵌入在 HTML 中的数据,使用脚本解析;若需要字段或封装格式而非原始 HTML,则使用抓取 API。

现有资源

应使用什么

JSON 字符串或字节

json.loads

JSON 文件(UTF-8)

json.load 搭配 encoding="utf-8"

以 BOM 开头的文件

encoding="utf-8-sig" 然后 json.load / json.loads

请求 / HTTP 响应

raise_for_status() + Content-Type 检查 + response.json()

嵌套路径需设置默认值

.get() 链或 dig()

需要类型 / 必填字段

解析后采用 Pydantic 模型或 JSON Schema

货币或精确小数

parse_float=Decimal

重复键必须报错

object_pairs_hook 检测器

每行抓取数据仅包含一条记录

JSON Lines (*.jsonl)

数组过大,超出内存容量

ijson 流处理

对吞吐量要求极高的编码/解码

orjson(请根据您的有效载荷进行测试)

嵌入 HTML 中的数据

JSON-LD / __NEXT_DATA__ / 其他 script JSON

通过API从页面中选取的字段

WebScrapingAPI extract_rules

需要标题、成本、JSON-LD元数据

json_response=1 envelope

目标URL通过API返回JSON

json.loads(envelope["body"]) 当 type 是 json

常见问题

json.load 和 json.loads 之间有什么区别?

json.loads 解析内存中已存在的 JSON 字符串或字节流。 json.load 从文件样式对象中读取并解析该流。用于 loads 用于处理 API 请求主体和内存中的文本;使用 load 用于以正确编码打开的文件。对于相同的 JSON 内容,两者返回的 Python 类型相同。

如何在 Python 中解析来自 API 的 JSON 响应?

使用 Requests(或其他 HTTP 客户端)发送请求,调用 raise_for_status(),确认 Content-Type 是否为 JSON 格式,然后调用 response.json() 或 json.loads(response.text)。若收到 200 状态码的空白 HTML 页面,应视为传输问题而非 JSON 语法问题;若解码失败,请检查短内容预览。

为什么会出现“JSONDecodeError: Expecting value”?

解码器遇到本应开始 JSON 值的位置,但未找到有效内容——可能是正文为空、JSON 被截断、HTML 内容,或是类似 undefined。输出 e.pos 及其周围的字符。修复上游响应或传输问题;不要使用宽泛的 except.

如何在 Python 中安全地从 JSON 中获取嵌套值?

避免使用链式 data["a"]["b"] ,以免出现键缺失的情况。对于浅层路径,请使用 .get("a", {}).get("b") 处理浅层路径,或使用一个小型 dig(data, "a", "b", default=None) 辅助函数,该函数也接受列表索引。请记住,键缺失和显式 JSON null 会导致不同的结果。

如何在 Python 中验证 JSON 数据?

解析仅检查语法。之后 loads,请使用 Pydantic 模型(类型、可选字段、嵌套模型)或通过 jsonschema 包,通过Pydantic模型(类型、可选字段、嵌套模型)或JSON Schema文档进行验证。当浮点数舍入可能出错时,请将货币字段保留为 Decimal ,并将验证错误反馈给调用方。

如何在解析 JSON 时保持价格的小数精度?

传递 parse_float=Decimal 传递给 json.loads 或 json.load ,这样数值价格就会变成 decimal.Decimal 而不是 float。编码时,应将小数(或次级单位的整数)作为字符串序列化,并使用 default= 钩子将小数序列化为字符串(或次级单位的整数),并保持 allow_nan=False ,以确保非 JSON 数值常量不会混入输出中。

如何在 Python 中读取一个非常大的 JSON 文件?

请勿调用 json.load 。使用 ijson 遍历路径下的项目,例如 "items.item" ,同时文件仍保存在磁盘上。当您能够控制数据源时,建议优先使用 JSON Lines,因为每行都是一个独立的记录,您可以按行进行流式处理和恢复。

什么是 JSON Lines,何时应该使用它?

JSON Lines (application/jsonl) 每行以换行符分隔,每行存储一个 JSON 值。它适用于数据抓取和日志记录工作流,在这些场景中,您可以随数据到达时追加记录,并在稍后读取这些记录而无需加载整个文件。当某行无法解码时,会跳过空行并报告失败的行号。

orjson 能否完全替代 json?

并不完全是。orjson 通常速度更快,但 dumps 返回 bytes,选项标志取代了许多关键字参数,且缩进采用 OPT_INDENT_2。如果某个库期望使用 str from dumps,请添加 .decode()。在切换生产环境代码之前,请先对您的有效负载进行这两方面的性能测试。

如何使用 Python 从网页中提取 JSON-LD 或 __NEXT_DATA__ ?

__NEXT_DATA__ ?

使用 BeautifulSoup 解析 HTML,选择 script[type="application/ld+json"] 或 script#__NEXT_DATA__,并运行 json.loads 对标签文本进行处理。将 JSON-LD 作为对象、列表或 @graph 列表,然后通过 @type进行过滤。使用 dig()。对于客户端渲染的标签,请在解析前先获取渲染后的 HTML。

如何在 Python 中美化打印 JSON?

调用 json.dumps(data, indent=2, sort_keys=True) 以生成稳定且易于阅读的字符串,或者 json.dump(..., indent=2) 将文件写入磁盘时使用。对于非英语文本,添加 ensure_ascii=False 以处理非英语文本,确保字符不被转义。使用 orjson 时,传递 option=orjson.OPT_INDENT_2 并解码返回的字节,以获取字符串。

从任何页面获取结构化 JSON

如果您更希望获取命名字段,而不是维护 CSS 选择器,Scraper API 的 extract_rules 和 json_response 选项会返回 JSON,您可以将其直接传入 json.loads 以及上述辅助函数中。设置 WSA_API_KEY,从 Scraper API 产品页面开始,并在每次解析后进行验证。

关于作者

Suciu Dan, 联合创始人 @ WebScrapingAPI

Suciu Dan

联合创始人

Suciu Dan 是 WebScrapingAPI 的联合创始人,他撰写了关于 Python 网页抓取、Ruby 网页抓取以及代理基础设施的实用指南,这些指南专为开发者而设计。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。