Windows-1251时代的西里尔页面,搜索引擎抓回去的不是俄语,是一串认不出的拉丁字母

Windows-1251时代的西里尔页面,搜索引擎抓回去的不是俄语,是一串认不出的拉丁字母
张文保 更新 36 分钟阅读 2,860 阅读
本文目录
  1. 同一个西里尔字母,为什么会有五套不同的字节?
  2. 几套八位编码各自是怎么来的
  3. 几套编码在字节层的实际差别
  4. 为什么当年不能只用一套
  5. 用户那一侧的浏览器在怎么应付
  6. 一个字符多套字节、一个字符多个码位、多个字符一个模样,这三件事怎么分?
  7. 字节层:同一个字符的多套字节序列
  8. 码位层与字形层各自是另一回事
  9. 三层各自在哪一环出事
  10. 搜索引擎抓到没声明编码的页面,会怎么猜?
  11. 响应头、页面声明与实际字节三者打架时谁说了算
  12. 自动检测是按什么判断的
  13. 猜错之后页面在索引里变成什么
  14. 一个站里两种编码混用的后果
  15. 编码出问题,会以哪几种形式表现在收录上?
  16. 标题变成一串拉丁字母的那几种形态
  17. 页面被收了但一个关键词都对不上
  18. 重复内容判定被意外触发
  19. 地址栏里的西里尔字节,是另一笔账吗?
  20. 路径里的非拉丁字节怎么被转义
  21. 同一个词两套编码就是两个地址
  22. 查询参数与站内搜索的编码
  23. 迁到UTF-8该按什么顺序动,才不会中途更乱?
  24. 先盘清现状:哪几处在声明编码
  25. 数据库、模板、响应头要同时动
  26. 转换本身怎么做才不丢字符
  27. 双写与回滚方案
  28. 迁完之后,收录要重新走一遍吗?
  29. 已收录页面的更新节奏
  30. 哪类页面掉得最久
  31. 迁移窗口期的流量预期
  32. 老编码留下的历史资产要不要保?
  33. 外链锚文本里的乱码
  34. 旧地址与旧参数的处理
  35. 归档页面与静态文件
  36. 迁完UTF-8之后,哪些西里尔问题还在?
  37. 大小写与排序
  38. 同形字符带来的新问题
  39. 用户实际打出来的形态
  40. 这套编码账在别的书写系统上通用吗
  41. 希腊、希伯来与阿拉伯的对应情形
  42. 东亚的多套编码
  43. 拉丁语族的变音字符
  44. 常见问题解答
  45. 怎么快速判断一个站到底用的是哪套编码?
  46. 数据库里混着两套编码,还能安全迁移吗?
  47. 迁移之后收录多久能恢复?
  48. 地址里的西里尔字符要不要一起换成拉丁转写?
  49. 响应头和页面里的声明不一致,改哪个更保险?
  50. 用户能手动切换编码看正常,是不是就不算大问题?
  51. 迁移完成后还需要保留对老编码的兼容吗?
  52. 权威参考资料

摘要:西里尔字母在字节这一层从来不止一套写法。同一个字母在几种八位编码里分别是不同的数值,页面没把用的是哪一套说清楚,抓取方只能猜,猜错就把整页读成一串没有意义的拉丁字符。这类站点在结果里的表现不是排名低,而是标题本身就不成词、关键词一个都对不上。这篇讲清五套编码的字节差别、声明打架时谁说了算,以及迁到UTF-8之后收录要重新走一遍的那部分代价。

同一个西里尔字母,为什么会有五套不同的字节?

几套八位编码各自是怎么来的

拉丁字母的运气比较好,基本字母全部落在单字节编码的前一百二十八个位置里,全世界通用。

西里尔字母没赶上这班车,只能挤进单字节编码剩下的那一百二十八个位置。

问题是这一百二十八个位置由谁来分配,从来没有一个各方都接受的答案。

于是不同的系统各自排了一套:操作系统厂商排了一套,早期网络社群排了一套,国际标准组织又排了一套。

每一套都能完整装下西里尔字母,每一套给同一个字母分配的数值都不一样。

这不是谁做错了,是那个年代的技术条件下必然出现的结果。

刻意把西里尔字母排到与读音相近的拉丁字母固定偏移处,背后有个很现实的原因:那个年代不少通信链路只可靠传输七位,最高位会被抹掉。抹掉之后剩下的字节正好落在拉丁字母区,收件人虽然看到的是一串拉丁字母,勉强还能拼出原意。

几套编码在字节层的实际差别

拿最常见的那个表示价格的西里尔词做例子,它在几套编码里的字节完全对不上。

操作系统厂商那一套把字母按字母表顺序连续排列,字节值是一段连续区间。

早期网络社群那一套刻意做了另一种安排:把西里尔字母排在与它读音接近的拉丁字母相差固定值的位置上。

这样做的好处是即使高位被通信设备抹掉,剩下的拉丁字母还能勉强读出大意。

国际标准组织那一套又按自己的规则排了第三种顺序,跟前两种都不重合。

