2009年2月28日星期六

Unicode, A programmer should know

首先说说这个标题,因为我最近非常迷星战,所以顺带也喜欢上了Yoda大师说话的腔调,以后可能会用的更多,情别见怪(反正也没人看)。

“曹操”为什么会变成“变巨”一文中,我提到了Unicode是多语言共存的解决方案,本文将详细介绍Unicode,以及为什么解决了各种令人头疼的编码问题。

那unicode解决了哪些问题呢?其实最主要的是这几个问题:

  1. 人们希望有一个单一的字符集,能包含这个星球上所有的字符
  2. 每个字符都有一个独立的编号,不会出现一个编号对应不同字符的事情
  3. 可以根据需要变换不同的编码方式,但是不会影响字符编号
那么看看unicode是怎么解决这些问题的呢?

Unicode 是一个国际标准,它主要包含了:
  • 超大的字符集,可以有1,000,000个字符,code space为[0x0-0x10FFFF]
  • 每个字符的code point编码方法
  • 一个标准的字符集
  • 一组字符属性的枚举值
  • 提供字符集的多种编码
有很多人把unicode 与utf8,utf16这些编码混为一谈,看完本文后,应该不会再存在这样的混淆了。

Unicode有其标准的书写格式。要表示一个字符的Unicode码,可以这样写:
U+0041 
这就是大写字母A的Unicode码,U+表示这是Unicode码,后面的十六进制数字是这个字符的unicode code point。比如hello的code point就是U+0048 U+0065 U+006C U+006C U+006F。可以看到,这些码值去掉前面的00,就跟ASCII码一样了,是的,unicode最大限度跟ASCII保持兼容。

Unicode的字符集可以说包括了这个星球上所有的字符。包括一些我们可能永远也不会遇到的字符。不过Unicode标准把一些最常用的字符放在0x0000-0xFFFF这个区间中,这样有些系统实现的时候就不需要用那么多空间来存放不常用的字符了。这个区间被称为BMP(Basic Multilingual Plane)。Unicode一共有17个Plane,有6个已经被占用,剩下的都是空的。如下表所示:

Plane RangeDescriptionAbbreviation
0 0000-FFFFBasic Multilingual PlaneBMP
1 10000-1FFFFSupplementary Multilingual PlaneSMP
2 20000-2FFFFSupplementary Ideographic PlaneSIP
3-13 30000-DFFFFcurrently unassigned
14 E0000-EFFFFSupplementary Special-purpose PlaneSSP
15 E0000-EFFFFSupplementary Private Use Area-A 
16 E0000-EFFFFSupplementary Private Use Area-B 

在BMP中的任何一个code point都用4位16进制数来表示。其他的plane中的code point用5-6位。

Unicode的编码问题是我们最关心的。Unicode允许使用不同的编码方式,也就是你平时看到的utf8, utf16之类的名字。他们的特点在于编码出来的值各不相同,但是解码之后,都对应到一个唯一的unicode code point。这样保证了unicode编码之间可以无损(在BMP中)转换以及映射(mapping)的唯一性。Unicode中把一个值的code point及其使用某种编码得到的码值称为一个映射(mapping)。

在unicode发展过程中,出现过多种编码。我这里就介绍几种主要的编码,在此过程中出现过两个组织,各自发展出一套映射方法,称为Unicode Transformation Format(UTF)和 Universal Character Set(UCS)。 后来两个组织合并在一起,UTF和UCS也随之合并了。简单的说它们之间的对应关系是UTF后面跟的是一个编码的位数,UCS后面跟的数字是编码的字节数(以八位作为一个字节),比如UTF-16和UCS2几乎是同一个东西。但实际上UCS2只实现了BMP内的字符编码,而UTF16则实现了所有unicode编码。但为了简单期间,我们在这里认为它们是一个东西。

