bigywater.tech
Scrapy · 7

Response 与编码:TextResponse 怎么决定用哪种编码解码

同一份 GBK 网页,为什么有的能抓对、有的变成乱码?这篇把 Response.encoding 的每一级来源都拿出来冲突一遍,再看自动探测在多短的文本上会出错,以及头里声明错编码后 Scrapy 的表现。

5 级
encoding 的来源顺序:构造参数 > BOM > 头 > > 自动探测
≥ 12 字
GBK 文本自动探测才稳定判成 gb18030(3、6 个字被判成 cp949)
cp1252
latin1、iso-8859-1、ascii 以及不认识的名字最终都映射到它

判断顺序

实测 · 本机执行输出
  头 charset=gbk,正文 gbk                          → encoding=gb18030    解码结果与原文一致
  没有头,<meta charset=gbk>,正文 gbk                 → encoding=gb18030    解码结果与原文一致
  头 charset=utf-8 + <meta charset=gbk>,正文 gbk   → encoding=utf-8      解码结果含 14 个替换字符
  头 charset=gbk + <meta charset=utf-8>,正文 utf-8 → encoding=gb18030    解码结果含 1 个替换字符
  BOM(utf-8-sig)+ 头 charset=gbk,正文 utf-8        → encoding=utf-8      解码结果与原文一致
  构造参数 encoding=gbk,头 charset=utf-8,正文 gbk      → encoding=gbk        解码结果与原文一致
  没有头、没有 meta,正文 gbk(自动探测)                      → encoding=cp949      解码结果与原文不一致(乱码)
  没有头、没有 meta,正文 utf-8(自动探测)                    → encoding=utf-8      解码结果与原文一致

TextResponse.encoding 先看构造参数,再依次看 BOM、HTTP 头的 charset、正文里的 <meta>,最后才自动探测。头和 <meta> 冲突时头赢,即使头是错的。

自动探测的可靠性

实测 · 本机执行输出
  真实编码                 3         6        12        25        50   (格子里是探测出的编码;★ 表示解码结果与原文不一致)
  gbk             cp949★    cp949★   gb18030   gb18030   gb18030
  big5         big5hkscs big5hkscs big5hkscs big5hkscs big5hkscs
  shift_jis        cp932     cp932     cp932     cp932     cp932
  euc-kr      big5hkscs★     cp949     cp949     cp949     cp949

先试 ascii、utf-8,再交给 charset-normalizer。文本很短时(3、6 个字)GBK 会被判成 cp949,结果乱码;GBK 页面最好让服务器发对头,或在代码里显式指定编码。

声明错误的后果

实测 · 本机执行输出
  text/plain; charset=utf-8,正文 GBK 的 JSON:r.encoding = utf-8 ;r.text = '{"����": "����", "����": 3}' ;替换字符 12 个
  r.json() = {'����': 3}
  application/json(没有 charset),正文 GBK:r2.encoding = cp949
  r2.json() = {'츰俚': '탔녔', '鑒좆': 3}
  同上,构造时 encoding=gbk:r3.json() = {'名字': '爬虫', '数量': 3}

头里声明错误的编码不会抛异常,.text 里是替换字符;JSON 的键如果全是被替换的字符,会合并成同一个键,数据丢失。

编码名规范化

实测 · 本机执行输出
  charset=utf8          → r.encoding = utf-8
  charset=UTF-8         → r.encoding = utf-8
  charset=gb2312        → r.encoding = gb18030
  charset=GBK           → r.encoding = gb18030
  charset=gb18030       → r.encoding = gb18030
  charset=latin1        → r.encoding = cp1252
  charset=iso-8859-1    → r.encoding = cp1252
  charset=ascii         → r.encoding = cp1252
  charset=windows-1252  → r.encoding = cp1252
  charset=shift_jis     → r.encoding = cp932
  charset=euc-kr        → r.encoding = cp949
  charset=big5          → r.encoding = big5hkscs
  charset=x-unknown     → r.encoding = cp1252

w3lib 把各种写法映射到 WHATWG 的标准名:gb2312/GBK → gb18030,latin1/ascii → cp1252 等。

响应类型

实测 · 本机执行输出
  text/html                     → HtmlResponse  有 .text
  text/html; charset=utf-8      → HtmlResponse  有 .text
  application/xhtml+xml         → HtmlResponse  有 .text
  application/xml               → XmlResponse   有 .text
  text/xml                      → XmlResponse   有 .text
  application/json              → JsonResponse  有 .text
  text/plain                    → TextResponse  有 .text
  application/javascript        → TextResponse  有 .text
  image/png                     → Response      .text 抛 AttributeError: Response content isn't text
  application/octet-stream      → Response      .text 抛 AttributeError: Response content isn't text
  application/pdf               → Response      .text 抛 AttributeError: Response content isn't text
  (无 Content-Type)              → Response      .text 抛 AttributeError: Response content isn't text

只有 TextResponse 及其子类(HtmlResponse、XmlResponse、JsonResponse)才有 .text 和选择器;二进制类型是普通 Response。

完整抓取

实测 · 本机执行输出
  头 charset=gbk                               → encoding=gb18030  <title> = '爬虫入门'
  头无 charset,<meta charset=gbk>               → encoding=gb18030  <title> = '爬虫入门'
  头 charset=utf-8(错的),<meta charset=gbk>      → encoding=utf-8    <title> = '��������'
  头无 charset、无 meta(自动探测)                     → encoding=big5hkscs <title> = '鰾單⻌藷'

真实抓取的结果与单元实验一致:头对或 <meta> 对就能还原,头错或什么都没有就乱码。

分步演示

响应编码
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Scrapy 2.19.0 / Twisted 26.4.0 / lxml 6.1.3 / parsel 1.12.1(临时虚拟环境);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、重定向、压缩、断连等行为,并记录收到的原始请求;每个实验在临时目录里新建 Scrapy 项目或单文件 spider,命令输出来自真实的 scrapy 进程;计时用 time.perf_counter,数字每次运行会有小幅抖动。第一到五节直接在虚拟环境里构造 HtmlResponse / TextResponse(不经网络);第六节用本机服务器发 GBK 字节;编码探测使用 Scrapy 2.19 自带依赖(w3lib 2.5.0、charset-normalizer)
    • 没有验证:真实网站上各编码页面的探测成功率(这里只有四种语言各一小段文字);charset-normalizer 不同版本的差异;HTTP 压缩响应解压后的编码判断;XmlResponse 里 声明与头冲突时的顺序;请求的 encoding 参数(Request.encoding)对响应的影响
    京ICP备2026067502号-1京公网安备11010502063487号