【翻译】每个程序员都需要了解的Unicode和字符集知识

2017-07-25

【翻译】每个程序员都需要了解的Unicode和字符集知识


2017-07-25


返回目录

#字符集

原文: The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)

翻译:陈昊(Sevenschan)

PS: 从来没有翻译过一篇文章的作者这么喜欢讲段子。。翻译起来很费劲。。而且真正有用的干货说实话不多。。

有对那个神秘的Content-Type标签产生过好奇吗?当把它放在HTML里面之后会发生什么你不知道的事呢?

你试过收到一封来自保加利亚的朋友的email,但是都是一堆“???? ????????? ????????? ???”吗?

我很伤心的发现有很多的程序员都不能完全的了解字符集、编码、Unicode等等的神秘世界。几年前,一个FogBUGZ的测试人员想知道是否可以用日语来处理收到的电子邮件。日语?他们有收到日语的邮件(是不是哪个不可言喻的网站的验证邮件)?人家很傻很天真一点都不懂哟。当我仔细看看我们用来解析MME邮件信息的商业ActiveX控件的时候,我们发现它对于字符集的处理是错得离谱,所以我们不得不写一段英雄般的代码来逆转错误并且重做一遍正确的。当我查看另外的商业库的时候,mmp,一个样,字符都被搞得支离破碎。我跟这些库的作者反应,得到的回答是他们“没有任何办法”。

同时,我发现世界第一的语言PHP几乎完全无视字符编码问题,因为它使用8位字符,这样的设计使它几乎没有可能开发优秀的国际化应用。我想,足够就是最好了吗。

所以我发布了一个公告:如果你是2003年以后的程序员,但你不知道关于字符的基础知识,不知道字符集,不知道编码,不知道unicode的话,老子会捉住你,然后把你晾在游艇上半年。我发誓我会这样做的。

更重要的一点事: 它其实一点都不难

在这篇文章里,我会把每个工作的程序员都需要知道的点都塞进你的脑袋里。要你知道 “plain text = ascii = 8位字符” 不仅是错,是大错特错。你就像那些不相信细菌的医生一样。所以,请你求求你在看完这篇文章以前,不要再写一句代码了。

在我开始之前【译者注:这篇文章的前戏真长。。】,我应该提醒你如果你是那些罕见的熟悉国际化的人,你会发现我谈论的点都是很浅显的。我是真的只是尝试去设置最低界线,来帮助各位可以编写更好的代码。还有一点,字符集只是在系统国际化中的一个小部分,我也只能在一篇文章里面谈论一个小部分,反正老子说了算。

历史观

最简单了解这些东西的方法就是看看它们的时间线。

你可能会以为我要讲老掉牙的字符集,例如EBCDIC。嘿嘿,老子怎么会被你们凡人猜到。 EBCDIC已经跟你们的生活毫无关系了,我们也不需要讲到那么远的历史。

回到半年后,当Unix出世以后并且K&R还在写C语言的时候,所有东西都很简单。EBCDIC就是使用的方案。唯一有关系的老的好的字符就是英文字母,我们把它成为ASCII,允许它们使用32到127之间的每个数字来表示每个字符。 空格是32,大写字母“A”是65,等等。这样可以很方便的保存在7位(bit)里。现代电脑大部分都是使用8位字节,因此你不仅可以存储每一个ASCII字符,你还有很多位置去做坏事,你可以存一些稀奇古怪的字符。少于32位的代码称为不可打印代码。

