第一部分: 字符集历史
电子计算机技术是从美国开始发展起来的,因为美国使用的文字为英文,美国规定的计算机信息交换用的字符编码集是人们熟知的扩展的ASCII码,它以8bit字节为单位存储,ASCII的0-31及127为控制符,32-126为可见字符,包括所有的英文字母,阿拉伯数字和其他一些常见符号,128-255的ASCII码则没有定义。
ASCII对英语国家是够用了,但对其他西欧国家却不够用,因此,人们将ASCII扩展到0-255的范围,形成了ISO-8859-1字符集。值得一提的是,因为考虑到程序中处理的信息大多是西文信息,因此有些WEB容器(如:Tomcat4.x)在处理所接收到的request字符串时,如果您没指定request的编码方式则系统就缺省地采用ISO-8859-1,明白这一点对理解后面的问题会有帮助。
相比西方的拼音文字,东方的文字(如中文)的字符数要大得多,根本不可能在一个字节内将它们表示出来,因此,它们以两个字节为单位存储,以中文国标字符集GB2312为例,它的第一个字节为128-255。系统可以据此判断,若第一个字节大于127,则把与该字节后紧接着的一个字节结合起来共两个字节组成一个中文字符。这种由多个字节存储一个字符的字符集叫多字节字符集(MultiByte Charsets),对应的象ASCII这种用一个字节存储一个字符的字符集叫单字节字符集(SingleByte Charsets)。在GB2312字符集中,ASCII字符仍然用一个字节存储,换句话说该ASCII是该字符集的子集。
GB2312只包含数千个常用汉字,往往不能满足实际需要,因此,人们对它进行扩展,这就有了我们现在广泛使用的GBK字符集,GBK是现阶段Windows及其他一些中文操作系统的缺省字符集。它包含2万多个字符,除了保持和GB2312兼容外,还包含繁体中文字,日文字符和朝鲜字符。值得注意的是GBK只是一个规范而不是国家标准,新的国家标准是GB18030-2000,它是比GBK包含字符更多的字符集。
我国的台湾地区使用的文字是繁体字,其字符集是BIG5,而日本采用的字符集则是SJIS。它们的编码方法与GB2312类似,它们的ASCII字符部分是兼容的,但扩展部分的编码则是不兼容的,比如这几种字符集中都有"中文"这两个字符,但他们在各自的字符集中的编码并不相同,这就是用GB2312写成的网页用BIG5浏览时,看到的是乱糟糟的信息的原因。
可见,在字符集的世界里,呈现给我们的是一个群雄割据的局面,各字符集拥有一块自己的地盘。这给各国和各地区交换信息带来了很大的困难,同时,也给国际化(本地化)编程造成了很大的麻烦。
常言道:"分久必合",随着国际标准ISO10646定义的通用字符集(Universal Character Set即UCS)的出现,使这种局面发生了彻底的改观。UCS 是所有其他字符集标准的一个超集. 它保证与其他字符集是双向兼容的. 就是说, 如果你将任何文本字符串翻译到 UCS格式, 然后再翻译回原编码, 你不会丢失任何信息。UCS 包含了用于表达所有已知语言的字符。不仅包括拉丁语、希腊语、 斯拉夫语、希伯来语、阿拉伯语、亚美尼亚语和乔治亚语的描述、还包括中文、日文和韩文这样的象形文字、 以及平假名、片假名、 孟加拉语、 旁遮普语果鲁穆奇字符(Gurmukhi)、泰米尔语、印.埃纳德语(Kannada)、Malayalam、泰国语、 老挝语、 汉语拼音(Bopomofo)、Hangul、 Devangari、Gujarati、Oriya、Telugu 以及其他数也数不清的语。对于还没有加入的语言,由于正在研究怎样在计算机中最好地编码它们, 因而最终它们都将被加入。
ISO 10646 定义了一个 31 位的字符集。然而, 在这巨大的编码空间中, 迄今为止只分配了前 65534 个码位 (0x0000 到 0xFFFD)。 这个 UCS 的 16位子集称为基本多语言面 (Basic Multilingual Plane, BMP)。 将被编码在 16 位 BMP 以外的字符都属于非常特殊的字符(比如象形文字), 且只有专家在历史和科学领域里才会用到它们。
UCS 不仅给每个字符分配一个代码, 而且赋予了一个正式的名字。 表示一个 UCS 值的十六进制数, 通常在前面加上 "U+", 就象 U+0041 代表字符"拉丁大写字母A"。 UCS 字符 U+0000 到 U+007F 与 US-ASCII(ISO 646) 是一致的, U+0000 到 U+00FF 与 ISO 8859-1(Latin-1) 也是一致的。这里要注意的是它是以16bit为单位存储,即便对字母"A"也是用16bit,这是与前面介绍的所有字符集不同的地方。
历史上,在国际标准化组织研究ISO10646标准的同时,另一个由多语言软件制造商组成的协会也在从事创立单一字符集的工作,这就是现在人们熟知的 Unicode。幸运的是,1991年前后ISO10646和Unicode的参与者都认识到,世界上不需要两个不同的单一字符集。他们合并双方的工作成果,并为创立单一编码表而协同工作。两个项目仍都存在并独立地公布各自的标准,都同意保持ISO10646和Unicode的码表兼容,并紧密地共同调整任何未来的扩展。这与当年在PC机上的操作系统MS-dos与PC-dos的情形有些相象。后面,我们将视ISO10646和Unicode为同一个东西。
有了Unicode,字符集问题接近了完美的解决,但不要高兴得过早。由于历史的原因:一些操作系统如:Unix、Linux等都是基于ASCII设计的。此外,还有一些数据库管理系统软件如:Oracle等也是围绕ASCII来设计的(从其8i的白皮书上介绍的设置系统字符集和字段的字符集中可以间接地看到这一点)。在这些系统中直接用Unicode会导致严重的问题。用这些编码的字符串会包含一些特殊的字符, 比如 '/0' 或 '/', 它们在 文件名和其他 C 库函数参数里都有特别的含义。 另外, 大多数使用 ASCII 文件的 UNIX 下的工具,如果不进行重大修改是无法读取 16 位的字符的。 基于这些原因, 在文件名, 文本文件, 环境变量等地方,直接使用Unicode是不合适的。
在 ISO 10646-1 Annex R 和 RFC 2279 里定义的 UTF-8 (Unicode Transformation Form 8-bit form)编码没有这些问题。
UTF-8 有以下一些特性:
UCS 字符 U+0000 到 U+007F (ASCII) 被编码为字节 0x00 到 0x7F (ASCII 兼容)。 这意味着只包含 7 位 ASCII 字符的文件在 ASCII 和 UTF-8 两种编码方式下是一样的。
所有 >U+007F 的 UCS 字符被编码为一个多个字节的串, 每个字节都有标记位集。 因此,ASCII 字节 (0x00-0x7F) 不可能作为任何其他字符的一部分。
表示非 ASCII 字符的多字节串的第一个字节总是在 0xC0 到 0xFD 的范围里, 并指出这个字符包含多少个字节。 多字节串的其余字节都在 0x80 到 0xBF 范围里。 这使得重新同步非常容易, 并使编码无国界,且很少受丢失字节的影响。
UTF-8 编码字符理论上可以最多到 6 个字节长, 然而 16 位 BMP 字符最多只用到 3 字节长。
字节 0xFE 和 0xFF 在 UTF-8 编码中从未用到。
通过,UTF-8这种形式,Unicode终于可以广泛的在各种情况下使用了.
第二部分: 从Unicode到UTF-8的转换
From: http://hi.baidu.com/winnyang/blog/item/d5fd4f3d7b1cbdc19e3d62aa.html
Unicode是一个字符集,而UTF-8是 Unicode的其中一种,Unicode是定长的都为双字节,而UTF-8是可变的,对于汉字来说Unicode占有的字节比UTF-8占用的字节少1 个字节。Unicode为双字节,而UTF-8中汉字占三个字节。
网魂小兵 http://xdotnet.cnblogs.com
UTF-8编码字符理论上可以最多到6个字节长,然而16位BMP(Basic Multilingual Plane)字符最多只用到3字节长。下面看一下UTF-8编码表:
U-00000000 - U-0000007F: 0xxxxxxx
U-00000080 - U-000007FF: 110xxxxx 10xxxxxx
U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
xxx 的位置由字符编码数的二进制表示的位填入, 越靠右的 x 具有越少的特殊意义,只用最短的那个足够表达一个字符编码数的多字节串。注意在多字节串中, 第一个字节的开头"1"的数目就是整个串中字节的数目。而第一行中以0开头,是为了兼容ASCII编码,为一个字节,第二行就为双字节字符串,第三行为3 字节,如汉字就属于这种,以此类推。(个人认为:其实我们可以简单的把前面的1的个数看成字节数)
网魂小兵 http://xdotnet.cnblogs.com
为了要将Unicode转换为UTF-8,当然要知道他们的区别到底在什么地方。下面来看一下,在Unicode中的编码是怎样转换成UTF-8的,在UTF-8中,如果一个字符的字节小于0x80(128)则为ASCII字符,占一个字节,可以不用转换,因为UTF-8兼容ASCII编码。假如在Unicode中汉字“你”的编码为“u4F60”,把它转换为二进制为100111101100000,然后按照UTF-8的方法进行转换。可以将Unicode二进制从地位往高位取出二进制数字,每次取6位,如上述的二进制就可以分别取出为如下所示的格式,前面按格式填补,不足8位用0填补。
unicode: 100111101100000 4F60
utf-8: 11100100, 10111101, 10100000 E4BDA0
从上面就可以很直观的看出Unicode到UTF-8之间的转换,当然知道了UTF-8的格式后,就可以进行逆运算,就是按照格式把它在二进制中的相应位置上取出,然后在转换就是所得到的Unicode字符了(这个运算可以通过“位移”来完成)。
网魂小兵 http://xdotnet.cnblogs.com
如上述的“你”的转换,由于其值大于0x800小于0x10000,因此可以判断为三字节存储,则最高位需要向右移“12”位再根据三字节格式的最高位为 11100000(0xE0)求或(|)就可以得到最高位的值了。同理第二位则是右移“6”位,则还剩下最高位和第二位的二进制值,可以通过与 111111(0x3F)求按位于(&)操作,再和11000000(0x80)求或(|)。第三位就不用移位了,只要直接取最后六位(与 111111(ox3F)取&),在与11000000(0x80)求或(|)。OK了,转换成功!在VC++中的代码如下所示(Unicode到UTF-8的转换)。
2 char utf8[ 3 + 1 ];
3 memset(utf8, 0 , 4 );
4 utf8 [ 0 ] = 0xE0 | (pUnicode >> 12 );
5 utf8 [ 1 ] = 0x80 | ((pUnicode >> 6 ) & 0x3F );
6 utf8 [ 2 ] = 0x80 | (pUnicode & 0x3F );
7 utf8 [ 3 ] = " /0 " ;
8 // char[4]就是UTF-8的字符“你”了。
当然在UTF-8到Unicode的转换也是通过移位等来完成的,就是把UTF-8那些格式相应的位置的二进制数给揪出来。在上述例子中“你”为三个字节,因此要每个字节进行处理,有高位到低位进行处理。在UTF-8中“你”为11100100,10111101,10100000。从高位起即第一个字节11100100就是把其中的"0100"给取出来,这个很简单只要和11111(0x1F)取与(&),由三字节可以得知最到位肯定位于12位之前,因为每次取六位。所以还要将得到的结果左移12位,最高位也就这样完成了0100,000000,000000。而第二位则是要把“111101”给取出来,则只需将第二字节10111101 和111111(0x3F)取与(&)。在将所得到的结果左移6位与最高字节所得的结果取或(|),第二位就这样完成了,得到的结果为 0100,111101,000000。以此类推最后一位直接与111111(0x3F)取与(&),再与前面所得的结果取或(|)即可得到结果 0100,111101,100000。OK,转换成功!在VC++中的代码如下所示(UTF-8到Unicode的转换)。
2 const char * utf8 = " 你 " ;
3 wchar_t unicode;
4 unicode = (utf8[ 0 ] & 0x1F ) << 12 ;
5 unicode |= (utf8[ 1 ] & 0x3F ) << 6 ;
6 unicode |= (utf8[ 2 ] & 0x3F );