编码其实并不神秘,可以说是随处可见。上溯到远古时期,猿人为了记录所见到的事物,就在岩壁刻下简单的图形,这些图形可以说是最古老的一种编码了。我们的汉语其实也是一种编码,李白同学为了描述瀑布美景,就写出了“飞流直下三千尺,疑是银河落九天”的千古名句。随着汉语的不断完善,我们遇见的每件具体物体和大部分抽象的概念都有了编码,比如“树”代表长有绿叶的高高的植被,“开心”代表了一种愉悦的精神状态。当然了,还有英语、法语、德语等等,它们每一个都是一种编码,可以表达自然万物以及抽象概念。

千百年来,文字这种编码足以应付我们的生存所需,直到计算机的出现。那么计算机有什么特别之处呢?这就要从计算机的诞生说起,可以说计算机的诞生主要归功于布尔、香农、图灵、冯·诺依曼。

  1. 布尔创立逻辑代数学,为数字电子计算机的二进制、开关逻辑元件和逻辑电路的设计辅平了道路。1854年,出版了名著《布尔代数》,后来发展成为现代计算机的理论基础——数理逻辑。
  2. 香农在其一篇硕士论文中指出,能够用二进制系统表达布尔代数中的逻辑关系,用“1”代表“真True”,用“0”代表“假False”,并由此用二进制系统来构筑逻辑运算系统。
  3. 1936年,图灵在论文《论可计算数及在密码上的应用》中,严格地描述了计算机的逻辑结构,首次提出了计算机的通用模型——“图灵机”,并从理论上证明了这种抽象计算机的可能性。
  4. 冯·诺依曼提出计算机基本结构和工作方式的设想,理论要点就是:数字计算机的数制采用二进制;计算机应该按照程序顺序执行。人们把冯诺依曼的这个理论称为冯诺依曼体系结构,所有的的计算机都采用的是冯诺依曼体系结构。

由此,我们知道计算机内部只有0和1两个数,无论是什么样的信息,在计算机内部都是用0和1来表示。尽管计算机内部用0和1来表达万物,但是计算机与外部的交互仍然采用人们熟悉和便于阅读的方式,期间的转换过程可以简单的称为编码、解码。

编码解码

计算机中用的最多的就是字符,所以这里会详细讲述字符编码。在讲述前,先来看几个概念:

  1. Character(字符): 计算机中,字符包括字素,类似字素的单元,可书写语言中的字母表、音节表等。例如:字母,从0到9的数字,常用标点符号,空白符,控制符等。中文的你、我、他,日文的に、ほ、ん、ご也都是字符。
  2. Grapheme(字素):书写文字的最小单元,类似于我们语言中的phonemes(音素)
  3. Glyph(字形):字素的表现形式,下图为a的不同字形表示(图片来自wiki)。

    a的不同字形

最开始的计算机不像现在的这般强大,老式计算机不能制作、浏览图片,不能观看视频,只能用来简单地操作字符。这时就需要一套方案规定字符在计算机内部如何表示。

开天辟地:ASCII码

60年代,美国制定了一套字符编码标准,对应英语中用到的字符和相应的二进制表示。这套标准被称为ASCII码,它一共规定了128个字符的编码,每个字符均用8个二进制位表示(最高位均为0),这128个字符包括:

  1. 32个控制字符:十进制(0~31),比如ESC (escape),二进制表示为00011011。
  2. 空格(space),二进制表示为00100000;DEL (delete),二进制表示为01111111。
  3. 标点以及运算符:(33~47,58~64,91~96,123~126),比如‘+’表示为00101011。
  4. 数字(48~57),大写字母(65~90),小写字母(97~122)。比如‘a’表示为01100001。

百花齐放:各种编码

英语字符用128个符号编码就足够了,但是其他语言仅用这128个就不一定够了,比如法语中的é就无法用ASCII表示。于是,一些欧洲国家决定充分利用ASCII码中闲置的最高位,这样法语中的é可以编码为10000010。这样,这些欧洲国家的编码体系最多支持2^8=256个字符。