一切都很好,假设你的母语是英语。因为我们用8位去存储字符,很多人就会都想到:“嘿嘿,我们可以用128-255位置来存储我们自己的符号。”image 麻烦的是,太多人在同一时间有这个想法了,对于这些富余的位置各有各想法各有各精彩。IBM的电脑提供了一堆叫OEM字符的东西,用来提供欧洲的重音字符和一堆线条。。横的、竖的、垂直右边有个小铃铛的等等。你可以用这些线条字符在屏幕上去堆一个精美的盒子出来,实际上你依然可以在使用8088电脑的干衣机上看到它们的身影。实际上,随着美国以外的人们开始买属于自己的电脑以后,不同的128个字母都被赋予了他们各人的目的。例如,在一些电脑上面字符编码130会显示%uE9,但是在以色列的电脑上它就会显示 %u5D2。所以当美国人想发送他们的r%uE9sum%uE9s 到以色列的话,就会变成了r%u5D2sum%u5D2s。在很多案例中,例如俄罗斯,他们对这128位之后的字符编码的使用有着大量不同的想法,所以你很难去可靠的交换俄罗斯的文件。

最后,OEM决定编纂ANSI标准。在ANSI标准里面,所有人都同意如何使用128位以下的位,这几乎与ASCII相同,但是从128位开始到最后一个编码位的使用就大有不同。这些不同的系统被称为编码页(Code Page)。例如在以色列的DOS里面使用的编码页被称为862,希腊用户用的称为737。他们使用的128位以下都是相同的,但是从128位以后开始就充斥了各种有趣的字母了。MS-DOS国家版本里面有几十个这些编码页,可以处理英语到冰岛语,甚至还可以在同一台电脑里面处理世界语和加利亚语!屌爆!但是,希伯来语和希腊语在同一台计算机上是完全不可能的,除非你编写自己的自定义程序用来显示所有使用位图的图形,因为希伯来语和希腊语需要不同的编码页与高数字。

与此同时,亚洲有着成千上百个字符,这些远远不能放进8位里面。这一样会通过称为DBCS的五花八门的系统来解决,“双字节字符集”里面存着字符一些占用了一个字节,一些占用了两个字节。这样在一个字符串中很容易可以向前移,但是很难向后移。程序员最好不要使用s++和s-去向前或者向后移字符串,鼓励使用例如window里面AnsiNext和AnsiPrev的方法来避免混乱。

但是,只要从来不把一个字符串移动到另一台电脑的话,或者不讲另一种语言的话,把一个字节当作一个字符或者把一个字符当作是八位是可行的。但当然,随着互联网发展,字符串从一台电脑发送到另一台电脑变得相当普遍。就在这个时候,Unicode出现了。

Unicode

Unicode致力于创造一个可以适应所有地球上操作系统的单一字符集。有些人误解Unicode只是一个简单的16位编码,每个字符占用16位所以只能保存65536个字符。这!一点!都不对!这是一个属于Unicode的神话,你不懂别bb。

实际上,关于字符,Unicode有自己的一套独到的思考方式,如果你不能了解它的想法,你会感受不到它的好。

现在,我们来假设一个字母映射到一些位中,你可以把它存在硬盘或者内存中。

1
2
3
4



A -> 0100 0001

在Unicode中,一个字母映射会被映射到一个称为 编码点 [0]的东西上,当然它只是一个概念。如何将 编码点 在硬盘或者内存中表现出来才是完整的故事。

在Unicode中,字母A只是一个柏拉图式的念想,只是漂浮在天堂上:

1
2
3
4



A

这个柏拉图式的A跟B不一样,跟a也不一样,但是跟A 和 AA 都一样。不同字体之间的字符A 都是代表同一个A,但是不同于小写 “a”,看起来无可非议,但是在一些语言里面弄清楚一个字母是有很多争议的。德国的字母 %uDF 是真实存在的字母还是只是一种特殊的写作方式?如果一个字母在单词里面最后一笔发生了变化,这是一个新的字母吗?Hebrew说是的,Arabic说不是。无论如何,在Unicode联盟里面的那堆聪明人经过大量的讨论辩论,这些问题已经解决了。