三套编码放在一起看,同一个字母有三个不同的数值,任何一方都不知道对方在用哪一套。

判断一份老数据到底属于哪一套,其实不用通读全文:找一个高频小写字母,看它的字节数值落在哪个区间就行。三套编码给同一个字母分配的值互不重合,一个字节就能定案,这比任何自动检测都快也都准。

这套判断还能反过来用:拿到一段乱码,按几套编码逐一还原试一遍,能还原出通顺词句的那一套就是原编码。整个过程可以写成十几行脚本批量跑,比人工一条条猜快得多。

为什么当年不能只用一套

技术上当然可以只用一套,实际上做不到,因为存量太大。

操作系统默认用哪一套,用户在本机创建的文档就是哪一套,改不动。

邮件系统、论坛程序、数据库驱动各自有各自的默认值,跨系统传一次就可能被转一道。

更麻烦的是有些环节不做转换只做透传,字节原样传过去,标签上却写着另一套的名字。

于是同一个站里出现两种编码混着跑的情况,而且往往没人知道是从哪一步开始混的。

能统一的时候当然要统一,但绝大多数西里尔站点面对的是已经混了好几年的现状。

还有一层物理原因:当时的终端和打印机把字模表烧在固件里,字节值到字形的对应关系是硬件决定的。换一套编码意味着这批设备全部要换,成本落在实实在在的机器上,不是改一行配置能解决的。

用户那一侧的浏览器在怎么应付

浏览器早就见惯了这种局面,所以它们都内置了一套猜测逻辑。

猜错的时候用户会看到满屏乱码,然后手动去菜单里换一个编码,页面就正常了。

这个手动换编码的动作,在西里尔市场的用户里熟练度相当高,属于上网基本技能。

正因为用户能手动补救,站长常常意识不到自己的页面有问题。

但抓取程序不会去点那个菜单,它只会按自己的判断处理一次,然后把结果存下来。

用户能救的那一步,恰恰是搜索侧完全救不了的那一步。

这里有个很坑的细节:浏览器会把用户手动选过的编码记住,同一个站下次打开就直接按上次的选择渲染。于是站长自己每天看到的都是正常页面,而任何一个第一次来的访客看到的是乱码,自查在这一步就已经失效了。

更麻烦的是搜索引擎的抓取程序不但不会手动切换,还可能在不同时间用不同策略处理同一个页面。于是同一个地址在索引里前后存过两个版本,历史数据变得完全不可比。

一个字符多套字节、一个字符多个码位、多个字符一个模样,这三件事怎么分?

字节层:同一个字符的多套字节序列

这三种问题在症状上有点像,处理办法完全不同,混在一起讨论一定会绕晕。

本篇讲的是第一种:字符是同一个字符,码位也是同一个码位,只是落到字节上有好几种排法。

它出问题的环节在传输与存储,也就是响应头、页面里的声明、数据库连接和文件本身。

典型症状是整页整段地变成一串不成词的字符,人一眼就能看出不对。

因为症状明显,这类问题反而是三种里最容易发现的,只是修起来牵扯的环节最多。

修的办法也很单一:把所有环节统一到同一套编码上,没有别的巧办法。

一句话定义出问题的环节典型症状
字节层(本篇)同一个字符在不同编码方案下是不同的字节响应头、页面声明、数据库、文件整页变成不成词的字符串
码位层同一个字符在字符集里有两个合法码位输入法、内容来源、比对与去重两份内容看着一样却匹配不上
字形层两个不同的字符长得一模一样人眼判断、域名与商品名审核看不出异常,却被判为可疑

字节层的错误有一个很有价值的特征:它是系统性的、可逆的。因为背后是一张固定的映射表,每个字符都被换成了确定的另一个字符,没有信息丢失。这意味着乱码文本能被反推回原文,抢救老数据的可能性就建立在这一点上。

码位层与字形层各自是另一回事

第二种问题出在码位上:字符集里给同一个字符收了两个合法码位,两者都不算错。

这种情况在带附加符号的拉丁字母里最常见,一个字母可以用组合形式表示,也可以有一个独立码位。

它的症状很隐蔽,两份内容在屏幕上一模一样,做字符串比对时却对不上。

第三种问题出在字形上:两个完全不同的字符,画出来的样子没有任何区别。

西里尔字母表里有好几个字母跟拉丁字母长得一样,肉眼分不出来,机器却知道它们是不同的字符。

这一类的症状最反常识:页面看起来完全正常,却会被当成刻意混排来处理。

三层的检查顺序不能随意:一定要先查字节层,再查码位层,最后查字形层。顺序反了会被前一层的噪声淹没,比如页面整体乱码的时候去查同形字符混排,检测器会报出成千上万条假阳性,把真正的问题埋掉。

三层各自在哪一环出事

字节层的事故发生在页面被读取的那一刻,抓取方拿到字节但不知道该怎么解释。

码位层的事故发生在内容被比对的时候,去重、匹配和索引都可能因此错位。

字形层的事故发生在内容被审核的时候,域名、品牌名和商品标题是重灾区。