但是这样似乎是饮鸩止渴,不同的国家都有不同字符,如果他们都是利用ASCII码的最高位来扩展能表达的字符个数,就会遇见编码相同但代表字符不同的情况。比如法语中编码10000010代表é,而在希伯来语编码中却代表了字母Gimel (ג),在俄语中又会代表另一个字符。

另外,许多国家的字符数太过于庞大了,比如汉字就多达10万左右。这个时候必须使用多个字节(一个字节8个bit)。简体中文常见的编码方式GB2312就是使用两个字节表示一个汉字,所以理论上可以表示2^16=65536个汉字。在本文的最后部分将简单介绍中文编码方案

一统天下:Unicode字符集

在百花齐放的年代,各个国家之间没有一个统一的编码,导致同样的二进制串可以被解释成不同的符号。因此我们在打开文本文件时,必须要知道它的编码方式,不然就得不到自己想要的信息,呈现在我们眼前的将是一堆毫无意义的字符,这就是所谓的乱码。

更糟糕的是不能在一个文件里同时使用不同语言的字符。如果我想同时使用中文和日文,那么文件编码设为日文编码(常用的为Shift-JIS、EUC-JP)的话,就不能涵盖中文,如果设为中文编码的话,就不能使用日文。

于是人们急需要一种包罗万象的编码方式,这种编码最好能够涵盖世界上所有的符号。这时候,Unicode字符集应运而生,最初人们天真地认为用2个字节(16位,65536个码值)就可以表示世界上所有语言的文字符号。但是当初这个想法太过于草率了,因为东亚(中日韩)字符非常多,65536个字符并不够表示所有字符。所以Unicode规范进行了扩编,截止2014.6.16,最新版本的Unicode 7.0.0包含了超过110000个字符的编码。

Unicode字符集规定了字符的编码,但不包括这些字符的各种字形的编码。Unicode定义了从0 hex 到 10FFFF hex一共1,114,112个码值(code points),每个码值U+hhhh的形式表示,其中每一个“h”代表一个十六进制数。Unicode中有一部分保留码值,并没有定义任何字符。

不过需要注意的是Unicode字符集只是规定了字符和二进制之间的对应关系,却没有规定在存储和传输时具体落实为几个字节,如何表示,所以仅有Unicode字符集是不够。

发扬光大:UTF-8编码

字符的码值是一回事,在存储和传输时,具体落实为几个字节,如何表示,又是另一回事,码值的具体表示形式,就由字符编码方式来规定。

Unicode有很多种实现方式,比如UTF-8,UTF-16(字符用两个字节或四个字节表示)和UTF-32(字符用四个字节表示)。互联网上使用最多的就是UTF-8,W3C建议网页中使用UTF-8作为默认的编码方式。

UTF-8是一种变长的编码方式,使用1-4个字节将Unicode中的1,112,064个码值都进行了编码。码值在Unicode中越靠前,一般使用频率就越高,UTF-8编码时使用的字节数也就越少。Unicode的前128个字符,和ASCII码一一对应,UTF-8编码也和ASCII编码一致。

总体来说,UTF-8的编码规则很简单,只有两条:

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。
  2. 对于n字节的符号(1<n<5),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

下表总结了UTF-8的编码规则,字母x表示了可用编码的位。

Unicode符号范围 UTF-8编码方式
U+0000 → U+007F 0xxxxxxx
U+0080 → U+07FF 110xxxxx 10xxxxxx
U+0800 → U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+10000 → U+1FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

依据上表解读UTF-8编码也十分容易,如果一个字节的第一位是0,则这个字节就代表了一个字符;如果一个字节的第一位是1,则从第一位起连续有几个1,就表示当前字符占有多少个字节。

