OneCoder翻译 每个程序员必知的知识,UniCode和字符集(上)

今天在处理了一个编码的问题,激发了笔者强烈的弄清编码问题的好奇心。遂先有了前面强烈推荐的文章:字符编码介绍 通俗易懂 强烈推荐。下面是上篇文章中提到的延伸阅读里,第一篇文章的翻译。水平有限,各位看官,勉强理解一下,错误之处还望指出。

你是否曾迷惑于Content-Type标签?你知道你需要写在HTML文件中,但是却不知道他是做什么的。

你是否层收到来自保加利亚的朋友的邮件,主题却是:”???? ?????? ??? ????”。

我曾经吃惊的发现有许多程序员没有真正理解字符集,编码,Unicode等相关问题的含义。若干年前,FogBUGZ的一个测试人员对他们是否可以处理日文的邮件感到好奇。日语?他们有日文邮件?这个我不清楚。当我近距离了解到我们层用来解析MIME邮件的商业版的ActiveX系统时,我发现他对字符集的处理完全是错误的。所以,我们不得不去写”夸张”的代码去恢复他做的错误的转换,然后重做正确的转换。当我发现,另一个商业版的包在字符的实现上也犯了同样错误。我理解这个包的开发者的想法,他们”不能做任何事”。就像许多开发者一样,他仅希望这个问题会莫名其表的消失。

但是事实不会那样。当我发现,著名的web开发工具PHP,也几乎完全忽略了字符编码的问题,轻率的用8位表示字符,从而使得开发出 一个好的国际化的web应用几乎不可能的时候,我认为够了,这够了。

所以在这里,我声明:如果你是一个工作在2003年的程序员,(OneCoder注:这篇文章写于2003年。)并且你不知道基本的字符,字符集,编码和Unicode的知识,并且被我抓到,我将要惩罚你在潜艇里拨洋葱皮6个月,我发誓,我会做到。并且,更重要的是:他并不难。

在这篇文章里,我将要告诉你每个开发人员必须知道的事情。告诉你,普通文本(plain text)=ascii=字符(characters) 都是8位的,不仅是错误的,而是无可救药的错误的。并且,如果你仍在以这样的理解编程,那较之于一个医生不相信细菌有过之而无不及。在读完这篇文章之前,不要在动手写任何代码了。

在我开始之前,我需要提醒你,如果你是少数的了解国际化的人,你可能会觉得我整篇的讨论都很浅显。我确实尽力的试图将门槛降到最低,以使每个人可以理解这是什么并且能满怀希望写出处理各种语言的代码,而不仅仅是不包含音调的英文。同时,我还要提醒你,字符的处理仅仅是开发出国际化产品的一小部分工作,然而我一次只能说明一个问题,所以今天,我会讲解字符集。

历史回顾

最简单的理解这些事情的方式自然是从头去回顾。

你可能会认为,我将会讲EBCDICOneCoder注:Extended Binary Coded Decimal Interchange Code) 这样非常古老的字符集。呵呵,我不会的。EBCDIC 跟你的生活没太大关系,我们不会去讲那么古老的东西。

追溯到Unix诞生,K&R发明了C语言的时代,任何东西都很简单。EBCDIC 就是在那个环境下产生的。

当时包含的字符仅仅是,古老的无重音的英文字符,我们给这些字符以一些代码,称为ASCIIASCII可以通过32-127之间的数字,来代表每一个字符。例如:空格(space)是32,A是65等等。这可以正好用7位进行存储。在那个时代,大部分的计算机都是采用8位的,所以,你不仅仅可以保存每一个可能的ASCII字符,你还可以有完整的一位去做任何你想做的事情:比如,WordStar(OneCoder注:可能是文字处理软件),用最高位来标识最后一个单词的最后一个字母,这也迫使WordStar仅可以处理英文文章。32以下的代码被成为不可打印的字符,是用来诅咒的。呵呵,开玩笑了。他们其实是操作字符,比如,代码7可以是你的计算机发出嘟嘟的声音;12可以是换行等。

如果你是说英语的,那么一切都很好。

因为一字节有8位,很多人就会想,”我们可以用128-255来代表我们想要的东西”。不过我问题是,很多人都同时有同样的想法,他们又都有自己对于128-255这些字符的规划。IBM-PC创造了OEM字符集,可以表示一些带有音调的欧洲语言和一些可以用来化字符的线,水平线,垂直线,带有拐角的水平线等。你可以用这写线条字符,绘制出一些整洁的表格和线条,你仍然可以在干洗店里那些8088系列的计算机里看到这些字符。事实上,当美国以外的人们开始购买PC机开始,各种各样的OEM字符集开始被设计出来,并且都是用高位的128个字符为自己所用。例如,在某些PC上,字符码130代表é, 在以色列的一些计算机上,130代表希伯来字母ג。所以,当美国人发送résumés给以色列人的时候,以色列人可能会收到rגsumגs。在很多情况下,比如俄罗斯,同样有很多各种各种的设计针对高位的128个字符,所以你可能无法准确的翻译俄罗斯文档。