三层的检查方法也不同:字节层看声明是否一致,码位层看规范化是否做过,字形层看有没有混排。

本篇只处理第一层,另外两层各有各的判定规则,跟编码迁移不是同一件事。

把三层分清楚之后你会发现,很多所谓的编码问题其实压根不在编码上。

一个跑了多年的老站三层同时中招是很常见的。这时候修复顺序同样不能颠倒:字节层没修干净之前,码位层的规范化和字形层的混排检测跑出来的结果全都不可信,等于白跑一遍还浪费了排查时间。

搜索引擎抓到没声明编码的页面,会怎么猜?

响应头、页面声明与实际字节三者打架时谁说了算

一个页面里可以在三个地方说明自己用的是哪套编码,三处经常互相矛盾。

第一处是服务器返回的响应头,它在页面内容之前到达,优先级最高。

第二处是页面头部的声明标签,它写在文件里,但要先把文件当成某种编码读出来才能看到。

第三处是字节本身,它是唯一的事实,另外两处都只是说法。

规范里写清楚了响应头优先于页面内声明,HTML 4.01关于文档字符表示的章节把这个顺序定得很明确。

麻烦在于很多服务器会自作主张地在响应头里加一个默认值,而那个默认值往往是错的。

页面内声明有个先有鸡还是先有蛋的尴尬:要读到这行声明,就得先假定一种编码把文件读开。规范因此要求它必须出现在文件很靠前的位置,超出那个范围写的声明,处理方可以合法地当作没看见。

实际排查时建议把三处的值并排写在一张表里,每种页面类型一行。九成的编码问题在这张表填完的那一刻就已经找到了,剩下一成才需要真正去看字节。

自动检测是按什么判断的

三处都没有可靠信息时,处理方只能看字节分布做统计判断。

不同编码下常用字母的字节值落在不同区间,出现频次的模式也不一样。

文本足够长的时候这种判断相当准,短文本就很容易翻车。

所以标题短、正文少的页面是编码检测错误的高发区,恰恰这类页面又是列表页和分类页。

更糟的是同一个站里有的页面猜对有的页面猜错,问题看起来毫无规律。

把这件事交给统计猜测,本身就是不该出现的状态,声明清楚成本极低。

检测失效的临界长度大约在几十个字节这个量级,低于它统计特征就不成立了。这正好解释了为什么分页导航、标签页、筛选结果页这类文字极少的页面,是同一个站里最先出乱码的那一批。

猜错之后页面在索引里变成什么

猜错的后果不是页面被丢掉,而是页面以另一种面貌被存了下来。

存下来的是一串按错误编码解释出来的字符,它们在目标语言里毫无意义。

这些字符串照样会被切词、照样会建索引,只是永远没有用户会搜到它们。

从站长这一侧看,页面收录数字看着正常,流量却接近于零。

这种组合最容易被误判成排名问题,然后团队开始改标题、加内容、找外链,全都打不到点上。

保哥接手过一个做户外电源的站,排查两周才发现问题在响应头的一个默认值上。

还有一层连带后果:那串乱码字符在字符分布上很像某种西欧语言,于是这个页面会被判成西欧语言的内容。判错语言之后,它可能被投放到完全不相干的语言市场里,进一步稀释掉本来就不多的曝光。

这种情形跟单纯的排名下滑要分开看:排名问题至少说明系统认得出你的词,而编码问题是系统压根没拿到你的词。2003年那次影响很广的算法调整之后,很多站长养成了掉流量先怀疑算法的习惯,遇到这类问题反而绕了远路。

一个站里两种编码混用的后果

更常见的情况是站里大部分页面正常,某个模块的页面不正常。

通常是后加的功能模块用了另一套默认值,比如评论、站内搜索结果或者从外部同步来的数据。

这种情况下自动检测会在同一个域名下得出不同结论,站点的整体信号变得混乱。

排查的办法是逐个模板取一段字节出来直接看数值,不要靠浏览器渲染的结果判断。

浏览器已经替你猜过一次了,你看到的是猜测的结果,不是原始事实。

用命令行工具把原始响应抓下来看,是这一步唯一可靠的方法。

混用最常见的入口不是自己写的代码,而是从外部同步进来的数据:供应商的商品表、物流状态回传、支付通知。这些接口的编码约定往往写在很旧的对接文档里,双方谁都没有再确认过,直到某天某个字段开始出现乱码。

编码出问题,会以哪几种形式表现在收录上?

标题变成一串拉丁字母的那几种形态

最典型的形态是一串带各种附加符号的拉丁字母,看着像某种西欧语言但读不通。

这是把西里尔编码的字节按西欧编码解释出来的结果,每个西里尔字母都被换成了一个西欧字符。

另一种形态是一串问号,这说明中途有一次转换失败,字符被替换成了占位符。

问号这种形态比乱码更糟,因为信息已经彻底丢了,改回来的唯一办法是从源头重新取数据。

还有一种形态是每个字符之间夹着奇怪的符号,那通常是把双字节序列按单字节读的结果。

三种形态对应三种不同的出错环节,看清楚是哪一种,就知道该去查哪一段。

