网页乱码的分界不在1024字节,做SEO的脚本比浏览器早八千字节就瞎了
本文目录
- 那条传了十几年的1024字节规矩,今天还成立吗?
- 但同一批字节,工具那边是另一副样子
- 实验台是怎么搭的
- 规范到底把这个窗口写成了什么?
- 把声明推到128 KB,浏览器为什么还认得出来?
- 后路一:推倒重来
- 后路二:自动检测
- UTF-8凭什么能被认出来,GBK就不行
- 同一批字节喂给五种工具,谁先瞎掉?
- requests的.text:从第一个字节起就没对过
- html.parser:第8192字节是它的悬崖
- lxml和html5lib:位置上不输浏览器,却栽在不声明上
- 只读前1 KB:那条规矩真正的归属
- 三个地方都能写编码,到底听谁的?
- 把这条链翻译成排查顺序
- meta有两种写法,长度差三倍
- 121个站的编码声明,都写在什么位置?
- 位置分布:绝大多数写得很靠前
- 越过1024那条线的8个站
- 但这8个站一个都没出事
- 顺带三条小观察
- 为什么PHP站几乎踩不到这个坑?
- 兜底还不止一层
- 那么真正会翻车的,是哪一类站?
- 迁移期是高发窗口
- 抓取管道这一侧的风险,比浏览器那侧高得多
- 搜索结果里的表现
- 不用查十六进制也行:乱码的形状会告诉你是哪一层错了
- 该按什么顺序查,又该怎么配才稳?
- 排查:四步,按优先级从高到低
- 配置:三条就够
- 写脚本时的三条硬规矩
- 常见问题解答
- 编码声明必须写在前1024字节以内吗?
- 响应头和meta都写了编码,但两边不一样,听谁的?
- 为什么我用requests抓中文站全是乱码?
- 页面完全不写编码声明会怎样?
- 用BeautifulSoup抓页面,选哪个解析器不容易踩编码的坑?
- 我的站是PHP做的,需要专门配编码吗?
- 页面乱码变成一串生僻汉字,和变成方块有什么区别?
- 把gb2312改写成GBK会有影响吗?
- CDN会不会把编码这件事搞坏?
- 怎么确认搜索引擎抓回去的是不是乱码?
- 权威参考资料
摘要:保哥搭了个实验台,把编码声明一格一格往后推,推到第131072字节浏览器照样认得出来,中文一个都没花。可同一批字节喂给做SEO常用的那几个Python库,有的在第8192字节就崩了,有的从第一个字节起就没对过。规范里那个1024的数字确实写着,只是它管的不是浏览器最终显示什么,而是预先扫一眼那一步。真正说了算的顺序是:字节顺序标记、HTTP响应头、然后才轮到meta。
那条传了十几年的1024字节规矩,今天还成立吗?
只要搜过网页乱码,八成会撞见这句话:编码声明必须写在HTML的前1024字节以内,否则浏览器就来不及了。这句话有出处,规范里确实有这个数字。
保哥想验一下它今天还准不准,于是在服务器上另起了一个nginx,把编码声明用注释填充一格一格往后推:1000、1024、1030、2048、4096、8192,一直推到131072字节。正文是同一段中文,用GBK编码发出去,声明写gb2312。然后拿真实浏览器一个个打开,读 document.characterSet。
结果是全部正确识别,一个字都没乱。128 KB的偏移,是规范那个数字的128倍。
但同一批字节,工具那边是另一副样子
把同样这些页面喂给平时写SEO脚本会用的几个库,画风立刻变了。有的在第8192字节崩掉,有的压根就没对过一次。这就是本篇要处理的那道裂缝:页面在浏览器里好好的,在你的抓取工具里是一堆问号,两边都没坏。
这和head的边界由解析器判定是同一个母题的两半——一个是位置,一个是字节窗口。区别在于,那一篇里浏览器和工具的分歧是结构上的,这一篇里的分歧直接体现成能不能读懂中文。
实验台是怎么搭的
这类问题必须自己造样本,因为现网找不到干净的对照——你没法让别人的站把声明挪一挪再让你测一次。
做法是在服务器上另起一个nginx,只发静态文件,开三个端口,唯一的区别是响应头怎么发:
| 端口 | Content-Type | 用途 |
|---|---|---|
| 第一个 | 裸的text/html,不带charset | 主实验组,让规范的嗅探算法真正跑起来 |
| 第二个 | text/html; charset=utf-8 | 对照组,测响应头的优先级 |
| 第三个 | text/html; charset=gb2312 | 反向对照组 |
页面这边生成了50多个静态文件:编码声明的偏移从0一格格推到131072,正文分UTF-8和GBK两套,另外几个专测不声明和字节顺序标记。每个文件里嵌一小段脚本,把 document.characterSet 读出来显示在页面上,这样浏览器最终用了什么编码是它自己报的,不是我猜的。
为什么非得另起一个nginx,而不是在生产站上开个目录,后面那节会讲——那本身就是本篇的一个发现。
规范到底把这个窗口写成了什么?
去翻HTML标准的解析章节,1024出现在一个叫预扫描字节流以确定其编码的小节里。它规定的动作是:在正式解析之前,先拿前面一段字节快速扫一眼,找有没有meta声明;这一段的长度由实现决定,标准给的建议值是1024字节。
关键在于,预扫描只是整个编码嗅探算法里的一步,而且是排在后面的那一步。完整顺序是这样的:
| 顺序 | 依据 | 说明 |
|---|---|---|
| 1 | 字节顺序标记 | 文件开头那三个字节,优先级最高,谁也盖不过它 |
| 2 | HTTP响应头里的charset | 服务器说的,压过页面里写的 |
| 3 | 预扫描前1024字节找meta | 就是那条广为流传的规矩 |
| 4 | 自动检测或地区默认值 | 前三步都没结果时的兜底 |
还有一条常被漏掉的:预扫描没找到,不代表事情就结束了。标准明确写了,解析过程中如果后来遇到了一个跟当前编码不一致的meta声明,用户代理可以改变编码——办法是把整个导航算法重跑一遍。这一条就是浏览器能在128 KB之后翻盘的依据。
把声明推到128 KB,浏览器为什么还认得出来?
因为它有两条后路,而工具通常一条都没有。
后路一:推倒重来
预扫描扫不到,浏览器先用猜的编码开始解析;解析到那个meta时发现猜错了,就把已经建好的树全扔掉,换正确的编码从头再解析一遍。用户看到的只是一次略慢的加载,看不到中间那次作废。
这也解释了为什么这条规矩虽然过时了,但把声明写在最前面依然值得做:你省下的是一次整篇重解析。声明在第82字节和在第8万字节,最终显示一样,中间的功夫差着一整轮。
这里得把话说严谨:保哥没有量这次重解析到底多花了几毫秒。实验台的页面只有几KB,量出来的差值淹没在噪声里,没有参考价值。要在真实的几百KB页面上做这个测量,得隔离掉网络、缓存和渲染的干扰,那是另一个题目。所以这里只说机制上多跑了一轮,不给数字——上一篇里那些落点结论是二值的、量得准,性能这一项不是,就别硬凑。
后路二:自动检测
实验台上还放了一组完全不声明编码的页面。UTF-8那一份,浏览器判成UTF-8,中文正常;GBK那一份,浏览器判成UTF-8,中文全成了方块。
| 页面 | meta声明 | HTTP头 | 浏览器判定 | 中文 |
|---|---|---|---|---|
| UTF-8正文 | 无 | 无 | UTF-8 | 正常 |
| GBK正文 | 无 | 无 | UTF-8 | 乱码 |
| UTF-8正文 | 推到131072 | 无 | UTF-8 | 正常 |
| GBK正文 | 推到131072 | 无 | GBK | 正常 |
UTF-8的字节序列有很强的自校验特征,猜错的概率极低,所以现代浏览器对它基本免疫。GBK就没这个待遇——它的字节看上去和一堆别的编码没什么两样,不声明就只能靠蒙。
这张表其实已经把结论给出来了:如果你的站是UTF-8,这一整套机制大概率轮不到你操心;如果站里还有GBK的老页面,声明就是唯一的生命线。
UTF-8凭什么能被认出来,GBK就不行
这不是浏览器偏心,是两种编码的结构不一样。
UTF-8的每个多字节序列都有固定形状:首字节的高位标明这个字符总共几个字节,后续字节的高两位必须是10。一段随机字节要想通篇满足这个约束,概率低到可以忽略。所以扫一遍全文,只要没出现违规组合,判成UTF-8几乎不会错。
GBK就没有这种冗余。它的双字节范围和别的东亚编码大量重叠,同一串字节用GBK、Big5、Shift_JIS解出来都是“看着像字”的结果,只是内容完全不同。老编码页面那笔账里西里尔字母那几套编码是一样的道理——单靠字节分不出来,只能靠声明。
顺带解释一个常见困惑:为什么有的地方写gb2312、有的写GBK,结果好像都一样。因为编码标准里把gb2312这个标签直接映射到了GBK的解码器,两个名字对应同一套实现。写哪个都行,但既然实际能力是GBK,写GBK更诚实。
同一批字节喂给五种工具,谁先瞎掉?
浏览器有后路,脚本没有。保哥把实验台上那批页面原样抓下来,喂给五种最常见的消费方式。
| 声明位置 | requests的.text | html.parser | lxml | html5lib | 只读前1 KB |
|---|---|---|---|---|---|
| 第0字节 | 乱码 | 正常 | 正常 | 正常 | 找得到 |
| 第1000字节 | 乱码 | 正常 | 正常 | 正常 | 找得到 |
| 第1024字节 | 乱码 | 正常 | 正常 | 正常 | 找不到 |
| 第2048字节 | 乱码 | 正常 | 正常 | 正常 | 找不到 |
| 第8192字节 | 乱码 | 乱码 | 正常 | 正常 | 找不到 |
| 第65536字节 | 乱码 | 乱码 | 正常 | 正常 | 找不到 |
| 第131072字节 | 乱码 | 乱码 | 正常 | 正常 | 找不到 |
| 完全不声明 | 乱码 | 正常 | 乱码 | 乱码 | 找不到 |
四列四种败法,值得逐个说。
requests的.text:从第一个字节起就没对过
13组测下来无一例外全是乱码,而且它自报的编码一律是ISO-8859-1。原因是老规矩:HTTP响应头里是text类型但没写charset时,按上古版本的规定默认ISO-8859-1。它完全不看HTML里的meta——那不在它的职责范围内,它只是个HTTP客户端。
这条最值得警惕,因为 r.text 是所有教程里的第一行代码。用它去抓一个没在响应头声明编码的中文站,抓回来的每个字都是错的,而程序不会报任何错。正确写法是把字节交出去,让解析器自己定:
# 会乱码:requests 按 ISO-8859-1 解,完全不看 meta
soup = BeautifulSoup(r.text, 'lxml')
# 正确:把原始字节交给解析器,由它按标准嗅探
soup = BeautifulSoup(r.content, 'lxml')一个 .text 换成 .content,差别就是整站中文对不对。
html.parser:第8192字节是它的悬崖
Python自带那个解析器在1024之后还撑了很久,一直到8192才崩。保哥把这个边界二分了一遍:
| 声明起始位置 | 声明结束位置 | 结果 |
|---|---|---|
| 第8100字节 | 第8122字节 | 正常 |
| 第8150字节 | 第8172字节 | 正常 |
| 第8180字节 | 第8202字节 | 乱码 |
| 第8192字节 | 第8214字节 | 乱码 |
分界卡在8150和8180之间,而 <meta charset="gb2312"> 这串正好22字节:8150加22是8172,还在8192以内;8180加22是8202,出界了。所以它的窗口是8192字节,而且要求整条声明完整落在窗口里,露出去一个字符都不行。
于是三个数字凑齐了,而且互不相同:
- 规范建议的预扫描窗口:1024 字节
- Python自带解析器的实际窗口:8192 字节
- 浏览器实测仍能翻盘的位置:至少 131072 字节
lxml和html5lib:位置上不输浏览器,却栽在不声明上
这两个库对声明位置完全不敏感,推到128 KB照样识别,跟浏览器打平。但页面完全不声明编码时,它们两个都乱,而浏览器正常。差的就是那条自动检测的后路——库不做这件事,因为它们拿到的只是一段字节,没有浏览器那套上下文。
只读前1 KB:那条规矩真正的归属
最后一列模拟的是流式管道:只取前1024字节,用正则找编码声明,找不到就用默认值。这一列的表现完全符合规范那条1024的描述——超过就是找不到。
这一列的存在解释了那条规矩为什么会流传这么久:它准确描述了预扫描这一步,而今天真正只做预扫描、不做重启的,是工具,不是浏览器。规矩没错,只是它管的对象在这十几年里换人了。
三个地方都能写编码,到底听谁的?
实验台开了三个端口,唯一区别是响应头怎么发:一个发裸的text/html不带charset,一个发charset=utf-8,一个发charset=gb2312。同一批文件,三个端口各跑一遍。
| 响应头 | 文件实际编码 | meta声明 | 字节顺序标记 | 浏览器判定 | 中文 |
|---|---|---|---|---|---|
| gb2312 | UTF-8 | utf-8(第0字节) | 无 | GBK | 乱码 |
| utf-8 | GBK | gb2312(第0字节) | 无 | UTF-8 | 乱码 |
| gb2312 | UTF-8 | gb2312 | 有 | UTF-8 | 正常 |
| 无 | GBK | gb2312 | 无 | GBK | 正常 |
前两行是一组对称的实验:meta写在第0字节,位置不能再靠前了,照样一个字都不算数——响应头一发话,meta就出局。第三行更狠:响应头说gb2312、meta也说gb2312,两边一致,结果那三个字节的标记把两边一起否了。
把这条链翻译成排查顺序
- 先看响应头。一条命令的事:
curl -sI 你的地址 | grep -i content-type。这里写的是什么,页面里写什么基本无关。 - 再看文件头三个字节。有标记就以标记为准,而且它会盖掉响应头。编辑器悄悄加上的那三个字节能引发的麻烦不止白屏这一种。
- 最后才看meta。前两步都没声明,它才轮得上。
顺带说个反直觉的:现网121个站里带字节顺序标记的是0个。这东西优先级最高,但没人主动用它,它只会在你不知情的时候混进来。
meta有两种写法,长度差三倍
同一件事,HTML里有两种写法:
| 写法 | 字节数 | 说明 |
|---|---|---|
<meta charset="utf-8"> | 22 | HTML5的短写法 |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> | 68 | 老写法,效果完全相同 |
两者等价,预扫描都认。但在窗口这件事上,长的那个天生吃亏——它自己就占掉68字节,而且它整条都必须落在窗口里才算数,露出去一个字符就前功尽弃。前面二分出来的那个8192边界,卡的正是声明的结束位置而不是起始位置。
老模板里这两种写法并存的情况很常见,有的还两条都写。两条都写不算错,解析器取先出现的那条,但它平白多占了几十字节的窗口预算。
121个站的编码声明,都写在什么位置?
光有实验台不够,得看看真实世界长什么样。同一批125个域名的首页,抓到并解析成功121个。
位置分布:绝大多数写得很靠前
| 分位 | meta charset的字节偏移 |
|---|---|
| 最靠前 | 第28字节 |
| 四分之一分位 | 第44字节 |
| 中位 | 第82字节 |
| 四分之三分位 | 第197字节 |
| 九成分位 | 第690字节 |
| 最靠后 | 第311364字节 |
九成分位还在690,说明主流做法是把它写在最前面。但尾巴拖得非常长。
越过1024那条线的8个站
| 站点类型 | 声明位置 | 是规范窗口的几倍 |
|---|---|---|
| 某新闻门户 | 第311364字节 | 304倍 |
| 某综合电商 | 第5124字节 | 5倍 |
| 某科技媒体 | 第3408字节 | 3.3倍 |
| 某鞋类电商 | 第2761字节 | 2.7倍 |
| 某邮件营销平台 | 第2012字节 | 2倍 |
| 某建站平台 | 第1986字节 | 1.9倍 |
| 某体验研究机构 | 第1410字节 | 1.4倍 |
| 某搜索资讯站 | 第1212字节 | 1.2倍 |
8个站,占写了meta那114个站的7%。第一名那个新闻门户尤其离谱,它的 </head> 落在第2398966字节,整个head有2.3 MB,编码声明埋在里面第31万字节的位置。
但这8个站一个都没出事
因为它们的HTTP响应头里全部写了charset=utf-8。按前面那条优先级链,响应头一发话,meta在第几字节根本无所谓。
把这个交叉做完,得到本篇最该记住的一个数字:meta超窗口、同时HTTP头又没声明——这个真正致命的组合,121个站里命中0个。
| 组合 | 站数 |
|---|---|
| 响应头与meta都写了,且两边一致 | 103 |
| 响应头与meta都写了,但不一致 | 0 |
| 只有响应头,没写meta | 6 |
| 只有meta,响应头没写 | 11 |
| 两处都没写 | 1 |
那103个站里,meta其实一个字都没起作用——它们真正生效的是响应头。而真正靠meta活着的只有11个站,这11个才是需要关心声明位置的。至于两处都没写的那一个,它是个几KB的跳转壳子,正文没有中文,蒙对蒙错都无所谓。
顺带三条小观察
- 声明的值高度统一:113个站写utf-8,1个写utf8(少了个连字符,标准的编码标签表里两者都认,不影响)。
- 5个站把
<title>排在了charset前面,偏移分别是32对85、43对208、50对136、70对149,还有一个是1905对1986。前四个差距很小无关痛痒,最后那个把两者一起推过了1024。 - 7个站压根没写meta charset,全靠响应头。meta标签体检工具大概率会给它们扣分,可它们一点问题都没有。
为什么PHP站几乎踩不到这个坑?
这个发现是在搭实验台时被迫撞出来的。
保哥一开始用PHP写测试页,代码里明明白白只发了 Content-Type: text/html,不带charset。结果curl一看,响应头是 text/html;charset=UTF-8。
查下来是PHP的 default_charset,出厂值就是UTF-8,它会自动往Content-Type里补上。也就是说,只要页面是PHP输出的,编码声明这件事默认就已经被兜住了,你在HTML里写不写、写在第几字节,全都无所谓。
兜底还不止一层
| 兜底层 | 默认行为 | 怎么确认 |
|---|---|---|
| PHP的default_charset | 出厂UTF-8,自动补进响应头 | php -i | grep default_charset |
| nginx的charset指令 | 出厂是off,但面板类环境普遍开着 | 查配置里有没有charset那一行 |
| CDN或反向代理 | 各家不同,有的会重写 | 对比源站与边缘的响应头 |
为了让规范行为真正跑起来,保哥最后是另起了一个nginx、把charset关掉、只发静态文件,才拿到那个裸的 text/html。换句话说,今天想在生产环境里复现这个经典坑,得先拆掉两层保险。这也从侧面解释了为什么现网121个站的致命组合是0。
反过来说,风险最高的恰恰是那些没有这两层兜底的场景:纯静态站点直接由CDN发出、对象存储托管的页面、把HTML当附件下发的接口。这些地方响应头里有没有charset,取决于上传时那个Content-Type元数据填了什么,而那个字段经常是空的。
那么真正会翻车的,是哪一类站?
把前面所有条件叠在一起,能筛出一个很窄的画像。同时满足这四条才会出事:
- 正文不是UTF-8(在中文语境里,基本等于GBK或GB2312的老页面)
- HTTP响应头里没有charset
- 没有字节顺序标记
- meta声明写得太靠后,或者压根没写
四条同时成立的站确实少,但它有个特点:一旦成立,中招的往往是整站,不是一两个页面。因为这四条全是站级配置。
迁移期是高发窗口
最典型的场景不是“一直这样”,而是“刚改过”。老站从GBK迁到UTF-8的过程中,正文已经转好了,响应头还留着gb2312;或者反过来,静态文件换了服务器,新服务器不发charset了。老编码页面迁移那笔账里最容易漏的就是这一步——大家都盯着文件本身转没转,很少有人回头查响应头。
抓取管道这一侧的风险,比浏览器那侧高得多
把前面那张五工具对照表再看一遍,会发现一件事:浏览器是这条链上唯一有两条后路的角色,其余全靠声明。而今天读你页面的东西里,浏览器占的比例正在下降。
| 消费方 | 能重启解析吗 | 能自动检测吗 |
|---|---|---|
| 浏览器 | 能 | 能 |
| 搜索引擎的渲染环节 | 用的是浏览器内核,能 | 能 |
| 只抓HTML不渲染的那一遍 | 看实现,多数不能 | 看实现 |
| 各类审计工具、脚本 | 基本不能 | 基本不能 |
| 只读前N字节的流式管道 | 不能 | 不能 |
这张表和抓取与渲染分几步那套模型是叠在一起的:渲染那一遍问题不大,先抓HTML那一遍才是薄弱环节。而先抓的那一遍决定了标题和描述最初被记成什么。
搜索结果里的表现
编码错了,页面标题和描述抓回去就是一串认不出的字符,直接进搜索结果。这类问题的排查特征很鲜明:
- 浏览器里打开完全正常,因为它有重启解析和自动检测两条后路
- 抓取工具、第三方审计平台看到的是乱码,因为它们没有
- 你去问同事,一半人说没问题,一半人说满屏方块,取决于他们用什么打开的
遇到这种各执一词的场面,别急着判断谁看错了。直接把字节调出来看十六进制,是唯一不会骗人的办法——中文字符在UTF-8里是三个字节、在GBK里是两个字节,一眼就能分。
不用查十六进制也行:乱码的形状会告诉你是哪一层错了
实验台上两个方向的错配都跑了一遍,同一句中文,乱出来的样子完全不同。
| 错配方向 | 乱码形态 | 为什么长这样 |
|---|---|---|
| UTF-8的字节被当成GBK解 | 一串生僻汉字,例如“淇濆摜鐨勭紪鐮佺” | 三个字节被切成一个半汉字,每两字节凑出一个真实存在但毫不相干的字 |
| GBK的字节被当成UTF-8解 | 一串方块或问号,例如“����” | 字节不满足UTF-8的结构约束,解码器只能吐出替换字符 |
这就是“网页乱码为什么会变成生僻字”这个问题的答案。看见生僻汉字,说明内容是UTF-8而声明说了GBK;看见方块和问号,说明内容是GBK而声明说了UTF-8。方向一眼就能定,剩下的只是去查是响应头写错了还是meta写错了。
还有第三种形态值得一并记住:整页只有中文乱、英文和数字完全正常。这说明编码错配确实存在,因为这两套编码对ASCII范围的处理是一样的。如果连英文都乱了,那多半不是编码问题,该去查是不是文件本身损坏或者被压缩过一次没解开。
顺便,地址栏里的中文是另一笔账,走的是百分号转义那套规则,和页面编码不是同一回事。中文网址的编码看起来相似,排查路径完全不同,别混在一起查。标题里那些特殊符号跨平台显示不一致也是独立的一类,属于字体和字符集支持问题,不是解码错配。
该按什么顺序查,又该怎么配才稳?
排查:四步,按优先级从高到低
- 查响应头。
curl -sI看Content-Type有没有charset、写的是什么。有的话,它就是答案,后面几步只是确认一致性。 - 查前三个字节。
curl -s 地址 | head -c 3 | xxd,出现efbbbf就是有标记。它会盖掉响应头。 - 查meta的偏移。不只是看写没写,要看写在第几字节。把原始字节存下来搜一次位置即可。
- 用两种解析器各跑一遍。结果不一致的地方,就是你的工具链和浏览器分道扬镳的地方。
配置:三条就够
- 把charset写进响应头,这是唯一真正稳的一条。它优先级高、和页面内容解耦、改一次全站生效。nginx加一行charset指令,PHP站默认就有。
- meta照旧写,位置尽量靠前。它是响应头缺失时的兜底,也是页面被另存为本地文件之后唯一的依据——那时候响应头已经不存在了。
- 别主动加字节顺序标记。它优先级最高、最难排查,而且在某些场景会引发别的麻烦。真要保险,靠响应头。
写脚本时的三条硬规矩
- 拿
r.content,不要拿r.text。这一条能挡掉最多的事故。 - 别用只读前N字节找声明的写法做全站判断,除非你确定这批站的声明都写在最前面。
- 如果结论要拿去和浏览器里看到的对齐,记得工具没有重启解析和自动检测这两条后路,差异是系统性的,不是偶发。
常见问题解答
编码声明必须写在前1024字节以内吗?
对浏览器来说不必须。实测把声明推到第131072字节,浏览器照样正确识别,因为它可以推倒重来。但对只做预扫描的工具来说这条仍然成立,而且不同工具的窗口还不一样。写在最前面依然是最省事的做法,它省下的是一次整篇重解析。
响应头和meta都写了编码,但两边不一样,听谁的?
听响应头的。实测里meta写在第0字节都无效,响应头说gb2312就按gb2312解。所以两边不一致时,改meta是没用的,得去改服务器配置。
为什么我用requests抓中文站全是乱码?
大概率是用了 r.text。响应头没写charset时,requests按老规矩默认ISO-8859-1,而且它完全不看HTML里的meta。把 r.text 换成 r.content 交给解析器,问题就没了。
页面完全不写编码声明会怎样?
UTF-8的正文基本没事,浏览器的自动检测认得出来。GBK的正文会被判成UTF-8,中文全花。而且这一档lxml和html5lib也会乱,它们不做自动检测。
用BeautifulSoup抓页面,选哪个解析器不容易踩编码的坑?
在编码这件事上,lxml和html5lib都比Python自带那个强,两者对声明位置都不敏感。自带那个在第8192字节就会崩。但更重要的是喂给它原始字节而不是已经解错的字符串。
我的站是PHP做的,需要专门配编码吗?
基本不用。PHP的default_charset出厂就是UTF-8,会自动往响应头里补charset。真正要留意的是纯静态站点、对象存储托管的页面,还有CDN会不会把响应头改掉。
页面乱码变成一串生僻汉字,和变成方块有什么区别?
方向正好相反。生僻汉字说明内容是UTF-8而被当成了GBK解,三个字节被切成一个半汉字;方块和问号说明内容是GBK而被当成了UTF-8解,字节不满足UTF-8的结构约束只能吐替换字符。看一眼形状就能定方向,不用查十六进制。
把gb2312改写成GBK会有影响吗?
没有。编码标准里gb2312这个标签本来就映射到GBK的解码器,两个名字对应同一套实现。写GBK更贴近实际能力,但改不改都不影响解码结果。
CDN会不会把编码这件事搞坏?
会,而且方向是双向的:有的边缘节点会补上源站没写的charset,有的会把源站写的换掉。判断办法是绕过边缘直连源站再取一次响应头,两边对比。做整站审计时这一项值得单列,因为它改的是全站每一个页面。
怎么确认搜索引擎抓回去的是不是乱码?
不要用浏览器确认,它有两条后路会替页面把问题遮住。用curl拿原始字节,按响应头声明的那个编码去解一次,看中文对不对。直接看响应头这一类工具比在浏览器里点右键有用得多。
权威参考资料
本文标题:《网页乱码的分界不在1024字节,做SEO的脚本比浏览器早八千字节就瞎了》
本文链接:https://zhangwenbao.com/charset-declaration-byte-window-parser-encoding-seo.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0