TimothyQiu's Blog

keep it simple stupid

字符编码

分类:技术,闲扯

我们平时所说的「文本」基本上都是在说「电脑屏幕上的字符」,但是小学生都知道「计算机只懂 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  !

那么,输入并显示一个字符的过程就变成了:

  1. 你按下键盘的 H 键
  2. 电脑在内存中保存 0x48
  3. 电脑在屏幕上显示 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                   世                  界
  1. 字节序列(十六进制)
  2. 字节序列(二进制)
  3. 字节序列 --[GBK 解码]--> 码位(二进制)
  4. 字节序列 --[GBK 解码]--> 码位(十六进制)
  5. 码位 --[GBK 字符集]--> 字符

那么,几个之前没有讲的概念就比较明白了。

危机!乱码的陷阱

于是,全世界人民都一本满足了,但是这番和谐景象的背后却隐藏着天大的危机。之前,大家都用着自己的字符编码相安无事,但全球化却导致乱码横行。

聪明的你也许已经发现了:之前我们说过,法国人的 è 用 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  閟
  1. 法国人写的文字
  2. 法国人根据 latin-1 将文字转换为码位、并将码位编码得到实际保存的字节序列
  3. 希腊人根据 latin/greek 将序列解码得到码位、并将码位转换为字符,得到的文字
  4. 中国人根据 GBK 解码后得到的码位
  5. 中国人根据 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
  1. 码位(十六进制)
  2. 码位(二进制)
  3. 根据 UTF-8 编码规则将码位分为三段
  4. 为每一段加上前缀,编码完成(二进制)
  5. 编码完成(十六进制)

UTF-16

UTF-16 则是一套以 16 位为一个编码单位的可变长编码。会将一个码位编码为 1 到 2 个双字节。编码算法大同小异并不重要,但有一个问题却亟待解决:字节序。

不同的系统,字节序可能不同。小端序系统的 0x1234 实际上是 34 12,把这个字节序列发给大端序系统,34 12 就会被理解成 0x3412。

于是,Unicode 中引入了一个特殊字符:字节序标(BOM),码位 U+FEFF。用于加在被编码的数据之前,表示编码时的字节序。于是,解码时,首先读出第一个双字节:

当然,UTF-16 还有派生的 UTF-16LE 和 UTF-16BE,实际上就是按字节序特化的版本。

UTF-8编码

已有 4 条评论 »

  1. 博主相当风趣啊 :-)

  2. 周保晶 周保晶

    讲得十分清楚了!

  3. 陈晨 陈晨

    通俗易懂,看这么多关于字符编码的文章,这一篇最清楚的,易懂的

  4. 小保哥 小保哥

    每次想起编码问题时都会翻出这篇文章。

添加新评论 »