三种形态的可抢救程度完全不同:映射型的能百分之百恢复,问号型的信息已经彻底丢失只能重取,边界错位型的能恢复大部分但接缝处要人工核对。先判形态再定抢救方案,比一上来就写脚本靠谱得多。

页面被收了但一个关键词都对不上

编码错误最隐蔽的表现是页面收录正常、快照正常、就是没有任何关键词能匹配上。

原因是索引里存的是错误解释出来的字符串,而用户搜的是正确的西里尔词。

这两串字符在系统里没有任何关系,不会被当成同一个词的不同写法。

诊断这个问题有个很快的办法:把结果页里显示的那串乱码原样复制去搜一下。

如果能搜到自己的页面,说明索引里存的确实是乱码版本,问题就锁定了。

这个办法只要两分钟,比任何猜测都快。

这个反查办法还能顺手量化影响面:把那串乱码限定在自己的域名下搜一遍,返回多少条就大致是有多少页面以乱码形态入了库。有了这个数,跟团队讨论要不要停下来先修编码时,就有了一个具体的量。

重复内容判定被意外触发

同一份内容如果在两个地址下分别以两种编码提供,字节完全不同,文字却是同一份。

更常见的是反过来:两份不同的内容因为都被读成了乱码,反而在字符层面变得相似。

大量页面被读成乱码时,这些页面的字符分布会异常接近,因为它们共用同一批错误映射出的字符。

这会让原本各不相同的页面在相似度判断上互相靠拢,最后只保留其中一部分。

所以编码错误在大站上的表现,往往是收录量莫名其妙地掉了一大块。

这一点跟内容质量没有任何关系,改内容改不好。

乱码页面之间的相似度被拉高,是因为它们共用同一批高频错误字符。原本内容毫不相干的两个页面,在字符层的相似度可能高达八成,这个数值足以让相似度判断把它们归成一类,而人看着这两个页面完全想不通哪里像。

真要确认是不是这个原因,可以抽二十个被合并掉的页面,把它们的原始字节取出来做一次字符频次统计。如果这二十个页面的高频字符高度重合而正文主题各不相同,基本就能定案。

地址栏里的西里尔字节,是另一笔账吗?

路径里的非拉丁字节怎么被转义

地址只允许出现有限的一批字符,西里尔字母不在其中。

所以西里尔词出现在地址里时,会先被转成字节,再把每个字节写成百分号加两位数值的形式。

RFC 3986关于统一资源标识符的规范把这套转义规则写得很清楚,关键是它没规定字节是按哪套编码产生的。

这就留下了一个口子:同一个西里尔词,按不同编码转义出来是两串完全不同的地址。

两串地址指向同一个页面时,等于凭空多出一份重复。

而它们看起来都是一堆百分号和数字,人眼根本对比不出区别。

这个留白在实际项目里的后果是:两个程序员各自写出的链接生成函数都完全合规,产出的地址却对不上。争论谁写错了没有意义,规范层面确实没规定,团队必须自己定一条约定并写进代码规范里。

同一个词两套编码就是两个地址

这类重复的产生方式通常很不起眼:站内不同模块生成链接时用了不同的编码。

比如分类导航用一套,面包屑用另一套,两处指向同一个页面却写出两个地址。

抓取方会把它们当成两个页面分别抓取,然后再去判断是不是重复。

判断为重复固然浪费抓取,判断不为重复更麻烦,权重被分成两半。

排查办法是把站内所有链接抓一遍,按百分号编码后的形式去重,重复的形态会立刻暴露。

这一步跟页面内容编码是两件独立的事,改好了页面不代表地址也好了。

转义序列里的字母大小写也算一处差异,有的系统把两种写法当同一个地址,有的当两个。这样一来同一个西里尔词理论上能产生四种地址形态,全站抓一遍去重时要把大小写也一起归一化,否则数出来的重复量会偏小。

查询参数与站内搜索的编码

查询参数这一段的编码规则更松,历史上留下的做法更杂。

表单提交时用什么编码,取决于页面本身声明的编码,这就把两件事又绑在了一起。

页面声明改了,表单提交的字节跟着变,站内搜索的日志格式也会变。

迁移编码时如果没同步处理这一段,站内搜索会在某一天突然全部搜不到东西。

更隐蔽的是历史日志:迁移之后的日志和迁移之前的日志用的是两套字节,直接合并统计会出错。

所以迁移那天要在日志里打一个标记,后面做分析时按这个时间点分段处理。

还有个连带影响:表单提交用的编码继承自页面声明。页面编码一改,站内搜索的日志格式就在那一天断代,前后两段日志不能直接合并统计。做搜索词分析的人如果不知道这个断点,会得出一个完全错误的趋势结论。

迁移当天最好在日志里主动写一行标记,写明切换时间与新旧编码名称。半年后有人来分析历史搜索词时,这一行就是唯一能让他知道该怎么分段处理的线索。

迁到UTF-8该按什么顺序动,才不会中途更乱?

先盘清现状:哪几处在声明编码

动手之前先做一件事:把站里所有会声明或影响编码的地方列成一张清单。

