Extreme Thinking
字符编码详解:ASCII, Unicode, UTF8, GB2312, GBK

2014-12-11


1. ASCII码

在计算机内部,所有的信息最终都表示为一个二进制的字符串。每一个二进制位(bit)有0和1两种状态,因此一个字节(八个二进制位)就可以组合出256种状态,每一个状态对应一个符号,就是256个符号,从00000000到11111111。

ASCII是American Standard Code for Information Interchange的缩写,ASCII编码第一次以规范标准的型态发表是在1967年,最后一次更新则是在1986年。 ASCII码一共规定了128个字符的编码,包括95个可显示字符(32-126)和33个不可显示字符(0-31,127),可见字符包括0-9,A-Z,a-z,英式标点符号等字符。这128个符号只占用了一个字节的后面7位,最前面的1位统一规定为0。

2. ISO-8859-1编码

英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示,其他使用拉丁字母的语言(主要是欧洲国家的语言),都有一定数量的附加符号字母,于是,国际标准化组织(ISO)及国际电工委员会(IEC)联合制定了一系列8位字符集的标准,0-127用来兼容ASCII编码,128-255用来定义这些国家的附加字母,不同国家的附加符号是不一样的,因此共定义了15个单字节字符集,其中最常见的是 ISO-8859-1,即西欧语言。

  • ISO-8859-1: Latin-1, Western European, 西欧语言

这15字符集编号为1-11,13-16,没有12,所有这15个字符集都兼容ASCII编码,所以英语可以使用任意一个来表示。

3. Unicode

在ISO-8859-x字符集里,128-255部分代表的字母是不一样的。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字。

世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。

可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就像它的名字所表示的,这是一种所有符号的编码,Unicode为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

Unicode至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为2014年6月16日公布的7.0.0。

目前的Unicode字符分为17组平面(Plane),包括基本多文种平面(Basic Multilingual Plane,简写BMP,它又简称为“零号平面”, Plane 0)和16个辅助平面,每平面拥有65536(即2个字节)个代码点。BMP和辅助平面合计需要占据21位的编码空间,比3字节略少,但事实上辅助平面字符仍然占用4字节编码空间,与UCS-4保持一致。

基本平面编码用U+hhhh表示,包含了常见的绝大多数字符,包括英文字符,西欧字符,常见汉字等等,基本满足各种语言的使用。U+0000-U+00FF完全兼容ISO-8859-1编码。

Plane 1摆放拼音文字(主要为现时已不再使用的古老文字)、手写文字和音符等符号,用于学者的专业论文中使用这些古老或过时的语言书写符号。Plane 2放置的都是一些罕用的汉字或地区的方言用字,主要为中日韩统一表意文字扩展区。

4. Unicode的编码实现: UTF-8, UTF-16

Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)

例如,如果一个仅包含基本7位ASCII字符的Unicode文件,如果每个字符都使用2字节的原Unicode编码传输,其第一字节的8位始终为0。这就造成了比较大的浪费。

4.1 UTF-8

UTF-8是使用最广的一种Unicode的实现方式,是一种变长的编码方式,它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。UTF-8的编码规则很简单,只有二条:

  • 1)对于ASCII字符,字节的第一位设为0,后面7位为ASCII码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
  • 2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

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

  • 0xxxxxxx - 单字节,ASCII码
  • 110xxxxx 10xxxxxx - 2字节,重音文字、希腊字母或西里尔字母等使用2字节来存储
  • 1110xxxx 10xxxxxx 10xxxxxx - 3字节,基本平面的其他字符使用3字节来存储,譬如汉字
  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - 4字节,辅助平面使用4字节来存储

对于ASCII字符,在UTF-8里对应单字节,对于Unicode Plane 0里的其他字符,可能对应UTF-8的2个字节或3个字节,中文是3字节。对于以英文为主的文档,使用UTF-8编码可以有效节省空间,对于中文为主的文档,可能会增加50%空间。

4.2 UTF-16

UTF-16的编码规则很简单:基本平面的字符占用2个字节,辅助平面的字符占用4个字节。也就是说,UTF-16的编码长度要么是2个字节(U+0000到U+FFFF),要么是4个字节(U+010000到U+10FFFF)。