每个柏拉图式的字母会由Unicode联盟通过每个字母表来赋予一个魔术代码给它们,它是这个样子的:U+0639 。 这个魔术代码就是 编码点 。 编码点里面的 U+ 代表 “Unicode” ,后面的数字是十六进制数。 U+0639 是阿拉伯的字母 Ain。 英语的字母A则是 U+0041 。你可以通过 Windows 2000/xp 里面的 charmap 来查询它们,或者登陆Unicode官网查询。

Unicode可以定义的字母数量并没有真正的限制,实际上它们超过了65,536,所以不是每个unicode字母都可以被压缩成两个字节,这只是一个神话。

好的,我们现在来个例子:

1
2
3
4



Hello

在Unicode里面,对应的五个编码点是:

1
2
3
4



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

反正就是一堆编码点数字什么的了。谈到这里,我们还没有提到如何把这些编码保存在内存里面和如何在一封电子邮件里面重现。

Encodings (编码)

是时候让 编码 出场啦。
Unicode编码最早的想法就是来源于那个两个字节的神话,只需要将这些数字分别存在两个字节里。就变成这样:

1
2
3
4



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

是吧?好像不太快吧?不可以是这样吗?

1
2
3
4



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

在技术上,我相信可以做到的。事实上,每个实现者都希望以高地址和低地址的模式【译者注:这里提到的可以去看一下字节对齐更好理解】来存储他们的Unicode编码点。这样的话无论是任何CPU来处理,速度都是相当快的。所以人们不得不提出在每个Unicode字符串的开头存储一个FE FF的奇怪约定;这个被称为 Unicode Byte Order Mark(BOM),如果你交换了你的高低字节,那么它将会看起来是 FF FE 这样,那么其他人读到你的字符串的时候就可以知道需要把字节交换回来了。 当然了,不是每个Unicode 字符串开头都会有BOM标记的。
image

这看起来已经相当好了,当时有些程序员会抱怨。“看看那些 0 !”,他们说。因为美国人很少会用高于 U+00FF 的编码点。【译者注:然后这里一段都是讽刺德州人和加州人。。没什么营养,略去。】大意就是这些用惯了ANSI和DBCS字符集的人懒得转换使用Unicode,并且固定长度的Unicode会导致一些字符会保存大量的0字节,因此Unicode被忽略了很多年。

因此,辉煌的UTF-8被提出了。 UTF-8是使用另一个系统来存储你的Unicode编码点字符串,那些模式U+数字在内存里使用8位字节,而在UTF-8里面,每个0-127的编码点都被存在了一个独立的字节里。只有当编码点大于等于128的时候才会只用第二第三个字节,实际上,最高可以允许使用六个字节。

image

这种做法使得UTF-8看起来就跟ASCII一样整洁,因此美国佬没有发现出区别。
只有世界上其他的人才能跳出思维圈圈。具体来说,Hello, 编码点串为 U+0048 U+0065 U+006C U+006C U+006F,会被保存为48 65 6C 6C 6F。看啊哇塞!这跟保存在ASCII或者地球上其他的OEM字符集都一样啊!现在,你可以大胆地使用重音字母或者希腊字母或者克林贡字母,即使你不得不使用几个字节来保存一个编码点,但是美国佬们永远都不会注意到。(UTF-8也有一个不错的属性,那个想要使用单个0字节作为空终止符的无效旧字符串处理编码不会被截断)。

到目前为止,我已经讲述了三种Unicode编码。传统的保存在两个字节的方法被称为UCS-2(因为它有两个字节)或者UTF-16 (因为它有16位),而且你依然需要区分这是高位UCS-2还是低位UCS-2。还有很受欢迎的UTF-8。

实际上有一些其他的Unicode编码方法。 有一个叫UTF-7,非常像UTF-8,但是它保证高位总是为零,因此如果你要通过某种恶意的警察邮件系统来传递Unicode,UTF-7是足够的了。还以一种UCS-4,会把每个编码点保存到4个字节里面,优点是每个编码点都能草存到相同大小的字节里面,但是,节俭的德州人会很在意这些浪费的。

