1 Unicode简介
Unicode(统一码、万国码、单一码)是一种在计算机上使用的字符集。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符集。Unicode用数字0-0x10FFFF来映射这些字符,最多可以容纳1114112 -- 2048 = 1112064个字符,或者说有1112064个码位(应该是1112064,百度百科说是1114112不太对,有2048个码位为代理区),码位就是可以分配给字符的数字。UTF-8、UTF-16、UTF-32都是将数字转换到程序数据的编码方式。
通用字符集(Universal Character Set,UCS)是由ISO制定的ISO10646(或称ISO/IEC10646)标准所定义的标准字符集。UCS-2用两个字节编码,UCS-4用4个字节编码。Unicode采用了与ISO10646-1相同的字库和字码;ISO也承诺,ISO 10646将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致。
UCS-4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个平面(plane)。每个平面根据第3个字节分为256行 (row),每行有256个码位(cell)。将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。每个平面有2^16=65536个码位。Unicode计划使用了17个平面,一共有17*65536=1114112个码位。在Unicode5.0.0版本中,已定义的码位只有238605个,分布在平面0、平面1、平面2、平面14、平面15、平面16。其中平面15和平面16上只是定义了两个各占65534个码位的专用区(PrivateUse Area),分别是0xF0000-0xFFFFD和0x100000-0x10FFFD。所谓专用区,就是保留给大家放自定义字符的区域,可以简写为PUA。平面0也有一个专用区:0xE000-0xF8FF,有6400个码位。平面0的0xD800-0xDFFF,共2048个码位,是一个被称作代理区(Surrogate)的特殊区域。第一个平面称为基本多文种平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes)。
如前所述在Unicode 5.0.0版本中,238605-65534*2-6400-2048=99089。余下的99089个已定义码位分布在平面0、平面1、平面2和平面14上,它们对应着Unicode目前定义的99089个字符,其中包括71226个汉字。平面0、平面1、平面2和平面14上分别定义了52080、3419、43253和337个字符。平面2的43253个字符都是汉字。平面0上定义了27973个汉字。
在表示一个Unicode的字符时,通常会用“U+”然后紧接着一组十六进制的数字来表示这一个字符。在基本多文种平面(英文为 Basic Multilingual Plane,简写 BMP。它又简称为“零号平面”, plane 0)里的所有字符,要用四位十六进制数(例如U+4AE0,共支持六万多个字符);在零号平面以外的字符则需要使用五位或六位十六进制数了。
2 Unicode实现方式
Unicode的实现方式不同于字符集编码,一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为UTF("Unicode/UCS Transformation Format"的首字母缩写),即把Unicode字符转换为某种格式之意。通常使用的实现方式为UTF-8编码和UTF-16编码。
2.1UTF-8编码方式
UTF-8以字节为单位对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的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。从上表可以看出,4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位。
假设要将 U+2A6A5 (16进制,对应汉字‘’) 转成 UTF-8 编码:
Ø 2A6A5在区域010000 -10FFFF内,所以占4字节。
Ø 0x2A6A5 = 000 101010 011010 100101
Ø 0x2A6A5 = 11110000 10101010 10011010 10100101
Ø 即0xF0AA9AA5
2.2 UTF-16编码方式
UTF-16是Unicode字符集的一种转换方式,即把Unicode的码位转换为16比特长的码元串行,以用于数据存储或传递。UTF-16编码规则如下:
2.2.1 从U+D800到U+DFFF的码位(代理区)
因为Unicode字符集的编码值范围为0-0x10FFFF,而大于等于0x10000的辅助平面区的编码值无法用2个字节来表示,所以Unicode标准规定:基本多语言平面内,U+D800..U+DFFF的值不对应于任何字符,为代理区。因此,UTF-16利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。
但是在使用UCS-2的时代,U+D800..U+DFFF内的值被占用,用于某些字符的映射。但只要不构成代理对,许多UTF-16编码解码还是能把这些不符合Unicode标准的字符映射正确的辨识、转换成合规的码元. 按照Unicode标准,这种码元串行本来应算作编码错误.
2.2.2 从U+0000至U+D7FF以及从U+E000至U+FFFF的码位
第一个Unicode平面(BMP),码位从U+0000至U+FFFF(除去代理区),包含了最常用的字符。UTF-16与UCS-2编码在这个范围内的码位为单个16比特长的码元,数值等价于对应的码位。BMP中的这些码位是仅有的码位可以在UCS-2被表示。
2.2.3 从U+10000到U+10FFFF的码位
辅助平面(Supplementary Planes)中的码位,大于等于0x10000,在UTF-16中被编码为一对16比特长的码元(即32bit,4Bytes),称作 code units called a 代理对(surrogatepair),具体方法是:
Ø 码位减去0x10000, 得到的值的范围为20比特长的0..0xFFFFF(因为Unicode的最大码位是0x10ffff,减去0x10000后,得到的最大值是0xfffff,所以肯定可以用20个二进制位表示),写成二进制形式:yyyy yyyy yyxx xxxx xxxx。
Ø 高位的10比特的值(值的范围为0..0x3FF)被加上0xD800得到第一个码元或称作高位代理(highsurrogate), 值的范围是0xD800..0xDBFF。由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead surrogates)。
Ø 低位的10比特的值(值的范围也是0..0x3FF)被加上0xDC00得到第二个码元或称作低位代理(lowsurrogate), 现在值的范围是0xDC00..0xDFFF。 由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail surrogates)。
Ø 最终的UTF-16(4字节)的编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
按照上述规则,Unicode编码0x10000-0x10FFFF的UTF-16编码有两个WORD,第一个WORD的高6位是110110,第二个WORD的高6位是110111。可见,第一个WORD的取值范围(二进制)是1101100000000000到1101101111111111,即0xD800-0xDBFF。第二个WORD的取值范围(二进制)是1101110000000000到1101111111111111,即0xDC00-0xDFFF。上面所说的从U+D800到U+DFFF的码位(代理区),就是为了将一个WORD(2字节)的UTF-16编码与两个WORD的UTF-16编码区分开来。
由于高位代理、低位代理、BMP中的有效字符的码位,三者互不重叠,搜索是简单的: 一个字符编码的一部分不可能与另一个字符编码的不同部分相重叠。这意味着UTF-16是自同步(self-synchronizing):可以通过仅检查一个码元就可以判定给定字符的下一个字符的起始码元。 UTF-8也有类似优点,但许多早期的编码模式就不是这样,必须从头开始分析文本才能确定不同字符的码元的边界。
由于最常有的字符都在基本多文种平面中,许多软件的处理代理对的部分往往得不到充分的测试。这导致了一些长期的bug与潜在安全漏洞,甚至在广为流行得到良好评价的应用软件
2.2.4 示例: UTF-16 编码程序
假设要将 U+2A6A5 (16进制,对应汉字‘’) 转成 UTF-16 编码. 因为它超过 U+FFFF, 所以他必须编码成32位(4个byte)的格式,如下所示:
V = 0x2A6A5
Vx = V - 0x10000
= 0x1A6A5
= 00011010 0110 1010 0101
Vh = 00 0110 1001 // Vx 的高位部份的 10bits
Vl = 10 1010 0101 // Vx 的低位部份的 10bits
w1 = 0xD800 //结果的前16位元初始值
w2 = 0xDC00 //结果的后16位元初始值
w1 = w1 | Vh
=1101 1000 0000 0000
| 000110 1001
=1101 1000 0110 1001
= 0xD869
w2 = w2 | Vl
=1101 1100 0000 0000
| 10 1010 0101
=1101 1110 1010 0101
= 0xDEA5
所以这个字符U+64321最后正确的UTF-16编码应该是:
0xD8690xDEA5
而在小尾序中最后的编码应该是:
0x69D80xA5DE
因为这个字超过了U+FFFF,所以无法用UCS-2的格式编码
16进制编码范围 |
UTF-16表示方法(二进制) |
10进制码范围 |
字节数量 |
U+0000---U+FFFF |
xxxxxxxx xxxxxxxx |
0-65535 |
2 |
U+10000---U+10FFFF |
110110yyyyyyyyyy 110111xxxxxxxxxx |
65536-1114111 |
4 |
UTF-16比起UTF-8,好处在于大部分字符都以固定长度的字节(2字节)存储,但UTF-16却无法兼容于ASCII编码。
2.2.5 UTF-16与UCS-2的关系
UTF-16可看成是UCS-2的父集。在没有辅助平面字符(surrogatecode points)前,UTF-16与UCS-2所指的是同一的意思。但当引入辅助平面字符后,就称为UTF-16了。现在若有软件声称自己支持UCS-2编码,那其实是暗指它不能支持在UTF-16中超过2bytes的字集。对于小于0x10000的UCS码,UTF-16编码就等于UCS码。
2.2.6 MicrosoftWindows操作系统内核对Unicode的支持
Windows操作系统内核中的字符表示为UTF-16小尾序,可以正确处理、显示以4字节存储的字符。但是Windows API实际上仅能正确处理UCS-2字符,即仅以2字节存储的,码位小于U+FFFF的Unicode字符。其根源是MicrosoftC++语言把wchar_t数据类型定义为16比特的unsignedshort,这就与一个wchar_t型变量对应一个宽字符,可以存储一个Unicode字符的规定相矛盾。相反,Linux平台的GCC编译器规定一个wchar_t是4字节长度,可以存储一个UTF-32字符,宁可浪费了很大的存储空间。下例运行于Windows平台的C++程序可说明此点:
// 此源文件在Windows平台上必须保存为Unicode格式(即UTF-16小尾)
// 因为包含的汉字“”,不能在Windows默认的代码页936(即gbk)中表示。
// 该汉字在UTF-16小尾序中用4个字节表示,
// Windows操作系统能正确显示这样的在UTF-16需用4字节表示的字符
// 但是WindowsAPI不能正确处理这样的在UTF-16需用4字节表示的字符,把它判定为2
// 个UCS-2字符
#include
int main()
{
const wchar_t lwc[]=L"";
MessageBoxW(NULL, lwc, lwc, MB_OK);
int i = wcslen(lwc);
printf("%d\n", i);
int j = lstrlenW(lwc);
printf("%d\n", j);
return 0;
}
2.3UTF-32编码方式
UTF-32编码以32位无符号整数为单位。Unicode的UTF-32编码就是其对应的32位无符号整数。由于UTF-32编码对每个字符都会用4个字节来存储,但大多数字符用2个字节就能表示,所以4个字节就比较浪费空间了,因此UTF-32用得比较少。
2.4 字节序
如果直接使用与Unicode编码一致(仅限于BMP字符)的UTF-16编码,由于每个字符占用了两个字节,在不同操作系统中,对字节顺序的理解是不一致的。这时同一字节流可能会被解释为不同内容,如某字符为十六进制编码4E59,按两个字节拆分为4E和59,在Mac上读取时是从低字节开始,那么在MacOS会认为此4E59编码为594E,找到的字符为“奎”,而在Windows上从高字节开始读取,则编码为U+4E59的字符为“乙”。就是说在Windows下以UTF-16编码保存一个字符“乙”,在Mac OS环境下打开会显示成“奎”。此类情况说明UTF-16的编码顺序若不加以人为定义就可能发生混淆,于是在UTF-16编码实现方式中使用了大端序(Big-Endian, 简写为UTF-16 BE)、小端序(Little-Endian,简写为UTF-16 LE)的概念,以及可附加的字节顺序记号解决方案。目前在PC机上的Windows系统和Linux系统对于UTF-16编码默认使用UTF-16LE。UTF-32也同样会有这样的问题。
根据字节序的不同,UTF-16可以被实现为UTF-16LE或UTF-16BE,UTF-32可以被实现为UTF-32LE或UTF-32BE。例如:
Unicode编码 |
UTF-16LE |
UTF-16BE |
UTF32-LE |
UTF32-BE |
0x006C49 |
49 6C |
6C 49 |
49 6C 00 00 |
00 00 6C 49 |
0x020C30 |
43 D8 30 DC |
D8 43 DC 30 |
30 0C 02 00 |
00 02 0C 30 |
那么,怎么判断字节流的字节序呢?Unicode标准建议用BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为BOM的字符“零宽无中断空格”。这个字符的编码是FEFF,而反过来的FFFE(UTF-16)和FFFE0000(UTF-32)在Unicode中都是未定义的码位,不应该出现在实际传输中。下表是各种UTF编码的BOM:
UTF编码 |
Byte Order Mark |
UTF-8 |
EF BB BF |
UTF-16LE |
FF FE |
UTF-16BE |
FE FF |
UTF-32LE |
FF FE 00 00 |
UTF-32BE |
00 00 FE FF |
但是由于BOM只是建议添加的,不是强制的,有些软件和系统就没有这个头;所以在读取字符时,只能从编码范围上来检测当前字符的字节序,但是这保证不了100%正确。
3 ANSI编码
不同的国家和地区制定了不同的标准,由此产生了 GB2312,BIG5, JIS 等各自的编码标准。这些使用2个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。 当然对于ANSI编码而言,0x00~0x7F之间的字符,依旧是1个字节代表1个字符,这一点是ASNI编码与Unicode编码之间最大也最明显的区别。
对于我国常用编码:GB2312有6763个汉字,GBK有21003个汉字,GB18030-2000有27533个汉字,GB18030-2005有70244个汉字。
Unicode5.0中,如果不算兼容区,目前有70217个汉字。让我们比较一下Unicode的70217汉字和GB18030-2005中的70244汉字:
GB18030-2005 |
Unicode 5.0 |
对应的Unicode编码 |
CJK统一汉字的20902汉字 |
CJK统一汉字的20902汉字 |
0x4E00-0x9FA5 |
CJK统一汉字扩充A的6582汉字 |
CJK统一汉字扩充A的6582汉字 |
0x3400-0x4DB5 |
CJK统一汉字扩充B的42711汉字 |
CJK统一汉字扩充B的42711汉字 |
0x20000-0x2A6D6 |
CJK部首补充区的14个部首 |
未计入 |
2E81, 2E84, 2E88, 2E8B, 2E8C, 2E97, 2EA7, 2EAA, 2EAE, 2EB3, 2EB6, 2EB7, 2EBB, 2ECA |
CJK兼容汉字区的21个汉字 |
未计入 |
F92C, F979, F995, F9E7, F9F1, FA0C, FA0D, FA0E, FA0F, FA11, FA13, FA14, FA18, FA1F, FA20, FA21, FA23, FA24, FA27, FA28, FA29 |
“ |
CJK统一汉字区新增了这8个字符 |
0x9FB4-0x9FBB |
未计入 |
CJK统一汉字区新增的14个字符 |
0x9FA6-0x9FB3 |