这几天帮朋友写一点代码,需要做多语言支持,顺便了解多语言字符集的原理,不想挺有意思,特别是字符集的演化和发展过程,实际上也反映了在需求逐步增加的背景下一个软件设计如何进化的过程。于是乎上网一阵狂看,拼拼凑凑写出这篇文章。
从ASCII编码说起
我们需要了解的最早编码是ASCII码。它用7个二进制位来表示,由于那个时期生产的大多数计算机使用8位大小的字节。那时候用到的字符很少,26个大小写英文字母还有数字再加上其他常用符号,也不到100个,因此用户不仅可以存放所有可能的ASCII字符,而且有整整一位空余下来。有些通讯系统利用剩下最高位1比特来做通讯中的奇偶校验。如果你技艺高超,也可以将该位用做自己离奇的目的:WordStar中那个发暗的灯泡实际上设置这个高位,以指示一个单词中的最后一个字母,同时这也宣示了WordStar只能用于英语文本。
为了支持非英语拉丁语系,于是出现了OEM字符集和ISO8859-1
随着计算机的广泛引用,人们发现仅仅用ASCII的127个字符是不够的,例如无法表示很多拉丁语系国家的字母表。由于字节有多达8位的空间,因此许多人在想:“呀!我们可以把128~255之间的编码用做个人的应用目的。”问题在于,同时产生这种想法的人相当多,而且在128~255之间的各个位置上应该存放什么这一问题上,真是仁者见仁智者见。事实上,只要人们开始在美国以外的地方购买计算机,那么各种各样的不同OEM字符集都会进入规划设计行列,并且各人都会根据自己的需要使用高位的128个字符。如此一来,甚至在同语种的文档之间就不容易实现互换。
ASCII可被扩展,最优秀的扩展方案是ISO8859-1,通常称之为Latin-1。Latin-1包括了足够的附加字符集来写基本的西欧语言。最后,这个人人参与的OEM终于以ANSI标准的形式形成文件。实际上,这个标准并不能说是一个字符集,而是一系列字符集,根据所在国籍的不同,处理编码128以上的字符有许多不同的方式。这些不同的系统称为代码页。同时,每个人都认同如何使用低端的128个编码,这与ASCII相当一致。
ISO8859-1详见:http://en.wikipedia.org/wiki/ISO/IEC_8859-1
为了支持亚洲语系,又出现了多字节字符集(MBCS)和中文字符集
但是人们又发现,256个字符对于拉丁语系语言是足够的,但对于亚洲国家语言是远远不够的!因此这些国家的人为了用上电脑,又要保持和ASCII字符集的兼容,就发明了多字节编码方式,相应的字符集就称为多字节字符集(MBCS,Multiple Byte Character Set)。例如中国使用的就是双字节字符集编码(DBCS,Double Byte Character Set):GB2312, GBK。
GB2312码就是中华人民共和国国家汉字信息交换用编码,基本集共收入汉字6763个和非汉字图形字符682个,用两个字节的区位码表示;区位码加上0x2020,就是国标码;国标码加上0x8080,就是机器内码。所谓机器内码,其实也就是存储在我们的硬盘上的实际内容啦!由于GB2312的机器内码永远大于0xA0, 因此兼容单字节的ASCII编码也就不成问题了!(0x00-0x7F)。可以看到,从区位码到机器内码的转换体现了从定义到实现规范的过程。这个过程既保证了中文编码定义的独立性,同时也通过转换巧妙解决了和ASCII编码兼容的问题。
由于GB2312只支持数量较少的简体汉字,因此扩展定义了GBK编码,支持了几乎所有的汉字——没错,K就是扩展的意思。和GB2312类似,GBK亦采用双字节表示,总体编码范围为 8140-FEFE,首字节在 81-FE之间,尾字节在40-FE之间,剔除xx7F一条线。总计23940个码位,也兼容ASCII编码。
GB2312详见:http://baike.baidu.com/view/25492.htm?fr=ala0_1_1
GBK详见:http://baike.baidu.com/view/25421.htm
为了支持多语言同时显示,Unicode横空出世
但是问题还没有解决,虽然各个不同的语系都找到了独立表示自己字符的方法,但是如果想把不同的语言在同一份文档中显示,就搞不定了!因此,人们发明了Unicode字符集。Unicode字符集涵盖了目前人类使用的所有字符,并为每个字符进行统一编号,分配唯一的字符码(Code Point)。
Unicode字符集将所有字符按照使用上的频繁度划分为17个层面(Plane),每个层面上有216=65536个字符码空间。其中第0个层面BMP(Basic Multilingual Plan),基本涵盖了当今世界用到的所有字符。其他的层面要么是用来表示一些远古时期的文字(难道是梵文?甲骨文?楔形文字?),要么是留作扩展。我们平常用到的Unicode字符,一般都是位于BMP层面上的。目前Unicode字符集中尚有大量字符空间未使用。完整的Unicode编码区间分配可以从网上找到。
上面讲的只是给地球上每一种语言的每一个字符分配一个Unique的code,这是语言学家要搞定的问题。那么如何在计算机系统中实现呢?这是咱伟大IT工程师们的工作。有人也许会说,直接储存字符编码不就得了吗?其实这有两个问题:一是浪费空间,99%的应用只需要用到BMP平面字符,也就是说只需要2个字符,但为了兼容考古学家的楔形文字而不得不将小学生的作业文档也用4个字节来存储显然极不划算。其二,咱做计算机的都知道要抽象和实现分离,降低耦合度嘛,假设将来地球人和火星人建立了睦邻友好的邦交关系,大量的邮件需要兼容火星文那怎么办?地球人发现所有的编码都用完了…… 于是,UCS-2, UCS-4, UTF-8, UTF-16等各种编码实现方式就轮番上场了。
Unicode字符集的定长编码实现:UCS-2/UCS-4/UTF-32
由于大部分的人们都只使用BMP层面的字符,由于BMP层面上有216=65536个字符码,因此我们只需要两个字节就可以完全表示这所有的字符了,这就是双字节定长的UCS-2编码(Universal Character Set coded in 2 octets)。这带来了两个问题:1、考古学家有意见:我的字符怎么输入?2、美国佬也有意见:我只需要127个ASCII字符,你给我整两个字节,这不浪费我的Money吗?
为了需要支持所有字符,人们于是又设计了UCS-4编码,顾名思义,也就是用4个定长字节来存储的Unicode编码(实际上只用了31位,最高位必须为0)。显然,相比UCS-2,UCS-4带来的是更大的空间浪费。
那么,UTF-32又是什么意思?根据维基百科,UTF-32可被认为是UCS-4的一个子集,但是这个子集实际上已经能够存放所有已经定义的Unicode字符了,所以可以认为UTF-32和UCS-4基本上是同一回事。反正这两个字符集不常用,管它呢。
UTF-32/UCS-4详见:http://en.wikipedia.org/wiki/UTF-32/UCS-4
为了解决UCS编码的问题,人们又发明了UTF-16
既然UCS-2存在无法支持非BMP字符的硬伤,而UCS-4又存在巨大的空间浪费,因此UTF-16编码就应运而生了。UTF就是Unicode Transform Format的意思。UTF-16为了支持Unicode全字符集的编解码,采用了变长编码,以2个字节为单位对Unicode进行编码。对于BMP字符集,使用2个字节表示,对于非BMP字符,则需要4个字节结对。为了使计算机能够区分2个字节和4个字节的字符,UTF-16的设计者保留了一个特殊区间,在此不赘述。
According to维基百科,随着历史滚滚潮流,在Unicode标准2.0以后,UTF-16已经取代了UCS-2。
UTF-16/UCS-2详见:http://en.wikipedia.org/wiki/UTF-16/UCS-2
UTF-16之改进:UTF-8
UTF-16编码一定程度上解决了UCS编码的问题,但对于ASCII字符集也需要使用两个字符来存储,而且高字节是0x00,这引起了欧美国家的强烈抗议,因为大家都知道,0x00一般被认为是字串、文件结束的标志,很容易引起应用程序的错误解析。因此,聪明的人们又发明了UTF-8编码。这里要详细介绍一下这种聪明的编码方法:
UTF-8以1个字节为单位对Unicode字符进行编码,是一种变长编码方式。从Unicode到UTF-8的映射关系是:
Unicode编码(16进制) ║ UTF-8 字节流(二进制)
------------------------------------------------------------
000000 - 00007F ║ 0xxxxxxx
000080 - 0007FF ║ 110xxxxx 10xxxxxx
000800 - 00FFFF ║ 1110xxxx 10xxxxxx 10xxxxxx
010000 - 10FFFF ║ 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
一个UTF-8字符的第一个字节使用n个高位的1和一个0来表示该字符由n个字节组成。后续的字节的高两位都是10。其余的xxxx就是Unicode的实际内容。看到了吧?1个字节的UTF-8编码实际上兼容了ASCII编码。据说Unicode 汉字内码的汉字区为4E00-9FA5,不知是不是完全准确,但根据上面的规则,汉字Unicode的UTF-8编码都是3个字节(但千万不要说UTF-8是3字节的定长编码哦)。
UTF-8是迄今为止应用最广泛的Unicode编码方式。
UTF-8详见:http://en.wikipedia.org/wiki/UTF-8
一个实现细节:字节序
一个Unicode字符的UCS-2, UCS-4和UTF-16编码都是由2个或4个字节组成,在不同的计算机系统中,可能是高字节在前,低字节在后,或者反过来。以汉字“严”的UCS-2编码为例,Unicode码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。
这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big- Endian)敲开还是从小头(Little-Endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。因此,第一个字节在前,就是“大头方式”(Big endian),第二个字节在前就是“小头方式”(Little endian)。
所以,UCS-2, UCS-4和UTF-16编码分别有BE和LE两种不同的实现方式。至于为什么会出现这两种不同的实现,我也不是很清楚:)。
辨别字节序的方法:BOM
那么同学们会问了:应用程序该怎么判断字节流的字节序呢?Unicode标准建议用BOM(Byte Order Mark)来区分字节序。这个BOM可不是供应链中的Bill Of Material哦。
Unicode建议,在传输字节流前,先传输被作为BOM的字符。这个字符的编码是FEFF,而反过来的FFFE(UTF-16)和FFFE0000(UTF-32)在Unicode中都是未定义的码位,不应该出现在实际传输中。下表是各种UTF编码的BOM:
UTF编码 ║ Byte Order Mark
UTF-16LE ║ FF FE
UTF-16BE ║ FE FF
UTF-32LE ║ FF FE 00 00
UTF-32BE ║ 00 00 FE FF
UTF-8 ║ EF BB BF
有的同学会有问题:怎么没有看到UCS-2的BOM呢?根据维基百科:The BOM is not optional in UCS-2。聪明的同学也许还会问:UTF-8并不存在BE和LE的问题,为什么也有BOM?原来这是为了告诉应用程序:这个字节流的编码方式是UTF-8。
打开Windows记事本的“另存为”对话框,你可以发现下面有几个选项:ASNI, UTF-8, Unicode, Unicode Big Endian。其中,ASNI表示使用GBK + ASCII编码存储;UTF-8自然表示使用UTF-8编码存储,如果你用UltraEdit的十六进制模式编辑这个文件,就可以发现文件头的EF BB BF BOM标志;Unicode表示使用UTF-16 Little Endian存储,如果你存入字符b,实际上文件的内容是FF FE 62 00;而对于Unicode Big Endian,如果你存入字符b,实际上文件的内容是FE FF 00 62。
需要说明的是,并不是所有的系统都支持LE和BE选项。因此,你可以在UltraEdit的另存为格式中看到“无BOM”这样的选项。
字符集之间的转换
实现从字符集A到字符集B完整正确转换的前提是:字符集B是字符集A的子集。
Unicode的各种实现(UTF-8, UTF-16, UCS-2, UCS-4等)是可以通过一定的规则互相转换的。但是Unicode和GB2312/GBK之间没有固定的转换规则,它的转换过程依赖于一个二维数组的映射。
那我用的究竟是什么字符集?
Microsoft简体版中文Windows 95就是以GBK为内码,又由于GBK同时也涵盖了Unicode所有CJK汉字,所以也可以和Unicode做一一对应。而MS从NT时代开始就采用了UTF-16编码,很多流行的编程平台,例如.Net,Java,Qt还有Mac下的Cocoa等都是使用UTF-16作为基础的字符编码,例如代码中的字符串,在内存中相应的字节流就是用UTF-16编码过的。
------------------------------------------------------------
参考:
1、关于字符编码,你所需要知道的
2、http://www.mvpoo.com/shenmeshiascii-unicode-utf-8-gb2312/