前言
如果你经常使用计算机处理一些老旧文件,或者访问一些非常老且停止维护的网站,大概,可能有时候你会看到这种东西:
| 常见编码错误 |
|---|
| é>>˜è®¤ç¼--ç |
| ???? |
| 缘ç  |
| 锟斤拷锟斤拷 |
| ??? |
| ĦĦĦĦĦ |
| 鐢ㄦ埛鍚� |
| ì½"ë"" |
| Ð§Ð¸Ñ |
| £ティロォラ」 |
| エラー文字化け |
| \xE6\x97\xA0\xE6\x95\x88 |
| �ܺ�µ� |
| Êý×Ö±àÂë |
| 鏃犻敊璇紦瀛� |
饿滴嘛!这是什么啊!!!可读性这一块不如原神自创的文本,这些东西完全看不了吧!
不过计算机内是没有黑箱的,到底发生了什么呢?为什么会发生这些错误?
字符的存储
如果你一直认为计算机其实是超天才,那你接下来可能要失望了------计算机其实是名副其实的大笨蛋。
在人工智能以前,计算机是无法理解人类的语言文字的,想要让它帮忙处理人类的文字,就必须先规定一套计算机能够看懂的规则。
计算机在最底层只能做这些事情:
- 存储信息,只负责保存
- 传输信息,把信息从一个地方传递到另一个地方
- 运算,最底层只能做加减和位运算,还有逻辑运算与或非(底层计算机连平方,余弦函数,乘法除法都不会做,现在能实现,是因为人类用上面这些最基础的概念规定出了新的计算方法,当然这里涉及数学很深,比较计算机里没有无限,所有小数点后的信息都要储存在实际的物理存储中,很多时候是用泰勒展开去估算的)
- 判断,判断两个数是否相等,判断是不是0,判断谁大谁小。
没了。
对,没了。
顺带一提,由于计算机只能使用二进制(这是由现实世界的物理条件决定的,因为 通电/断电 是最简单的可控状态,状态检测一旦出现电压波动,很容易出问题),所以上述的信息,也是存储为二进制的哦!
编码与字符集
看看这个吧!
json
01101000 01100101 01101100 01101100 01101111 00100000 01110111 01101111 01110010 01101100 01100100
这就是ASCII码的 hello world 的二进制表达,电脑也只能看懂这个形式的信息。
电脑那边算是解决了,可是人类看不懂二进制啊。由于物理限制,计算机只能用二进制,只能委屈人类也折腾了。为了解决这个问题,人类发明了一本词典,请想象一下:
词典规定,a这个字母,在二进制中对应的二进制码就是01100001,以此类推,然后再把这个信息写进计算机里,这样人类输入 a ,计算机就知道是01100001了。
恭喜你发明了美国ASCII码,人类历史上第一个字符编码就这样诞生了。从那以后的人类,只需要输入字符,不需要记住对应的二进制,电脑就会自己转化成二进制信息,自己进行储存加工,大大降低了人类的使用操作门槛。
ASCII够用吗?
上文提到的ASCII码只储存了这些信息:
- 大小写字母
- 数字
- 英文标点和制表符标点
- 控制打印机的命令(因为早期计算机和打印机是紧密相关的,所以这些也顺便都写进去了)
把所有用到的字符排列开,一共128个字符,刚好对应2的7次方,只需要使用7位二进制就够用了。而计算机底层的存储单位是字节(byte),刚好是8位二进制,最高一位空出留下0,剩下7位就可以存放所有128个字符。
也就是说,ASCII码只使用1字节就可以表达一个字符,对内存来说也非常轻松。
随着其他国家计算机水平的发展,ASCII码的问题很快就暴露了...
如果法国人想用自己的母语在计算机上打字,但是ASCII码上没有法语,于是法国人就会扩充一个字节,然后在原本的ASCII码表的最后一位上继续增加法语字母,德国人,希腊人...都会这么做!
于是问题就发生了,在所有人的电脑上,前128个字符都是一样的,编码也不会出问题。
但是从129位开始,129在法国人电脑上是法语字母,在德国人电脑上是德国字母,如果一个法国人想把文件传递给德国人,德国人打开之后就变成乱码了!完全没法沟通。
更糟糕的是,每个国家都在推行自己的ASCII改码,霎时间,全世界的编码非常混乱,如果你当时在搞互联网,可能没办法同时搞所有的编码,外人访问你的网站,打开就全部都是乱码哩------这也太糟糕了。
另一个难以忽略的事实是,以中国为首的汉字文化圈,和英文字母的存储存在巨大的差异。拉丁文衍生出的西方文字,只需要储存最简单的字母,就可以拼出所有单词,但中文,日语,韩语,需要储存每一个汉字和其他字符,才能正常使用。
ASCII码不够用了
GB2312 与 GBK
为了解决中国人用不了电脑的问题,中国在1980年发布了**《信息交换用汉字编码字符集·基本集》,简称GB2312**,是中国第一个汉字编码国家标准,具备着里程碑级别的意义。
为了解决汉字数量过多的问题,GB2312采用双字节编码,一共是16byte,理论能够存储2的16次方个字符,同时兼容ASCII码,中国人和无数海外华人终于能够使用官方编码,在计算机上写下属于自己的语言了。
GB2312发布的同年,王选团队就成功研制出华光I形激光照排系统,完全基于GB2312编码,彻底解决了汉字印刷问题,到1993年时,全国99%的报社和90%的出版社都用上了该系统,铅排中文成为了过去式,原本的铅中毒,重体力劳动也得到了解决。
由于GB2312完全兼容ASCII,在外贸上也取得了巨大的成功,此时的GB2312打破了西方人"汉字不适合计算机"的嘲弄,无数从业者几代人的努力,在短时间就打造了极具特色的中文互联网生态。这一切都是GB2312奠基的。
但是,GB2312是完美的吗?
事实上,由于汉字独特的结构,GB2312并没有像ASCII一样采用全顺序排列的记录方式,因为使用了2个字节,到底是哪两个字节连起来才算中文呢?万一把两个字节拆开,当成两个ASCII码解读怎么办?
为了解决这个问题,中国工程师让汉字的最高位为1(还记得刚才我们提到的ASCII 一共127位吗,一个字节是8位,最高位空出来了),只要最高位为1,那么就拼接其后的一个字节,让两个字节合起来表达一个汉字字符。
此外,GB2312的存储还是分区的,一共分为94个区,理论上限是94的二次方,8836个码位,实际储存了6763个汉字+682个符号,剩下的位置有的留空,有的给用户自定义。
分区储存是GB2312的重要特点,汉字不能像英文字母一样顺序排列,因为汉字存在常用字和生僻字,需要按照常用程度分区,便于用户快速对比字符集找出对应的字符。
16--55 区:一级常用汉字(3755 个)
56--87 区:二级次常用汉字(3008 个)
可是,这些字真的能够完整表达所有汉字吗?
2023年实行的官方信息处理标准规定了中文目前一共有88115个汉字,北京国安汉字库则储存了91251个汉字,而台湾的异体字字典(包括大量异体字和古文字)至少存了10万个汉字。
尽管常用汉字不超过4000个,但在处理人名,地名等容易出现生僻字的情况,还是无法照顾到所有情况。
云南傈僳族全村人原本姓nià(上下结构:上为少一横的 "鸟",下为 "甲"),是傈僳族以鸟为图腾的古老姓氏。可是GB2312里压根没有这个汉字,这意味着身份证无法录入,火车票,飞机,银行卡,社保全都办不了。
全村人只能改姓鸭------这对于把姓氏看得很重的中国人来说,是一件很令人伤心的事情。
GB2312的时代下,这种事情层出不穷,尽管GB2312对于中国人来说意义非凡,可它确实还是不够用了。
孩子们别怕,GBK来了
为了解决GB2312容量太小的问题,1995 年由原电子部、国家技术监督局联合发布为指导性技术规范(非强制国标),成为中文Windows系统下的实际使用字符集,简称GBK。
GBK完整兼容GB2312,字数数量大幅提升至21886个字符,完整覆盖了几乎所有常用汉字,原本那些户籍,银行,社保,车票的问题,系统也支持录入原本的文字了。
更厉害的是,GBK完全占用了2字节的全部存储,在不增加储存负担的情况下,就多存了15000多汉字。
GBK一直从win98,win7,win10,win11一路走来,即便到现在也有很多电脑在使用GBK编码。
虽然GBK名义上并没有持续很长时间,到2000年就被升级为GB18030-2000,2022年时GB18030-2022就已经收录了8.8万字符,基本上解决了缺字的问题。但GBK的普及推广程度却是所有中文字符集中最广泛的,也是第一次真正实现了简繁互通的字符集,尽管不是强制国标,却被Windows在内的一众厂商全线采用,成为相当长时间里的中文系统的主流编码。
即便到现在,最新的win11里也装载了GBK编码,感兴趣的话可以在系统-时区和语言里找一找。
计算机编码的国际化
在很长一段时间里,计算机编码的国际化都是一个头痛的问题。
每个国家都有自己的编码,尽管多数编码都是新版兼容旧版,但不同国家,不同地区之间想要传递信息,也总是会遇到很多不方便。
GBK只是解决了中国人自己写字的问题,即便能够兼容ASCII,面对小国家的小众文字来说也太不方便了。而且很多国家直至今日发展水平依然相当落后,指望这些人自己独立研发出一套能用的编码字符集简直是天方夜谭。
++"我们必须要真正实现一套可以全人类互通信息的字符集"++
在一系列需求的呼唤下,万国码------UTF-8诞生了。
早期 Unicode 的那些事
还没有推出utf-8时,Unicode就推出过16位定长编码(utf-16),每个字符强制2字节储存,对于原本1字节存储的ASCII码中的英文来说,意味着文件体积要翻倍。而且由于早期Unicode完全不兼容ASCII,许多旧软件,旧系统,旧协议全部失效,要专门做适配。
而对于早已习惯ASCII的各种厂商来说,Unicode只会增加自己的负担,因此万国码受到了诸多阻力。
各国之间心照不宣,万国码迟迟不能通用。
字符集之神utf-8降临电脑,它的存储量是3个GBK(雾
为了解决早期Unicode的缺陷,Ken Thompson(肯・汤普森) 和 Rob Pike(罗布・派克) 于 1992 年 9 月 在 贝尔实验室(Bell Labs) 共同设计推出了utf-8的雏形,于1996年纳入 ISO/IEC 10646 国际标准。
utf-8彻底解决了早期Unicode的缺陷,不再使用定码储存,而是采用变长码位储存信息,不仅完美兼容了ASCII,而且原本的英文字符体积也不会增大,它的原理是这样的:
- 如果是1字节→兼容ASCII,和原始ASCII完全一致,第一个字节是0xxxxxxx
- 如果是3字节→第一个字节是1110xxxx,剩下的字节是10xxxxxx
- 如果是4字节→第一个字节是11110xxx,剩下的字节是10xxxxxx
你发现了吗?这种可边长的字节储存,完美兼容了ASCII,GBK只要略作修改也可以完美支持,例如:
json
11100100 10111000 10101101
按照上面的规则,第一个字节是1110xxxx,因此要拼接后面的两个字节,合起来在utf-8表中,其实代表汉字 中 。
不仅如此,utf-8覆盖几乎人类全部现行文字 + 古文字 + 符号+emoji表情。
UTF-8具体包含哪些语言 / 文字
1. 全球所有主流现代语言
-
欧洲:英、德、法、西、意、葡、荷、瑞典、波兰、匈牙利、捷克等
-
东欧 / 南欧:俄、乌克兰、塞尔维亚、保加利亚(西里尔字母)
-
中东:阿拉伯、波斯、希伯来
-
非洲:斯瓦希里、豪萨、约鲁巴、北非各种语言
-
美洲:印第安土著语言、夏威夷语
-
亚洲:
- 中日韩(CJK):汉字(简繁全含)、日文假名、韩文谚文
- 东南亚:泰、老挝、柬埔寨、缅甸、越南、印尼、马来
- 南亚:印地、梵语、孟加拉、旁遮普、泰米尔、乌尔都
2. 中国相关的全部文字
-
简体汉字、繁体汉字(全量,超 9 万字)
-
各少数民族文字:
-
藏文
-
蒙文
-
维吾尔文
-
哈萨克文
-
彝文
-
傣文
-
朝鲜文 / 韩文
等等基本全覆盖
-
3. 古典 / 历史文字
- 古埃及象形文字
- 楔形文字
- 玛雅文字
- 古希腊、哥特文、卢恩文
- 甲骨文、金文、小篆(部分已收录)
- 粟特文、突厥文、西夏文、女真文(部分)
4. 大量符号系统
- 盲文
- 音乐符号
- 数学符号、数理逻辑符号
- 棋牌符号(麻将、扑克、象棋)
- 标点、注音、拼音、声调符号
- 单位符号、货币符号
5. 表情符号 Emoji
所有 Emoji 本质上都是 Unicode 字符,UTF-8 都能存。
孩子们我浑身上下只剩小拇指了可以玩Web互联网和json吗
包的包的,必须能。
上面只是提到了utf-8的优点,但并不是utf-8普及的关键原因,utf-8最终普及开来,离不开互联网的支持。
2000年后,跨国贸易进入了前所未有的繁荣期,万物互联不再是一句空话,互联网必须使用utf-8,而json,xml等存储格式甚至直接要求使用utf-8编码。json是目前应用最广泛的文本数据格式,而且是互联网老资历JavaScript的对象表示,说白了只要你想搞网站就绕不开utf-8。
在多个国家的推动下,utf-8成为了目前主流的编码字符集。
不过,由于设备更新的滞后性,依然有许多电脑直至今日还在使用GB2312,这也为后面吐槽的问题埋下了伏笔。
UTF-8就真的完美无缺吗?
utf-8宛如秦皇大一统一样(阿嘎嘎嘎琴始皇。。。我不开玩笑啦!),将文字符号集中了起来,使得大家不需要转换字符集,就可以愉快的阅览外国友人发来的信息了。
但utf-8也存在很多设计的缺陷,其中最严重的问题就是可变字符串长度计算容易出错。
utf-8为了真正能够容纳所有的人类文字,采取了动态长度的方法,但是一些老旧程序中,例如c语言的strlen函数,是按照字节长度计算的,并不真正按照字符算。
来看一个简单的问题:
c
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "你好tom";
printf("字符串的长度为:%zu\n", strlen(str));
return 0;
}
以上是一个c语言编写的简单程序,使用了一个函数strlen,这是c标准库的函数,用于统计字符串的长度,那么想想它的输出结果应该是几呢?
你,好,t,o,m...嗯,一共是五个字,那么结果一定是5吧!
啊,为什么你的电脑算出来是7,我的电脑算出来时9呢,这又是怎么回事呢?
这是因为 strlen 函数的原理是统计字符串的字节数来计算长度的,在GBK编码下,一个汉字是两字节,所以最终结果是2+2+1+1+1=7字节,返回长度为7。而在utf-8编码下,一个汉字的长度是3字节,所以最终结果是3+3+1+1+1=9字节,返回长度为9。
这个简单的示例不难看出,在一些老旧程序中,utf-8面临着较为严重的兼容问题,并且这种动态可变长度,往往可能产生意想不到的错误分割问题。
不仅如此,由于utf-8对字节有严格的格式限制,例如续字节必须以 10 开头,如果出现 传输丢包,错误截断,文件损坏,手动修改字节,非常容易出现无法读取的错误序列。
此外,utf-8还存在很多看起来几乎一模一样的字,单单一个a就分为:
- 拉丁 A:
AU+0041 → UTF‑8:41 - 全角 A:
AU+FF21 → UTF‑8:EF BD A1 - 希腊 Α:
ΑU+0391 → UTF‑8:CE 91 - 西里尔 А:
АU+0410 → UTF‑8:D0 90 - 拉丁 a:
aU+0061 →61 - 全角 a:
aU+FF41 →EF BD 81 - 希腊 α:
αU+03B1 →CE B1 - 西里尔 а:
аU+0430 →D1 80
这些a在外观上极为相似,在密码校验,网址输入等安全问题上,尽管计算机可以根据编码正确识别,但人类却无法分辨这个外观,容易出现各种操作错误。
我们需要明确知道,即便utf-8是直至今日最完善,最好用,最广泛的编码字符集,但本身也存在着诸多问题。
锟斤拷 和 烫烫烫
回到最开始的问题本身,相信你已经对编码错误有着相当深的理解了。
编码错误的本质,就是用非预期的编码打开了文件,把同一段二进制数字翻译成了错误的字符。
这段二进制数字本身是没有变化的,例如欧洲人用ISO-8859-1编码编写了一个文档,我们使用GBK打开,由于字典根本不对应,呈现的结果也是错误的。
而锟斤拷和烫烫烫基本上是最常见的编码错误。
锟斤拷
用一句话说就是 ,UTF‑8 替换符 U+FFFD(字节 EF BF BD)被错误按 GBK 双字节解码,拼出「锟斤拷」。原本用utf-8保存的文件,如果按GBK编码打开,就会满屏锟斤拷。
大致原理:
- 遇到无法解码的字符,系统用
U+FFFD(替换字符),对于utf‑8 为EF BF BD - 连续多个替换符 →
EF BF BD EF BF BD... - 按 GBK 双字节分组:
EF BF→ 锟BD EF→ 斤BF BD→ 拷
- 重复出现 → 刚好全部替换成了锟斤拷
烫烫烫,屯屯屯
用一句话说就是 ,VS Debug 模式下,未初始化栈内存被填 0xCC,按 GBK 双字节显示为「烫」。
-
大致原理:
- Windows VC 调试器为了方便定位未初始化内存,把栈内存填
0xCC(对应 x86 断点指令int 3) - 连续
0xCC 0xCC→0xCCCC - GBK 编码中
0xCCCC= 烫 - 未初始化字符串被打印 → 满屏「烫烫烫烫」
- Windows VC 调试器为了方便定位未初始化内存,把栈内存填
堆内存未初始化通常填
0xCD→0xCDCD= 屯 → 「屯屯屯」什么你不知道堆和栈吗,哎呀不要紧的,你只需要知道这种情况是编码错误就好了!
结尾
其实编码的发展,从来都是 "解决旧问题,产生新麻烦" 的过程。
ASCII 解决了英文存储,GB2312 让汉字走进计算机,GBK 补齐了生僻字的坑,UTF-8 实现了全球互通,每一步都在朝着 "让人类和计算机顺畅沟通" 的目标靠近。没有完美的编码,只有最适合当下需求的选择 ------ 就像 UTF-8 虽有小缺陷,但依然是现在互联网的 "通用字典"。
也正因如此,编码史更像一部不断妥协、不断兼容、不断向前的沟通史,浓缩着无数工程师和从业者的努力。过去上帝让巴别塔倒塌,而如今的人类用技术一点点消除彼此的隔阂。