最近稍微研究了一下汉字的几种编码方式,感觉收获颇多,在此罗列出来,以供需要的朋友参考。因为网上讨论此话题的文章也颇多,我的一点见解似乎早就被前辈们覆盖到,所以,本文就当是写给自己的小小的总结吧。
GB2312
似乎是最普通的编码方式。80年代诞生,收录6000多汉字。几乎所有的中文系统都会支持GB2312。
GB2312的编码方式和古老的区位码颇有渊源。汉字的区位码每两位加上0xA0就是计算机中的GB2312码。比如“啊”区位码是1601,GB码是0xB0A1。B0 = 16 + A0,A1 = 01 + A0。
几乎所有的关于GB2312的资料都会说,它使用两个字节来存放汉字和符号。我对此颇为怀疑。在我的印象里,GB码似乎都是DBCS(double-byte character sets),即同时使用1个字节和2个字节两种方式来表示一个字符。不知道是不是我的理解问题。在Windows中有一个小程序“字符映射表”。这是个很有用的东西。通过它查找,Windows中,中文简体字符集的编码是同时用1个字节和2个字节来表示的。当高位是0x00~0x7f时,为一个字节,高位为0x80以上时用2个字节表示。
GBK
是GB2312的扩展方案,使用了原来编码空间的一些空白,增加了一些汉字,因此向下兼容GB2312。是Windows中文系统的缺省字符集。
可以用Windows的记事本看一看GBK的编码。打开记事本写几个字:“在CSDN写Blog”。保存类型为“ANSI”。注意ANSI是英语文字的编码方式(美国国家标准么),可不是汉字的。这里“ANSI”的意思就是采用系统默认的字符编码方式编码。
用一个16进制编辑器打开保存的文件,内容是:
D4 DA 43 53 44 4E D0 B4 42 6C 6F 67
分析一下:
D4
|
DA
|
43
|
53
|
44
|
4E
|
D0
|
B4
|
42
|
6C
|
6F
|
67
|
在
|
C
|
S
|
D
|
N
|
写
|
B
|
l
|
o
|
g
|
很明显单字节和双字节是一起使用的。
用过DOS的朋友都会记得汉字的乱码。若将上面每一个字节按照ASCII对应为字符,就可以看到乱码了:
╘┌CSDN╨┤Blog
因为ASCII扩展中0x80以上的字符都是非英语字母和制表符。因此汉字乱码看上去总是那么怪怪的。
DBCS总会碰到的一个问题就是错位。相信每个人都曾在网页上见到过一小片一小片的乱码。因为每一个汉字(准确的说是全角符号)都是两个字节拼成的。若是丢掉了其中一个字节,那么显示就会出现错乱。比如上面这一行字,我把开头的D4删掉,变成了:
贑SDN写Blog
因为第二个字节0xDA仍然大于0x7f,因此系统将0xDA43当作一个汉字输出了(可以查一下字符映射表证实一下),而剩下的“SDN”照常输出。若这是一长串的汉字没有任何一个半角字符做“缓冲”的话,就要乱成一片了。
GB18030
国内最新的编码方案。支持了更多的字符,甚至包括了蒙文,藏文之类。
GB18030的独特之处在于它向下兼容了GBK,但又扩展了编码空间。GB18030采用1字节,2字节,4字节三种方式来编码。其中:
1字节从0x00~0x7f;
2字节的高端从0x81~0xfe,低端从0x40到0x7e,以及0x80到0xfe;
4字节从0x81308130~0xfe39fe39。
GB18030有一个非常庞大的编码空间,几乎覆盖了现在所有编码方式的字符。它的4字节编码方式因为太过独特,似乎给微软造成了不少麻烦。现在WinXP和Win2K可以通过Add-Ones支持GB18030,它的代码页是54936,然而因为这些系统平台都不能支持4字节的码页(Code Page),因此内核仍然是GBK的,GB18030只是空有一个代码页而已。要想让这些系统支持GB18030,需要改写系统底层的许多代码。
在这些系统上,一些软件会认为系统的默认编码方式是GB18030(Java就是这样)。但实际上系统默认码页仍然是GBK。这些系统虽然有18030的字体(宋体18030),但并不能处理所有GB18030的符号,因为系统并不支持4字节的编码。但据我编程证实,JDK 1.5可以支持GB18030的四字节编码。
Unicode
国际统一的编码方式。据我理解,似乎Unicode标准试图用2个字节覆盖全世界所有的书写符号,两字节总共65535个编码位,据说现在还剩下3w多没有编码。前途无量,前途无量。
Unicode是真正的纯两个字节的编码方案。所有的字符一视同仁,原有的ASCII字符通过在高位加00来兼容Unicode。这使得Unicode成为非常危险的编码方案。一旦某一个字节丢失,其后的信息将全部作废。
在Windows的记事本中保存文件时可以选择Unicode以及Unicode Big Endian两种方式。在这里多说几句。关于Big Endian和Little Endian的含义不想多说。为什么Unicode要分大尾和小尾,而GBK不用?因为Unicode本身就是双字节码,双字节是放在一起作为一个单元的。而GBK本质上讲是单字节的,每一个字节是单独处理的。因此不能分大尾和小尾。在字符映射表中查到“在”的代码是0xD4DA,保存后就是0xD4DA。
“在CSDN写Blog”保存为Unicode编码后,文件内容是:
FF FE 28 57 43 00 53 00 44 00 4E 00 99 51 42 00 6C 00 6F 00 67 00
FF FE是Unicode码串的头,是固定的。不要以为是记事本这个软件的自创。其后每两个字节是一个字符。应该注意到英文字符的编码与ASCII是兼容的。
保存为Unicode Big Endian后的文件内容是:
FE FF 57 28 00 43 00 53 00 44 00 4E 51 99 00 42 00 6C 00 6F 00 67
编码的顺序和头的样子都变化了。
试试删掉一个字节。小心Unicode的头部不能动。以Little Endian的为例,删掉FF FE后面的28,文字变成了:
䍗匀䐀一餀䉑氀漀最
完全没有意义。
UTF
关于UTF的含义,我看到了两个不同的解释。在
http://www.dvpos.com/blog(周海汉的开发专栏)中的《深入剖析JSP和Servlet对中文的处理过程》(以下简称《深入》)一文中说是Unicode Text Format的缩写。在另一个地方
http://fmddlmyy.home4u.china.com中的《谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词》(以下简称《谈谈》)一文中说是UCS Transformation Format的缩写。而UCS是Unicode Character Set的缩写。感觉似乎后者更可能一些。
从《谈谈》一文中得知,UTF-8的编码规则为:
Unicode
|
UTF-8
|
0000 - 007F
|
0xxxxxxx
|
0080 - 07FF
|
110xxxxx 10xxxxxx
|
0800 – FFFF
|
1110xxxx 10xxxxxx 10xxxxxx
|
《深入》一文中对于以上规则有如下的文字描述:
1. 如果Unicode的16位字符的头9位是0,则用一个字节表示,这个字节的首位是“0”,剩下的7位与原字符中的后7位相同,如“/u0034”(0000 0000 0011 0100),用“34” (0011 0100)表示;(与源Unicode字符是相同的);
2. 如果Unicode的16位字符的头5位是0,则用2个字节表示,首字节是“110”开头,后面的5位与源字符中除去头5个零后的最高5位相同;第二个字节以“10”开头,后面的6位与源字符中的低6位相同。如“/u025d”(0000 0010 0101 1101),转化后为“c99d”(1100 1001 1001 1101);
3. 如果不符合上述两个规则,则用三个字节表示。第一个字节以“1110”开头,后四位为源字符的高四位;第二个字节以“10”开头,后六位为源字符中间的六位;第三个字节以“10”开头,后六位为源字符的低六位;如“/u9da7”(1001 1101 1010 0111),转化为“e9b6a7”(1110 1001 1011 0110 1010 0111);
试试把“在CSDN写Blog”转化成UTF-8编码,内容变成:
EF BB BF E5 9C A8 43 53 44 4E E5 86 99 42 6C 6F 67
原来的字头FF FE根据规则变成了EF BB BF。即:
原来:1111 1110 1111 1111(FF FE à FE FF,因为Little Endian)
UTF:1110 1111 1011 1011 1011 1111,即EF BB BF
再试试“在”的编码,
原来:0101 0111 0010 1000(“在”的Unicode码是0x5728)
UTF:1110 0101 1001 1100 1010 1000,即E5 9C A8。
由此,验证了UTF-8的编码,而且注意到,UTF-8也被当成了单字节码一样对待,因为并没有大尾小尾的区别。
对于汉字居多的文本来说,UTF-8将大部分是每个汉字三个字节的情况,反而会增加了码长。但UTF-8的一大好处就是避免了Unicode“一个烂鱼坏得一锅腥”的糟糕情况。因为UTF-8每一个字有严密的编码规则,如果传输中丢掉了一个字节,结果不会影响很多。比如我们的例子中,删掉除头之外第一个字节E5,会使其后的9C A8都不符合编码规则,这样会识别出坏掉的字,丢掉它们之后,后面的“CSDN写Blog”仍然会保留。
ISO-8859-1
最后说一下ISO-8859-1是因为它根本就不是汉字编码。据“维基百科”记载,ISO 8859是给除英语之外的其他字母文字做编码规范的。它占用ASCII码的0xA0~0xFF,每个字符集扩展96个字符。ISO-8859(多了一个连字号)则是ISO 8859加上原有的ASCII中的字符构成的编码规范。
说到ISO-8859-1因为它是许多未汉化的西文操作系统的编码,比如Linux。最主要的原因是我发现我的手机的缺省编码也是它。许多西文的数据库等等也只认识8859规范。
我们常接触的就是8859-1,实际上8859这套规范很大,似乎有16套之多(ISO 8859-1~ISO 8859-16)。
8859规范其实很简单。相对于DOS时的ASCII,它只不过是另一种扩展ASCII的方式。回头看一下DOS下的汉字乱码(GBK部分有写),在ISO-8859-1编码方式下显示汉字时的乱码原理本质上和DOS时是一样的,它们都是将汉字的双字节解释成两个单独的ASCII字符,只不过两者对于ASCII的扩展是不一样的,因此乱码的样子看起来也不太一样。但是在内存中,汉字的编码并没有丢失。只要改变一种编码方式,真相就会大白。
将“在CSDN写Blog”,转化为ISO-8859-1编码显示出来是:
ÔÚCSDNдBlog
这也是IE显示一些中文网页时出现的乱码的种类之一。
P.S. 我用Java做这些编码之间的转换实验时无意中发现Java支持GB18030的四字节表示方法。在将把ISO-8859-1转化后的例子直接getBytes()时,发现结果是:
81 30 88 38 81 30 89 33 43 53 44 4e 81 30 88 34 81 30 85 37 42 6c 6f 67
这些“81 30”实在让我迷糊了半天。后来注意到GB18030的编码方式才明白,81 30 88 38正好在GB18030四字节编码的范围内。猜测GB18030使用扩展出的四字节部分编码这些西欧字符,而Java认为我的机器的默认编码是GB18030,使用getBytes()时就采用了GB18030进行编码,而将ISO-8859-1转化出的那些西欧文字转化到GB18030时,就产生了这些四字节的编码。
参考链接:
周海汉的开发专栏:
http://www.dvpos.com/blog
微软网站的
New Chinese Encoding GB-18030:
http://www.microsoft.com/globaldev/drintl/columns/015/default.mspx?gssnb=1