清单至少要包括服务器全局配置、站点级配置、程序里的响应头设置、模板文件本身的保存编码。

还要加上数据库的字符集、连接时的字符集设置、以及所有对外接口的编码约定。

这张清单通常比想象的长,一个跑了几年的站列出十几处很正常。

列完之后逐项标注当前值,就会看到哪几处对不上,问题往往就藏在那两三行里。

这一步花半天,能省掉后面反复试错的两周。

清单里最容易漏的是三类平时没人访问的模板:错误提示页、发给用户的邮件模板、以及后台导出的数据文件。它们不出现在日常浏览路径上,所以从来不被检查,却常常是全站唯一还留着老编码的地方。

清单做完不要扔,把它固化成一份检查表,以后每次上新模块都过一遍。编码问题的复发率相当高,因为新模块的作者未必知道这个站有过这段历史。

数据库、模板、响应头要同时动

编码迁移最忌讳分批做,因为任何一处没跟上,就会产生新的混合状态。

数据库存的是老编码、模板已经按新编码保存、响应头还写着老值,这三种排列组合能产生九种不同的错法。

正确的做法是在一个维护窗口里把所有环节一起切换,中间不接受外部请求。

窗口时间取决于数据量,多数中型站点在两小时以内能完成。

切换前要把整站备份一份原始字节,不是备份显示结果,是备份文件和数据库的原始内容。

备份这一步没做,转换出错之后就真的回不去了。

坚持在一个窗口里做完的原因可以算出来:假设有八个环节,每个环节两种状态,中途可能出现的组合数是两百多种。逐个切换等于逐个制造新的错法,而且每种错法的症状都不太一样,排查成本远超停机两小时。

转换本身怎么做才不丢字符

转换的原理是把老编码的字节先解释成字符,再按新编码写回字节。

这一步的前提是必须知道老编码到底是哪一套,猜错就会批量产生问号。

如果数据库里本来就混着两套编码,那就要先按来源把数据分组,分别用不同的老编码转换。

分组的依据可以是创建时间、来源模块或者某个特征字节,具体看这个站的历史。

转换脚本要先在副本上跑一遍,随机抽两百条记录人工核对,确认没有问号再动正式库。

RFC 3629定义的UTF-8编码形式是转换目标的权威依据,字节长度与合法范围都以它为准。

正式转换之前建议加一道往返测试:把老编码的字节转成新编码再转回去,理论上应该与原始字节完全一致。不一致的那些记录就是含有异常字节的记录,把它们单独捞出来人工处理,剩下的可以放心批量跑。

转换脚本还要处理一类边角数据:那些本来就不是文本的字段,比如存了二进制内容或者序列化结构的列。对它们做编码转换会直接损坏数据,转换前必须按字段类型把这些列排除掉。

双写与回滚方案

规模大的站可以做一个过渡方案:数据库先双写,两套编码各存一份。

双写期间对外只提供新编码的那一份,老那一份留作回滚用。

回滚的触发条件要事先写明,比如乱码页面比例超过某个数值就回滚。

没有明确触发条件的回滚方案等于没有方案,真出事的时候没人敢下决定。

双写会带来额外的存储和写入成本,跑一到两周就可以关掉。

关掉之前记得确认新编码那份数据已经完整覆盖了老数据的全部记录。

双写期间可以给新旧两份内容各算一个长度或校验值,逐条比对。比对不上的记录几乎都指向转换脚本的边界情况,比如超长字段被截断、含有控制字符的记录。这是发现脚本隐藏问题最有效的办法,比人工抽样可靠得多。

回滚演练最好在正式窗口之前真跑一次。很多回滚方案纸面上完整,实操时才发现某个环节没有对应的逆操作,等真出事再发现这一点,代价就完全不同了。

迁完之后,收录要重新走一遍吗?

已收录页面的更新节奏

迁移完成的那一刻,索引里存的还是老的那份内容,无论它是正确的还是乱码的。

更新要等抓取方再来一次,而重新抓取的节奏取决于这个页面平时被抓的频率。

首页和主要分类页通常几天内就会更新,深层页面可能要几周甚至几个月。

这段时间里,站里会同时存在已更新和未更新两种状态的页面。

加速的办法是提交站点地图并把更新时间标注清楚,这是当时唯一有效的加速手段。

除此之外没有捷径,剩下的只能等。

全站铺开的周期可以自己粗略测出来:挑二十个分布在不同层级的页面,从日志里翻出它们最近几次被抓取的时间间隔,取一个分布。首页那一档和最深那一档的间隔差多少倍,全站更新完就大致要几个周期。

这段时间里站点地图的更新时间字段要如实填写,填成迁移当天的日期是合理的,因为内容确实变了。全站统一填一个假的近期日期反而会降低这份文件的可信度。

哪类页面掉得最久

掉得最久的是那些本来就抓取频率很低的页面,通常是层级深、内链少的商品页。

这类页面在老编码时代可能就已经收录得很勉强,迁移之后要重新排队。

另一类掉得久的是原本靠乱码字符串意外获得过一点流量的页面,它们本来就是异常状态。