对于Unicode基本平面的字符,使用2个字节,UTF-16码与Unicode码是一样的。

对于辅助平面,使用4个字节,在基本平面里,从U+D800到U+DFFF是空段,16个辅助平面共需2^4 x 2^16 = 2^20,即20位表示,UTF-16将这20位拆成两半,前10位映射在U+D800到U+DBFF,称为高位(H),后10位映射在U+DC00到U+DFFF,称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。

UTF-16编码导致ASCII码会增大1倍,但好处是基本平面的字符和UTF-16是一一对应的,在文件传输和存储时一般不用UTF-16编码,但很多程序内部会使用UTF-16编码,这样字符串的查找和操作效率最高,譬如Java,.Net等较新的语言,都使用Unicode为内部编码。

4.3 UCS-2

UCS-2又是什么编码?曾经有两个团队,不约而同想搞统一字符集。一个是1988年成立的Unicode团队,另一个是1989年成立的UCS团队。等到他们发现了对方的存在,很快就达成一致:世界上不需要两套统一字符集。1991年10月,两个团队决定合并字符集。也就是说,从今以后只发布一套字符集,就是Unicode,并且修订此前发布的字符集,UCS的码点将与Unicode完全一致。

UCS的开发进度快于Unicode,1990年就公布了第一套编码方法UCS-2,使用2个字节表示已经有码点的字符。UTF-16编码迟至1996年7月才公布,明确宣布是UCS-2的超集,即基本平面字符沿用UCS-2编码,辅助平面字符定义了4个字节的表示方法。

所以,UCS-2是2字节编码,对应了Unicode基本平面。

4.4 UTF-32

UTF-32是一种最直接的也是最笨的编码方法,使用固定的4个字节表示一个Unicode字符,UTF-32字节内容一一对应Unicode编码,因此转换最简单,缺点是浪费空间,ASCII字符会增大4倍,所以实际上没有人使用这种编码方法。与UTF-32对应的是UCS-4编码。

4.5 Little endian和Big endian

Unicode码可以采用UCS-2格式直接存储。以汉字"严"为例,Unicode码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。

这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。

因此,第一个字节在前,就是"大头方式"(Big endian),第二个字节在前就是"小头方式"(Little endian)。 那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。 如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

4.6 实例

在Windows上打开"记事本"程序Notepad.exe,新建一个文本文件,内容就是一个"严"字,依次采用ANSI,Unicode,Unicode big endian 和 UTF-8编码方式保存。 然后,用文本编辑软件UltraEdit中的"十六进制功能",观察该文件的内部编码方式。

  • 1)ANSI:文件的编码就是两个字节"D1 CF",这正是"严"的GB2312编码,这也暗示GB2312是采用大头方式存储的。
  • 2)Unicode:编码是四个字节"FF FE 25 4E",其中"FF FE"表明是小头方式存储,真正的编码是4E25。
  • 3)Unicode big endian:编码是四个字节"FE FF 4E 25",其中"FE FF"表明是大头方式存储。
  • 4)UTF-8:编码是六个字节"EF BB BF E4 B8 A5",前三个字节"EF BB BF"表示这是UTF-8编码,后三个"E4B8A5"就是"严"的具体编码,它的存储顺序与编码顺序是一致的

5. 中文编码: GB2312、BIG5、GBK、GB18030

下面是几个中文编码:

  • GB2312(1980年) - 双字节编码,高字节和低字节均使用7位编码。收录简化汉字及符号、字母、日文假名等共 7445 个图形字符,其中汉字占 6763 个
  • BIG5 - 是通行于台湾、香港地区的一个繁体字编码方案,BIG5 码是双字节编码方案
  • GBK(1995年) - 双字节编码,向下与 GB2312 完全兼容,向上支持Unicode,包括:
    • GB2312 中的全部汉字、非汉字符号。
    • BIG5 中的全部汉字。
    • 与 ISO 10646 相应的国家标准 GB13000 中的其它 CJK 汉字,以上合计 20902 个汉字。
    • 其它汉字、部首、符号,共计 984 个
  • GB18030(2000年) - 是最新的汉字编码字符集国家标准, 向下兼容 GBK 和 GB2312 标准。 GB18030 编码是一二四字节变长编码,一字节部分与ASCII编码兼容