软件随想录(local.joelonsoftware.com/wiki)-2003年10月08日 每个软件开发者都绝对一定要会的Unicode及字符集必备知识(没有借口!)

2003年10月08日 每个软件开发者都绝对一定要会的Unicode及字符集必备知识(没有借口!) - The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode (No Excuse!)

 

The Joel on Software Translation Project:Unicode

From The Joel on Software Translation Project

每个软件开发者都绝对一定要会的Unicode及字符集必备知识(没有借口!)

作者:周思博 (Joel Spolsky)
译:Paul May 梅普华
Wednesday, October 08, 2003
属于Joel on Software, http://www.joelonsoftware.com


还搞不懂那个神秘的Content-Type tag吗?你知道的,就是那个应该放在HTML里却又永远不知道该设成什么内容的标签啊。

你曾经收到在保加利亚的朋友寄来,主题是「???? ?????? ??? ????」的电子邮件吗?

ibm.jpg

很多软件开发者并未真正完全理解字符集、字符编码、Unicode等等的神秘世界,当我发现不懂的人那么多时真的很失望。数年以前,某位beta测试人员想知道FogBUGZ是否能处理日文的电子邮件?他们竟然用日文写电邮?我完全不知道耶。我们用了一个商用ActiveX控制元件来分析MIME电邮讯息,当我仔细调查这个元件时,才发现它对字符集的处理完全错误,所以我们还写了些了不起的程序,把错误的转换还原后再重做正确的转换。我又去看看另一个商用程序库,它的字符编码实现也是完全不对。我联络该软件的开发者,结果他似乎有点认为没办法改善。他跟很多程序员一样,只希望这个问题能凭空消失。

不过问题并不会消失。PHP是个很普遍的web开发工具,不过它完全忽略字符编码问题,PHP很愉快地用8位元来处理字符,因此几乎不可能开发好的国际化web应用程序。当我发现这件事时,觉得真是够了

所以我要做一个声明:如果你在2003年还是个程序员,而你不知道字符、字符集、字符编码、以及Unicode的基本知识,我就要去你,我会让你在潜艇里关6个月剥洋葱。我发誓我一定会的。

另外还有一件事:

这并没有那么难。

我会在这篇文章中让你确实了解每个现役程序员都应该知道的事情。所谓「纯文字 = ascii = 字符都是8个位元」的说法不仅不对,而且还错得离谱;如果你还是照这个想法写程序,那么你大概不会比不相信细菌的医生好多少。在读完这篇文章之前请暂时不要写程序。

在我开始之前应该先提醒一下,如果你是极少数了解多国语言软件制作的人,会发现我的讨论有点过度简化。我只是想设立一个底线,让大家能了解这是怎么一回事,而且写出的程序有希望能处理任何语言的文字,而不是只认得没有重音符号的英文。另外我也要提醒你,字符处理只占建立多国语言软件的一小部份,不过我一次只能写一件事,所以今天只谈字符集。

历史的观点

要了解这些事,最简单的方法就是按年代来看。

你或许会认为我会讲些EBCDIC之类很古老的字符集。我不会,EBCDIC跟你的生活无关。我们并不用回溯到那么前面。

ascii.png

回到没那么古老的从前,Unix被发明而K&R正在写The C Programming Language的那个时代,当时每件事都非常简单。EBCDIC正在被淘汰。唯一重要的字符集就是古老美好的无重音英文字母,我们有一个对应的编码系统叫做ASCII,可以用32到127的数字表示每一个字符。空白是32,字母A是65,如此类推。这种方法可以把文字存成7个位元。当时大部份电脑的一个字节都是8个位元,所以储存全部ASCII字符之后有很多个位元没用到。如果你够邪恶,就会偷用这些空位元:事实上WordStar的坏蛋就用把最高位元设起来,代表一个单字中的最后一个字母。这是开玩笑的。空的位元被用来当控制字符,比如7会让你的电脑发出哔声,而12会让打印机把目前正在印的纸张送出并且卷入一张新纸。

所以天下太平,不过只限于英语系的人。

oem.png