这类页面的数据在迁移后会归零,不要把它当成迁移造成的损失。

要盯的是那些原本正常且有稳定流量的页面,它们如果两个月还没恢复,说明还有环节没改干净。

把这批页面单独列一张表跟踪,比看整站数字有用得多。

跟踪表要按抓取频率分组,不要按目录分组。同一个目录下的页面,抓取频率可以差十倍以上,按目录看会把两类完全不同的页面混在一格里,得出的恢复曲线毫无意义。

跟踪表建议只跟踪两个月,两个月还没恢复的页面要单独排查而不是继续等。经验上这批页面里绝大多数不是抓取慢,而是还残留着某处没改干净的编码声明。

迁移窗口期的流量预期

如果原来的编码是正确的,迁移之后流量基本不会掉,只有轻微波动。

如果原来的编码是错的,那迁移之后流量是从接近零开始往上长,谈不上掉。

真正会掉的是中间那种情况:一部分页面原来正确,迁移过程中被改坏了。

所以迁移的风险控制重点不在编码本身,在于转换过程有没有引入新的错误。

迁完之后第一周每天抽查五十个页面的实际字节,是成本最低的保险。

抽查发现的问题在第一周解决,和在第二个月解决,代价差着一个数量级。

预期要按三种基线分别定:原本编码正确的页面、原本全是乱码的页面、原本时对时错的页面。三者的曲线形状完全不同,混在一张整站图里看,最后大概率会得出一个既解释不了也指导不了行动的结论。

老编码留下的历史资产要不要保?

外链锚文本里的乱码

老站往往积累了不少外部链接,其中一部分的锚文本本身就是乱码。

这些锚文本是别人页面上的内容,你改不了,只能接受。

好消息是锚文本乱码不影响链接本身传递的价值,链接指向的地址才是关键。

所以处理原则是保住地址不变,锚文本乱不乱随它去。

如果链接来源方还在维护,可以发邮件请他们更新,但成功率不要抱太大期望。

把精力放在保住地址上,收益比逐个联系高得多。

乱码锚文本其实是一份免费的历史档案:从锚文本呈现的乱码形态,能反推出链接建立的那个时间点你的页面是什么编码状态。把这些线索排一遍时间线,往往比翻服务器变更记录还快找到编码是哪次改动开始混的。

如果链接方愿意配合,优先联系那几个流量确实带得动的来源,其余的不必逐个去谈。这件事的投入产出比很低,把同样的时间花在修站内地址一致性上收益高得多。

旧地址与旧参数的处理

迁移编码时最容易顺手做的一件坏事,是把地址里的转义形态也一起改了。

地址一改,所有外部链接和已收录记录全部失效,这个损失比编码问题本身还大。

正确的做法是地址保持原样,只改内容编码,两件事完全分开。

如果确实要改地址,那就单独做一次,中间隔开至少一个月,并且做好新旧对应关系。

两件事同时做,出了问题根本分不清是哪一件造成的。

选做哪门语言、投多少预算这类判断我在小语种优先级那篇里单独讲过,跟这里的技术迁移不是一个层面的决定。

两件事隔开做的期间,要先把当前的全量地址清单存一份快照。有了这份快照,后面真要改地址时,新旧对应关系可以自动生成而不用人工整理,同时它也是验证编码迁移没有意外改动地址的对照基准。

归档页面与静态文件

站里通常还躺着一批静态文件,它们不走程序,直接由服务器发出去。

这类文件的编码由文件本身决定,程序改了它们不会跟着改。

还有一批老的下载文件、说明文档、导出的数据表,它们的编码可能又是另一套。

这些文件不一定要立刻转换,但要在服务器配置里为它们单独声明正确的编码。

声明清楚之后它们至少不会被读错,转换可以排在优先级更低的时间做。

清单里漏掉静态文件这一项,是迁移之后最常见的尾巴。

这里有个反直觉的坑:服务器为静态文件统一加的编码声明,优先级高于文件内部可能存在的声明。配置的时候如果一刀切地给所有文件加上同一个字符集,那些原本就已经是新编码的文件反而会被读坏。

迁完UTF-8之后,哪些西里尔问题还在?

大小写与排序

统一编码解决的是字节问题,不解决字母本身的行为问题。

西里尔字母的大小写转换在多数环境下没有陷阱,但排序规则要按语言单独设定。

同样一批词,在不同的西里尔语言里正确的排列顺序并不相同。

字母表顺序也不完全一致,有的语言多几个字母,有的语言少几个。

所以站内的字母索引、筛选器和排序功能,要按具体语言配置而不是按字符集配置。

这一层的规则跟编码无关,迁移完还得单独处理一遍。

不同西里尔语言的字母表长度本身就不一样,有的多几个字母,有的少几个,排序位置也不完全一致。所以字母索引这类功能要按语言生成,把整套字符集的字母一股脑排出来,会出现本语言根本不用的字母占着位置。

还有一个跟编码无关但常一起出问题的点:字母的大小写映射在个别西里尔字母上不是一一对应的。做去重和地址归一化时如果统一转小写,个别词会被合并成同一个,要按语言确认一遍。