终于,这种人人自由的OEM方式,随着ANSI(OneCoder注:American Engineering Standards Committee)标注的出台被终结。根据ANSI标准,大家都遵循低位的128字符的定义,跟ASCII的定义一样,但是对于高位的128字符的处理,根据大家所处环境的不同,有各种各样方式。这些不同的系统被称作代码页。(OneCoder注:原文为code pages,可能需要翻墙访问)。因此,比如在以色列DOS使用的代码页为862,希腊为737。他们都有相同的低128位的,但是高128位不同,保存着各自有趣的字符。国际化版本的MS-DOS,有需要这样的代码页,处理了从英语到冰岛语等语言,甚至有些多语言的代码页,可以处理世界语和加利西亚语在同一台计算机上。Wow。然而,想在同一个计算机里处理希伯来语和希腊语是完全不可能的,除非你编写自定义的程序,通过位图来展示一切。因为,希伯来语和希腊语需要不同的代码页对高的128位有不同的解释。

同时,在亚洲,更夸张的是,亚洲的字母表有成千上万的字符,完全不可能用8位字符去表示。不过,这个问题有一种不寻常的混乱的解决方式,DBCS(double byte character set),双字节字符集,在这个字符集中,有些字母用一个字节存储,有些用两个。这使得对一个字符串进行前移操作比较简单,然而却几乎无法进行该死的后移操作。(OneCoder注:这里的意思笔者也不是很明白。原文:It was easy to move forward in a string, but dang near impossible to move backwards.)。开发者被告之不要使用s++和s–进行后移和前移,而是调用Windows' AnsiNextAnsiPrev函数,这些函数知道该如何处理这些混乱的东西。

然而,很多人仍然假定一个字节是一个字符并且一个字符是8位,只要你不将字符串从一个计算机移到另一个或者只说一种语言,这些永远是对的。然而,从Internet诞生那刻起,将字符串从一个计算机传递到另一个计算机是非常司空见惯的事情,因此这些混乱的事情一下都发生了。幸运的是,Unicode诞生了。

Unicode

Unicode是一个勇敢的尝试,用一个单一的字符集去包含这个星球所有合理编写的系统甚至像克林贡语这样假定出来的系统。一些人错误的认为Unicode是简单的16位编码,任何字符都需要用16位来表示,因此一共有65536个字符。这事实上是错误的。这是对Unicode最常见的误解,所以如果你也是那样认为的,不必难过。

事实上,Unicode有一种不一样的方式去考虑字符的问题,你必须去理解Unicode的思考方式,否则你无法理解任何事。

到目前为止,我们已经假定,一个字母映射到一些保存在磁盘或者内存中的字节:A -> 0100 0001

Unicode中,字母A是非实际的。它在天堂中游离:A

这个虚幻的A不同于B,也不同于a,但是与A和A相同。这种不同字体的A是同样的字符,但是与小写的a不同的想法,看起来并不难以理解,但是在一些语言中恰恰指出了一个字母可能引起争议的所在。德语中的ß是真实的字母还是仅仅是ss的有趣的书写方式?如果一个词尾的字母的外形发生改变,他是一个不同的字母吗?希伯来语说是,阿拉伯语说不是。不管如何,Unicode联盟中聪明的人们在过去十年中,在激烈的争论中,想出了解决方案,你不必再去担心那些事。他们已经完全解决了这个问题。

在任何字母表中的任何想象中的字符都被Unicode联盟赋予一个魔数,比如:U+0639。这个魔数被成为字符码(Onecoder 注:code point)。U+代表Unicode,数字是十六进制的。U+0639 **是阿拉伯字母Ain。英语中的AU+0041。你可以在Windows2000/xp的字符映射工具或者访问unicode**官网找到这些映射。

事实上,对于Unicode能表示的字符数没有严格的限制,他们已经超过了65536,所以不是每个Unicode字母都可以压缩到用两个字节表示,这很虚幻。

OK,现在我们有一个字符串:Hello,在Unicode中对应5个字符码:U+0048 U+0065 U+006C U+006C U+006F。是一组字符码。我们还没有提到任何关于这些字符码在内存中如何保存以及在email中如何展现的事。

编码

这就是编码的来历。最初的关于Unicode的编码想法是,我们就干脆用两个字节保存这些数字,这个想法导致了两字节的神话。因此,Hello变成了:00 48 00 65 00 6C 00 6C 00 6F

正确了?别这么早下结论。难道它不可能是:48 00 65 00 6C 00 6C 00 6F 00 吗?

好吧,从技术上讲,可以,我相信他可以。事实上,早期的实现者想用high-endianlow-endian两种模式保存Unicode字符码,不论哪种方式都是他们特定的CPU最快的处理方式。呵呵,夜以继日,现在就有了两种保存Unicode的方式。

未完待续……

Thanks a lot.