Python 爬虫中文乱码排查:严格解码、UTF-8 BOM 与 JSON 转义的 12 项实验

爬到的中文变成奇怪字符时,先保留响应字节,再确定在哪一步把字节变成了字符串。直接轮流尝试 UTF-8、GBK,直到程序不报错,容易得到一份"能够保存、内容却已经错了"的结果。

本文用离线合成数据复现三个常见误判:错误编码也能严格解码成功;往返字节一致也不能证明文字正确;JSON 中的 Unicode 转义不一定是乱码。2026-09-19 在 macOS / arm64、Python 3.12.14 运行,12 项检查通过,没有请求真实网站。

先定位是哪一层出了问题

HTTP 内容解压、字节到文本的解码、JSON 序列化、文件保存与终端显示是不同步骤。Requests 的 response.content 是响应体字节,但通常已由库处理 gzip 等内容编码,不能把它当成原始网络抓包。response.text 则会按选择的字符编码生成字符串。

调试时保存状态码、Content-Type、所选编码、有限长度的字节摘要和可复现样本。不要把 Cookie、令牌或业务原文整段打进日志。若返回的其实是错误页,也不能仅凭"出现中文"就认定拿到了目标数据。

Requests 官方文档说明可以检查和修改 response.encoding;当编码线索位于 HTML/XML 正文中时,要从字节内容分析,再设置正确编码。Requests 响应内容

下面只是接入已取得响应对象的方式,verified_encoding 必须来自该站点的实际证据,不是根据程序有没有报错随便选一个:

python 复制代码
raw = response.content
text = raw.decode(verified_encoding, errors='strict')

不报错,以及能够往返,都不是正确性证明

把一段 UTF-8 中文字节按 Latin-1 解码,可以完全不报错。再把错误字符串按 Latin-1 编回去,还能得到原来的字节。两个操作都满足编码规则,但中间的文字并不是原文。

所以 errors='strict' 的作用是暴露当前编码下的非法输入;它不负责替你选择真实编码。编码检测库给出的候选也应结合响应头、BOM、文档声明、已知样本与站点规则核对,不能当成绝对真值。不要只验证几个 ASCII 字符,因为许多编码对它们给出相同结果。

errors='replace' 会用替代字符处理部分错误,errors='ignore' 会丢弃错误数据。它们可以用于明确接受损失的展示场景,但不适合作为采集质量校验的默认修复。一旦只留下有损转换后的字符串,未必还能恢复原字节。Python codecs 错误处理

UTF-8 BOM 与 JSON 转义分别处理

带 UTF-8 BOM 的字节用 utf-8 解码,开头通常会留下 U+FEFF;用 utf-8-sig 解码会跳过开头的 UTF-8 签名。这不是自动识别任意文件编码的功能。也不要对全文随意删除 U+FEFF,因为出现在正文内部的字符可能另有含义。

如果打印变量已经是正确中文,写到 JSON 文件后看到 \u4e2d 一类表示,先用 json.loads 读回来检查值。默认 JSON 转义和直接显示中文可以表示同一份数据。希望文件便于人工阅读,可以设置 ensure_ascii=False,同时明确文件的 UTF-8 编码。这不会修复此前已经解错的文本。

可直接运行的完整实验

保存为 encoding_lab.py,执行 python3 encoding_lab.py。全部数据在本地生成,文件往返检查使用临时目录并自动清理:

python 复制代码
import codecs
import json
import tempfile
from pathlib import Path