同形字符带来的新问题

统一到一套字符集之后,反而更容易出现西里尔与拉丁字母混排的情况。

因为两套字母现在可以自由地出现在同一个字符串里,而其中好几对长得一模一样。

用户复制粘贴、编辑手动输入、外部数据导入,都可能带进混排的字符串。

混排的商品名在搜索时匹配不上,在人眼看来却完全正常,排查起来相当费劲。

解决办法是在内容入库时做一次字符白名单检查,发现混排就报警。

这属于字形层的问题,跟本篇讲的字节层是两回事,但它确实是迁移之后才凸显出来的。

混排检查要放在内容入库那一刻,不要放在展示层。放在展示层意味着每次渲染都要算一遍,成本高不说,也改不了已经存进数据库的脏数据。入库时拦一次,脏数据根本进不来,后续所有环节都省事。

用户实际打出来的形态

还有一层跟编码完全无关但同样影响匹配:用户到底打的是哪种形态。

有的用户手边没有西里尔输入法,就用拉丁字母按读音拼出来。

有的用户会省略某些字母上的附加记号,写出一个不完全规范的形态。

这些形态在字节层完全合法,在词表层却对不上任何一个正式写法。

处理它们要靠关键词覆盖,跟编码迁移是两条并行的工作线。

把编码修好之后,这一层才真正浮出水面,因为在那之前所有问题都被乱码掩盖着。

编码修好还有一个隐形收益:站内搜索日志第一次变得可读。在那之前几年的日志里存的都是按错误编码记下来的字符串,基本没有分析价值。修好之后攒上一两个月,就能拿到这个市场第一份真实的用户用词分布。

这一层的工作可以和编码迁移并行准备:迁移期间先把词表和覆盖方案做出来,等日志一变得可读就能立刻验证,不用再等一轮数据积累。

这套编码账在别的书写系统上通用吗

希腊、希伯来与阿拉伯的对应情形

凡是不在单字节编码前一百二十八位里的书写系统,都经历过同一段历史。

希腊字母、希伯来字母、阿拉伯字母各自都有过多套互不兼容的八位编码。

它们的症状与排查方法跟西里尔完全一致,只是具体的编码名字不同。

差别在于阿拉伯与希伯来还叠加了书写方向的问题,编码修好之后还有一层要处理。

所以在这几种书写系统上,编码迁移只是第一步,不是全部。

本篇的排查清单可以直接照搬,替换掉编码名称即可。

书写方向的问题有个特点:它只有在编码正确之后才会暴露出来。编码还乱着的时候,页面里全是不成词的字符,方向对不对根本看不出来。两个问题叠在一起时容易被当成一个问题处理,结果修完编码又冒出一批新现象。

东亚的多套编码

东亚的情况更复杂一些,因为那里的字符数量远超单字节能表示的范围。

所以东亚用的是多字节编码,同一个字符占两个甚至更多字节。

多字节编码的错误症状跟单字节不同:不是每个字符换成一个乱码,而是字符边界整体错位。

边界错位之后的结果更难看懂,也更难反推原文。

但迁移的方法论是一样的:盘清声明、一次切换、抽样核对、跟踪收录。

只是转换时要格外小心截断,多字节编码在截断处最容易产生无法恢复的损坏。

多字节编码按字节截断时会切出半个字符,而这半个字符有可能恰好是另一个合法字符的前缀,于是后面整段的字符边界全部错位。这也是为什么东亚站点的乱码经常不是从出错那一处开始,而是从那一处之后全篇都不对。

另外多字节编码里有些字节值同时可能是单字节字符,也可能是双字节字符的后半截,这种歧义让自动检测的准确率明显低于单字节场景。所以东亚站点更依赖明确声明,靠猜的成功率不高。

拉丁语族的变音字符

用拉丁字母的语言看起来最安全,其实也有自己的编码历史。

带附加符号的字母同样落在单字节编码的后半段,同样存在多套分配方案。

区别在于这类站点即使编码错了,页面里的大部分字母仍然可读,只有带符号的那几个字母变成乱码。

正因为只错几个字母,问题更容易被忽略,一放就是好几年。

而这几个字母往往正是区分词义的关键,错掉之后关键词一样匹配不上。

所以这类站点的编码检查,重点是抽查含附加符号的词而不是整页目检。

这类页面的自动检测反而更容易判对,因为绝大部分字节仍落在通用区间里,统计特征没被破坏。判对了整体,错的只有那几个带符号的字母,问题于是可以潜伏很多年——只有做关键词匹配时才会显出来。

常见问题解答

怎么快速判断一个站到底用的是哪套编码?

不要看浏览器的显示结果,浏览器已经替你猜过一次了。正确的做法是用命令行工具把原始响应完整取回来,先看响应头里有没有字符集声明,再把返回的字节按十六进制打印出来,找一个已知的西里尔词对照它的字节值。几套常见编码给同一个字母分配的数值差异很大,对照一次就能确定。如果响应头写着一个值、字节实际是另一套,那就已经定位到问题了。这个方法对静态文件同样适用,而且比任何在线检测工具都可靠,因为工具本身也可能在中间做了一次转换。排查时顺手把首页、分类页、详情页、搜索结果页各取一个样本,四类页面往往由不同模板生成,编码状态可能并不一致,一次全取比逐个页面反复确认省事。

