现在的位置: 首页 > 综合 > 正文

每一位开发人员必须、绝对要至少具备UNICODE与字符集知识(没有任何例外!)

2013年04月23日 ⁄ 综合 ⁄ 共 7568字 ⁄ 字号 评论关闭
这篇文章在讲述Unicode的时候有点罗嗦了,可以参考“Linux Locale详解”一帖,里面仅用一段文字就解释清楚了Unicode, UTF-8, GB2312这些的关系 

------------- 历史情景 --------------------------- 
在计算机出现的中期时代,人们开发了UNIX,K&R写出了《C程序设计语言》,一切都显得非常简单。EBCDIC才刚刚浮出水面。唯一很要紧的字符集就是有效而古老的无重音英语字母,而人们为这些字符开发了一套称为ASCII的编码,他能够使用32到127之间的数字表示各个字符(关于ASCII字符的更多信息,参见www.robelle.com/library/smugbook/ascii.html)。空格的编码是32,字母A的编码是65,等等。这种编码能够很方便的使用七个二进制位来表示。 

由于那个时期生产的大多数计算机使用8位大小的字节,因此用户不仅可以存放所有可能的ASCII字符,而且有整整一位空余下来。如果你技艺高超,可以将该位用作自己离奇的目的:WordStar中那个发暗的灯泡实际上设置这个高位,以指示一个单词中的最后一个字母,同时这也宣示了WordStar只能用于英语文本。码值小于32的代码称为非打印字符而作为杂项字符来使用,也就是当作小不点儿来使用。这些字符用作控制字符,比如,码值为7的字符使计算机发出“嘟嘟”声,而12对应的字符使当前走一页打印纸并换入新一页。 

对于一个说英语的人来说,这一切都是非常不错的。 

由于字节有多达8位的空间,因此许多人在想:“呀!我们可以把128-255之间的编码用作个人的应用目的”。问题在于,同时产生这种想法的人很多,而且在128-255之间的各个位置上放置什么这一问题上,真是仁者见仁,智者见智。IBM-PC有一种OEM字符集,它提供了一些欧洲语系的重音字符,以及一组画线字符-水平线条、竖直线条与右边有弯折的水平线条等等。 

事实上,只要人们开始在美国以外的地方购买计算机,那么各种各样不同OEM字符集都会进入规划设计行列,并且各人会根据自己的需要使用高位的128个字符。比如,在一些PC机器上编码为130的字符显示为é,而在以色列销售的计算机上它显示为希伯来语的第三个字母λ。因此,当美国人想把自己的履历(résumés)发给以色列时,显示出来的会是rλ sumλs。在使用诸如俄语之类的语种的多种情况下,对高端128个字符可能存在很多不同的处理思路。如此一来,甚至在同语种的文档之间就不容易实现互换。 