事实上,你正在思考使用Unicode编码点来表示柏拉图式字母,那些Unicode编码点也可以用任何老式的编码方案进行编码!举个例子,你可以对Unicode字符串Hello (U+0048 U+0065 U+006C U+006C U+006F) 进行ASCII编码,或者古老的OEM希腊语编码,或者Hebrew ANSI 编码,或者任何其他有上百年历史的编码方式。你会发现,都会是相同的结果:一些字母不会被显示出来!它会显示:? 或者�。

几百种传统的编码方法都只能正确的保存部分编码点,其他的编码点都会显示问号。一些流行的英文文本编码是Windows-1252(西欧语言的Windows 9x标准)和ISO-8859-1(也称为拉丁语1)(也适用于任何西欧语言)在尝试保存俄文或者Hebrew字母的时候,都会得到一大堆问号。UTF 7,8,16和32则再这方面都有着很好的表现。

关于编码的一个重要的事实

如果你已经忘光了我刚刚解释的一切,请记住一个事实。只有字符串而不知道它使用的编码,这是毫无意义的。你不可以再把头埋在洞里,假装这个文本使用的是ASCII。

没有没有这样的事情作为纯文本

如果你有一个字符串在内存,在文件或者在电子邮件信息中,你必须知道它使用的编码否则你不能正确地读到它。

“我的网站看起来乱七八糟”或者“她不能看懂我的电子邮件当我说了重音拼音”等等这些愚蠢的问题都归咎于天真的程序员,由于他根本不知道你的信息所使用的编码。

我们能不能找到有用的信息来显示字符串使用的编码方式呢?答案是有的,我们通过标准的方法来达到这个目的。对于电子邮件信息,你应该在表单的头部有一个字符串。

1
2
3
4



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

对于网页来说,最初的想法是网站服务器会返回一个类似 Content-Type 的HTTP头部信息以及网页本身 —— 不是HTML本身,作为在HTML页面发送之前的响应头之一。

但这会引起问题。假设你有很大的WEB服务器,拥有大量的页面和成百上千的页面是由不同语言的人提供的,那么所有使用的编码将会由Microsoft FrontPage选择认为适合的编码方法来生成。Web服务器本身不会真正知道每个文件的编码方式,因此无法发送Content-Type头文件。

如果你可以使用某种特殊标签将HTML文件的Content-Type正确放置在HTML文件本身,那将会很方便。当然,这会让纯粹主义者疯狂…你怎么能通过读HTML文件来确定什么编码?幸运的是,几乎每个常用的编码间32到127之间的字符都相同,所以你可以随时在HTML页面上得到这个,而不用担心使用“有趣”的字母:

1
2
3
4
5
6
7
8



<html>

<head>

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

要记住这个meta标签必须是块里面非常靠前定义的标签,因为当浏览器看到这个标签的时候就会停止页面的解析并且开始使用你指定的编码重新开始解析整个页面。

当浏览器找不到定义 Content-Type 的meta 标签而且http头也没有定义的时候怎么办?IE浏览器通常会做一个有趣的事情:它会尝试去猜测,基于各种语言的典型编码中的各种字节出现在典型文本中的频率,来决定使用什么语言和编码【译者注:IE大哥别乱猜啊。。】。因为各种旧的8位编码页往往将其国家字母放在128到255之间的不同范围内,并且因为每种人类语言都有不同的字母使用特征直方图,这实际上有一个可行的机会。这真的很奇怪,但这看起来的确可行,直到有一天他们写了一些完全不能根据字母频率分布来确认语言的东西出来,IE判定它是韩语并显示出来,证明这不是很好的方法。

这篇文章变得很长,我不可能涵盖所有关于字符编码和Unicode的知识,但是我希望如果你已经读到这里的话,尝试去更多的地方去了解Unicode,会让你更获益良多。

[0] 编码点,Code point,不同地方也有翻译成码点或代码点。