在计算机世界中,最经常遇到的字符集是ASCII(American standard code II),这个字符集中定义了英文26个字母和一些常用符号(阿拉伯数字和常用标点等)对应的二进制编码。ASCII码对应的范围是从0x00到0x7f。
ASCII码所能表示的字符一共是128个(包括控制字符),占一个字节,但最高位总是0。对于非英语的国家来说,这些字符是不够的,比如欧洲其它国家,有的字母有重音符号,有的字母与英语字母不一样(像数字中常用的拉丁字母)。于是,他们分别扩展了ASCII码的最高位,因此可以表示256个字符。这些编码都不是统一的,虽然低位的128(0到127)个字符总是和ASCII保持一致,但高位的128(128到255)个字符就各有侧重了。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。
比起亚洲来说,欧洲这样的字母文字地区的编码问题还不算严重。亚洲很多国家都是符号文字(像中国的汉字,日文的假名,非汉字韩文等),这些符号成千上万。一个字节用来表达这些字符是远远不够的,ASCII以及扩展都满足不过这些国家和地区的要求。所以各个国家和地区又纷纷组织编辑了适合于自己字符编码集,比如常见的有中国的GB码,日本的JIS等等。这些编码集都至少用到了2个字节以上来表示自己的文字符号。
Unicode
这些编码集合虽然局部地解决了一些问题,但是又带来了新的问题,那就是,他们都不是统一的(统一的标准只有ASCII码),当各种编码文件之间进行交流的时候,就会产生问题。这非常不利于信息的交流和传递。遇着问题越来越频率地发生,人们对于统一的编码的渴望也就越来越强烈,于是,UNICODE就横空出世了,它满足了人们的两个主要要求:
每个需要表示的字符,都要在unicode中有自己的"位置"。
每个字符都有独立的统一的编码。
Unicode的历史可以追溯到1987年,当时Xerox的Joe Becker和Apple公司的Lee Collins、Mark Davis几个人就开始进行投入到创造一种通用字符集的工作中去了,次年的8月,Joe Backer发表了第一份名为Unicode 88的草案,拉开了unicode发展的序幕。当时这份草案是基于16bit的模型,把所有字符都使用16个bit来表示,因为他们认为,在现代社会中仍在广泛使用的字符个数远远少于16384(2的14次方)个,用16位表示已经足够了。但unicode后来的发展远远超出了这个泛围,许许多少古老的、罕用的或废弃的字符被源源不断地加入到unicode标准中。
最新的unicode(5.0)已经定义了多达1,114,112个字符代码点(code points),编码范围从0到0x10ffff。这些code points被划分成17个字符平面(planes),每个平面包含65536个code points(256行 X 256列)。 如下表:
各平面的字符编码范围:
Plane | Range | Description | Abbreviation |
0 | 0000-FFFF | Basic Multilingual Plane | BMP |
1 | 10000-1FFFF | Supplementary Multilingual Plane | SMP |
2 | 20000-2FFFF | Supplementary Ideographic Plane | SIP |
3 to 13 | 30000-DFFFF | currently unassigned |
|
14 | E0000-EFFFF | Supplementary Special-purpose Plane | SSP |
15 | F0000-FFFFF | Supplementary Private Use Area-A |
|
16 | 100000-10FFFF | Supplementary Private Use Area-B |
|
Unicode只是定义了每个字符对应编码,其它的一概不管。比如,汉字"李"的unicode是十六进制数4E67,它是唯一的。对于各种系统和各种应用来说,知道一个unicode编码,就能确定一个字符,但对于字符的其它信息(字体,大小等),unicode就管不着了。
Unicode只是实现了统一编码的愿望,具体应用到实践中,还存在几个问题,比如,如何才能区别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?还有,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有两到三个字节是0,这对于存储来说是极大的浪费,英文文本文件的大小会因此大出二三倍,这是无法接受的。而且,对于现存有很多代码(比如C的标准库)都是以单字节的ASCII码为基础编写的,unicode中很多字符的编码中都有0x00,那么这些已存的代码,处理unicode就不再适合。这些问题都影响着unicode的推广。
从上面知道,除了unicode本身之外,我们还需要定义一些好的存储方案,使得实践中遇到的问题都能得到解决,能与现有的代码和处理系统的存储、处理兼容,又能与unicode兼容的方式,这样就最好了。目前,已经有很多种方案存在了,主要的有unicode Transformation(简称UTF)和Universal Character Set(简称UCS)。采用什么样的方案,取决于应用领域的具体情况(比如合适的存储空间、源代码的兼容性、以及各系统之间的相互操作等)。
在具体介绍某个方案前,我们需要明确的是这些方案与unicode的关系:
这些方案是unicode的具体实现形式,在该方案下,每个字符的存储要能和unicode的定义保持一一映射的关系(在下面UFT8的例子中我们可以看到)。在这个基础上,每种方案可以采用一些自己的规定来解决实践中遇到的各种问题。
下面简单介绍一下常遇到的unicode编码方案:
UTF8是最为常用的unicode字符编码之一,它是一种变长的编码方式,使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8的编码规则:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于unicode的起启的128个字符,UTF-8编码和ASCII码是完全一致的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。其它的二进制位,全部用于存储符号的unicode码。
UTF-8与Unicode的编码范围之间的关系如下表:
另一种常用的是UCS-2编码方式,很多时候,在遇到直接说unicode编码而不是UFT-N等的具体名称的时候,指的就是UCS-2这种编码,即直接用两个字节存入字符的Unicode码。UCS-2分little endian和bit endian。下面会进行简单的介绍。
Little endian和Big endian
Unicode码可以采用UCS-2格式直接存储。以汉字"李"为例,Unicode码是U+4E67,需要用两个字节存储,一个字节是4E,另一个字节是67。存储的时候,4E在前,67在后,就是Big endian方式;67在前,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,就表示该文件采用小头方式。很多文体编辑器都提供了这两种保存方式,有兴趣可以分别保存之后,用16进制编辑器查开文件的开头,就能进行验证了。