由于字节有8个位元的空间,所以很多人就开始想啦:「对了,我们可以把128到255的码拿来自己用。」问题是很多人同时都有这个想法,所以128到255的空间该怎么用,大家都各自有自己的想法。IBM-PC用了一种名为OEM字符集的东西,提供了某些欧洲语言用的重音字母和一堆线条绘图字符:水平线、垂直线、右边有个小吊钓的水平线等等。你可以用这些线条绘图字符在屏幕上拼出很漂亮的方框和线条,在乾洗店里的8088电脑还可以看到这种图案。事实上当PC开始卖到美国以外时,各种不同的OEM字符就被凭空创造出来,大家都把上面这128个字符拿来自己用。举例来说,字符码130在某些PC上会显示为é,不过在以色列卖的电脑上就变成希伯来文字母Gimel ( gimel.png ),所以当美国人把履历(résumé)寄到以色列就会变成r gimel.png sum gimel.png 。在很多情况下,比如说俄文好了,本身对于上面128个字符(值>127)就有很多不同的想法,所以甚至连俄文文件本身都无法可靠地互换。

后来这段OEM乱用区终于在ANSI标准里固定下来。在ANSI标准中,大家都同意小于128的字符定义(基本上和ASCII一致),不过由128开始的字符就有很多不同的处理方法,会依照你住的地方而定。这些不同的系统就叫做页码(code page)。举例来说以色列的DOS用叫862的页码,而希腊用户则是用737。它们在128以下是一样的,不过由128起就不同了,里面充满奇奇怪怪的字母。美国版本的MS-DOS有几十种页码,由英文到冰岛文都可以处理,甚至还有一些「多语」页码可以在同一台电脑上处理世界语和加利西亚语!了不起!不过要一台电脑同时处理希伯来文和希腊文是绝对不可能的,除非你自己写程序自己用图显示所有文字。因为希伯来文和希腊文对128以上字符的解释方法不同,必须用到不同的页码。

在同一时期亚洲发生的事情更夸张。由于亚洲的字母系统有几千个字母,不可能用8个位元表示。通常是用一种叫DBCS的麻烦系统来处理。DBCS是双字节字符集(Double Byte Character Set),字符集中的某些字母是一个字节来存,其他字则要用两个字节。在DBCS的字串中要向后移到下一个字很容易,不过几乎不可能往回移到前一个字。程序员被指示向后及往回移时不能用s++和s--,而是呼叫Windows的AnsiNext和AnsiPrev之类的函数,只有这些函数才知道怎么处理这些麻烦。

不过大多数人还是假装一个字节就是一个字符,而一个字符就是8个位元。只要不会把字串在电脑间移动,或者只用一种语言,这种想法大致上还是能用。不过当Internet兴起,在电脑间移动字串变成随时都在做的事,整团麻烦自然就爆出来了。还好这时已经发明了Unicode。

Unicode

Unicode是个勇敢的尝试,想用单一个字符集去涵括地球上所有合理的书写系统,另外也要包括克林贡语等杜撰的语文。有些人误认为Unicode只是个16位元码,里头每个字都要占16位元,所以总共有65,536个字符。事实上这并不正确。这是关于Unicode常见的误解,所以如果你也这么认为的话,不用难过。

事实上Unicode对字符有不一样的想法,你必须了解Unicode的想法,否则是搞不懂的。

到目前为止,我们都假设一个字母会对映到某些位元,这些可以存在磁碟或内存中:

A -> 0100 0001

在Unicode里一个字母是对映到一个叫code point的东西(还只是一个理论上的概念)。要如何在内存或是磁碟上表示code point就完全是另一回事。

在Unicode中,字母A是个精神上的观念。它只会漂浮在天堂里:

A

这个观念上的AB或者a都不一样,不过AA以及A都一样。Times New Roman字型的A和Helvetica字型的A是相同的字符,但和小写的"a"不一样,这种想法似乎没什么好争论的。不过在某些语言中,光是要决定一个字母什么就有得吵了。举例来说,德文字母β究竟真正的字母还是ss(译注:拉丁文的gei)的另一种特别写法呢?如果字母的形状在单字结束时会改变,改变之后要当作不同的字母吗?希伯来文说是,阿拉伯文却认为不是。不管如何,Unicode协会的聪明人已经在过去十年左右搞定了,虽然有一大堆政治争论伴随而来,不过你不用担心。他们已经完全搞定了。 Unicode协会把所有字母系统中每一个观念上的字母都分配一个魔术数字,这个数字的写起来就像是:U+0645。这个魔术数字就叫一个code point。U+的意思是Unicode,数字则是用十六进位表示。U+FEC9就是阿拉拍文的字母Ain。英文字母A则是U+0041。你可以用Windows 2000/XP的charmap工具把这些数字全找出来,到Unicode网站也可以找到。

Unicode可以定义的字母数量并没有实质限制,事实上可以超过65,536个,所以并不是所有的Unicode字母都能挤进两个字节里,不过反正那本来就是个迷思。

好吧,假设我们有个字串:

Hello

用Unicode来表示的话,这个字串会对映到下面五个code point:

U+0048 U+0065 U+006C U+006C U+006F.

就只是一堆code point。实际上也就是数字。不过我们还没有提过要如何储存到内存或在电邮讯息中表示。

字符编码

这就是字符编码上场的地方。

Unicode编码最初的想法导致了两个字节的迷思,简单说就是把那些数字都存成两个字节。所以Hello变成

00 48 00 65 00 6C 00 6C 00 6F

这样对吗?等一下!也有可能会是:

48 00 65 00 6C 00 6C 00 6F 00 ?

好吧,技术上是的,我的确相信可以这样写,而事实上早期的实现者希望能把Unicode码存成high-endian或low-endian模式,可以依据CPU用哪一种最快来决定。于是就有种储存Unicode的方法。所以人们被迫想出奇怪的作法,在每个Unicode字串的开头存一个FE FF;称之为Unicode Byte Order Mark。如果你把高低字节对调,标记就会变成FF FE,读字串的人就知道其他字节都要对调。不过外面的Unicode字串开头并不一定都会有这种字节顺序标记。

hummers.jpg

有一段时期这个方法好像还不错,不过后来有程序员在抱怨了。他们说:「看看那些零」,因为他们都是美国人,看到的都是很少用到U+00FF以上code point的英文文字。何况他们还是注重保育(哼)又崇尚自由的加州嬉皮。如果他们是德州佬,才不会在意要花掉两倍的字节呢(译注:指德州人少地方大,所以财大气粗)。不过那些加州糊涂蛋受不了字串储存空间会倍增的想法,而且外头已经有太多文件是用各种ANSI和DBCS字符集写的,要找谁来转换这些文件?新闻局吗?光是这个理由,就让大多数人就决定不管Unicode,几年下来情况就变得愈来愈糟了。

然后就有人发明UTF-8这个绝佳的点子。UTF-8是另一个储存系统,用8位元方式把Unicode code point(就是那些神秘的U+数字)存在内存中。在UTF-8中,由0-127的code point都存成一个字节。只有128和更大的code point会存成2或3或个字节,事实上最多可以用到6个字节。

utf8.png

这样做有个很巧妙的副作用,就是英文文字用UTF-8和用ASCII会完全一样,所以美国人根本不会觉得有啥不对。只剩世界上其他地方的人得跳火圈。具体来说Unicode为U+0048 U+0065 U+006C U+006C U+006F的Hello的,会被存成48 65 6C 6C 6F。看吧!这跟存成ASCII或ANSI或是地球上每一种OEM字符集的结果都一样。这样子一来,如果你斗胆敢用重音字母或希腊字母或是克林贡字母,就得用多个字节来储存一个code point,只是美国人永远不会发现。(UTF-8还有一个蛮好的特性。由于旧的字串处理程序并不知道Unicode,在处理字串时会用一个值为零的字节作为字串结尾。如果用UTF-8的话,这些旧程序不会中途截断字串。)

到目前为止我说了种Unicode编码的方法。全部都存成两个字节的传统作法叫做UCS-2(因为用两个字节)或是UTF-16(因为有16位元),不过你还是得分辨是high-endian UCS-2还是low-endian UCS-2。再来是普遍使用的新UTF-8标准,这个标准有良好的特性,在用只认识ASCII的旧程序处理用英文文字还是一切正常。

Unicode其实还有其他多种编译方式。其中之一叫UTF-7,非常像UTF-8不过保证最高位元一定是零,所以即使经过某个认为7位元很足够的严苛警察国家电邮系统,还是能亳发无伤地通过。另外也有每个code point都存成4个字节的UCS-4,好处是每个code point都容量都一样,不过连德州佬都不敢浪费那么多内存。

实际上现在你正在以概念性的字母(表示成Unicode code point)来思考事情,这些Unicode code point也可以用任何老式的编码方法来编码!举例来说,你可以把Unicode字串Hello(U+0048 U+0065 U+006C U+006C U+006F)编码成ASCII或旧的OEM希腊编码,也可以编成希伯来ANSI编码或是到目前为止已发明的数百种编码方式,不过有一个陷阱:某些字母可能会画不出来!如果某个Unicode code point在你所用的编码方式中没有对应的字符,通常就会看到一个小问号?或是一个小方框。在箭头后面你看到的是什么呢?-> 啮(译注:这个字是十六进位EFBF,用UTF-8好像是没有字,在大五码里就是「啮」,因为译文用big5编码,所以看到的是「啮」)?

