编码
要存储,编码的概念当然就被引入进来。
Unicode最早的编码想法,就是把每一个码点(code point)都存储在两个字节中,这也就导致了大多数人的误解。于是Hello就变成了:
这样对吗?如下如何?
技术上说,我相信这样是可以的。事实上,早期的实现者们的确想把Unicode的码点(code point)按照大端或小端两种方式存储,这样至少已经有两种存储Unicode的方法了。于是人们就必须使用FE FF作为每一个Unicode字符串的开头,我们称这个为Unicode Byte Order Mark。如果你互换了你的高位与低位,就变成了FF FE,这样读取这个字符串的程序就知道后面字节也需要互换了。可惜,不是每一个Unicode字符串都有字节序标记。
现在,看起来好像问题已经解决了,可是这帮程序员仍在抱怨。"看看这些零!"他们会这样说,因为他们是美国人,他们只看不会码点不会超过U+00FF的英文字母。同时他们也是California的嬉皮士,他们想节省一点。如果他们是得克萨斯人,可能他们就不会介意两倍的字节数。但是这样California节俭的人却无法忍受字符串所占空间翻倍。而且现在大堆的文档使用的是ANSI和DBCS字符集,谁去转换它们?于是这帮人选择忽略Unicode,继续自己的路,这显然让事情变得更糟。
于是非常聪明的UTF-8的概念被引入了。UTF-8是另一个系统,用来存储字符串所对应的Unicode的码点 (code points)-即那些神奇的U+数字组合,在内存中,而且存储的最小单元是8比特的字节。在UTF-8中,0-127之间的码字都使用一个字节来存储,超过128的码字使用2,3甚至6个字节来存储。
这显然有非常好的效果,因为英文的文本使用UTF-8存储的形式完全与ASCII一样了,所以美国人压根不会注意到发生了什么变化。举个例子,Hello -- U+0048 U+0065 U+006C U+006C U+006C U+006F,将会被存储为48 65 6C 6C 6F,这与ASCII、与ANSI标准、与所有这个星球上的OEM字符集显然都是一样的。现在,如果你需要使用希腊字母,你可以用几个字节来存储一个码字,美国人永远都不会注意到。(干吗得美国人注意,无语,美国人写的文章...)
到现在我已经告诉了你三种Unicode的编码方式,传统的使用两个字节存储的称之为UCS-2或者UTF-16,而且你必须判断空间是大端的UCS-2还是小端的UCS-2。新的UTF-8标准显然更流行,如果你恰巧有专门面向英文的程序,显然这些程序不需要知道UTF-8的存在依然可以工作地很好。
事实上,还有其它若干对Unicode编码的方法。比如有个叫UTF-7,和UTF-8差不多,但是保证字节的最高位总是0,这样如果你的字符不得不经过一些严格的邮件系统时(这些系统认为7个比特完全够用了),就不会有信息丢失。还有一个UCS-4,使用4个字节来存储每个码点(code point),好处是每个码点都使用相同的字节数来存储,可惜这次就算是得克萨斯人也不愿意了,因为这个方法实在太浪费了。
现在的情况变成了你思考事情时所使用的基本单元--柏拉图式的字母已经被Unicode的码点完全表示了。这些码点也可以完全使用其它旧的编码体系。比如,你可以把 Hello对应的Unicode码点串(U+0048 U+0065 U+006C U+006C U+006F)用ASCII、OEM Greek、Hebrew ANSI或其它上百个编码体系来编码,不过需要注意一点,有些字母会无法显示。如果你要表示的Unicode码点在你使用的编码体系中压根没有对应的字符,那么你可能会得到一个小问号"?",或者得到一个"�"。
许多传统的编码体系仅仅能编码Unicode码点中的一部分,其余全部会被显示为问号。比较流行的英文编码体系有Windows-1252(Windows 9x中的西欧语言标准)和ISO-8859-1,还有aka Latin-1。但是如果想用这些编码体系来编码俄语或者希伯来语就只能得到一串问号了。UTF 7,8,16,和32都可以完全正确编码Unicode中的所有码点。
关于编码的唯一事实
如果你完全忘掉了我刚刚解释过的内容,没有关系,请记住一点,如果你不知道一个字符串所使用的编码,这个字符串在你手中也就毫无意义。你不能再把脑袋埋进沙中以为"纯文本"就是ASCII。事实上,
那么我们如何得知一个字符串所使用的空间是何种编码呢?对于这个问题已经有了标准的作法。如果是一份电子邮件,你必须在格式的头部有如下语句:
Content-Type: text/plain; charset="UTF-8"
对于一个网页,传统的想法是Web服务器会返回一个类似于Content-Type的http头和Web网页,注意,这里的字符编码并不是在HTML中指出,而是在独立的响应headers中指出。
这带来了一些问题。假设你拥有一个大的Web服务器,拥有非常多的站点,每个站点都包括数以百计的Web页面,而写这些页面的人可能使用不同的语言,他们在他们自己计算机上的FrontPage等工具中看到页面正常显示就提交了上来,显然,服务器是没有办法知道这些文件究竟使用的是何种编码,当然 Content-Type头也没有办法发送了。
如果可以把Content-Type夹在HTML文件中,那不是会变得非常方便?这个想法会让纯粹论者发疯,你如何在不知道它的编码的情况下读一个HTML文件呢?答案很简单,因为几乎所有的编码在32-127的码字都做相同的事情,所以不需要使用特殊字符,你可以从HTML文件中获得你想要的Content-Type。
<html>
<head>
<meta http-equiv="Conent-Type" content="text/html" charset="utf-8">
注意,这里的meta标签必须在head部分第一个出现,一旦浏览器看到这个标签就会马上停止解析页面,然后使用这个标签中给出的编码从头开始重新解析整个页面。
如果浏览器在http头或者meta标签中都找不到相关的Content-Type信息,那应该怎么办?Internet Explorer做了一些事情:它试图猜测出正确的编码,基于不同语言编码中典型文本中出现的那些字节的颇率。因为古老的8比特的码页(code pages)倾向于把它们的国家编码放置在128-255码字的范围内,而不同的人类语言字母系统中的字母使用颇率对应的直方图会有不同,所以这个方法可以奏效。虽然很怪异,但对于那些老忘记写Content-Type的幼稚网页编写者而言,这个方法大多数情况下可以让他们的页面显然OK。直到有一天,他们写的页面不再满足"letter-frequency-distribution",Internet Explore觉得这应该是朝鲜语,于是就当朝鲜语来显示了,结果显然糟透了。这个页面的读者们立刻就遭殃了,一个保加利亚语写的页面却用朝鲜语来显示,效果会怎样?于是读者使用 查看-->编码 菜单来不停地试啊试,直到他终于试出了正确的编码,但前提是他知道可以这样做,事实上大多数人根本不会这样做。
在我的公司开发的一款Web页面管理软件CityDesk的最新版本中,我们决定像Visual Basic、COM和Windows NT/2000/XP所做的那样,整个过程中使用UCS-2(两个字节)Unicode。在我们写的C++代码中,我们把所有的char类型换成了wchar_t,所有使用str函数的地方,换成了相应的wcs函数(如使用wcscat和wcslen来替代strcat和strlen)。如果想在C中创建一个UCS-2的字符串,只需在字符串前面加L即可:L"Hello"。
当CityDesk发布页面的时候,它把所有的页面都转换成了UTF-8编码,而差不多所有的浏览器都对UTF-8有不错的支持。这就是"Joel On Software"(就是作者的首页)编码的方式,所以即使它拥有29个语言版本,至今也未听到有一个人抱怨页面无法浏览。
这篇文章已经有点长了,而且我也没有办法告诉你关于字符编码和Unicode的所有应该了解的知识,但读到现在我想你已经掌握到基本的概念,回去编程时可以使用抗生素而不是蚂蝗和咒语了,这就看做是留给你的作业吧。