字符编码这个东西实在是头疼,一会儿是ISO5589-1,一会儿是GB2312,一会儿又是UTF-8,60%以上的程序员对这个东西稀里糊涂,前些天查了一些文档,好好梳理了一下,把自己的心得分享给大家。
首先,我们来明确一些概念:
字符:它是抽象的最小文本单位。字符就是对某种意义的图画表示,或者说形状表示。“A”是一个字符,“¥”也是一个字符。
字符集:字符的集合。比如汉字字符集,拉丁字符集,全人类所有的字符的集合。
编码字符集:也就是一个字符集的编码形式,它为每一个字符分配一个唯一数字。
在没有包含全人类的字符编码集出来之前,各个国家都是你编你的,我编我的,也就出现了西欧语言(又叫Latin-1)编码字符集(ISO 8859-1),中文编码字符集(GB2312/BIG5/GBK/GB18030),日文编码字符集,CJK中日韩统一编码字符集等等。
这样可不行,得有对全人类字符的统一考虑才行,也就是我们通常说的国际化考虑,英文是Internationalization,这个单词可够长的,于是就简写做i18n,表示i开头n结尾的18个字母的单词。
为了i18n,于是出现了两个针对全人类所有字符的编码字符集,一个是UCS(Universal Character Set),它是国际标准化组织弄出来的,也就是ISO/IEC 10646;
另一个是Unicode(Universal Code),Unicode同时也是一个联盟的名字,也就是HP、Microsoft、IBM、Apple等几家知名的大型计算企业所组成的联盟集团,他们为了推进多种文种的统一编码而制定出了Unicode(通常我们说Unicode指的是UTF-16)。
这两个组织同时干了同一件事,就是给全人类的字符进行编码,这可不是一件好事,想当年巴比伦塔就是因为上帝搞乱了俺们人类的语言才没有建成的,所以在91年的时候,国际标准化组织做了让步,将Unicode和UCS进行统一,(也就是把Unicode并为ISO10646的第一个字面BMP,这个我们后面再讲)。
所以,实际上,现在的情况是:全人类现有所有字符是Unicode的一个子集,而Unicode又是UCS的一个子集。
代码点:是指可用于编码字符集的数字。编码字符集定义一个有效的代码点范围,但是并不一定将字符分配给所有这些代码点(哪有那么多字符啊?呵呵)。
有效的UCS的代码点范围是0~2的31次方。
有效的 Unicode 代码点范围是 U+0000 至 U+10FFFF。Unicode 标准始终使用十六进制数字,而且在书写时在前面加上前缀“U+”,比如“A”的编码书写为“U+0041”。
字符编码方案:是从一个或多个编码字符集到一个或多个固定宽度代码单元序列的映射。最常用的代码单元是字节,但是 16 位或 32 位整数也可用于内部处理。
ISO 10646就是UCS的字符编码方案。
UTF-32、UTF-16 和 UTF-8 是 Unicode 标准的编码字符集的字符编码方案。
前面说的ISO 8859-1,GB2312/BIG5/GBK/GB18030,CJK等,都是某种语言的字符编码方案。而不考虑语言的统一方案必定是ISO10646或者UTF-32、UTF-16、 UTF-8中的一种。
好了,有了这些概念,下面我可以来具体的讲讲ISO 10646和Unicode的编码方案了。
ISO/IEC 10646:它采用4个字节来表示一个字符,实际使用的是31个bit,采用十六进制全编码表示。
总体结构是一个四维的编码空间,先将所有的代码点分为128个三维组(group),每一组包含256个平面(plane),每一个平面包含256行(row),每一行包含256个字位(cell),又称谓“列”。其中group的值范围是从00到7F,plane、row、cell的值范围都是从00到FF全编码。整个编码字符集的每个字符都是由4个八位序列表示,按照组八位、面八位、行八位、列八位的顺序,具体表示如下:
Group-octet 组八位 | Plane-octet 平面八位 | Row-octet 行八位 | Cell-octet 字位八位 |
ISO/IEC 10646将其第一个平面(00组中的00平面)称作Basic Multilingual Plane(基本多文种平面),简称BMP。人类对BMP中的256×256=65536个字符的使用频率超过99.9%,这就使得人们对BMP格外青睐。ISO/IEC 10646规定BMP上的字符可以作为双八位编码字符集使用,即:在此平面上仅用行、列两个八位就可以表示一个编码字符。
按照行可以将BMP中的代码点分为下面四个区域:
A- Zone(00至4D行)为拼音文字编码区,拉丁文、阿拉伯文、日文的平假名及片假名、数学符号等等都在此区域编码。一句话,除了汉字以外,目前世界上已规范的文种都在此区域编码。
I- Zone(4E至9F行)为表意文字编码区,我们将其称作汉字区,通常人们所说的CJK统一编码汉字就放在这个区域,从4E00到9FA5共20902个编码汉字。
O- Zone(A0至DF行)是一个开放区域,未作定义,留作扩展用。(其中的D8至DF行为UTF-16的代理区,后面再讲。)
R- Zone(E0至FF行)为限制使用区,一些兼容字符、字符的变形显现形式、特殊字符等均放在此区。
韩文在1993年的ISO/IEC 10646-1中属于A –Zone的34-4D行,但后来由于扩充的需要被迁徙到O- Zone的AC至D7这44行(44X256=11264);而原来的34-4D行被如下分配:
⒈CJK扩充集A 3400→4DB5
⒉康熙字典214部首 2F00→2FD5
⒊CJK部首扩充 2E80→2EF3
⒋汉字结构符 2FF0→2FF8
⒌藏文 0F00→0FCF
⒍彝文 A000→A4C6
⒎蒙文 1800→18A9
UTF-16:也就是我们通常所说的Unicode编码,它用2个字节或者4个字节来表示一个字符。
前面说了,国际标准化组织和Unicode的合并是把Unicode并为ISO10646的第一个字面BMP,也就是说Unicode中代码点在U+0000到U+FFFF间的字符和BMP中的完全一样。也就是说,BMP中的字符,在UFT-16中是用2个字节来表示的。
而Unicode的代码点范围是U+0000 至 U+10FFFF ,所以我们把Unicode代码点在 U+10000 至 U+10FFFF 范围之间的字符称为增补字符。但增补字符在Unicode编码(UFT-16)中怎么表示呢?
前面讲到,在BMP中定义了一个代理区(Surrogate Zone)(D800至DFFF), 这个区域内没有定义任何的字符或符号。将这个区域平分为前后两个各容纳1024(1K)个编码的区域(D800-DBFF及DC00-DFFF),分别称作高半代理(high surrogate)及低半代理(low surrogate)区域。从这两个区域分别各取一个编码,由这两个编码组合成一个4 bytes代理对(surrogate pair)来表示一个编码字符,而且只有将这两个代理对(surrogate pair)结合在一起才能表示一个字符,单独使用其中的任何一个都没有意义。
高低半字符的编码位置各为1,024=4×256,因此UTF-16总计可提供(4×256)×(4×256)=16×65536个编码位置,亦即16个字面,也就是U+0000 至 U+10FFFF。对BMP而言,当然无需使用UTF-16转码,所以UTF-16的转码主要应用于ISO10646的第1~第14字面(第15字面为专用字面),也就是说只有第1~第14字面的字符才需要两个UTF-16编码来表示,即4个字节表示一个增补字符。
从上面的分析我们看到,用2个字节的Unicode就可以表示使用频率超过99.9%的BMP中的字符了。但现实的情况是,现在的计算机和网络中主要使用的字符绝大多数属于ASCII字符集,也就是基本拉丁字符集(U+0000 至 U+007F)的127个字符。针对这种情况,Unicode组织特别设计了UTF-8编码。
UTF-8:使用1~4个字节的序列对 Unicode 代码点进行编码。
U+0000 至 U+007F 使用1个字节编码,U+0080 至 U+07FF 使用2个字节,U+0800 至 U+FFFF 使用3个字节,而 U+10000 至 U+10FFFF(增补字符) 使用4个字节。编码规则如下:
U+0000 – U+007F: | 0xxxxxxx |
U+0080 – U+07FF: | 110xxxxx 10xxxxxx |
U+0800 – U+FFFF: | 1110xxxx 10xxxxxx 10xxxxxx |
U+10000 – U+10FFFF: | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Java modified UTF-8:
Java内部对UTF-8进行了一定修改,这样,它和标准UTF-8编码就在一定程度上不能兼容了,原因有两点:
1、经修订的 UTF-8 将字符 U+0000 表示为双字节序列 0xC0 0x80,而标准 UTF-8 使用单字节值 0x0。
2、经修订的 UTF-8 通过对其 UTF-16 表示法的两个代理代码单元单独进行编码来表示增补字符。每个代理代码单元由三个字节来表示,这样,经修改的UTF-8就需要6个字节来表示一个增补字符,而标准 UTF-8 使用4个字节序列表示一个增补字符。
Java在 java.io.DataInput 和 DataOutput 接口和类中使用经修订的 UTF-8 实现。而标准 UTF-8 由 String 类、java.io.InputStreamReader 和 OutputStreamWriter 类、java.nio.charset以及许多其上层的 API 提供支持。
这可真是个大陷阱哪,虽然用到增补字符的机会不多,但U+0000却是经常使用的,用Java做开发的同志们一定要注意了!
Tip:在Linux中,可以用od –x FileName或者hexdump FileName这样的命令来输出文件的16进制形式,这样就可以看到U+0000之类的不可打印字符了,:)
UTF-32:即将每一个 Unicode 代码点表示为相同值的 32 位整数。很明显,它是内部处理最方便的表达方式,但是,如果作为一般字符串表达方式,则要消耗更多的内存。所以实际上它只有存在的意义,而没有实用的意义,没有哪个傻瓜会真的去做UTF-32编码。
通过这些介绍,相信大家对编码的相关概念和i18n问题、ISO 10646的BMP、Unicode及其3种编码方式有了一个大概的了解,至少以后再看到一些奇怪的编码代号应该不会茫茫然而不知所措了。
另外,在Linux的 /usr/share/i18n/charmaps/ 目录下有一些压缩包,存的就是各种编码方案对应的编码和Unicode代码点的对应,需要的时候可以参考。