def run():
    checks = []

    def check(name, result):
        assert result, name
        checks.append(name)

    original = '中文价格:19.90元'
    raw = original.encode('utf-8')
    check('utf8_strict', raw.decode('utf-8', errors='strict') == original)
    wrong = raw.decode('latin-1', errors='strict')
    check('wrong_codec_can_succeed', wrong != original)
    check('roundtrip_is_not_semantic_proof', wrong.encode('latin-1') == raw)

    gb = original.encode('gb18030')
    check('gb18030_explicit', gb.decode('gb18030') == original)
    try:
        gb.decode('utf-8', errors='strict')
    except UnicodeDecodeError:
        check('wrong_utf8_rejected', True)
    else:
        raise AssertionError('wrong_utf8_rejected')

    bom = codecs.BOM_UTF8 + raw
    check('utf8_keeps_bom', bom.decode('utf-8').startswith('\ufeff'))
    check('utf8_sig_removes_bom', bom.decode('utf-8-sig') == original)

    broken = b'A\xffB'
    try:
        broken.decode('utf-8', errors='strict')
    except UnicodeDecodeError:
        check('invalid_byte_rejected', True)
    else:
        raise AssertionError('invalid_byte_rejected')
    check('replace_loses_original_byte', broken.decode('utf-8', errors='replace') == 'A\ufffdB')
    check('ignore_drops_original_byte', broken.decode('utf-8', errors='ignore') == 'AB')

    escaped = json.dumps({'text': original})
    visible = json.dumps({'text': original}, ensure_ascii=False)
    check('json_escape_same_value', '\\u' in escaped and json.loads(escaped) == json.loads(visible))
    with tempfile.TemporaryDirectory() as folder:
        path = Path(folder) / 'sample.json'
        path.write_text(visible, encoding='utf-8')
        check('utf8_file_roundtrip', json.loads(path.read_text(encoding='utf-8'))['text'] == original)

    return {'passed': len(checks), 'checks': checks,
            'observations': {'wrong_latin1_roundtrip': wrong.encode('latin-1') == raw,
                             'wrong_latin1_correct_text': wrong == original,
                             'json_escaped': escaped, 'json_visible': visible}}


if __name__ == '__main__':
    print(json.dumps(run(), ensure_ascii=False, indent=2))

12 项结果怎样解读

本机输出 passed 为 12。检查覆盖 UTF-8 正常解码、错误 Latin-1 成功但文字错误、错误解码仍可字节往返、GB18030 显式解码、GB18030 当 UTF-8 被拒绝、两种 BOM 行为、非法字节拒绝、replace/ignore 的损失、JSON 两种表示的值相等、UTF-8 文件往返。

其中最值得保留的反例结果是:wrong_latin1_roundtrip 为 true,但 wrong_latin1_correct_text 为 false。它直接说明"能编回去"不是文字正确的充分条件。

接到采集任务里的做法

为每类页面维护有依据的解码策略与正反样本;策略变化后先回放样本,再处理新数据。解码失败进入错误队列,保留能定位问题的字节证据,不用忽略错误把失败改成成功。写入数据库或 JSON 后,再读回关键字段比较,避免只看控制台截图。

本文没有实现浏览器的完整 HTML 编码探测算法,没有覆盖所有字符集,也没有验证任何真实网站当前采用的编码。对于动态页面、错误的 HTTP 声明及混合来源内容,应使用相应格式的解析器并补充样本,不能从这 12 项检查推导出通用乱码修复器。

补充资料:Python JSON 参数UTF-8-SIG

相关推荐
天天被压力1 小时前
【跨市场数据实战 #08】可转债折价机会怎么筛:3个接口抓比价、列表和实时盘口
java·人工智能·python
weixin199701080161 小时前
[特殊字符]️《从0到1搭多平台二手ERP中台:闲鱼+淘宝+京东+拼多多+Mercari统一调度》(附Python源码)
python
王国强20091 小时前
uv 深入指南:重新理解 Python 的包管理、依赖解析与项目工程化
python
巡山小钻风来也1 小时前
【保姆级教程】自定义数据集微调PP-OCRv6文本检测模型
python·ocr·paddlepaddle
baopixiaoz1 小时前
BeeQuant × BeeAgent:用AI加速策略验证
大数据·人工智能·python·区块链
浩瀚地学2 小时前
deepagents学习打卡day04
python·agent
安易算力2 小时前
PUE优化工程实践:从1.5到1.2的制冷架构与气流组织改造路径
网络·python·容器·架构·kubernetes
troy1282 小时前
Codex 安全盲区:代码漏洞生成实测
windows·python·ci/cd·pycharm·django·github·fastapi
嵌入式学习菌3 小时前
Workbuddy自动写一个RS485 / LoRa 参数调试工具
python