多达数百种的传统编码方式都只能正确储存部份的code point,而其他code point则是全部变成问号。常见的英文文字编码有Windows-1252 (Windows 9x对西欧语言的标准)和ISO-8859-1又名Latin-1(也是用于西欧语言),不过想要用这些编码方式储存俄文或希伯来文时就会得到一大堆的问号。而UTF 7, 8, 16和32通通都能正确的储存任何一个code point。

关于字符编码最重要的一个事实

如果你完全不记得我刚说的东西,请至少记住一件超级重要的事实。光有字串却不知道编码方式是不行的。你不能再把头埋在沙里假装「纯」文字就是ASCII。

根本就没有纯文字这种东西。

假设你有一个字串,不管是在内存或在文件还是在电邮讯息里,你都必须知道字串用的编码方式,才能正确解译出来并呈现给使用者。

「我的网站都是乱码」或「她看不到我用重音符号写的电邮」之类的笨问题,几乎全部都是因为某位天真的程序员不了解一个单纯的事实:如果不知道某个字串的编码方式是UTF-8还是ASCII还是ISO 8859-1 (Latin 1)还是Windows 1252 (西欧),根本不可能正确显示出来,甚至连在哪结束可能都找不到。大于127的code point有上百种编码方式,连猜都猜不到。

我们要如何保存某个字串的编码资讯呢?好吧,是有一些标准方法可以用。以电子邮件来说,邮件表头应该会有一个字串:

Content-Type: text/plain; charset="UTF-8"

如果是网页的话,最原始的想法是在网页之外,再让web服务器传回一个类似的Content-Type http header。不是放在HTML里面,而是在传HTML网页之前先送的header。

这样做会有问题。假设你有一个很大的web服务器,很多使用各种语言的人在里面放了很多网站和网页,所有网页的编码方式都是由微软FrontPage自动产生。Web服务器本身其实并不知道各个文件的编码方式,所以也没法子传出正确的Content-Type header。

利用某些特别的tag把HTML文件的Content-Type放在HTML文件里比较方便。当然这会让纯粹主义者抓狂...你怎么能在不知道编码方式之前HTML文件呢!?幸运的是,几乎所有编码方式由32到127的字符都是一样的,所以不需用到怪字母就能在HTML网页取到这些资讯:

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

不过这个meta tag一定得放在<head>段落非常前面的地方。因为网页浏览器一看到这个tag就会停止分析,然后改用你指定的编码方式重新解译整个网页。

如果浏览器在http header或meta tag都找不到Content-Type时会怎么做呢?Internet Explorer会做一件很有趣的事:它会依据各字节在各种常见语言编码中出现的频率,猜测网页所用的语言及编码方式。由于各种旧的8位元页码通常把该国的字母放在128到255范围内不同的位置,而各种人类语言的字母使用频率都有不同的分布特性,所以这种做法的确有机会成功。这种做法真的很奇怪,不过似乎的确很有效。效果好到那些天真到不知道要用Content-Type header的网页制作者根本不知道自己错了,因为他们的网页用浏览器来看时一切正常。等到某一天,当他们写的内容不符合所用语言的字母频率分布时,Internet Explorer就会把它认成韩文来显示。我认为这也证明Postel's Law中关于「发送时严谨,接收时宽松」的论点实在不是一个良好的工程原则。不管如何,当遇到这个用保加利亚文写却显示成韩文(还不是有意义的韩文)的网页时,可怜的读者要怎么办呢?他会用由选单选 检视|编码,然后尝试各种不同的编码(里面有十几种东欧语言)直到看起来对为止。不过当然是要他会这招才行,不过大多数人都不会。

rose.jpg

我们公司有出一套网站管理软件CityDesk,从上一版起我们决定内部全部使用UCS-2(2个字节)的Unicode,它也是Visual Basic、COM、以及Windows NT/2000/XP的标准字串类型。写C++程序时只要在字串声明时用wchar_t("wide char")代替char,再用wcs函数代替str函数(比如用wcscatwcslen代替strcatstrlen)即可。要在C程序里建立一个UCS-2字串常数,只要在字串前面加个L就好了,就是这样:L"Hello".

当CityDesk发行网页时会把网页转成多年来广受浏览器支持的UTF-8编码。这也是Joel on Software29种语言版本编辑的方式,而且还没有人跟我抱怨过有问题。

这篇文章写到这里已经很长了,反正我也不可能写完所有关于字符编码和Unicode的事情。不过我想你既然都读到这里了,应该也学够了可以回去写程序,这次别再用水螅和咒语了,改用现代的抗生素吧。这就是我留给你的工作。

这些网页的内容为表达个人意见。
All contents Copyright © 1999-2006 by Joel Spolsky. All Rights Reserved.

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值