下面来看一下如何根据字符的Unicode码值对其进行UTF-8编码。以汉字“学”为例,“学”的Unicode码值为U+5b66,处于上面表格的U+0800 → U+FFFF段内,因此UTF-8编码用三个字节来存储。然后将U+5b66从最低位到最高位依次填入到上面格式中的x,并在多出的高位上补0即可。

5b66转换为二进制:0101101101100110, 填入到:1110xxxx 10xxxxxx 10xxxxxx 中的x标记位,结果如下:11100101 10101101 10100110,这样就得到了“学”的UTF-8编码,转换为16进制就是e5ada6

我们来简单验证一下:

1
2
3
4
>>> u'学'
u'\u5b66'
>>> u'学'.encode("UTF-8")
'\xe5\xad\xa6'

如果我们知道了一个字符的UTF-8编码,那么同样可以得到它的Unicode码值。比如说我知道有以下UTF-8编码e78bbc,展开二进制为11100111 10001011 10111100,第一个字节有三个连续的1,因此这三个字节表示一个字符,字符的码值为00111001011111100,转换为16进制为U+72fc,Unicode中U+72fc即为‘狼’。同样,我们来验证一下:

1
2
3
4
>>> '\xe7\x8b\xbc'.decode('utf-8')
u'\u72fc'
>>> print '\xe7\x8b\xbc'.decode('utf-8')

百花齐放之中文编码

在前面,我们提到了gbk2312中文编码方案,下面将详细讲解一下中文编码。老外建立字符编码标准时显然没有考虑我们历史悠久的中文,只有128个字符的ASCII码无论如何也无法为我所用。于是,中国国家标准总局随后就发布了GB2312码,即中华人民共和国国家汉字信息交换用编码,并于1981年5月1日实施。GB2312字符集中除常用简体汉字字符外还包括希腊字母等可能会用到的字符,但是未收录繁体中文汉字和一些生僻字。

在我们学习GB2312的编码规则前,先来看以下几个概念:

1、区位码,为了便于计算机接受、辨认、处理汉字,我们为中文常用的汉字、符号、数字等编了唯一的数码,这就是区位码。区位码是由4位十进制数字组成的,因为我们把所有的国标汉字与符号组成一个94×94的矩阵,每一行称为一个区,每一列称为一个位,要表示一个汉字只需要给出行号(01~94)与列号(01-94)即可。我们可以用google搜索区位码查询系统,就可以方便地在线查询汉字的区位码了,例如“学”字的区位码为4907。

2、国标码,区位码无法直接用于识别汉字,因为可能与通信使用的控制码00H~1FH(也就是0~31,ASCII码的前32个)冲突,于是乎在每个汉字的区号和位号必须分别加上32(00100000,16进制20H),就得到所谓的国标码,也叫交换码。

00110001 00000111
00100000 00100000
———————————————————
01010001 00100111

用十六进制表示为D1A7。

3、 内码,文本中通常会混合使用中文字符和英文字符,因此有时候无法识别两个字节是两个单独的ASCII字符,还是一个汉字字符。因此,GB2312规定汉字的两个字节的最高位都为1,即在国标码的基础上加上128,这种高位为1的双字节汉字编码即为GB2312汉字的内码。

01010001 00100111
10000000 10000000
———————————————————
11010001 10100111

用十六进制表示为D1A7,内码也就是字符用GB2312编码的结果了。来验证一下:

1
2
>>> u'学'.encode('gb2312')
'\xd1\xa7'

GB2312并未包含繁体字和生僻字,因此在1995年出现了《汉字编码扩展规范》(GBK),GBK完全兼容GB2312,另外还收录了汉字部首符号、竖排标点符号等字符。Unicode3.1出现后,新的中文编码GB18030也随之诞生,GB18030编码向下兼容GBK和GB2312,并收录了所有Unicode3.1中的字符。

更多阅读

字符编码笔记:ASCII,Unicode和UTF-8
浅谈编码
字符编码
Wiki: Unicode
Wiki: Glyph
Wiki: UTF-8
关于字符集的最基本知识
中文编码杂谈