2012-04-25
本文主要讨论Unicode的编码及其各种实现,着重讨论UTF-16,UTF-8的实现规则,以及Big-endian和Little-Endian的存储顺序。
一、Unicode编码
Unicode出现之前已经有各种编码标准:ANSI、ISO8859-1、GB2312、GBK以及BIG-5等。Unicode试图统一各种编码,在Unicode演进过程中,也有自身不断修复的过程:刚开始的时候用16位表达65535个字符,认为已经足够收集所有的字符;后来随着大量中文、韩文和日文等表意文字的加入,已经超出了65535个字符,16位已经不能描述所有的字符集了。
在Unicode字符集中的某个字符对应的代码值,称作代码点(Code Point),用16进制书写,并加上U+前缀。比如,‘田’的代码点是U+7530;‘A’的代码点是U+0041。
Unicode定义的字符集已经超过16位所能表达的范围,把所有这些CodePoint分成17个代码平面(Code Plane):
- U+0000 ~ U+FFFF划入基本多语言平面(Basic MultilingualPlane,简记为BMP);
- 其余划入16个辅助平面(Supplementary Plane),代码点范围U+10000 ~ U+10FFFF。
虽然这样划分,但并不是每个Plane中的Code point都对应有字符,这里面有保留的,还有特殊用途的。
二、Unicode编码的实现
Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的,但是在实际存储和传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)。
对Unicode编码的主要有UTF-16BE、UTF-16LE、UTF-8、UTF-7以及UTF-32等实现方式,目前常用的实现方式是UTF-16LE、UTF-16BE和UTF-8。
2.1 UTF-16
UTF-16是用16bit编码来表达Unicode,这样表达范围是216(即65536),也就是UTF-16的代码单元(Code Unit)为16bits。如果表达BMP内的字符,用一个UTF-16的Code Unit就可表达,对于辅助平面内的字符,UTF-16有巧妙的设计。
落在BMP内,从U+D800到U+DFFF之间的Code Point区段是永久保留不映射到字符, UTF-16利用这保留下来的0xD800-0xDFFF区段的CodePoint来对辅助平面内的字符的Code Point进行编码。
对U+0000.. U+D7FF以及U+E000.. U+FFFF的编码
UTF-16与UCS-2对这个范围内的CodePoint进行编码,采用单个16bit长的CodeUnit,数值等价于对应的Code Point。BMP中的这些Code Point是仅有的可以被UCS-2表示的Code Point。
对U+10000.. U+10FFFF的编码
辅助平面(Supplementary Planes)中的CodePoint,在UTF-16中被编码为一对16bit长的Code Unit(即32bit,4Bytes),称作代理对(surrogate pair)。
具体方法是:
UTF-16解码 |
||||
hi \ lo |
DC00 |
DC01 |
… |
DFFF |
D800 |
10000 |
10001 |
… |
103FF |
D801 |
10400 |
10401 |
… |
107FF |
⋮ |
⋮ |
⋮ |
⋱ |
⋮ |
DBFF |
10FC00 |
10FC01 |
… |
10FFFF |
- Code Point减去0x10000, 得到的值是长度为20bit(0..0xFFFFF);
- 步骤1得到数值的高位的10比特的值(值范围为0..0x3FF)被加上0xD800得到第一个Code Unit或称作高位代理(high surrogate)或前导代理(lead surrogate)。取值范围是0xD800..0xDBFF。
- 步骤1得到数值的低位的10比特的值(值范围为0..0x3FF)被加上0xDC00得到第二个Code Unit或称作低位代理(low surrogate)或后尾代理(trail surrogate)。取值范围是0xDC00..0xDFFF。
这样,这个范围内的字符就被编码成了一个代理对[lead surrogate,trail surrogate]:两个16bits的Code Unit,取值范围分别是0xD800..0xDBFF和0xDC00..0xDFFF。而BMP中得到的Code Unit的范围是0x0000..0xFFFF(0xD800..0xDFFF是保留的,不包含其中),所以这三个区段是相互不重叠的,在解码时很容易实现。
UTF-16解码[高位代理+低位代理]得到的Code Unit对与Code Point的对应关系如上表所示。
下面以对U+64321的UTF-16编码为例,看一下对于辅助平面内的字符是如何编码的:
V = 0x64321
Vx = V - 0x10000
= 0x54321
= 01010100 0011 0010 0001
Vh = 01 0101 0000 // Vx 的高位部份的 10 bits
Vl = 11 0010 0001 // Vx 的低位部份的 10 bits
w1 = 0xD800 // 结果的前16位元初始值
w2 = 0xDC00 // 结果的后16位元初始值
w1 = w1 | Vh
= 1101 1000 0000 0000
| 01 0101 0000
= 1101 1001 0101 0000
= 0xD950
w2 = w2 | Vl
= 1101 1100 0000 0000
| 11 0010 0001
= 1101 1111 0010 0001
= 0xDF21
所以,这个字 U+64321 最终的 UTF-16 编码是:
0xD950 0xDF21
UTF-16的Code Unit是16bits,两个字节。存储一个Code Unit的时候,还有存取的先后顺序问题,也就是Endian问题,这在后面章节讲述。
2.2 UTF-8
UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,使用一至四个字节为每个字符编码:
- Unicode范围为U+0000..U+007F 的128个ASCII字符只需一个字节编码;
- Unicode范围为U+0080..U+07FF的字符需要二个字节编码;
- Unicode范围为U+0800..U+FFFF的其他BMP中的字符(这包含了大部分常用字)使用三个字节编码;
- Unicode 辅助平面的字符(其他极少使用的字符)使用四字节编码。
对上述提及的第四种字符而言,UTF-8使用四个字节来编码似乎太耗费资源了。但UTF-8对所有常用的字符都只用三个字节表达,而且UTF-16编码对前述的第四种字符同样需要四个字节来编码,而如果是ASCII居多的字符,UTF-8能极大的节约存储空间。UTF-8逐渐成为电子邮件、网页及其他储存或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。互联网邮件联盟(IMC)建议所有电子邮件软件都支持UTF-8编码。
对CodePoint各个范围内的字符进行UTF-8编码的规则如下:
Code point |
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+D800到U+DFFF之间的区段在Unicode字符集的定义中没有具体字符使用的,被用来在UTF-16编码中对辅助平面的字符进行编码。
下面以“田”(Code Point为U+7530)为例,看如何对其进行UTF-8编码:
- U+7530落在U+0800..U+FFFF区间,采用三字节编码;
- 0x7530转换为二进制为111 010100 110000;
- 代入表中,得到111001111001010010110000;
这样,得到“田”(U+7530)的UTF-8编码:0xE7 94 B0。
知道UTF-8的编码规则,我们可以对于UTF-8编码中的任意字节B,进行下面解码:
- 如果B的第一位为0,则B为ASCII码,并且B独立的表示一个字符;如果B的第一位为1,第二位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的一个字节,并且不为字符的第一个字节编码(字符的第一个字节之外的后编码);
- 如果B的前两位为1,第三位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的第一个字节,并且该字符由两个字节表示;
- 如果B的前三位为1,第四位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的第一个字节,并且该字符由三个字节表示;
- 如果B的前四位为1,第五位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的第一个字节,并且该字符由四个字节表示。
2.3 UCS-2 vs UTF-16,UCS-4 vs UTF-32
UCS-2每个字符占用2个字节。UCS-2是UTF-16的子集。在没有辅助平面前,UTF-16与UCS-2所指的是同一的意思。但当引入辅助平面字符后,UTF-16加入了对辅助平面内的字符的支持。现在若有软件声称自己支持UCS-2编码,那其实是暗指它不支持UTF-16中超过2bytes的字集。亦即,对于小于0x10000的UCS码,UTF-16编码就等于UCS码。Java早期版本对Unicode的支持,就只是UCS-2的支持,现在加入了对UTF-16的完整支持。
UCS-4与UTF-32的意义一致,对每个字符都使用4字节(31位字符集,加上恒为0的首位,共需占据32位)。理论上最多能表示231个字符,完全可以涵盖一切语言所用的符号。虽然每一个Code Point使用固定长定的字节看似方便,对于普通只需要2个字节存储的常用字占绝大对数的字符集来说,却极大的浪费了空间,并没怎么得到应用。
三、Big-Endian/Little-Endian与BOM
在讲UTF-16编码方式时说到,UTF-16编码的Code Unit是2个字节,这两个字节在传输和存储过程中,高/低位位置不同,是不同的字符。比如,“田”的UTF-16编码是0x7530,但是如果存成0x3075,就变成了“ふ”,成了另外的字符。
所以,为了识别一个编码过的字符的存储顺序,必须用特殊字符来指示。Unicode字符中U+FEFF被用来指示这种存储顺序,被称作Byte Order Mark(BOM)。
- Big-Endian:最低位地址存放高位字节,可称高位优先,内存从最低地址开始按顺序存放(高数位数字先写)。最高位字节放最前面。
- Little Endian:最低位地址存放低位字节,可称低位优先,内存从最低地址开始按顺序存放(低数位数字先写)。最低位字节放最前面。
BOM在Big-Endian系统上存储为FE FF;而在Big-Endian系统上存储则为FF FE。所以在以Big-Endian存储的UTF-16(UTF-16BE)的文件的开头,用FEFF指示;以Little-Endian存储的UTF-16(UTF-16LE)的文件的开头,用FFFE指示。
BOM的UTF-8编码为11101111 1011101110111111 (EF BB BF),所以一般EF BB BF被放在文本的开头,用来指示其编码为UTF-8。
四、Unicode编码实践
在Windows的文本编辑工具记事本上,选择“另存为”的时候,用户可以选择不同的编码选项,对应编码选项有“ANSI”,“Unicode”,“Unicode big endian”,以及“UTF-8”。因为Windows的存储方式是Little-Endian,所以“Unicode”,“Unicode big endian”对应的分别是UTF-16LE和UTF-16BE。
读者可以试着编写一串字符,然后分别用不同的编码保存,再用可以16进制编写的纯文本编辑工具(如,Ultra-edit)来检验一下具体的编码实现和存储顺序。下面是笔者将“田海立(U+7530, U+6D77, U+7ACB)”以不同编码方式保存,得到的结果:
田海立_UTF-16BE.txt
FEFF75306D777ACB
田海立_UTF-16LE.txt
FFFE3075776DCB7A
田海立_UTF-8.txt
EFBBBFE794B0E6B5B7E7AB8B
为了明确起见,BOM的编码用粗体标注;田的编码用红色标注;海的编码用绿色标注;立的编码用蓝色标注。可以看到,记事本(Notepad)存储的Unicode编码的文件的开头位置,用BOM的相应编码指示了编码格式。
【后记】历史
最近需要用到Unicode的编码实现方式,又收集了一下资料。发现早在06年的时候,笔者就准备总结一下Unicode的编码实现,文档里也已经有了提纲。现在也不记得当时什么原因给耽搁了,好在现在及时总计归纳。好脑子不如烂笔头啊。如果当初总结下来,现在也不用再浪费时间收集资料。
希望,这次的总结能比较完善,以后再用到Unicode编码,只要参考此文即可!(当然前提是Unicode标准别又演进了^_^)
【附】基本概念对照
- Code Point代码点或码位
- Code Unit代码单元或码元,是指一个已编码的文本中具有最短的比特组合的单元。对于UTF-8来说,码元是8比特长;对于UTF-16来说,码元是16比特长;对于UTF-32来说,码元是32比特长。
- BMP - Basic Multilingual Plane
- UTF - Unicode Transformation Format
- BOM – Byte Order Mark
- UCS - Universal Character Set