有关Unicode

1 篇文章 0 订阅
1 篇文章 0 订阅

原作者:Joel Spolsky

原文:http://www.joelonsoftware.com/articles/Unicode.html

译者注 1:笔者翻译本文的主要目的是为了自学,所以翻译的比较粗,并且掐头去尾。

译者注 2:本文的翻译过程中,特地保留了一些技术性词汇的英文原词,因为译者认为牵强地翻译这些词汇是愚蠢的行为。

译者注 3:本文并非逐字翻译,一些译者人为不重要的部分被省略了。


  • 一段历史

想要理解这件事,最简单的角度就是从时间发展的角度来看。

你可能会以为我会要讲EBCDIC那些非常老的字符集。你错了。EBCDIC已经跟我们的生活没有任何关系,我们不用从那么早开始。

我们从稍微晚一点的时候开始讲起,那时候Unix正在被发明,K&R也正在编著The C programming Language。一切都非常简单。EBCDIC也正在渐渐退出历史舞台。那时候人们只关心没有重音的英文字符。我们有ASCII表,因此所有的英文字符都可以用32到127之间的数字来表示,比如’A’是65。7个bits就足够来表示这些了,而那时候的电脑已经普遍使用8个bits为一个字节。所以不但可以用一个字节来表示所有的ASCII字母,还多出一个bit。这个多出来的bit可以被你用来做你自己想做的事情。比如WordStar的那些人就把每一个词的最后一个字母的最高bit(多出来的bit)设为1。32以下的那些字符被称为unprintable(不可打印)字符,他们是“控制字符”。比如7号字符可以让你的计算机“滴”一声;而12号字符会让打印机略过当前的纸张剩余部分,而直接进入下一张纸。

由于一个byte有八个bits,人们开始想:我们可以用128-255这些字符来做一些有趣的事情。有很多人产生了这个想法,并且用自己各自的想法去实现。这就造成了128-255这些字符的定义因人而异了。比如IBM-PC发明了广为人知的OEM字符集,它包括带重音的字母(一些欧洲文字会用到),还有许多用来划线的字符:水平线;垂直线;最右端带有一些零零碎碎装饰的水平线等等不一而足。你可以用这些划线字符在屏幕上画出很漂亮的方块和线条。这些东西在当今的一些干洗店中使用的电脑上还是可以找到的。但是一旦人们开始从美国以外的地方购买电脑,各种各样的OME就出现了,它们都用了各自的方法来定义128-255这些字符。比如说,在某些电脑上,130号字符被定义为é,但那些在以色列销售的电脑,130号字符被定义为希伯来文的Gimel字母。所以如果一个美国人把自己的简历(英文:résumé)发送到以色列的话,就会得到很奇怪的结果。在许多其他地方,比如苏联,128-255的定义方法更是多种多样,所以根本不可能顺利地传输一份苏联的文件。

终于,大家各自为营的局面被终止了,因为出现了ANSI标准。在这个ANSI标准中,0-127的定义大家没有任何歧义,就跟ASCII一样;但对于128-255,不同的地方有不同的定义方法。这些不同的定义方法被称为不同的code page。比如以色列的DOS系统用的就是862号page code;希腊用的是737号。它们之间唯一的不同就是128-255这些字符。在美国用的MS-DOS有好几种code page,用来处理各种不同的语言。当时甚至有“多语言code page”,可以实现在同一台电脑上既能处理世界语也能处理加利西亚语。但是很多其他情况就不行了,比如如果你想在同一台电脑上处理希伯来文和希腊文,你只能自己重新定义和编写128-255字符集,并且用点阵把它们画出来,因为希伯来文和希腊文用的是不同的code page,也就意味着它们对于128-255的定义是不同的。

与此同时,在遥远的亚洲,人们在思考更加疯狂的问题。因为很多亚洲语言有上千个字符,一个byte根本不够用。这个问题通常是由一个非常二的叫做DBCS(“double byte character set”)的系统来解决的。在这个系统里,有些字符被存在一个byte里,而另一些则被存在两个bytes里面。这导致了正向遍历一个字符串是容易的,但如果想逆向的话是几乎不可能的。那时的程序员一般会避免使用s++或者s--这种方法来向前或者向后移动,而一般会选择使用微软出的AnsiNext和AnsiPrev来处理这个烂摊子。

谈不管怎么说,大部分人仍然生活在“一个byte就够用了”的气泡里。只要他们不把一个文件从一台计算机拷贝到另一台,或者讲另外一种语言,这个原则是够用的。但是,在因特网出现之后,文件从一台计算机拷贝到另一台变成了一件非常常见的事情,于是乎世界大乱。

好在,Unicode被发明了。

  • Unicode

Unicode的野心很大,它想用一个字符集来包括这个星球上所有的书写系统,甚至包括像克林贡这种语言。有些人误认为所谓Unicode其实就是使用两个byte也就是16bits来保存字符,那就可以保存65,536个不同的字符。但事实上,这并不完全正确。这是一个非常常见的错误。Unicode对于字符有自己全新的理解。想要理解Unicode,就必须使用它的一套哲学去思考问题。

到现在为止,我们一直认为,每一个字符对应的是硬盘或内存中的一些bits,比如:

A —> 0100 0001

在Unicode中,一个字母对应的是一个code point。不要被这个词吓到,这只是一个存在于理论中的概念。这些code point如何对应到硬盘或内存中的bits,就是一件完全不同的事情了。

