字符编码
我们平时所说的「文本」基本上都是在说「电脑屏幕上的字符」,但是小学生都知道「计算机只懂 0101」,那么电脑究竟是怎么处理三千世界中如此纷繁复杂的文字的呢?
开天辟地阿斯克
电脑是美国人发明的,所以最初的人们也只需要考虑「如何让电脑明白英文的字母和符号」的问题。
于是 ASCII(美国信息交换标准代码)诞生了。它参考了电报报文的设计,将 0 到 127 都赋予了对应的字母和符号。于是可以被电脑处理的数字序列 72 101 108 108 111 32 87 111 114 108 100 33
就可以通过查 ASCII 码表翻译成 Hello World!
。当然,用更「计算机」一些的十六进制表示每一个数,就是:
48 65 6c 6c 6f 20 57 6f 72 6c 64 21
H e l l o W o r l d !
那么,输入并显示一个字符的过程就变成了:
- 你按下键盘的 H 键
- 电脑在内存中保存 0x48
- 电脑在屏幕上显示 0x48 号图形
也就是说,电脑不需要能够「明白」字符,只需要能够用数字「表示」字符。
百家争鸣,被榨干的单字节
随着时代发展,电脑开始在各地使用。以法语、希腊语等为母语的人发现 ASCII 真心坑爹,没有 è、é 没有 α、β、θ、让我们怎么活?
好消息是:计算机终于迎来了以 8 位为 1 字节的时代,而 ASCII 只为 0x00 到 0x7F 规定了对应字符。也就是说,ASCII 只占了低 7 位 0XXX XXXX
,还有 0x80 到 0xFF 这余下的 128 个码位可以让人糟蹋。
这一利好消息的发现让人们大为振奋。
法国人开心地用 0xE8 表示 è,用 0xE9 表示 é;希腊人欢乐地用 0xE1 表示 α,用 0xE2 表示 β,用 0xE8 表示 θ……
E 9 7 1 7 5 6 9 7 6 6 1 7 5
1110 1001 0111 0001 0111 0101 0110 1001 0111 0110 0110 0001 0111 0101
é q u i v a u
所有 0 开头的字节在 ASCII 里找对应字符,所有 1 开头的字节在各自定义的字符集里找对应字符,问题解决了。
传说的巨龙,汉字的秘密
于是电脑来到了中国,但是中文不同于字母文字,如何给数万汉字编码就成了大问题。
一位伟人一拍脑袋:「一个字节只有 0 到 255,但两个字节就有 0 到 65535 啦!《新华字典》也就一万多个字,用两个字节表示一个汉字不就行了嘛~讨厌!」
没错,这就是一种后世所谓的多字节字符集(MBCS)。我们熟悉的 GB2312 和 GBK 使用的都是如下形式的二进制编码:
0XXX XXXX
1XXX XXXX XXXX XXXX
如果一个字节为最高位为 0,那么后续的 7 位表示一个字符(128 个码位)。如果最高位为 1,那么后续 15 位表示一个字符(32768 个码位)。
GB2312 前辈利用了这 32768 个码位中的 7445 个,而后辈 GBK 则利用了 21886 个。
例如二进制数据 48 69 20 CA C0 BD E7
的解析:
1| 4 8 6 9 2 0 C A C 0 B D E 7
2| 0100 1000 0110 1001 0010 0000 1100 1010 1100 0000 1011 1101 1110 0111
3| /100 1000 /110 1001 /010 0000 /100 1010 1100 0000 /011 1101 1110 0111
4| /4 8 /6 9 2 0 /4 A C 0 /3 D E 7
5| H i 世 界
- 字节序列(十六进制)
- 字节序列(二进制)
- 字节序列 --[GBK 解码]--> 码位(二进制)
- 字节序列 --[GBK 解码]--> 码位(十六进制)
- 码位 --[GBK 字符集]--> 字符
那么,几个之前没有讲的概念就比较明白了。
- 字符集:某种字符编码所能表示的所有字符集合。如:H、世
- 码位:为字符集中的每一个字符分配的唯一编号。如:0x48(H)、0x4AC0(世)
- 编码:将码位转换为字节序列。如:0x48→0x48、0x4AC0→0xCAC0
- 解码:将字节序列转换为码位。如:0x48→0x48、0xCACO→0x4AC0
危机!乱码的陷阱
于是,全世界人民都一本满足了,但是这番和谐景象的背后却隐藏着天大的危机。之前,大家都用着自己的字符编码相安无事,但全球化却导致乱码横行。
聪明的你也许已经发现了:之前我们说过,法国人的 è 用 0xE8 表示,而希腊人的 θ 也用 0xE8 表示。有一天,法国人写了封 Email 给希腊人:
Jeux de caractères codés
希腊人收到一看:
Jeux de caractθres codιs
这是毛啊?于是转发给了中国人,中国人打开一看:
Jeux de caract鑢es cod閟
擦,顿时感觉自己没文化了……于是回复:
我看不懂……
希腊人无辜地打开一看:
Ξ?Ώ΄²»Ά?‘‘
这这这……于是转发给法国人,法国人也一头雾水:
ÎÒ¿´²»¶®¡¡
「我还是删了吧,妈妈说不要跟外国人发邮件……」
1| J e u x d e c a r a c t è r e s c o d é s
2| 4a 65 75 78 20 64 65 20 63 61 72 61 63 74 e8 72 65 73 20 63 6f 64 e9 73
3| J e u x d e c a r a c t θ r e s c o d ι s
4| 4a/65/75/78/20/64/65/20/63/61/72/61/63/74/68 72/65/73/20/63/6f/64/68 73
5| J e u x d e c a r a c t 鑢 e s c o d 閟
- 法国人写的文字
- 法国人根据 latin-1 将文字转换为码位、并将码位编码得到实际保存的字节序列
- 希腊人根据 latin/greek 将序列解码得到码位、并将码位转换为字符,得到的文字
- 中国人根据 GBK 解码后得到的码位
- 中国人根据 GBK 字符集将码位转换到的字符
所以,二进制文本数据就相当于密文,而编码和解码如同加密和解密,只有用正确的密钥才能得到明文,也只有用正确的字符编码才能得到码位。然后通过码位在字符集里取得最终的字符。
(历史上,常常将字符集与字符编码等同起来。因为大部分字符集都是 8 位的,编码/解码形同虚设,N 编码后还是 N,可以直接 1:1 映射。例如上面例子中的 latin-1 和 lantin/greek,编码都是相同的 1:1 编码,只是字符集中相同码位对应着不同字符而已。)
大逆转,万国码的光荣
打开浏览器菜单,肯定存在一个叫做「编码」的选项,点开就能看到这世界上至少存在着多少种流行的字符编码。如果浏览器「自动检测」检测得不对,网页就乱码了。索性懒人是社会进步的阶梯。为了免去百家争鸣带来的麻烦,试图让全世界「书同文」的 Unicode 诞生了。
Unicode 标准化了一个字符集,包含了世界上所有的字符,每一个字符都拥有唯一的码位 U+XXXX。(起初的码位是 16 位的,可以容纳 65536 个字符。其后不断扩展,现今已经扩展到了 U+10FFFF。)
Unicode 还提供了几套编码方案,来将 U+XXXX 的码位编码为字节序列,例如:UTF-8 和 UTF-16。
UTF-8
UTF-8 顾名思义,是一套以 8 位为一个编码单位的可变长编码。会将一个码位编码为 1 到 4 个字节。
U+ 0000 ~ U+ 007F: 0XXXXXXX
U+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXX
U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX
U+10000 ~ U+1FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
例如「萌」在 Unicode 中的码位为 U+840C,对应的 UTF-8 编码为 E8 90 8C
:
1| 8 4 0 C
2| 1000 0100 0000 1100
3| 1000 010000 001100
4| 11101000 10010000 10001100
5| E 8 9 0 8 C
- 码位(十六进制)
- 码位(二进制)
- 根据 UTF-8 编码规则将码位分为三段
- 为每一段加上前缀,编码完成(二进制)
- 编码完成(十六进制)
UTF-16
UTF-16 则是一套以 16 位为一个编码单位的可变长编码。会将一个码位编码为 1 到 2 个双字节。编码算法大同小异并不重要,但有一个问题却亟待解决:字节序。
不同的系统,字节序可能不同。小端序系统的 0x1234 实际上是 34 12
,把这个字节序列发给大端序系统,34 12
就会被理解成 0x3412。
于是,Unicode 中引入了一个特殊字符:字节序标(BOM),码位 U+FEFF。用于加在被编码的数据之前,表示编码时的字节序。于是,解码时,首先读出第一个双字节:
- 0xFEFF 当前的字节序和编码时的字节序相同
- 0xFFFE 当前的字节序和编码时的字节序相反
当然,UTF-16 还有派生的 UTF-16LE 和 UTF-16BE,实际上就是按字节序特化的版本。