数据库里混着两套编码,还能安全迁移吗?

能,但要先分组。混合编码的库不能用一条命令统一转换,那样一定会把其中一部分转坏。做法是先找出区分两套数据的特征:多数情况下是按创建时间分界,因为编码通常是在某次升级后才改变的;如果时间分不开,就用字节特征判断,两套编码的合法字节范围不完全重合,写一个判定函数逐条打标。分组打标之后按组分别转换,转换完再抽样核对。这个过程比统一转换慢,但它是唯一不会造成不可逆损坏的做法。整个过程务必先在副本上完整跑一遍。打标之后建议把每组的记录数记下来,如果某一组只有寥寥几十条,那多半是异常数据而不是真的另一套编码,直接人工处理比写规则更快也更稳妥。

迁移之后收录多久能恢复?

取决于原来的状态和页面的抓取频率。如果原来编码是正确的,迁移只是格式变化,通常两三周内主要页面就会更新完毕,深层页面一两个月。如果原来是乱码状态,那不是恢复而是从头开始,主要页面几周内能进来,全站铺开要按内容量算,几万页的站三到六个月是常见节奏。加快的办法只有两个:把站点地图整理干净并标注更新时间,以及把重要页面的内部链接理顺让抓取更容易到达。除此之外没有别的加速手段,耐心等待是必要的一部分。这段时间里最该做的不是反复提交,而是把内部链接理顺,让重要页面从首页出发三步之内能到达,抓取路径顺畅带来的加速效果比任何提交动作都明显。

地址里的西里尔字符要不要一起换成拉丁转写?

如果地址已经被大量收录并且有外部链接指向,就不要动。地址变更的损失通常大于转写带来的收益,尤其是在编码迁移的同一个窗口里做,出了问题根本分不清原因。如果这个站还很新,或者正好要重做地址结构,那可以考虑用拉丁转写,好处是复制粘贴时不会变成一长串百分号,用户看到的地址也更短。不管选哪种,关键是全站只用一种规则,最忌讳一部分页面用转写、一部分用转义,那样等于自己制造了两套地址体系。如果决定改,一定要先把现有地址的完整清单导出来存档,并且规则要写成可重复执行的脚本,将来任何一次新增页面都走同一套规则,人工命名迟早会出现例外。

响应头和页面里的声明不一致,改哪个更保险?

两个都要改成一致,但如果只能先改一个,改响应头。因为响应头的优先级更高,而且它对所有类型的文件都生效,包括那些不走程序的静态文件。页面里的声明只在响应头缺失或者不含字符集信息时才起作用,把它当成后备。改完之后一定要实际请求一次确认,很多服务器配置存在多层覆盖关系,站点级配置可能被目录级配置或者程序里的设置覆盖掉,改了以为生效其实没生效的情况非常常见。改完之后不仅要请求页面本身,还要请求几个静态文件和一个报错页,这三类响应经常由不同层级的配置控制,只验证页面很容易漏掉后两类。

用户能手动切换编码看正常,是不是就不算大问题?

对用户体验来说算小问题,对搜索来说是致命问题。用户看到乱码可以点几下菜单换个编码,抓取程序不会这么做,它按自己的判断处理一次就把结果存下来了。这就造成一个很有欺骗性的局面:站长自己打开页面一切正常,因为浏览器记住了他上次手动选的编码;而索引里存的是完全另一份东西。所以判断这类问题绝不能用自己的浏览器,一定要用干净的环境或者直接看原始字节。这个认知差是很多老站几年都没发现问题的根本原因。想验证得更彻底,可以换一个从没访问过这个站的环境打开一次,或者干脆用命令行取原始响应,两分钟就能确认真实状态,不必依赖任何工具的判断。

迁移完成后还需要保留对老编码的兼容吗?

页面内容不需要,站内搜索的入口需要保留一段时间。原因是用户的书签、外部站点的搜索表单、以及一些老的聚合工具可能还在按老编码提交查询参数。做法是在接收查询参数时加一层判断,如果按新编码解释不出合法字符,就尝试按老编码再解释一次。这层兼容逻辑保留三到六个月,看日志里老编码请求的比例降到可忽略之后再撤掉。页面内容侧则不建议做双编码提供,那会直接制造重复,得不偿失。兼容逻辑要写日志,记录每次触发的来源和参数,撤掉之前先看这份日志确认来源已经稀少,凭感觉决定什么时候撤,往往会误伤还在正常使用的老入口。

权威参考资料

分享到
标签
版权声明

本文标题:《Windows-1251时代的西里尔页面,搜索引擎抓回去的不是俄语,是一串认不出的拉丁字母》

本文链接:https://zhangwenbao.com/cyrillic-encoding-legacy-windows1251-utf8-indexing-cost.html

版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交