你曾经有没有对Content-Type的标签感到好奇过?这个经常被要求输入到HTML中,但是却从来不清楚它是什么的东西。 你有没有曾经收到过从保加利亚的朋友发来的邮件,标题是“???? ?????? ??? ????”? 当我发现非常多的软件工程师并没有花费很多心思去搞清楚字符集,字符编码,unicode这些东西的时候,我感觉到非常的失望。几年前,一个FogBUGZ的beta系数测定仪想知道它是否能处理从日本来的邮件。日本?它们居然有从日本来的邮件?我不知道该咋办。当时被用于解析MIME邮件的是商业控件ActiveX, 当我仔细研究了它的解析原理后,发现它实际上正在对字符集做完全错误的事情。所以,我们不得不写一些英雄的代码来撤销他们所做的错误转换,并且正确的重做了。当我们查看了另外一个商业库时,发现它也一样对字符编码做了完全错误的实现。我联系了哪个软件包的开发者,他稍稍有点认为他们“不能对这个错误的实现做任何事情”。就像很多程序员一样,他只希望这件事情能过去。 但是,不会的。我发现很流行的web开发工具PHP也几乎忽略了字符集的问题,它快活的为字符用了8bits,使得我们在用PHP开发web的时候几乎不能对国际化做很好的应用。我想,该结束了。 我宣布要做一件事情:如果你是一个工作在2003年的程序员,并且你还不知道基础的字符,字符集,编码和Unicode;并且被我逮到的话,我将会惩罚你在潜水艇里面剥洋葱六个月。我发誓我会的! 另外一件事情: 它并没有那么难 在这篇文章中,我将详细的告诉你,实际上这应该是每个工作中的程序员应该知道的事情。这所有的一切都是关于“plain text = ascii = characters 是8bits”不仅是错误的,而且它完完全全是错误的。如果你正字按照这种方式写代码,那你比一个不相信细菌的医生也好不到哪里去。请你再认真读完这篇文章之前不要再写任何一行代码啦! 在我开始之前,我需要提醒你,如果你是少有的知道国际化的人之一,那么你会发现我的整个讨论过于简单化了。我确实是只想在这设立一个最小的标准,以至于每个人都能懂发生了什么,并且能写正确的代码,也希望这些代码能正确的显示任何语言的文字,而不是只显示英文字符,不能处理其他类型的文字。而且我也需要提醒你,字符处理也只是创建国际化软件中的一小部分工作。但实际上,我在一个时间也只能写某一个事情,所以,今天就是讲字符集。
历史原因 理解这些东西最好的方式就是按时间排序。 你可能认为我要在这谈论最古老的字符集,如:EBCDIC。但是,我不会的,EBCDIC跟你的生活毫无关联。我们不需要追溯到那么久远。 回溯到半古时代,当Unix被发明,以及K&R正在写《The C Programming Language》的时候,一切都是简单的。EBCDIC就要出局了。唯一重要的字符就是古老的无重音的英语字符。我们称之他们为ASCII。ASCII,即能用32-127表示它的每一个字符。空格用32表示,大写字母"A"用65表示,等等。这种方式能够很方便的存储再7个bit上。当前很多计算机都是使用一个bytes有8个bit的方式,所以你不仅可以存储每一个ASCII字符,而且还可以有一个bit的空余空间。如果你很调皮的话,你可以用这一个bit用于你自己其他的目的。比如:WordStar的昏暗的灯泡,实际上就是打开高位来暗示单词的最后一个字母,导致WordStar只能支持英文。小于32的编码是不可打印的,用于骂人,哈哈,开个玩笑。它们被用作控制字符,比如7,用于计算机报警声;12,用于控制当前页从打印机里退出来,并且进入一张新的打印纸。 如果,你是讲英语的人,那么多有的一切都很完美。 因为,一个byte可以表示8个bit,很多人都会想:“天哪,我们可以用128-255表示我们自己的目的啊。”问题就在于,很多人同时也是这么想的。这些人就会用128-255表示他们自己的想法。IBM的PC机就有类似于这样的后来被称之为OEM的字符集。这些字符集提供了一些欧洲语言中古老的字符集和一堆线性描述符,比如:双横线,竖线,单杠上的小吊铃悬挂在右侧,等等。你可以用这些线条再屏幕上画一些漂亮的模型,你也可以再你干洗店的8088电脑上运行并显示这些图形。事实上,当人们在美国以外的地方购买PC机的时候,各种不同的OEM字符集就被梦想出来啦。这些OEM字符集都是利用的高128位字节的字符。比如:一些PC机上用130表示字符é,但是在以色列售卖的PC机上130却表示希伯来语的字符(a),所以当美国人发送他们的résumés到以色列时,他们会收到的是rasumas。在很多情况下,比如俄罗斯,有大量的针对高128bit的不出处理情况,因此,你无法可靠的交换俄罗斯文件。 最终,这种OEM的自由选择被编入了ANSI标准。在ANSI标准中,每个人都必须遵循低128的表示方式,这个几乎跟ASCII一样,但是对于高128位的标准却有好几种不同的标准,这个标准取决于你的居住地点。这些不同的系统被称之为代码页。比如,在以色列DOS用的代码页叫862,希腊用的代码页叫737.这两个不同的代码页低128位都是一样的处理方式,但是存储着所有有趣字母的高128位处理方式却不相同。国际版本的MS-DOS有许多种这种代码页,他们处理着英语转换到冰岛语的所有事情,甚至还有少量的“多语言”代码页,他们能在同一台电脑上处理世界语转换到西班牙语。但是,实际上在同一台电脑上使用希伯来语和希腊月是完全不可能的,除非你编写了你自定义的程序来用位图显示所有的事情。因为希腊人和希伯来人需要用不同的代码页来解释高128位。 与此同时,需要考虑的是在亚洲,甚至正在做更加疯狂的事情。事实上,叶舟的字母表有上千种文字,这些文字也不可能用8bit来表示。这些经常是由DBCS系统用“双字节字符集”解决的,DBDC系统的“双字节字符”存储一个文字时,有时候用一个byte,有时候又用两个byte;这种处理方式非常的凌乱。在字符串中,往前移动很简单,但是想要往后移动几乎不可能。程序员们不被鼓励去用s++和s--往前和往后移动,但是鼓励他们去调用windows的AnsiNect和AnsiPrev去处理这些凌乱的方式。 但是,大多数人只是假装知道一个字符是一个字节,一个字符占8个bit;只要你从来没有把一个字符串从一个电脑移动到另外一台电脑,或者只讲一种语言;程序总是能正确的工作。但是,随着互联网的出现,把一个字符串从一台电脑上移动到另外一台电脑上是非常常见的事件,这时候混乱的事情就发生了。幸运的是,Unicode被发明出来了。 Unicode Unicode是一个勇敢的尝试,它创建了一个单独的字符集,它包含了地球上的每一个合理的文字系统,包括虚构的语言,如:克林顿贡语。有些人错误的认为Unicode就是16bit的代码,每个字符占用16位,因此就可以表示65536个字符。实际上,这是不对的。这是对Unicode最常见的错误观念,所以如果你也这么认为,不要难过。 事实上,Unicode对字符有不同的思考方式,因此,你需要用Unicode的方式去思考,否则一切都说不通。
到目前为止,你可以假设一个字母可以映射到某些bit,它们可以存储在你的硬盘或者内存中:
A->01000001
在Unicode中,一个字母映射到的地方称为指针,指针只是一个理论上的概念。这个指针在内存和硬盘上是怎么表示的又是另外一回事了。
在Unicode中,字母A是一个柏拉图的想法。它仅仅只是飘荡在天堂:
A
这个柏拉图式的A不同于B,也不同于a,但是是A和A,以及A是相同的。这种观点就是Times New Roman字体的A和Helvetica字体的A是同一种字符,但是和小写字母“a”不同,看上去没有什么争议。但是在某些语言中,仅仅弄清楚一个字母是什么都会引起争议。德语字母ß到底是一个真正的字母还是说仅仅是ss的一种奇怪的写法呢?如果一个字母的形状在单词的末尾改变了,它是不是就代表了另外一个字母呢?希伯来人说,是的;阿拉伯人说,不是的。
无论如何,Unicode联盟的聪明人在过去的十年左右里一直都在研究这个问题,伴随着大量的高度政治化的讨论,你不需要担心这个问题了,他们已经搞定了这个问题。
Unicode联盟给每一个字母表上的每一个柏拉图式的字母分配了一个魔法一样的数字,写起来是这样样子的:U+0639。这个神奇的数字被称作“码点”。U+表示“Unicode”,数字是十六进制的。U+0639是阿拉伯语的字母Ain。英语字母A就应该是U+0041。你可以在Windows 2000/XP上用字母映射表找到每一个字符或者也可以在Unicode的网页上面查找(home.unicode.org/)。
Unicode定义的字母数量并没有真正的限制,事实上他们已经超过了65536个,所以并不是每一个unicode的字母都能被塞进两个byte中,但是无论如何那都是一个误解。
好吧,比如我们有一个字符串:
Hello
在Unicode中,相关联的有这5个码点:
U+0048 U+0065 U+006C U+006C U+006F
仅仅是一群码点,真的只是数字。我们还并没有开始讲述在电子邮件中它们是怎么表示或者存储在内存里面的。
编码 编码开始了。 最早的Unicode编码观念就是用两个bytes来存储这些数字,也正是因为这个观念导致我们误解了两个byte。因此,Hello变成了 00 48 00 65 00 6C 00 6C 00 6F 对吗?没那么快!难道它不是: 48 00 65 00 6C 00 6C 00 6F 00 吗? 好吧,从技术角度上来说,我相信它是这样的,并且实际上早期的实现者希望用高字节序或者低字节序存储它们的Unicode码点。到底用哪种方式,取决于特定的CPU哪种更快就用哪种。瞧!现在是晚上,现在是早上,这里就有了两种存储Unicode的方式。所以在最开始针对每一个Unicode的字符人们就被迫想出了这种奇怪的转换方式存储FE FF。 这种方式叫做“Unicode字节序标记”。如果你正在交换高位和地位字节,就像上面说到的FF FE一样,当人们读到了你的字符串也会知道他们必须交换每一个字节。唉,并不是每一个Unicode的字符串开头都会有一个字节序标记的。 到目前为止,这些看上去都已经足够用了,但是程序员们开始抱怨了。“看看这些0!”他们说。因为他们是美国人,他们只需要使用英文,英文很少会使用到大于U+00FF的码点。此外,加利福利亚的自由开发者想节省他们的空间(冷笑)。如果他们是德克萨斯州人,倒是不会介意消耗掉两倍的字节。但是加利福利亚的懦夫不能接受存储大量的字符串要消耗双倍存储空间的主意。并且,无论如何,现在已经存在了这些使用各种ANSI和DBSC字符集的该死的文档,可是谁来转换他们呢?仅仅因为原因,大多数人都决定忽略Unicode,所以这期间的几年里事情变的越来越糟糕。 因此,UTF-8绝妙的被发明出来了。UTF-8是另外一种存储字符串的Unicode码点系统;那些神奇的U+数字,在内存里使用了8位的字节。在UTF-8里,0-127的每一个码点都被存储在一个字节里。只有大于等于128的码点会使用2-3个字节,实际上可以高达6个字节。
它有一个简洁整齐的作用,UFT-的英语文本看上去恰好和在ASCII中的一样;美国人甚至都不需要注意任何错误。只有其他国家的人需要跳过各种坑。比如:Hello这个字符,U+0048 U+0065 U+006C U+006C U+006将会被存储成48 65 6C 6C 6F。注意,这种存储方式和ASCII,ANSI以及世界上的各种OEM字符集的存储方式都一样。如果你大胆的用了重音的字母,或者希腊字母,或者克林贡语字母,你就必须要使用多个字节来存储一个码点,不过这些情况美国人永远不需要注意。(UTF-8还有一个不错的特性,即那些想要使用单个0字节作为空结束符来处理代码时,将不会把字符串截断)。
到目前为止,我已经告诉了你三种Unicode的编码方式。传统的用两个bytes存储的方法我们叫做UCS-2(因为它占了两个bytes)或者UTF-16(因为它有16位);但是你仍然需要解决它到底是用的高字节序的UCS-2还是低字节序的UCS-2。还有一个很流行的新UTF-8标准;如果你正好是只需要处理英语文本,并且除了ASCII以外其他标准都不知道的程序员,那么UTF-8的新特性可以让你的程序工作的很漂亮。
实际上,Unicode编码也还有相当多的其他方式。比如被称作UTF-7的编码,就是很像UTF-8,但是又能保证高位一直是0。所以,如果你需要通过一些严厉的国家警察邮件系统来传输Unicode字符的时候,7bits就是非常合适的;感谢它能毫发未损的存储。又比如:UCS-4,它存储每一个码点需要用4字节,它也又一个漂亮的特性,那就是每一个单独码点的存储都使用了相同数量的字节数,但是,天哪,即便是德克萨斯州的人也不敢那么浪费内存。
事实上,你考虑的是柏拉图式的理想字母,他们能被Unicode码点表示;这些码点也能用任何老派的教学方案编码。例如:你可以对Unicode字符串“Hello”(U+0048 U+0065 U+006C U+006C U+006F)进行ASCII编码,或者用老的OEM希腊编码,也或者用希伯来人的ANSI方式编码,或者用很久之前发明的其他的几百种编码方式之一种进行编码。但是有一个问题:有些字母可能不会展示出来!如果没用等同的Unicode码点表示这个字符,你需要尝试用你自己的定义方式来编码,你通常会遇到一个小问号吧标记:?,或者你确实很厉害,一个小盒子,你会得到一个什么呢?->�
上百种传统的编码方式都只能存储一部分码点,他们会将剩余其他的码点转换成问号标记。英文文本的一些常用编码是Windows-1252(Windows9X代表了西欧语言),ISO-8859-1,阿卡Latin-1(对任何西欧语言同样非常有用)。但是当尝试用这些编码方式存储俄语或者希伯来字母时,你就会遇到大量的问号标记。UTF7,8,16以及32都能够很漂亮的存储这里面的任何一个码点。
关于编码最重要的事实 如果你对我解释的所有的事情都忘记了,那也请记住一个重要的事实。拥有一个不知道用何种方式编码的字符串是毫无意义的。你不能再把头埋在沙子里假装“plain”文本就是ASCII编码。 没有纯文本这回事 如果你在内存里面,或者文件中,或者邮件中有一个字符串,你必须得知道它的编码方式,否则你就不能正确的向你的用户解释以及展示它。 一些遇到的问题,如:“我的网站看上去都是乱码”,以及“当我使用了口音语言时,她不能正确的看到我的邮件内容”,它们都可以可以归结为一个天真的程序员不知道一个简单的道理,如果不告诉我字符串用的是UTF-8的编码还是ASCII,亦或ISO 8859-1(Latin 1),或者Window1252(西欧语言),我都不能正确的展示它,甚至都不知道字符串在哪里结束。使用大于127码点的编码方式有上百种,谁知道使用的哪种? 我们如何保存字符串使用的是哪种编码方式的信息呢?我们有两种方式做这个事情。 方式1:在邮件信息中,你应该在表单的头部有一个字符串
Content-Type: text/plain; charset="UTF-8" 方式2:在web页面中。最初的想法就是web服务器应该返回web页面的时候在http的头部一起返回一个类似于Content-Type的信息。不是在HTML页面上写一个Content-Type,而是在发送HTML页面信息时候头部填上Content-Type。 以上想法的主要问题是:假设你有一个大型的web服务器,需要处理大量的站点,以及上百页的网页,这每一个网页都由不同的人以及不同的语言,并且还使用了各种不同的编码方式。这个编码方式实际上并不知道每一个文件的编码方式是什么,所以它也不能在头部填充一个Content-Type返回回来。 如果你在HTML文件中正确的填上Content-Type,使用一些特殊的标签就是一件非常方便的事情。当然,这会让纯粹主义疯狂...... 在你知道HTML的编码之前,你怎么阅读它呢?幸运的是,几乎所有的常用编码对32-127之间都做了几乎同样的事情,因此你可以正确的理解HTML页面而不是使用一些搞笑的字母:
`
`但是meta标签必须是部分最先要做的事情。因为,当web浏览器看到这个标签的时候它会停止当前的解析方式,并且用你指定的编码方式重新解释HTML页面。 如果web浏览器没有在Http头部或者meta标签里面找到任何Content-Type时,它会做什么呢?IE浏览器实际上会做一些非常有趣的事情:它会去猜测HTML页面用的是什么语言和编码方式,猜测的原则是基于各种语言对于文本的编码方式的频率。因为各种旧的8位编码的页面倾向于把它们国际化的字母放在128-255之间不同范围内。因为每一种人类的语言都有一个不同的字母使用直方图。所以这种猜测的方式实际上是有可能成功的。它确实很奇怪,但是经常却又能够正常的展示页面,当一个naïve web页面的作者不知道它们需要Content-Type头,并且看上去在浏览器上又是正常的时候。直到今天为止,当你用本地语言写一些东西的时候却并没有遵守“字母频率分配原则”时,IE浏览器以为是韩语,并且用韩语展示了它。由此也证明了我的观念:关于Postel's法则,即“发送的时候保守,接受的时候自由”确实不是一个好的工程原则。无论如何,当保加利亚人写的网站出现在韩国的时候,这个可怜的网站阅读者该怎么做呢?他会使用视图|编码菜单,并且尝试用一堆(至少有一打西欧语言)不同的编码方式,直到画面逐渐清晰。如果他知道那些大多数人不知道的事情。
我公司发布的最新版本的CityDesk网站管理软件,就是用了UCS-2(两个字节)Unicode来做所有的事情。VB,COM,以及Windows NT/2000/XP使用USC-2作为它们的原生字符串类型。在C++的代码里,我们只申明一个字符串为wchar_t("wide char")而不是char,并且我们使用wcs方法而不是str方法(比如wcscat和wcslen而不是strcat和strlen)。为了在C代码中创建准确的UCS-2字符串,你只需要在字符串前加一个L,就像这样:L"Hello"。
当CiteDesk发布了它们的网页后,它会转换成UTF-8的编码方式,这种方式能够很好的被浏览器支持很多年。这就是Joel On Software的所有29种语言版本的编码方式,而且到目前为止我还没有听说过任何一个人不能正确的浏览它们。
这篇文章越来越长啦,而且我也不能够覆盖到关于编码和解码的所有事情。但是我希望如果你已经读到这里了,那么你知道的已经够多了,可以回去编码了。使用正确的编码而不是各种咒语,这个任务我现在就交给你们啦。
参考文章:
https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/