有没有想过那个神秘的Content-Type
标签?你知道的,那个你应该放进HTML中却从来不知道应该是什么的东西?
你是否曾经收到一封email来自于你的朋友在保加利亚的主题为“???? ????? ???? ????”?
我很失望的发现,有很多软件开发人员对字符集,编码,Unicode等神秘领域并不完全掌握。几年前,一位 FogBUGZ 的 Beta 测试人员在想它是否能够处理日语的电子邮件。日语?他们有email用日语?我不知道。当我仔细检查我们用于解析 MIME 电子邮件消息的商业 ActiveX 控件时,我们发现它在字符集方面实际上做了完全相反的事情,因此我们不得不付出大量努力编写的代码来撤销它所做的错误转换并重新进行正确的转换。当我看到另外的商业类库,它也一样,完全破坏了字符代码的实现。我与那个类库的开发者联系,他有点认为他们对此无能为力,像许多程序员一样,他们只希望这些问题能以某种方式解决。
但这是不可能的。当我发现流行的 Web 开发工具 PHP 几乎完全忽略了字符编码问题,轻而易举地使用 8 位字符,几乎不可能开发出良好的国际化 Web 应用程序时,我想,够了。
所以我需要给出一个声明:如果你是一个在2003年工作的程序员并且你不知道基本的字符,字符集,编码和Unicode,并且被我发现你,我将会惩罚你在潜艇里剥6个月洋葱,我发誓我会的。
然后接下来:
那个并没有很难
在这篇文章中,我将告诉你每个工作中的程序员都应该知道的确切内容。关于“纯文本= ASCII = 字符是8位”的所有东西不仅是错误的,而且是完全错误的。如果你还在以那种方式编程,你就像一个不相信细菌的医生一样糟糕。在你读完这篇文章之前,请不要再写任何代码。
在开始之前,我需要提醒你,如果你是那些了解国际化的少数人之一,你可能会发现我的整篇讨论有点过于简化了。我只是试图设定一个最低标准,让每个人都能理解正在发生的事情,并编写能够处理除不包含重音字母的英语子集之外的任何语言文本的代码。并且我应该警告你,字符处理只是创建国际化软件所需的微不足道的一部分,但我只能逐个讨论,所以今天是字符集的问题。
一个历史视角
一个简单的方法去理解这些事物就是根据时间顺序。
你可以想我会在这里讨论非常古老的字符集如EBCDIC,好吧,我不会。EBCDIC不是一个和你生活相关的事物。我们不是非要回到那么久远的时间。
回到那个有些老旧的时代,当Unix被创造出来并且K&R正在被C语言编写,所有事情都非常简单。EBCDIC 正在逐渐淘汰。在那个时候,唯一重要的字符是古老纯正的英文,我们有一个用于表示他们的编码称之为ASCII,能够使用32到127之前的数字来表示每个字符。空格是32,字符“A”是65,等等。这些可以方便的存入7位二进制位中。如今的大部分电脑都是使用8位字节,所以不仅可以存入每个有可能的ASCII字符,而且你还有一个多余的位,如果你邪恶的话,可以用于自己的阴险用途:WordStar中的那些笨蛋实际上打开了高位,以表示单词中的最后一个字母,导致WordStar只能用于英文文本。小于32的字符被称为不可打印字符,并用于一些特殊的控制功能,比如回车、换行、制表符等。只是开玩笑。它们被用于控制字符,比如7会使你的电脑发出蜂鸣声,而12则会导致当前的打印纸飞出打印机并重新送进一张新的纸。
假设你是一个英文使用者,那么一切都是好的。
因为字节有8位存储空间,许多人想到,我们可以使用128-255字符用于我们自己的目的。这就是问题所在,许多人同时都有这个想法,他们有自己应该怎么使用128-255空间的计划。IBM-PC有一个被称为OEM字符集的东西,它为欧洲语言提供了一些带重音符号的字符,还有一些线条绘制的字符… 水平线、垂直线、带有小悬挂线的水平线等等。你可以使用这些线条字符在屏幕上制作漂亮的框和线条,在你干洗店的8088电脑上仍然可以看到这些线条。实际上,一旦人们开始在美国以外购买PC,就会想出各种不同的OEM字符集,这些字符集都使用了前128个字符来实现它们自己的目的。例如,在某些电脑上,字符代码130会显示为é,但在以色列销售的计算机上,它是希伯来字母Gimel(ג),因此当美国人向以色列发送简历时,它们会显示为rגsumגs。在许多情况下(例如俄语),对于上128个字符有许多不同的想法,因此您甚至不能可靠地交换俄语文档。
过了一段时间,这种OEM自由竞争的局面得到了ANSI标准的规范。在ANSI标准中,人们都同意在128以下的字符上采取与ASCII几乎相同的方式,但在128及以上的字符上处理的方式因不同国家而异。这些不同的系统被称为“代码页”。例如,在以色列DOS中,使用的是称为862的代码页,而希腊用户则使用737。它们在128以下的部分相同,但在128及以上的部分则不同,因为所有有趣的字母都在这里。 MS-DOS的不同国家版本都有数十种这样的代码页,处理从英语到冰岛语等所有内容,甚至还有一些“多语言”代码页可以在同一台计算机上运行世界语和加利西亚语!哇!但是,如果要在同一台计算机上运行希伯来语和希腊语,除非编写自己的自定义程序,以位图形式显示所有内容,否则完全不可能,因为希伯来语和希腊语需要不同的代码页,具有高数字的不同解释。
与此同时,在亚洲,为了考虑亚洲语言具有成千上万的字母,这些字母永远不会适合8位。这通常通过混乱的DBCS系统解决,即“双字节字符集”,其中一些字母存储在一个字节中,而其他字母需要两个字节。向前移动字符串很容易,但向后移动几乎不可能。程序员被鼓励不要使用s++和s–向前和向后移动,而是调用诸如Windows的AnsiNext和AnsiPrev之类的函数,这些函数知道如何处理整个混乱。
但大多数人仍然假装一个字节就是一个字符,一个字符就是8位,只要从一个计算机不移动一个字符串或只说一种语言,它就可以正常工作。但当互联网出现后,很常见从一个计算机移动字符串到另一个计算机,这时整个混乱就开始崩塌。幸运的是,Unicode已经被发明出来了。
Unicode
Unicode是一项努力,旨在创建一个包括地球上几乎所有合理书写系统以及一些虚构系统(如克林贡语)的单一字符集。有些人错误地认为Unicode只是一个16位编码,每个字符占16位,因此有65,536个可能的字符。实际上,这是关于Unicode的单一最普遍的神话,所以如果你有这样的想法,不要感到难过。
实际上,Unicode有一种不同的字符表达方式,你必须理解Unicode的思考方式,否则什么也不会有意义。
到目前为止,我们假设一个字母映射到一些位,你可以将它存储在磁盘或内存中:
A -> 0100 0001
在 Unicode 中,字母映射到一种被称为“代码点”的东西,仍然只是一个理论概念。这个代码点在内存或磁盘上的表示方式是完全不同的。
在 Unicode 中,字母 A 是一种柏拉图式的理念,它只是漂浮在天空中:
A
这个理念上的A与B不同,与小写的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上的charmap实用程序或访问Unicode网站找到它们。
实际上,Unicode 可以定义的字母数量没有实际限制,事实上,它们已经超过了65,536,因此并不是每个Unicode字母都可以真正地压缩为两个字节,但这本来就是一个迷思。
假设我们有一个字符串:
Hello
这个对应的Unicode是以下5个代码点:
U+0048 U+0065 U+006C U+006C U+006F.
只是一堆代码点,实际上是数字。我们还没有说过如何将它存储在内存中或在电子邮件中表示。
编码
这就是编码所涉及的内容。
早期的Unicode编码想法,导致了关于使用两个字节的传说,其想法是:嘿,让我们只是用两个字节来存储这些数字。所以,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 字节顺序标记(BOM),如果你在高位和低位之间交换字节,它看起来会是 FF FE,那么读取你的字符串的人就知道他们需要交换每个字节。呼~ 不是所有的 Unicode 字符串在使用时都在开头带有字节顺序标记。
有一段时间,这似乎足够好了,但程序员们开始抱怨。他们说:“看那些零!”,因为他们是美国人,看的是很少使用U+00FF以上的码点的英文文本。此外,他们是加州的自由嬉皮士,想要保护(嗤之以鼻)。如果他们是德克萨斯人,他们就不会介意耗费两倍的字节。但这些加州人无法忍受将字符串存储所需的空间翻倍的想法,而且已经有了各种使用各种 ANSI 和 DBCS 字符集的文档,谁来转换它们?难道是我?光是因为这个原因,大多数人决定忽视Unicode几年,与此同时,情况变得更糟。
因此,UTF-8的妙点子被发明了。UTF-8是另一种系统,用于使用8位字节在内存中存储您的Unicode代码点字符串,即那些神奇的U +数字。在UTF-8中,从0到127的每个代码点都存储在一个字节中。只有128及以上的代码点使用2、3甚至多达6个字节进行存储。
这样做的好处是,英文文本在UTF-8中看起来与在ASCII中相同,所以美国人甚至不会注意到有什么问题。只有世界其他地方的人需要跳跃。具体来说,Hello,对应于U+0048 U+0065 U+006C U+006C U+006F,在UTF-8中将被存储为48 65 6C 6C 6F,这恰好与它在ASCII、ANSI以及全球各种OEM字符集中的存储方式相同。现在,如果你大胆使用重音符号、希腊字母或克林贡字母,你将不得不使用多个字节来存储一个单独的代码点,但美国人永远不会注意到。(UTF-8还具有一个好的特性,那就是无知的旧字符串处理代码想要使用单个0字节作为空终止符号将不会截断字符串)。
到目前为止,我已经告诉了你三种编码Unicode的方法。传统的用两个字节存储的方法称为UCS-2(因为它有两个字节)或UTF-16(因为它有16位),你仍然需要弄清楚是高位优先的UCS-2还是低位优先的UCS-2。还有一种流行的新UTF-8标准,它有一个好的特性,如果你使用的是英文文本和脑残的程序完全不知道ASCII以外的任何内容,它也可以正常工作。
还有许多其他的Unicode编码方式。有一种叫做UTF-7的编码方式,它非常类似于UTF-8,但保证高位始终为零,因此如果必须通过某种严格的邮件系统传递Unicode,该系统认为只需要7位,那么它仍然可以通过。还有UCS-4,它在每个编码点中存储4个字节,这个编码方式有个好处,即每个编码点都可以使用相同数量的字节存储,但即使是得克萨斯州人也不会浪费那么多内存。
事实上,现在你可以按照旧的编码方案来编码Unicode代码点!例如,你可以用ASCII或旧的OEM希腊编码或希伯来语ANSI编码或已发明的几百种编码之一来编码Hello的Unicode字符串(U+0048 U+0065 U+006C U+006C U+006F),但有一个陷阱:一些字符可能不会出现!如果在你试图用特定编码方案来表示Unicode代码点时,该编码方案中没有相应的Unicode代码点,你通常会得到一个小问号:?或者,如果你非常熟练,一个方块。你得到了哪一个?-> �
有数百种传统的编码方式,只能正确存储一些编码点,并将所有其他编码点更改为问号。一些英文文本的流行编码方式是Windows-1252(Windows 9x的西欧语言标准)和ISO-8859-1,也称为Latin-1(也适用于任何西欧语言)。但是,尝试在这些编码中存储俄语或希伯来字母,你会得到一堆问号。UTF 7、8、16和32都有一个好处,即能够正确存储任何编码点。
关于编码最重要的一点事实
如果你完全忘记了我刚刚讲解的所有内容,请记住一个极其重要的事实。没有知道所使用的编码方式的字符串是没有意义的。你不能再将头埋进沙子里,假装“普通”文本是ASCII编码。
“Plain Text” 并不存在。
如果你有一个字符串,无论是在内存中、文件中,还是在电子邮件消息中,你都必须知道它使用的编码方式,否则你就无法正确地解释或显示给用户。
几乎所有愚蠢的“我的网站看起来像乱码”或“当我使用重音符号时她看不懂我的电子邮件”问题都可以归结为一个天真的程序员不理解这个简单的事实:如果你不告诉我某个特定字符串是使用 UTF-8 还是 ASCII 还是 ISO 8859-1 (Latin 1) 还是 Windows 1252 (西欧),你根本无法正确地显示它,甚至无法确定它在哪里结束。有超过一百种编码方式,而且在代码点 127 以上,所有的赌注都是无效的。
我们如何保存有关字符串使用的编码信息呢?好吧,有标准的方法来做到这一点。对于电子邮件消息,你需要在标题中有一个形式为
Content-Type: text/plain; charset=”UTF-8”
对于网页,最初的想法是 Web 服务器将返回类似的 Content-Type HTTP 标头以及网页本身,这个标头不是在 HTML 本身中,而是作为发送 HTML 页面之前发送的响应标头之一。
这会带来问题。假设你有一个大型的 Web 服务器,有很多站点和许多人贡献的数百个页面,使用各种不同的语言和编码,每个页面都使用其 Microsoft FrontPage 副本适合生成的编码。Web 服务器本身实际上不知道每个文件的编码是什么,因此无法发送 Content-Type 标头。
如果你可以使用某种特殊标记将 HTML 文件的 Content-Type 直接放在 HTML 文件本身中,那将很方便。当然,这让纯粹主义者感到疯狂……在不知道编码是什么之前,怎么读取 HTML 文件?!幸运的是,几乎所有常用编码在字符 32 和 127 之间都会做相同的事情,因此你总是可以在 HTML 页面上做到这一点而不开始使用奇怪的字母:
1
2
3
<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决定它是韩语并以此显示,这证明了我认为Postel的法则关于“在发出时保守,在接受时自由”不是一个好的工程原则。不管怎样,这个用保加利亚语编写但看起来像韩语(甚至不连贯的韩语)的网站的可怜读者该怎么办?他使用“查看 | 编码”菜单并尝试一些不同的编码(东欧语言至少有十几种)直到图片变得更清晰。如果他知道要这样做,而大多数人并不知道。
对于我公司发布的网站管理软件CityDesk的最新版本,我们决定在内部使用UCS-2(双字节)Unicode来处理所有事务,这是Visual Basic、COM和Windows NT/2000/XP使用的本地字符串类型。在C++代码中,我们只需将字符串声明为wchar_t(“宽字符”),而不是char,并使用wcs函数而不是str函数(例如,使用wcscat和wcslen,而不是strcat和strlen)。要在C代码中创建字面UCS-2字符串,只需在前面加上L即可,如L“Hello”。
当CityDesk发布网页时,它会将其转换为UTF-8编码,这种编码已经得到Web浏览器多年的良好支持。Joel on Software的所有29个语言版本都是以这种方式编码的,我还没有听到有人在查看它们时遇到任何问题。
这篇文章已经有相当长的篇幅,关于字符编码和Unicode的知识实在繁多,我无法一一详述。但我希望阅读至此的你,已经了解到足够的信息,让你在编程时能够运用现代方法,而非依赖古老的招式。至此,我将告辞,祝你编程顺利。