下面这篇文章出自 Mark Pilgrim 所编写的《Dive Into Python3》,这是我所看过的讲解文本编码的最通俗易懂的一篇文章。
I’m telling you this 'cause you’re one of my friends.
My alphabet starts where your alphabet ends!
你是否知道 Bougainville 人有世界上最小的字母表?他们的 Rotokas 字母表只包含了12个字母: A, E, G, I, K, O, P, R, S, T, U, 和 V。另一方面,像汉语,日语和韩语这些语言,它们则有成千上万个字符。当然啦,英语共有26个字母 — 如果把大写和小写分别计算的话,52个 — 外加少量的标点符号,比如!@#$%&
当人们说起“文本”,他们通常指显示在屏幕上的字符或者其他的记号;但是计算机不能直接处理这些字符和标记;它们只认识位(bit)和字节(byte)。实际上,从屏幕上的每一块文本都是以某种字符编码(character encoding)的方式保存的。粗略地说就是,字符编码提供一种映射,使屏幕上显示的内容和内存、磁盘内存储的内容对应起来。有许多种不同的字符编码,有一些是为特定的语言,比如俄语、中文或者英语,设计、优化的,另外一些则可以用于多种语言的编码。
在实际操作中则会比上边描述的更复杂一些。许多字符在几种编码里是共用的,但是在实际的内存或者磁盘上,不同的编码方式可能会使用不同的字节序列来存储他们。所以,你可以把字符编码当做一种解码密钥。当有人给你一个字节序列 — 文件,网页,或者别的什么 — 并且告诉你它们是“文本”时,就需要知道他们使用了何种编码方式,然后才能将这些字节序列解码成字符。如果他们给的是错误的“密钥”或者根本没有给你“密钥”,那就得自己来破解这段编码,这可是一个艰难的任务。有可能你使用了错误的解码方式,然后出现一些莫名其妙的结果。
你肯定见过这样的网页,在撇号(’)该出现的地方被奇怪的像问号的字符替代了。这种情况通常意味着页面的作者没有正确的声明其使用的编码方式,浏览器只能自己来猜测,结果就是一些正确的和意料之外的字符的混合体。如果原文是英语,那只是不方便阅读而已;在其他的语言环境下,结果可能是完全不可读的。
现有的字符编码各类给世界上每种主要的语言都提供了编码方案。由于每种语言的各不相同,而且在以前内存和硬盘都很昂贵,所以每种字符编码都为特定的语言做了优化。上边这句话的意思是,每种编码都使用数字(0–255)来代表这种语言的字符。比如,你也许熟悉ascii编码,它将英语中的字符都当做从0–127的数字来存储。(65表示大写的“A”,97表示小写的“a”,&c。)英语的字母表很简单,所以它能用不到128个数字表达出来。如果你懂得2进制计数的话,它只使用了一个字节内的7位。
西欧的一些语言,比如法语,西班牙语和德语等,比英语有更多的字母。或者,更准确的说,这些语言含有与变音符号(diacritical marks)组合起来的字母,像西班牙语里的ñ。这些语言最常用的编码方式是CP-1252,又叫做“windows-1252”,因为它在微软的视窗操作系统上被广泛使用。CP-1252和ascii在0–127这个范围内的字符是一样的,但是CP-1252为ñ(n-with-a-tilde-over-it, 241),Ü(u-with-two-dots-over-it, 252)这类字符而扩展到了128–255这个范围。然而,它仍然是一种单字节的编码方式;可能的最大数字为255,这仍然可以用一个字节来表示。
然而,像中文,日语和韩语等语言,他们的字符如此之多而不得不需要多字节编码的字符集。即,使用两个字节的数字(0–255)代表每个“字符”。但是就跟不同的单字节编码方式一样,多字节编码方式之间也有同样的问题,即他们使用的数字是相同的,但是表达的内容却不同。相对于单字节编码方式它们只是使用的数字范围更广一些,因为有更多的字符需要表示。
在没有网络的时代,“文本”由自己输入,偶尔才会打印出来,大多数情况下使用以上的编码方案是可行的。那时没有太多的“纯文本”。源代码使用ascii编码,其他人也都使用字处理器,这些字处理器定义了他们自己的格式(非文本的),这些格式会连同字符编码信息和风格样式一起记录其中,&c。人们使用与原作者相同的字处理软件读取这些文档,所以或多或少地能够使用。
现在,我们考虑一下像email和web这样的全球网络的出现。大量的“纯文本”文件在全球范围内流转,它们在一台电脑上被撰写出来,通过第二台电脑进行传输,最后在另外一台电脑上显示。计算机只能识别数字,但是这些数字可能表达的是其他的东西。Oh no! 怎么办呢。。好吧,那么系统必须被设计成在每一段“纯文本”上都搭载编码信息。记住,编码方式是将计算机可读的数字映射成人类可读的字符的解码密钥。失去解码密钥则意味着混乱不清的,莫名其妙的信息,或者更糟。
现在我们考虑尝试把多段文本存储在同一个地方,比如放置所有收到邮件的数据库。这仍然需要对每段文本存储其相关的字符编码信息,只有这样才能正确地显示它们。这很困难吗?试试搜索你的email数据库,这意味着需要在运行时进行编码之间的转换。很有趣是吧…
现在我们来分析另外一种可能性,即多语言文档,同一篇文档里来自几种不同语言的字符混在一起。(提示:处理这样文档的程序通常使用转义符在不同的“模式(modes)”之间切换。噗!现在是俄语 koi8-r 模式,所以241代表 Я;噗噗!现在到了Mac Greek模式,所以241代表 ώ。)当然,你也会想要搜索这些文档。
现在,你就哭吧,因为以前所了解的关于字符串的知识都是错的,根本就没有所谓的“纯文本”。
Unicode 编码系统为表达任意语言的任意字符而设计。它使用4字节的数字来表达每个字母、符号,或者表意文字(ideograph)。每个数字代表唯一的至少在某种语言中使用的符号。(并不是所有的数字都用上了,但是总数已经超过了65535,所以2个字节的数字是不够用的。)被几种语言共用的字符通常使用相同的数字来编码,除非存在一个在理的语源学(etymological)理由使不这样做。不考虑这种情况的话,每个字符对应一个数字,每个数字对应一个字符。即不存在二义性。不再需要记录“模式”了。U+0041总是代表 ‘A’,即使这种语言没有 ‘A’ 这个字符。
初次面对这个创想,它看起来似乎很伟大。一种编码方式即可解决所有问题。文档可包含多种语言。不再需要在各种编码方式之间进行“模式转换”。但是很快,一个明显的问题跳到我们面前。4个字节?只为了单独一个字符?这似乎太浪费了,特别是对像英语和西语这样的语言,他们只需要不到1个字节即可以表达所需的字符。事实上,对于以象形为基础的语言(比如中文)这种方法也有浪费,因为这些语言的字符也从来不需要超过2个字节即可表达。
有一种 Unicode 编码方式每 1 个字符使用 4 个字节。它叫做 UTF-32,因为32位 = 4字节。UTF-32是一种直观的编码方式;它收录每一个Unicode字符(4字节数字)然后就以那个数字代表该字符。这种方法有其优点,最重要的一点就是可以在常数时间内定位字符串里的第N个字符,因为第N个字符从第4×Nth个字节开始。另外,它也有其缺点,最明显的就是它使用4个“诡异”的字节来存储每个“诡异”的字符…
尽管 Unicode 字符非常多,但是实际上大多数人不会用到超过前65535个以外的字符。因此,就有了另外一种 Unicode 编码方式,叫做UTF-16(因为16位 = 2字节)。UTF-16将0–65535范围内的字符编码成2个字节,如果真的需要表达那些很少使用的“星芒层(astral plane)”内超过这65535范围的Unicode字符,则需要使用一些诡异的技巧来实现。UTF-16编码最明显的优点是它在空间效率上比UTF-32高两倍,因为每个字符只需要2个字节来存储(除去65535范围以外的),而不是UTF-32中的4个字节。并且,如果我们假设某个字符串不包含任何星芒层中的字符,那么我们依然可以在常数时间内找到其中的第N个字符,直到它不成立为止这总是一个不错的推断…
但是对于 UTF-32 和 UTF-16 编码方式还有一些其他不明显的缺点。不同的计算机系统会以不同的顺序保存字节。这意味着字符U+4E2D在UTF-16编码方式下可能被保存为4E 2D或者2D 4E,这取决于该系统使用的是大尾端(big-endian)还是小尾端(little-endian)。(对于UTF-32编码方式,则有更多种可能的字节排列。)只要文档没有离开你的计算机,它还是安全的 —— 同一台电脑上的不同程序使用相同的字节顺序(byte order)。但是当我们需要在系统之间传输这个文档的时候,也许在万维网中,我们就需要一种方法来指示当前我们的字节是怎样存储的。不然的话,接收文档的计算机就无法知道这两个字节4E 2D表达的到底是U+4E2D还是U+2D4E。
为了解决这个问题,多字节的Unicode编码方式定义了一个“字节顺序标记(Byte Order Mark)”,它是一个特殊的非打印字符,你可以把它包含在文档的开头来指示你所使用的字节顺序。对于UTF-16,字节顺序标记是U+FEFF。如果收到一个以字节FF FE开头的UTF-16编码的文档,你就能确定它的字节顺序是单向的(one way)的了;如果它以FE FF开头,则可以确定字节顺序反向了。
不过,UTF-16还不够完美,特别是要处理许多ascii字符时。如果仔细想想的话,甚至一个中文网页也会包含许多的ascii字符 — 所有包围在可打印中文字符周围的元素(element)和属性(attribute)。能够在常数时间内找到第Nth个字符当然非常好,但是依然存在着纠缠不休的星芒层字符的问题,这意味着你不能保证每个字符都是2个字节长,所以,除非你维护着另外一个索引,不然就不能真正意义上的在常数时间内定位第N个字符。另外,朋友,世界上肯定还存在很多的ascii文本…
另外一些人琢磨着这些问题,他们找到了一种解决方法:
UTF-8是一种为Unicode设计的变长(variable-length)编码系统。即,不同的字符可使用不同数量的字节编码。对于ascii字符(A-Z, &c.)utf-8仅使用1个字节来编码。事实上,utf-8中前128个字符(0–127)使用的是跟ascii一样的编码方式。像ñ和ö这样的“扩展拉丁字符(Extended Latin)”则使用2个字节来编码。(这里的字节并不是像UTF-16中那样简单的Unicode编码点(unicode code point);它使用了一些位变换(bit-twiddling)。)中文字符比如“中”则占用了3个字节。很少使用的“星芒层字符”则占用4个字节。
缺点:因为每个字符使用不同数量的字节编码,所以寻找串中第N个字符是一个O(N)复杂度的操作 — 即,串越长,则需要更多的时间来定位特定的字符。同时,还需要位变换来把字符编码成字节,把字节解码成字符。
优点:在处理经常会用到的ascii字符方面非常有效。在处理扩展的拉丁字符集方面也不比UTF-16差。对于中文字符来说,比UTF-32要好。同时,(在这一条上你得相信我,因为我不打算给你展示它的数学原理。)由位操作的天性使然,使用UTF-8不再存在字节顺序的问题了。一份以utf-8编码的文档在不同的计算机之间是一样的比特流。
下面这篇短文节选自《LaTeX Notes雷太赫排版系统简介》。
众所周知电脑内部采用二进制编码,因为它易于用电子电路实现。所有字符在电脑内部都是用二进制表示的,字符集的二进制编码被称为字符编码,有时人们也会混用这两个术语。
1963 年 ANSI发布了基于电报码的 ASCII,这就是最早的字符编码,它用 7 位 (bit) 表示了 27 = 128 个字符,只能勉强覆盖英文字符。美国人发明了电脑,他们优先考虑英语是可以理解的。后来随着电脑技术的传播,人们呼吁把字符编码扩充到 8 位也就是一个字节 (byte) ,可以涵盖 28 = 256 个字符。
于是 ISO 在 1980 年代中期推出了 ISO 8859,256 个字符显然也不能满足需要,所以 8859 被分为十几个部分:从 8859-1 (西欧语言) 、8859-2 (中 欧语言) ,直到 8859-16 (东南欧语言) ,覆盖了大部分使用拉丁字母的语言文字。
在 ISO 标准完全定型之前,IBM 就有一系列自己的字符编码,也就是代码页 (code page) ,比如 437 (扩展 ASCII) 、850 (西欧语言) 、852 (东欧语 言) 。IBM 代码页通常被用于控制台 (console) 环境,也就是 MS-DOS 或 Unix Shell 那样的命令行环境。
微软将 IBM 代码页称为 OEM 代码页,自己定义的称为 ANSI 代码页,比如 1252 (西欧语言) 、1250 (东欧语言) 、936 (GBK 简体中文) 、950 (Big5 繁体中文) 、932 (SJIS 日文) 、949 (EUC-KR 韩文) 等。
1981 年,中国大陆推出了第一个自己的字符集标准 GB2312,它是一个 94×94 的表,包括 7445 个字符。GB2312 通常采用双字节的 EUC-CN 编码,所以后者也常常被称为 GB2312 编码;其实 GB2312 还有另一种编码方式 HZ,只是不常用。
GB2312 中没有朱镕基的“镕”字,于是它在 1993 年被扩展为 GBK,包含 21886 个字符。GBK 不是正式标准。2000 年发布的 GB18030 包含 70244个字符,采用四字节编码。GB18030 之前还出现过一个 GB13000,但是没有形成气候。
1990 年 ISO 推出了通用字符集 (universal character set,UCS) ,即 ISO 10646,意图一统江湖。它的容量是一百多万个字符,目前实际使用的有十万个左右。UCS 有两种编码:双字节的 UCS-2 和四字节的 UCS-4。
ISO 之外还有个希望一统江湖的组织:统一码联盟 (e Unicode Consor-tium) ,它于 1991 年推出了 Unicode 1.0。后来两家组织意识到没必要做重复工作,于是双方开始合并成果,携手奔小康。Unicode 从 2.0 版开始采用与ISO 10646-1 相同的编码。
Unicode 主要有三种编码:UTF-8、UTF-16、UTF-32。UTF-8 使用一至四个 8 位编码。UTF-16 用一或两个 16 位编码,基本上是 UCS-2 的超集,和 ASCII 不兼容。UTF-32 用一个 32 位编码,它是 UCS-4 的一个子集。
IETF 要求所有网络协议都支持 UTF-8,互联网电子邮件联盟 (Internet Mail Consortium,IMC) 也建议所有电子邮件软件都支持 UTF-8,所以它已成为互联网上的事实标准。