最后,这个人人参与的OEM终于以ANSI标准的形式形成文件。在ANSI标准中,每个人都认同如何使用低端的128个编码,这与ASCII相当一致。不过,根据所在国籍的不同,处理编码128以上的字符有许多不同的方式。这些不同的系统称为代码页(关于代码页的更多信息,参见www.i18nguy.com/unicode/odepages.html#msftdos)。这样一来,比如说在以色列DOS使用称为862的代码页,而希腊用户使用的代码页是737。这两个代码页在128以下是一样的,但在128以上则不相同,该代码段存放的是一些古怪的字母。MS-DOS的国家版本有几十个这样的代码页,负责将所有的英语信息转换成冰岛语。它们甚至还拥有几个“使用多语种”的代码页,从而可以在同一台计算机上处理世界语与加利西亚语!呜哇!不过,要说一下,在同一台计算机上处理希伯来语与希腊语是完全不可能的事情,除非自己编写一个定制程序以显示使用位映象图形的任何内容,因为希伯来语与希腊语需要能够对高端编码做出不同解释的不同代码页。 

同时,甚至更为令人头疼的事情正在逐步上演,亚洲国家的字符表有成千上万个字符,这样的字符表是用8位二进制无法表示的。该问题的解决通常有赖于称为DBCS(double byte character set,双字节字符集)的繁杂字符系统。这个字符系统将一些字符存储为一个字节,而让另外一些字符占据两个字节。虽然在字符串中向前移动很容易,但向后移动几乎不可能做得很棒。我们不鼓励程序员使用s++与s--的代码形式在字符串中前后移动,而是提倡调用Windows的AnsiNext与AnsiPrev函数,这两个函数知道如何去应付所有的繁杂局面。 

不过,仍然需要指出一点,多数人还是姑且认为一个字节就是一个字符,以及一个字符就是8个二进制位,并且只要确保不将字符串从一台计算机移植到另一台计算机,或者说一种以上的语言,那么这几乎总是可以凑合。当然,只要一进入Internet,从一台计算机向另一台计算机移植字符串就成为家常便饭了,而各种复杂状况也随之呈现出来。令人欣慰的是,Unicode随即问世了。 

------------- UNICODE --------------------------- 
Unicode勇往直前地创建一种单一字符集,试图囊括地球上所有合理文字体系,以及诸如一些Klingon之类的人为书写体制。一些人错误地认为,Unicode就是一种每个字符占用16个二进制位,从而总共可以表示65 536个可能的字符的16位字符编码方案。这并不十分正确。它是关于Unicode的惟一最为流行的神话,因此,要是你也这样想,大可不必感到难过。 

事实上,Unicode在考虑字符方面给出了一种不同的思路。你需要去理解它的这种思考方式,否则一切都显得毫无意义。 
直到现在,我们一直认定字母映射为磁盘或者内存的某些位: 

A -> 0100 0001 

在Unicode中,一个字母映射为一种称做代码点(code point)的对象,它仍然只是一个理论上的概念。该代码点如何在磁盘或者内存中进行表示完全是一种微妙的个体行为。 

在Unicode中,字母A是一个柏拉图式的理想,它只能飘荡在天国之中: A 
这个柏拉图式的A不同于B,也不同于a,但与 字母A 或者 斜体A 或者 不同字体的A 却是一样的。认为Times New Roman字体的A与Helvetica字体中的A是相同的字符,但不同于小写字母a的想法,看起来并不会引起很大的争议,但在某些语言中仅仅指出字母是什么完全可能引起争议。德语的字母ß是真正的字母,抑或仅仅是ss的一种花样书写形式?如果一个字母的形状在单词的末尾变化了,那么它是一个不同的字母吗?希伯来语系认为是,而阿拉伯语系则说不。不管怎样,聪明的Unicode人在最近大约十年里一直在设法处理这个方面的问题,与此相伴的是大量高度政治化的争论。不过,你用不着担心,他们已经把它全部处理妥当了。 

Unicode组织为各种字母表中每个理想化的字母分配一个神奇的编号,其书写形式为U+0645。这个神奇的编号称为一个代码点。U+的含义是“Unicode”,这些数字是十六进制的。U+0639表示阿拉伯字母Ain。英语字母A是U+0041。使用Windows 2000/XP的charmap工具程序或者访问Unicode网站,可以找到所有的字母及其编码(参见www.unicode.org)。 

Unicode能够定义的字母个数其实没有限制,实际用到的总字符个数已经远远超出了65536的范围。可见,并不是每个Unicode字母能够真正挤压成两个字节,不过,这终归还是一个神话。 

好了,我们来看一个字符串:Hello 
该字符串在Unicode中对应如下形式的5个代码点: 
U+0048 U+0065 U+006C U+006C U+006F 
这不过是一簇代码点。其实,就是一些编号。到此为止,我们还没有谈到如何在内存中存放这类字符串或者怎样在电子邮件中表示它们。 

------------- 编码 --------------------------- 
是给出编码知识的时候了。 

关于Unicode编码,最为容易同时也因此导致2字节神话出现的想法是:“嗨,让我们将这些编号各自用两个字节存放吧!”于是,“Hello”变成了 
00 48 00 65 00 6C 00 6C 00 6F 
对吧?不要这么性急!可以存为 
48 00 65 00 6C 00 6C 00 6F 00 (注意观察,这里将每个双字节都逆序摆放了) 
吗? 

喏,从技术上讲是可以的,我就相信这一点。事实上,早期的实施人员就想以高端或者低端两种存放模式在内存中表示Unicode码。不管特定的计算机在何种模式下运行最快,喔,也不管是傍晚还是清晨,现在已经有了两种存放Unicode码的方式。于是,人们被迫提出在每个Unicode字符串的开头存放一个FE FF标记的怪异协定。这个标记称为Unicode字节顺序标记(Unicode Byte Order Mark)。要是交换了它的高低位字节,该标记就成为FF FE。这样一来,阅读字符串的人就知道必须将字符串的每两个字节进行一次交换(关于字节顺序标记的更多信息,参见msdn.microsoft.com/library/default.asp?url=/library/en-us/intl/unicode_42jv.asp)。吁!苍茫世界当中不是每个Unicode字符串都会在开头放一个字节标记。 

曾几何时,这样的做法好像是足够好了。但是,程序员却一直没有停止抱怨:“瞧瞧所有那些零!”要知道,作为美国人,他们阅读的英语文本很少用到U+00FF以上的代码点。还有,他们是来自加利福尼亚州的无拘无束的嬉皮士,保守与冷嘲热讽是他们的天性。如果他们是得克萨斯人,就不会在乎要狂吃双份的字节。不过,那些咕咕叽叽的加利福尼亚人不会忍受将字符串占据的存储空间加倍的想法。而且,不管怎么说,已经存在的一些该死的文档都赫然使用着各种各样的ANSI与DBCS字符集,谁会去将它们全部转换过来呢?仅仅是这个原因,大多数人决心在几年之内对Unicode视而不见。不过,与此同时,情况却变得尤为糟糕。 

于是乎,光彩照人的UTF-8(参见www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt)概念就横空出世了。UTF-8是另外一种存放字符串的Unicode代码点的体系,它在内存中使用8位字节存放那些神奇的U+编号(关于UTF-8的更多信息,参见www.utf-8.com)。在UTF-8中,从0~127的每个代码点存放在单个字节里面,只是128以上的代码点才使用2个字节、3个字节,乃至多达6个字节的空间来进行存储。 

这具有使英语文本看起来在UTF-8与ASCII中显得完全一样的边界净化效应。如此一来,美国人用不着在意有什么地方不对了。只是世界上其他地方的人得从铁箍中跳过去。具体地说,编码为U+0048 U+0065 U+006C U+006C U+006F的“Hello”字符串将存储为48 65 6C 6C 6F。瞧!这与地球上存储为ASCII、ANSI与各种OEM字符集的形式完全相同。 

现在,如果你非得固执地使用重音字母或者希腊字母或者Klingon字母,那么就必须使用几个字节来存放单个代码点,但是美国人永远不会意识到有什么异样。(UTF-8同样具有后面这种不错的属性:现有的那种想用单个0字节作为空终止字符的陈旧而愚昧的字符串处理程序,不会截掉或破坏字符串。) 

到此为止,我已经说出了Unicode的三种编码方式。将编码存储为2个字节的传统方法称为UCS-2(因为它有2个字节)或者UTF-16(因为它有16位),不过仍然需要弄清它是高端存放模式的UCS-2,还是低端存放模式的UCS-2。还有一种流行的新UTF-8标准,它同样可以很自然地使用,要是你碰巧处理令人满意的英语文本,并拥有完全不知道世间除了ASCII之外还有其他何物的死脑筋程序的话(简言之,使用UTF-8编码可以兼容原来那些只使用ASCII编码的程序,参见www.zvon.org/tmRFC/RFC2279/Output/chapter2.html)。 

其实,还有一些其他的Unicode编码方式。一种称之为UTF-7的编码体系与UTF-8有许多相似之处,只不过它保证高位总是为0。这样一来,要是你必须通过某类极权国家的严密电子邮件系统传递Unicode,它可以在高压之下仍然毫发无损。这种电子邮件系统认为用7位二进制表示一个字符完全够用了,谢谢。另外一种称为UCS-4的编码方式用4个字节存放一个代码点,它虽然具有这样一种良好性能:将各个代码点都存储为个数相同的字节,不过,天哪!即使得克萨斯人也不会鲁莽到浪费那么多的内存。 

事实上,既然以Unicode代码点形式的柏拉图理想字母来思考问题,Unicode代码点也就能够按任何守旧的编码方案进行编码了!比如,可以用ASCII编码方案、老式OEM希腊编码方案、希伯来ANSI编码方案或者几百种现有编码方案之一来编码Unicode字符串“Hello”(U+0048 U+0065 U+006C U+006C U+006F),只是要抓住一点:一些字母可能是不出现的!如果在试图使用的编码方案中没有相应Unicode代码点的等价内容,那么通常会显示一个小问号“?”,或者更为先进一些的话,就显示一个方框。 

传统编码方式不下几百种,不过它们仅仅能够正确地存放一些代码点,将所有其他的代码点转变为问号。流行的英语文本编码方案有Windows-1252(西欧语种的Windows 9x标准)与ISO-8859-1,即Latin-1(同样对任何西欧语种都有用)。不过,如果试图用这类编码方式存放俄语或者希伯来语字母,那么得到的结果就会是一簇问号。UTF 7,8,16与32都具有能够正确存放任何代码点的优良特性。 

------------- 编码中唯一最重要的事实 --------------------------- 
即使你将我刚才所说的一切忘得一干二净,我也要请你记住一个极其重要的事实。有一个字符串,而不知道它所使用的编码方案是毫无意义的。你再也不用将脑袋紧贴在沙地上而假想“纯”文本就是ASCII。 

如果在内存、文件或者电子邮件中有一个字符串,那么应该知道它使用的是什么编码方案,否则就不能将它正确地解释或者显示给用户。 

其实,几乎所有诸如“Web网页好像是乱码”或者“在使用重音字母时无法阅读电子邮件”之类的愚蠢问题,都可以归结到天真的程序员身上,他不了解这样一个简单的事实,即如果不告诉我某个特定字符串是用UTF-8,或者ASCII,或者ISO 8859-1(Latin 1),或者Windows 1252(西欧)编码的,那么我就不能正确地显示,乃至弄清楚它在什么地方结束。编码方案数以百计,而在代码点127以上,拍脑袋已经没有效果了。 

如何保存字符串用到的编码方案信息?喏,做这件事情有许多标准的方式。对于电子邮件信息,需要在表窗体标题放一个字符串 
Content-Type: text/plain; charset="UTF-8" 

对于Web页,最初的想法是Web服务器通过网页本身返回一个类似于Content-Type的http——不是在HTML当中而是作为一个响应标题在HTML页面之前返回。这会引起一些问题。假设一个大型Web服务器拥有许多站点,成千上万的网页是由许多人使用多种不同语言发布的,网页使用任何在Microsoft FrontPage看来很适合产生的编码方案。由于Web服务器实际上不知道每个文件究竟是用什么编码方案写成的,因此它将无法发送Content-Type标题。 

如果能够使用某种特殊标记将HTML文件的Content-Type内容自然地放在HTML文件当中,会是很方便的。当然,这会让纯化论者发疯。不过,在知道HTML文件使用了什么编码方案之前,如何读取HTML文件呢?!幸运的是,几乎每个用得很普遍的编码方案,对码值32与127之间的字符都以相同方式进行处理。因此,人们总是可以充分展示HTML页而不必使用一些很稀奇的字母: 

<html> 
<head> 
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 

不过,这里的meta标记确实位于<head>段中很靠前的位置。因为只要Web浏览器看到该标记,它就会停止解析页面,并在使用指定的编码方案重新解释整个页面之后进入下一轮。 

如果Web浏览器在http报头与meta标记中都找不到任何Content-Type,该怎么办呢?Internet Explorer确实做了件非常有趣的事情:基于不同语言以典型编码方案得到的典型文本中各种字节出现的频率,试图猜出使用了什么语言与编码方案。由于各类不同的旧8字节代码页倾向于将其母语字母放在128与255之间的不同范围中,并且人类使用的每个语种具有不同特征的字母使用概率分布,因此使这种做法确实有了发挥作用的机会。是的,它确实显得有点怪异。不过,这种做法通常能够满足那些从不知道要使用Content-Type报头的幼稚网页编写人员,在Web浏览器中阅读网页的需要,并且看起来还很不错。 

直到有一天,编写的网页确实与母语的字母使用概率分布情形不一致,Internet Explorer则认定它是朝鲜语而加以显示。在我看来,这表明了一点,Jon Postel的格言“宽进窄出”实在不是一条好的工程原则(Jon Postel的话,引自信息科学学院的1981年9月发布的文档RFC791-Internet Protocol)。不管怎么说,一旦用保加利亚语写的网站却以朝鲜语的形式出现(甚至是毫不相干的朝鲜文),可怜的读者该怎么办呢?使用菜单“View | Encoding(视图|编码)”试图应用一组不同的编码方案(东欧语种可用的编码方案至少有一打),直到图片能够较为清楚地显示出来为止。即使这位仁兄知道如此行事,可大多数人却不知道这一招。 

对于由本人的公司Fog Creek Software发布的Web站点管理软件最新版本CityDesk[2] 而言,我们决定在内部用UCS-2(2字节)Unicode处理一切。这种编码形式也是VisualBasic、COM与Windows NT/2000/XP作为其固有字符串类型使用的。在C++代码中,字符串就定义为wchar_t而不是char,并且使用以wcs打头的函数而不是以str打头的函数(比如说,使用wcscat与wcslen,而不是strcat与strlen)。要用C代码生成UCS-2文本字符串,就得像L"Hello"一样在字符串前面放一个L。(本人认为,这种说法在windows下可能是对的,但是在Linux或其他OS中是否有这样的说法或是有这样的API需要商榷一下)

抱歉!评论已关闭.