UTF-16/UCS2
最直观的编码方式就是一一映射。一个unicode码对应一个UTF-16编码。比如上面所说的hello,其编码为00 48 00 65 00 6C 00 6C 00 6F。但是问题马上来了,在大端法(big endian)和小端法(little-endian)的机器上编码得到的串的顺序不一样。在大端法的机器上,同样hello经过编码得到的是48 00 65 00 6C 00 6C 00 6F 00。而48 00也是一个合法的unicode code point,因此把在大端法机器上保存的文件拿到小端法机器上读就会解析成完全不同的东西。人们于是想到用最前面的两个字节来标明字节顺序。这两个字同样是unicode码,U+FEFF,一般被称为BOM(Byte Order Mark)。FE FF表明大端法,FF FE表明小端法。

UTF-16使用2个字节来进行编码,因此只能编码到BMP的code point。对于其他的plane,UTF-16采用一种额外的编码方式,达到3,4字节的编码。但是一般情况下,UTF16指的就是2字节的编码。这样的编码非常方便,所有的字符都占用一个unsigned short,对于传统的c字符串函数就非常方便。

在windows上,为了方便utf16被直接称为unicode编码,与ANSI对应。你在使用notepade时可以保存成unicode,指的便是utf16。

下图显示了在linux上使用utf16编码



比较下在windows下使用utf16(unicode)编码,注意到BOM了吗?




UTF-8
UTF-8可以说是用得最广泛的编码方式了,很多Linux的发布版都使用UTF-8作为其系统编码。UTF-8的产生原因可能是由于有人觉得UTF-16太占用空间了。在美国英语系统下,每一个字符都有一个00,字符串大小凭空增加了一倍。于是人们研究出UTF-8这个可以兼容ASCII的变长编码方式。UTF8编码出来的字符占1-4字节。

具体占位如下图所示:


对于在U+0000到U+007F区间(是不是很熟,对了,就是ASCII码的区间)内字符,使用1个字节来编码,因此完全兼容ASCII码,而且省空间。

[U+0080, U+07FF] 区间内的字符,基本上是欧洲各国字符集,使用2个字节来编码。
图中的yyy是code point中的高位字节,由于最多到7,因此只要3个bit就可以编码。
图中的xxxxxxxx是code point中的低位字节,从00到FF,需要用的所有8个bit,在编码时,把低位的6个bit与10结合作为一个字节,剩下的2个bit与yyy,110结合作为一个字节。

[U+0800, U+FFFF] 区间内的字符,包括中文,日文,韩文等都使用3个byte来编码。详细的编码规则在图中已经解释的很清楚了,我这里不再重复。

下图显示了在linux使用utf8编码,于utf16的截图相比,utf8对曹操这两个中文各用了3个byte进行编码。



比较下在windows上使用utf8编码,首先是字节顺序不同。然后你还可以看到utf8的BOM,0xEF 0xBB 0xBF。这BOM不仅和系统,还和使用的软件有关。notepad无论什么编码都会加上BOM。



UTF32/UCS4
这是最浪费空间的一种编码方式,它也是一一映射,但是UTF32映射了所有的unicode code point。对于每一个code point,UTF32如其名字所示,使用32bit也就是4个字节来进行编码。比如A=00 00 00 41。

很多程序使用utf32作为内部编码,在保存的时候它可以无损的转换成其他编码。而且因为现在的硬件总线的关系,这种编码的寻址和读写更快。比如Apache的Xerce2 就是这么做得。



还有其他很多编码,比如国标的GB18030。只要符合unicode标准,你也可以创造自己的编码。


讲了这么多,最初的问题也算回答得差不多了。Jeffrey Richter[1], Joey Spolsky[2], Charles Petzold[3]等人都在各自的著作中强调unicode对现代编程的重要性。虽然我人微言轻,但是我还是想附和一句:unicode以及编码的知识应该是每一个专业的程序员需要了解的知识。现在,停止ANSI程序,开始用unicode编写你的正式程序吧。




注1:《Programming Applications for Microsoft Windows》, chapter 2, Jeffrey Richter
注2:《Joel on software》, Charpter 4, Joel Spolsky
注3:《Programming windows》,chapter 2, Charles Petzold

没有评论: