PAYATHON 2026

如何解决 json.loads 的 Invalid \escape 错误?

AI 小顾

结论

报错的原因是页面提供的 JSON 不符合语法规范。出错数据包含:

Jede\*r
Mitarbeitern\*innen

在 JSON 字符串中,反斜杠 \ 用于转义,但 \* 不是合法的 JSON 转义序列,因此 json.loads() 会抛出:

JSONDecodeError: Invalid \escape

设置 strict=False 也无法解析 \*。这个参数主要用于放宽对字符串内控制字符的检查,并不允许任意反斜杠转义。

JSON 中允许的转义形式

JSON 字符串支持以下常规转义:

\"  \\  \/  \b  \f  \n  \r  \t  \uXXXX

如果原数据需要保留反斜杠,应写成:

"Jede\\*r"

解析后,Python 字符串中才会包含:

Jede\*r

如果反斜杠只是排版时产生的多余字符,正确内容应是:

"Jede*r"

示例中的 ü、<br/> 等 HTML 实体不会触发 Invalid \escape。问题出在 \*。

优先方案:修正数据源

如果能够控制生成 JSON 的页面,应使用正规的 JSON 序列化器输出数据,不要手工拼接字符串。例如:

import json

data = {
    "description": r"Jede\*r bei STORMING"
}

json_text = json.dumps(data, ensure_ascii=False)
print(json_text)

输出中的反斜杠会得到正确编码:

{"description": "Jede\\*r bei STORMING"}

这种方式最可靠,因为问题会在数据生成时得到修复,爬虫不必猜测原始文本的含义。

无法修改页面时:针对已知错误进行清理

对于这个特定页面,如果能够确定 \* 中的反斜杠没有实际含义,可以在解析前将其删除:

import json

raw_json = company_jdata.get_text()

raw_json = raw_json.replace(r"\*", "*")
company_js = json.loads(raw_json, strict=False)

如果解析后的文本需要保留反斜杠,则应将输入中的单个反斜杠改成 JSON 要求的两个反斜杠:

raw_json = company_jdata.get_text()

raw_json = raw_json.replace(r"\*", r"\\*")
company_js = json.loads(raw_json, strict=False)

这两种处理方式的语义不同:

# 删除多余的反斜杠
r"Jede\*r" -> "Jede*r"

# 保留反斜杠字符
r"Jede\*r" -> r"Jede\*r"

具体采用哪一种,应根据字段的实际含义决定,不能盲目替换。

处理多种未知转义

如果第三方页面可能包含多种非法转义,可以只转义那些不属于 JSON 合法转义的反斜杠:

import json
import re

INVALID_JSON_BACKSLASH = re.compile(r'\\(?!["\\/bfnrtu])')

def repair_invalid_escapes(raw_json: str) -> str:
    return INVALID_JSON_BACKSLASH.sub(r"\\\\", raw_json)

raw_json = company_jdata.get_text()

try:
    company_js = json.loads(raw_json, strict=False)
except json.JSONDecodeError as exc:
    if exc.msg.startswith("Invalid \\escape"):
        repaired_json = repair_invalid_escapes(raw_json)
        company_js = json.loads(repaired_json, strict=False)
    else:
        raise

这种方法会把未知的 \*、\q 等内容解释为普通反斜杠加字符。不过,它并不是完整的 JSON 修复器。格式错误的 \u 转义、缺少引号或多余逗号仍然会导致解析失败。因此,它只适合在无法控制第三方数据时作为兼容措施。

定位具体错误内容

JSONDecodeError 会提供错误位置。打印该位置附近的原始文本,可以快速找到问题字符:

import json

raw_json = company_jdata.get_text()

try:
    company_js = json.loads(raw_json, strict=False)
except json.JSONDecodeError as exc:
    start = max(0, exc.pos - 80)
    end = min(len(raw_json), exc.pos + 80)

    print(f"JSON error: {exc}")
    print(repr(raw_json[start:end]))

这里应使用 repr(),因为它可以清楚地显示反斜杠、换行符和其他不可见字符。

在循环中跳过损坏页面

一个无效页面不应导致整个抓取任务终止。可以记录对应的 URL 和错误上下文,然后继续处理其他页面:

import json
import urllib.request
from urllib.request import Request
from bs4 import BeautifulSoup

for count_site, site in enumerate(sitelis, start=1):
    try:
        request = Request(
            site,
            headers={"User-Agent": "Mozilla/5.0"}
        )

        with urllib.request.urlopen(request, context=ctx) as response:
            company_data = response.read()

        soup = BeautifulSoup(company_data, "lxml")
        script = soup.select_one('script[type="application/ld+json"]')

        if script is None:
            print(f"[{count_site}] No JSON-LD found: {site}")
            continue

        raw_json = script.get_text()
        raw_json = raw_json.replace(r"\*", "*")

        company_js = json.loads(raw_json, strict=False)
        print(json.dumps(company_js, indent=4, ensure_ascii=False))

    except json.JSONDecodeError as exc:
        start = max(0, exc.pos - 80)
        end = min(len(raw_json), exc.pos + 80)

        print(f"[{count_site}] Invalid JSON: {site}")
        print(f"Reason: {exc}")
        print(f"Context: {raw_json[start:end]!r}")

    except KeyboardInterrupt:
        print("Program interrupted by user.")
        break

注意事项

不要进行下面这种全局替换:

raw_json = raw_json.replace("\\", "")

这样会破坏原本合法的 \\、\"、\n 和 \uXXXX,还可能改变字段内容。

示例中的 description 似乎还包含字符串内的原始换行。严格的 JSON 要求将这些换行编码为 \n。strict=False 可能暂时容忍部分控制字符,但无法把整体不规范的数据变成有效 JSON。

更稳妥的处理方式是先记录原始错误上下文,只修复已经确认的异常模式,并尽可能让数据提供方正确生成 JSON-LD。

备注:内容仅供参考。