爬虫拿到的网页乱码,往往不是页面坏了,而是你和服务器对“这段字节是什么编码”有不同的答案。这篇用本机服务器逐一复现:响应头、meta、实际字节三者不一致时 requests 和 lxml 分别怎么选,猜编码的库有多准。
UTF-8 页面,Content-Type: text/html; charset=utf-8
Content-Type='text/html; charset=utf-8';r.encoding='utf-8';r.apparent_encoding='utf-8'
r.text 的正文:开头 '爬虫工程:编码识' 结尾 '8,镕。' → 正确;lxml.html.fromstring(r.content):开头 'ç\x88¬è\x99«å·' 结尾 '\x95ã\x80\x82' → 乱码/不一致
UTF-8 页面,Content-Type: text/html(没写 charset)
Content-Type='text/html';r.encoding='ISO-8859-1';r.apparent_encoding='utf-8'
r.text 的正文:开头 'ç\x88¬è\x99«å·' 结尾 '\x95ã\x80\x82' → 乱码/不一致;lxml.html.fromstring(r.content):开头 'ç\x88¬è\x99«å·' 结尾 '\x95ã\x80\x82' → 乱码/不一致
UTF-8 页面,Content-Type: application/octet-stream(不是 text/*,没写 charset)
Content-Type='application/octet-stream';r.encoding=None;r.apparent_encoding='utf-8'
r.text 的正文:开头 '爬虫工程:编码识' 结尾 '8,镕。' → 正确;lxml.html.fromstring(r.content):开头 'ç\x88¬è\x99«å·' 结尾 '\x95ã\x80\x82' → 乱码/不一致
UTF-8 页面,有 <meta charset="utf-8">,Content-Type 没写 charset
Content-Type='text/html';r.encoding='ISO-8859-1';r.apparent_encoding='utf-8'
r.text 的正文:开头 'ç\x88¬è\x99«å·' 结尾 '\x95ã\x80\x82' → 乱码/不一致;lxml.html.fromstring(r.content):开头 '爬虫工程:编码识' 结尾 '8,镕。' → 正确
GBK 页面,<meta charset="gbk">,Content-Type 没写 charset
Content-Type='text/html';r.encoding='ISO-8859-1';r.apparent_encoding='gbk'
r.text 的正文:开头 'ÅÀ³æ¹¤³Ì' 结尾 'éF¡£' → 乱码/不一致;lxml.html.fromstring(r.content):开头 '爬虫工程:编码识' 结尾 '8,镕。' → 正确
GBK 页面,Content-Type: text/html; charset=gbk
Content-Type='text/html; charset=gbk';r.encoding='gbk';r.apparent_encoding='GB18030'
r.text 的正文:开头 '爬虫工程:编码识' 结尾 '8,镕。' → 正确;lxml.html.fromstring(r.content):开头 'ÅÀ³æ¹¤³Ì' 结尾 'éF¡£' → 乱码/不一致requests 的 r.encoding 来自响应头;text/* 类型没写 charset 时是 ISO-8859-1,于是 UTF-8 页面成了乱码,尽管 r.apparent_encoding 已经猜出了 utf-8。非 text/* 类型(如 application/octet-stream)的 r.encoding 是 None,r.text 反而会用猜测值。requests 不读 HTML 里的 <meta charset>,lxml 解析字节时读 meta、不看响应头。
响应头说 gbk,页面实际是 UTF-8(<meta charset="utf-8">)
Content-Type='text/html; charset=gbk';r.encoding='gbk';r.apparent_encoding='utf-8'
r.text 的正文:开头 '鐖�铏�宸ョ▼:' 结尾 '闀曘��' → 乱码/不一致;lxml.html.fromstring(r.content):开头 '爬虫工程:编码识' 结尾 '8,镕。' → 正确
响应头说 utf-8,页面实际是 GBK(<meta charset="gbk">)
Content-Type='text/html; charset=utf-8';r.encoding='utf-8';r.apparent_encoding='gbk'
r.text 的正文:开头 '���湤��:�' 结尾 '�F��' → 乱码/不一致;lxml.html.fromstring(r.content):开头 '爬虫工程:编码识' 结尾 '8,镕。' → 正确
响应头没写,<meta charset="gb2312">,页面实际是 GBK 且含 GB2312 里没有的字
Content-Type='text/html';r.encoding='ISO-8859-1';r.apparent_encoding='GB18030'
r.text 的正文:开头 'ÅÀ³æ¹¤³Ì' 结尾 'éF¡£' → 乱码/不一致;lxml.html.fromstring(r.content):开头 '爬虫工程:编码识' 结尾 'F-8,' → 乱码/不一致
用 gb2312 解码这段 GBK 字节(errors="replace"):结尾 ',�F。',是否与原文一致:False
用 gbk 解码这段 GBK 字节(errors="replace"):结尾 '8,镕。',是否与原文一致:True
用 gb18030 解码这段 GBK 字节(errors="replace"):结尾 '8,镕。',是否与原文一致:True响应头与实际字节不一致时 r.text 乱码;而用 lxml 解字节、页面有 meta 时能解对。GB2312 的字符集比 GBK 小:页面声明 gb2312 但包含“镕”这样的 GBK 字符,Python 的 gb2312 解码会把它替换掉,lxml 则在该字符处截断了后面的内容。把声明为 gb2312 的页面当 gbk 或 gb18030 解码更安全。
默认 r.text → 'ç\x88¬è\x99«å·¥ç¨\x8b:ç¼\x96ç\xa0\x81è'
r.encoding = r.apparent_encoding('utf-8') 之后 → '爬虫工程:编码识别示例,中文网页常见的 '
自己对字节解码 r.content.decode("utf-8") → '爬虫工程:编码识别示例,中文网页常见的 '
交给 lxml 字节(它自己看 meta;这个页面没有 meta)→ 'ç\x88¬è\x99«å·¥ç¨\x8b:ç¼\x96ç\xa0\x81è'
把 r.text(字符串)交给 lxml,如果文档带 XML 编码声明 → ValueError: Unicode strings with encoding declaration are not supported.最直接的办法:自己对 r.content 解码(比如先试 utf-8,失败再试 gb18030),或把 r.encoding 设成 r.apparent_encoding。给 lxml 传字节而不是字符串,但页面没有 meta 时它也会乱码;给 lxml 传字符串且文档带 XML 编码声明会抛 ValueError。
响应体以 BOM 开头:r.encoding='utf-8',r.text 的前 3 个字符 '\ufeff<h'(\ufeff 是 BOM),r.content.decode("utf-8-sig")[:3]='<ht'响应体开头的 BOM 在 utf-8 解码后变成 \ufeff 字符,留在 r.text 开头;用 utf-8-sig 解码才会去掉。
requests 当前用的检测库:charset_normalizer 3.4.9 语言 真实编码 1 句 3 句 10 句 简体中文 utf-8 utf_8 utf_8 utf_8 简体中文 gbk gb18030 ✗ gb18030 ✗ gb18030 ✗ 简体中文 gb18030 gb18030 gb18030 gb18030 繁体中文 utf-8 utf_8 utf_8 utf_8 繁体中文 big5 big5 big5 big5 日文 utf-8 utf_8 utf_8 utf_8 日文 shift_jis cp932 ✗ cp932 ✗ cp932 ✗ 日文 euc_jp cp949 ✗ euc_jis_2004 ✗euc_jis_2004 ✗ 韩文 utf-8 utf_8 utf_8 utf_8 韩文 euc_kr cp949 ✗ cp949 ✗ cp949 ✗ 法文(带重音) utf-8 utf_8 utf_8 utf_8 法文(带重音) latin-1 cp1250 big5 ✗ cp1252 共 36 个组合,猜得和真实编码不同的有 13 个(✗ 标出;GBK 文本被猜成 gb18030 之类的父集不一定造成乱码,下面再检查解出来的文字是否一致) 检测出编码后用它解码,得到的文字与原文不一致的有 3 个(共 36 个):日文/euc_jp/1 句 被猜成 cp949; 法文(带重音)/latin-1/1 句 被猜成 cp1250; 法文(带重音)/latin-1/3 句 被猜成 big5
用 charset_normalizer 对简体/繁体中文、日文、韩文、法文各取 1/3/10 句,36 个组合里 UTF-8 全对;名字不同的 13 个多是超集(GBK→gb18030、Shift_JIS→cp932、EUC-KR→cp949),文字没错;真正解错的 3 个是 EUC-JP 的 1 句(被猜成 cp949)和 latin-1 的 1 句、3 句(被猜成 cp1250、big5)。样本是我手写的十句,不代表真实网页的统计。