文本编码识别与乱码还原 — 这些字节到底说了什么,还有多少能原样拿回
给它一个文件,它就逐字节去读:开头有没有标记,整串是不是合法的 UTF-8、不是的话究竟断在哪一格,双字节能不能作为 GBK、Big5、EUC-KR 或 Shift_JIS 的字对走完全程,你自己的浏览器解码器又读出什么。粘一屏乱码进来,它会讲清哪种事故还能倒回去、哪一种已经把数据丢了。交回的是能读的文字,可以带 BOM 也可以不带,或者一个字节都不动地原样奉还。你的文件不会离开这台设备。
每种读法,并排放着
| 编码 | 你自己浏览器的解码器读出什么 | 这些字节反过来怎么答 |
|---|
拿回的这段文本有多重,用六种算法各称一遍
| 按什么数法 | 数字 | 这个数该拿去和哪条限制比 |
|---|
九种读法与各自的代价
为什么同一句话有这么多字节写法
字符是一个数,编码就是“把这个数写成字节”的那条规则。「中文」这两个字符是 U+4E2D U+6587:UTF-8 为它们花掉六个字节 —— E4 B8 AD E6 96 87,GBK 只花四个,D6 D0 CE C4。两个文件都没有错,可两个文件都不说自己是按哪条规则写的,除非有人在开头留下了一个标记;而就算有那个标记,它点名的也只是九族规则里的一族。
值得写一个逐字节去读的读法,理由全在这一段里;而任何一种读法都没法向你保证一个答案,理由也在这同一段里。用错的规则去读文件的字节不会崩溃 —— 它交回的是一段再正常不过、只是字符全错了的文本;而要找到对的那条规则,唯一的办法是把每一种都试一遍,再看各自回给你什么。所以下面那张表把九种读法并排摆开,每种两列:一列是你自己浏览器的解码器报上来的东西,另一列是不借解码器、照着字节区间把这串字节走一遍说出的东西。这两列对不上的地方,才是有意思的地方。
UTF-8 的序列凭什么算非法,而别的规则说不出这种道理
九种读法里 UTF-8 最严格,而它严格的原因全在安全上,不在省字节上。一个首字节许下后面跟着几个字节,允许的区间卡得很死:
| 首字节 | 欠几个 | 第二个字节必须在 | 为什么 |
|---|---|---|---|
C2-DF |
1 | 80-BF |
C0 和 C1 会把一个 ASCII 字符重新拼成两个字节 |
E0 |
2 | A0-BF |
E0 80 是在用三个字节写两个字节已经覆盖得住的东西 |
E1-EC、EE-EF |
2 | 80-BF |
寻常 |
ED |
2 | 80-9F |
ED A0 一起头就是代理区,而代理项不是字符 |
F0 |
3 | 90-BF |
三字节的写法已经够得到这一带了 |
F1-F3 |
3 | 80-BF |
寻常 |
F4 |
3 | 80-8F |
F4 90 会越过 U+10FFFF,那是天底下最大的码点 |
F5-FF、80-BF |
—— | 永远不行 | 没有这样的序列存在;80-BF 只配跟在一个首字节后面 |
九种读法里有八种会因为这类理由拒绝某个字节 —— 四个多字节家族各有各的首字节区间和尾字节区间,两种 UTF-16 读法则拒绝奇数的字节长度和落单的代理项 —— 只有 Windows-1252 做不到拒绝,因为一张 256 格的表给它的每一格都备着名字,从来没有人要求它对某个字节作出解释。这就是两列都印出来的原因:每个字节都拿到了一个回答,并不证明其中哪个回答是对的。
手写字节的那个循环拒的正是浏览器拒的同一批序列 —— C0 AF、E0 80 80、ED A0 80、F0 80 80 80、F4 90 80 80、F5 80 80 80 回来全是错;而一串在某个序列半途就结束的输入也一样被拒,并且它会说出自己停在哪一个字节上。
三种损坏,只有两种能倒回去
字节是好的,错的只是看它的那个人。 这是最常见也最好办的一种:磁盘上的文件还带着它当初被写下来的东西,只是某个程序用它自己的默认编码打开了。把文件交给上面的选择框,或者把它贴进来,然后指定你要的那种读法。什么都没丢。
读错的那一遍被当成文本存出来了。 这笔账是量出来的,不是回忆出来的:中文 的 UTF-8 是 E4 B8 AD E6 96 87。把这六个字节按 GBK 读,回来是三个字符 —— U+6D93 U+E15F U+6783,也就是「涓」,加上一个你的字体里画不出来的专用区码点,再加上「枃」。把那个结果再存成 UTF-8,文件里现在躺着九个字节,E6 B6 93 EE 85 9F E6 9E 83。要把它倒回去,就得把这三个字符重新编码回 GBK 的双字节字对,拿回那六个字节 —— 而这一步需要 GBK 表的另一半,整整 23,940 格。这样一张表是故意不带在这里的,所以粘贴框会拒绝,并且告诉你究竟是哪几个字符把它挡住了。改成把文件附上:字节本来就在那里,不借表也能走一遍。
数据已经没了。 解码器用不了的那个字节,它会写下替换符 U+FFFD,在 UTF-8 里是 EF BF BD。把两个这样的标记按 GBK 读,字对出来就是「锟斤拷」 —— 那三个字的意思是:有人在你拿到这个文件之前就已经对你的字节放弃了。标记不是原文的一种写法,它是一条“原文已被丢弃”的记录,而许多不同的原文会走到同一个标记上。没有任何东西能把它放回去,而诚实的回答就是说清损坏是哪一种,然后停住。
有一桩事故是真能倒回去的:一串字节被 cp1252 读过(或者被它的别名 latin1,浏览器把这两张表当同一张)。那次误读里的每个字符都坐在 U+0100 底下,所以每个字符正好指认一个字节,这一串根本不用查表就能重建 —— 䏿 又变回 E4 B8 AD E6 96 87,然后再读就是「中文」。cp1252 留下没定义的那五个字节(81、8D、8F、90、9D)浏览器会原样放过,所以它们也重建得出来。而一旦 U+00FF 之上出现某个字符,重建就到此为止,那条说明会带着位置说清楚。
开头那个标记是把双刃剑
EF BB BF 是三个字节,意思是“我是 UTF-8”,写下它们的是那些怕你猜不出来的程序。它们不是文本的一部分,而它们后来的遭遇,正是逐字节读的人收到的最常见的那类投诉:
- UTF-8 的解码器在你看到它之前就把它删掉了。解码器被要求别删,标记留在表里显示给你,再由那个循环自己剥掉,这样你才能看清这个文件到底有没有一个。
- Excel 的 CSV 导入要它。缺了这三个字节,Excel 就按你的 Windows 当时设的那张代码页去读文件,里面的中文变成「涓」;有了它们才读对。
JSON.parse拒绝它 —— 以这个标记开头的文档是一个语法错误,这是在这里量出来的,不是回忆的。- 你要公布的摘要值会变,因为摘要算的是字节,而标记就是字节。
- 按 GBK 读,这三个字节是「锘」加上一个多余的字符;按
cp1252读,它们是。锘縤d,name和id,name那两种投诉说的就是这件事。
另外的标记会自报家门:FF FE 和 FE FF 是 UTF-16 的,而以一个它们开头的 UTF-16 文件,是唯一能够确定的情形 —— 标记点明了字节序,而这种读法接着必须把每个字节都收下。FF FE 00 00 是那个难堪的:它是 UTF-32 的小端标记,同时又是 UTF-16LE 的标记后面跟着一个 NUL 字符,所以要先看满四个字节,才谈得上认下哪一种。UTF-32 不读:没有哪个浏览器的 TextDecoder 实现了它,而在一个网页里凭空发明一种每四个字节一猜的读法,不等于带上了那个编解码器。
检测只看形状,不看语料
文献里的编码检测是一门靠语料生意的学问:对一种语言多大程度使用哪些字符做统计模型,再配上一张这样的表。这里不带语料,也不带字对表,所以能看的只有形状;而形状不够用的时候,那条说明会讲出来:
- 一种读法交回控制字符或替换符,那它几乎总是错的,所以这些要扣分。
- 一种读法的字符散落在好几个文字区块里,那它错的方式是真文本不会有的:把 CJK 的字对按错的家族读,你会落到韩文里、或者专用区里、或者一次落到其中三个里。而一种语言的真实文本待在一两个区块内。整个排序看的基本就是这一条观察。
- 把大多数字符放进同一个 CJK、假名或韩文家族的读法会得到加分,那是关于形状的猜测,不是关于你的语言的猜测。
- 一串纯 ASCII 里根本没有证据。七种按字节宽的读法交回同样的字符,所以传出来的是这句话,而不是一次从来就不存在的检测;这样的文件里唯一指向某处的东西是 NUL 字节,因为西文一旦写成每字符两字节,NUL 就出现在每隔一个位置上。
- 当好几种读法都收下了整串、分数又相同,表里就这么写,把每种给的结果印出来,让你选。没有任何东西被平均掉。
要是你从清单里指定了一种读法,排序根本不参与 —— 你已经决定了 —— 而那条说明会点出哪几种读法直接拒绝了某个字节,因为那对这个文件仍然是有用的信息。
为什么输出只可能是 UTF-8
按原编码存回去,是所有人第一句要的东西,而浏览器做不到。TextEncoder 只有一种编码,就是 UTF-8;把 {encoding: "gbk"} 递给它,它接受这个选项,把自己的名字报成 utf-8,照样写出 UTF-8 字节。解码是九张表,编码是一张。
所以那个选择器只有三个诚实的档位,没有第四个:把字节按到达时的样子原样写出 —— 只有观众读错、你只是在挪文件时你要的就是这个;把文本写成不带标记的 UTF-8 —— JSON、摘要值、git 和每一个 Unix 工具要的是这个;或者带着标记写 —— Excel 要的是这个。其余的任何一种 —— GBK、Big5、EUC-KR、Shift_JIS 的输出 —— 需要的是这里拒绝携带的那张表,所以它属于 iconv、Python 的 codecs、.NET 的 Encoding.GetEncoding,或者 Excel 自己的“另存为”对话框。
问与答
问:我的文件会被上传到哪里吗? 不会。字节是由这个标签页自己的文件读取器读进来的,再由这个标签页脚本里的循环走完全程;这个文件从来没有递给过任何服务器,内容也没有哪一台服务器看到过 —— 而这件事在这里比在别处更要紧,因为人们拿来的文件,是订单、名册和病人名单的导出表。
问:为什么给我九种读法,而不是一个答案? 因为字节本身通常定不下来。一个不肯自报编码的文件,真正算证据的东西只有两件 —— 开头那个标记,和那些拒绝某个字节的读法 —— 而这两样都不足以定案时,把九种全摆出来就是诚实的回答。
问:它在一个程序里是乱码,在另一个里是好的。我该信谁? 谁也别信:文件没有变,变的只是每个程序在文件拒绝自报家门时替它挑的那个默认值。在这里把读法试到那行文字看起来像你的语言为止,然后带着标记存出去,下一个观众就没得猜了。
问:「锟斤拷」你们能倒回去吗?
不能,也没有东西会假装能。那三个字是替换符 EF BF BD 被 GBK 读出来的样子,意思是某个解码器已经对原始字节放弃、并在上面写了一个占位符。许多不同的原文会产出同一个标记,所以没有东西剩下可以反推。
问:我粘了「涓」这一路的垃圾进来,它说字节重建不出来。为什么? 因为那些字符每一个都是 GBK 的字对,把它变回两个字节意味着到那张 23,940 格的 GBK 表里去查它。这里带的是表的区间 —— 那足够走一个文件的字节 —— 而不是字对本身,而反向要的正是字对。别粘它的乱码,把文件附上,字节本来就在那里。
问:它说答案还没定,或者列出三种读法打平。我该怎么办? 把表里它们对着读一遍,留下拼得出你这门语言的那一个:这一行本该是中文、韩文,还是一次 Windows 事故,只有你知道。排序是关于字符形状的猜测,而排序是用来被推翻的,不是用来服从的。
问:多大的文件它能处理? 字节到 2 MB 为止。比这更大的文件不是一个 textarea 该干的活;真正的编辑器拿得住它,而编码这个问题在头几百字节里就已经定下来了。