在Unicode中,字母A是一个柏拉图式的完美存在。它不同与B,也不同于a,但是如果仅仅是字体不同的A之间是没有差别的。这貌似很好理解,但是在某些其他语言中,仅仅是辨别字母就有可能会引发争论。比如德文字母b就很容易被误认为是另一种写ss的写法。如果一个字母当出现在单词末尾的时候会变形,那它还是同一个字母么?在希伯来文中,答案是是;但在阿拉伯文种,答案是否。但是不管怎样,发明Unicode的那群人已经把这些问题都考虑到了。为此他们花了过去十年的时间,以及许多政治博弈。总之就是,你不必再为这些事情担心了,它们已经都被考虑到了。

在Unicode中,每一种语言中的每一个柏拉图式的字母都被赋予了一个奇妙数字。这个奇妙数字长这样:U+0639。这个奇妙数字被称为code point。U+表示是Unicode,而后面的数字是十六进制的。U+0639表示的是阿拉伯字母Ain。英文字母A的奇妙数字是:U+0041。你可以在Windows中的charmap工具里查找这些奇妙数字,也可以去Unicode的网站上查。

Unicode可以定义的不同字符数量是无限的。事实上现在已经超过了65,536 个并且已经不能用两个bytes来表示了。

不管怎样吧,假设现在我们有一个字符串:Hello,那么在Uniocde中,它对应了五个code point:U+0048 U+0065 U+006C U+006C U+006F。注意,这仅仅是几个code point,我们还没有讨论到如何把它们存储到计算机里去。

这就是编码发挥作用的时候了。

  • 编码

Unicode最早的想法是:为什么不把code point后面的数字存储在两个bytes里面呢?这也就是为什么很多人会把Unicode理解为两个bytes。如果是这样的话,上文中的“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模式(有关high-endian和 low-endian,情自行wiki)。为了这种情况人们不得不形成一种很奇怪的规定,也就是在每一个Unicode前面加上FF FE或者FE FF,这个东西叫做Unicode Byte Order Mark。

这一切看上去似乎可行,但不久后,程序员们开始抱怨了:“怎么他喵的这么多0?”因为他们是美国人,并且很少使用英语以外的语言,所以他们很少会用到U+00FF以上的字符。而且之前已经有ANSI和DBCS等各种各样麻烦的标准,谁又来负责转换呢?我吗?所以很多人故意忽略Unicode的存在忽略了很多年,与此同时,事情变得恶化了。

这就是为什么会有UTF-8这个聪明的发明。UTF-8是一种把Unicode code point存储进内存的系统,也就是那些带U+的奇妙数字。它把每一个奇妙数字都用8个bits来表示。在UTF-8中,每一个0-127的code point都被存储为1个byte,只有那些超过128的,才被存储为2,3甚至到6个bytes。这样做有一个很好的地方在于,所有的英文字符跟ASCII是一样的,所以那群骄傲的美国人就会觉得很开心,他们根本不需要做任何的适应。只有世界上其他地方的人需要来适应这个系统。比如说,Hello这个词,对应的code point是U+0048 U+0065 U+006C U+006C U+006F,那么在计算机中就被存为48 65 6C 6C 6F。注意了,这跟ASCII,ANSI以及每一个OME字符集都相符合。只有当你需要使用带注音的字符或者比如希腊字母的时候,你才需要多余的bytes来存储一个code point,但是美国人是不需要知道这些的。

到目前为止,我已经告诉了你三种存储Unicode的方式。传统的把Unicode存为两个bytes的方法叫做UCS-2,或者UTF-16,但你还需要搞清楚到底是high-endian UCS-2还是low-endian UCS-2。而比较流行的是UTF-8标准,它的好处是英文字母跟ASCII一样。

事实上,Unicode还有许多其他的编码方式,比如有一种叫做UTF-7,它很像UTF-8,但最高位bit永远是0。这也就意味着其实7个bits就足够存储了。还有一种叫做UCS-4,它的特点是,所有的code point都是4个bytes。这样的好处是,所有的code point都一样,但缺点也是显而易见:太耗费内存了。

好了,现在你可以用unicode的code point来想象你的那些柏拉图式的字母了,而这些字母也可以用老式的encoding方法来编码,只不过有些字母可能显示不出来。因为你用unicode来表示的这些字母可能跟你所使用的编码方式不吻合,于是你就得到许多问号。有很多传统的编码方式只能正确地表示一部分unicode的code point,而其他的字符都被表示为问号。比较流行的引文的编码方式有Windows-1252和ISO-8859-1,即Latin-1。但是如果你试图用这些编码方式来存储俄文或者希伯来文,你将会得到许多问号。而UTF 7, 8, 16, 和 32 等这些编码方式的一个特点就是,它们能够正确地存储任何code point。

  • 有关encoding最重要的事情

就算你忘记了我上面所解释的一切,请记住我下面这句最重要的话:如果我们有一个string却不知道它的encoding方式,那么这个string就没有意义。你不能还用固有的眼光来看问题,以为所有的“plain text”都是ASCII。

这个世界上没有”plain text”这件事。

如果你的内存中、文件中或者是邮件中有一些string,你必须知道它们是用什么方式编码的,这样才能正确地显示它们。所有的“邮件读不了”等类似的问题,都是因为有一个二逼程序员不能理解如果不告诉别人你的string是如何编码的,别人就读不了你的string。

好了,我们应该如何来保存有关编码的信息呢?有一些标准的方法来解决这个问题。比如在邮件中,header里应该有一句话:

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

而在网页中,原来的计划是让服务器返回一个类似Content-Type的东西,跟网页一起。注意,不是在网页中,而是在html网页发送前所发送的一些回复的头部。

(后面的懒得翻译了……有兴趣的自己去看原文吧。)

下面这张图是我从Wiki上截的,比较清楚地说明了UTF-8的组成。UTF-8是世界上最流行的encoding方式。


今天看到一道google面试题说的是给一个文件,要求写一段程序来判断它是不是UTF-8 encoding。根据上面这个表格,我们可以轻松地搞定这个程序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值