字符编码(1)-- 基本概念

 

在开发过程中,字符的编码问题经常让我们这些程序猿头疼不已,例如臭名昭著的“中文乱码”问题,我相信我不是唯一一个曾经“深受过其害”的人,因此我准备用几篇文章来总结一下字符编码相关方面的内容,力图做到通俗易懂,并以此希望能够给正处于水深火热中的XDJM们提供一点帮助。

首先是第一篇基本概念。本篇仅仅是粗略地讲解,因为字符编码这个话题很大,可以说它涉及到开发过程中的方方面面,要想细致深入是不可能的。幸运的是对于这次讨论的主题来说,我的目的是用一根绳子将字符编码所涉及到的知识点串起来,重点在原理上,至于繁枝末节的细节不会过多深入。


一、字符编码、字符集、字符传输编码

用一种特定的形式来表示字符的方法,就是字符编码。例如ASCII用0~127之间的整数来表示字符;Unicode将这个范围极大地扩展了,因此Unicode可以表示更多字符;还有莫斯码,这是一种使用“点”(dot)和“划”(dash)来表示字符或词组的编码,曾被广泛用于电报等无线电传输中。

在计算机中,字符通常用整数进行编码,根据不同的要求,一个字符可能需要一个或多个字节来表示。

字符集,或称字符编码集,是一个集合,其中的字符按照相同方式进行编码。例如ASCII、Unicode、莫斯码,等等。相同的编码在不同字符集中可能表示不同的字符。

字符传输编码也是一种字符的编码,主要是为了满足字符的传输、存储等实际应用的需要。字符传输编码基于某种字符集编码,但针对传输、存储等进行了优化,使其能够更好地满足实际应用。最常见到的字符传输编码可能要数UTF了,而UTF中最著名的又非UTF-8莫属。

 

二、设计一种字符编码的要点

字符编码的设计看似简单,好像只需将一堆字符按整数依次进行编号就完事了。但实际却完全相反,设计一个好的字符编码涉及到的和需要考虑的问题非常之多,绝不是简单的排列与组合。ASCII的专家组前后用了3年时间才出了第一个版本,第一个主要版本更是花费了7年时间。下面列举了一些在ASCII的设计过程中必须考虑的问题。

  1. 要包含哪些字符

    第一个要考虑的问题是ASCII要包含哪些字符。这个问题主要是权衡编码长度与字符总数之间的平衡——尽可能用最少的码位包含尽可能多的常用字符。开始时,专家组想将ASCII设计为6位编码,可以包含64个字符,后来增加到了7位。8位的方案也考虑过,但最终被否决了。

  2. 变长还是定长

    可变长度编码的优点是能够按照优先级(或使用频率)来安排字符,从而将最常用的字符以较短的长度进行编码,不常用的字符则使用较多的位来编码,从而达到总体最优。莫斯码和UTF-8都是变长编码。

    但变长编码的最大缺点是发生错误时会产生连带性,即一处发生了错误很可能会影响到其后多个甚至所有的位。这是由于变长编码需要在前面的位中包含一种标志,以指示其后面是否还有更多的位。如果标志位出错,会导致对后续多个位的解释都会发生错误。

    在ASCII的设计过程中,专家组关于是否要使用“换档键”(Shift Key,也称为上档键)产生过争论。如果当时使用换档键,现在的ASCII就是变长编码了。简单地说,换档键的作用就是改变对后续字符的解释,将一个字符变为另一个字符,例如在小写字母a前加上换档键时,需要将a解释为大写的A。

    如果使用换档键,ASCII可能只需要6位(但需要包含一个换档键的编码),但专家们权衡再三,最终没有采用这种方案,因此ASCII需要至少7位编码。

  3. 兼容性

    很少有发明和创造是孤立的,创造一种新的事物时总是需要考虑那些人们已经习惯了的旧的事物。一个有名的幽默小故事叫做“马屁股决定航天飞机的尺寸”(其真实性不得而知),说的是虽然马屁股与航天飞机两者在表面上完全看不出联系,但在人类逐渐发展和创造的过程中,从马屁股时代留来的影响却一直存在,甚至间接地决定了今天航天飞机的尺寸。

    当时存在于电气、电话和电报等行业的主流编码标准包括CCITT、ITA2、EBCDIC等,在设计ASCII时如何兼容这些编码是必须考虑的事情。

  4. 其他细节

    还有很多问题需要考虑,比如下面列举的一些ASCII的决策和方案(仅仅是所有细节中极小的一部分)。

    ASCII按照类别来组织字符,控制字符、图形字符、特殊字符、字母数字字符等都尽量集中并连续编码;

    为了排序上的方便,空格键被安排在图形字符前,因此其所在位置为0x20(十进制的32);

    与空格键一样,很多特殊字符也按照不同目的被安排在特定位置;

    专家们还考虑了使ASCII能够很容易地“降格”到6位编码,以适应特定需求;

    类似地,为了在使用ASCII时可以方便地使用其中的小写字母或大写字母的子字符集,ASCII没有将小写字母和大写字母交叉放置(aAbB这样);(举个例子,如果你了解正则表达式,就会知道可以使用[a-z]、[A-Z]来分别引用全部小写字母或大写字母,如果ASCII当时选择将大小写字母交叉放置,那么我们今天想单纯地判断小写字母集合或大写字母集合就会变得困难得多了)

    大写字母和小写字母的编码值相差16,因此相同的字母的大写和小写编码仅有一位不同,无论是比较还是计算都非常方便;

    数字0~9的ASCII编码值是48~57(十六进制的0x30~0x39),所以将编码值减去0x30,或者直接取字节的低4位(编码的二进制形式中7位的高3位都是011)就能得到字符的实际值。

    等等……


尽管在设计时需要考虑和权衡的细节如此之多,但令人惊讶的是,专家们当年的决策几乎都是最优的,直到今天,无数人直接或间接地从ASCII的简单、高效、方便等特点中获益。所以不得不感慨当年专家们的高瞻远瞩啊!


三、为什么需要字符传输编码

有多种原因使得一种字符集的原始编码不能很好地应用于传输、存储等过程中。这不只是技术或设计的原因。因此很多时候,还需要为某个字符集设计一套用于实际传输的编码方式。

  •     为了提高传输效率

    现在谈到ASCII,可能我们都认为这是一种8位编码,即每个字符为一个字节。确实是这样。但在早期,ASCII码却是7位编码,所有字符的编码集中在0~127之间,因此每个字符的最高位总是0。因为在那个时候什么都贵,内存、网络、甚至硬盘(其实那时候还没有硬盘,只有纸带或软盘,存储容量非常有限),这些硬件的限制使得人们不得不最大限度地节约每一个字节,甚至每一位。所以虽然在设计上,ASCII是8位编码,但早期的很多系统和应用中都使用7位来传输,然后再通过程序对接收到的字节进行分割,每7位为一个字符,这样就能同时提高传输效率和节约存储空间。

    另一个例子是UTF-8。UTF-8是可变长度的编码,每个Unicode字符被映射为1~4个字节的编码,其中前128个字符与ASCII完全相同,而每个汉字被编码为3个字节。UTF-8的这种特点使得它很适合编码英文字符占多数的内容,此时它的效率接近单字节编码,但如果通篇都是汉字,可能UTF-8并不是最佳的选择,此时它比原始的Unicode效率还要低。

  •     为了适应传输要求

    字符集对其中的每个字符都指定了一个编码,但却不会规定应该如何用其进行传输或存储。因为这是与实际应用相关的,无法强制规定。最常见也是最著名的一个例子就是大小端问题或称字节序问题。简单来说,字节序问题就是当需要一次处理多个字节时,规定哪个字节在先哪个在后。

    例如当程序读到“FF FE”时(16进制),如果按照读取时的先后顺序进行解释,这个数就是“FFFE”,如果以相反的顺序解释就成了“FEFF”。在这里,前一种方式就是大端,后一种是小端。不要在意细节,只需明白不同的解释会得到完全不同的结果。

    我们熟知的X86及其兼容指令集系统采用小端方式,PowerPC处理器采用大端方式,包括TCP/IP在内的多数网络协议也是大端方式。

    例如,采用UTF-16编码(一种双字节编码)的文本文件,其开头两个字节通常是“FF”和“FE”。在Unicode中,“FFFE”或“FEFF”都不代表任何有意义的字符,因此如果一个程序接收到或读取到了来自另一个程序或流中的字符序列时,发现开头是“FFFE”或“FEFF”,就知道这是一个采用UTF编码,并且采用了某种字节序——具体来说,如果开头是“FFFE”就是大端,另一种是小端(见前面的例子)。在文本文件或字符流中,像这种仅用于标记字节序的字符称为“BOM”,即Byte Order Mark。

    注意:单字节编码没有大小端问题,只有多字节编码才可能会有此问题,因为单字节编码(例如ASCII和UTF-8)一次只需处理一个字节。

四、Unicode、UCS及UTF

这几个词容易让人迷惑,现在是时候来解释这个问题了。

Unicode是一种字符集,UTF是Unicode的一种具体的传输格式,这个前面已经讨论过了。

UCS在不同的场合下有多种意思:

  •     首先是作为术语“通用字符集”来讲。此时可以认为它是一种想法,或一种提法,其目标是设计一个通用的字符集来囊括世界上所有的书写系统字符。Unicode和下面要谈到的ISO UCS是它的两种实现。

  •     作为一种标准或实现来理解。它是大名鼎鼎的国际标准化组织ISO所制定的“ISO/IEC 10646”标准,该标准类似于Unicode。上世纪90年代,人们认识到同时存在2种具有相同目标但实现不一致的的标准会带来麻烦,因此这两种标准最终走到了一起。现在ISO和Unicode已基本一致了。
  •     作为一种传输编码。和UTF是Unicode的传输编码一样,UCS传输编码实现了ISO UCS标准。它们包括3种具体实现:UTF-1、UCS-2和UCS-4。这里的数字后缀表示的是字节数,而不是像UTF中的那样表示位数。目前,UTF-1(确实是UTF-1而非UCS-1)已经被UTF-8所取代,UCS-2和UCS-4也逐渐地被UTF-16和UTF-32所取代,而这也说明了ISO UCS标准和Unicode确实在逐渐变得一致。

在非严格的场合,人们通常会将ISO UCS标准和Unicode混为一谈,统称为Unicode,这也是UCS标准没有Unicode出名的原因。对于第一种情况,作为一个术语来讲时,也可以用Unicode来代表。所以一般情况下,用Unicode(或UTF)来代替UCS不会引起什么问题。

 

五、Windows中的代码页和ANSI编码

Windows中的代码页(Code Page)与其他系统的“字符编码”是一个意思,Windows用代码页来设置系统的默认编码(即本地化编码,Locale)。例如代码页936表示简体中文。代码页最早是IBM发明的,而不是微软。其实很多系统都有使用代码页,只是Windows中的最为人所熟知。

代码页使用数字来代表某种编码,例如在Windows中,代码页936即GBK(或GB2312)编码,65001代表UTF-8。

在Windows中我们会经常用到系统自带的记事本,当使用记事本的保存功能时,在编码一栏中通常默认的编码是“ANSI”。ANSI编码实际上就是当前代码页指定的编码,因此设置的代码页不同,ANSI代表的编码也不同。


六、为什么人们不能统一用一种编码呢,比如Unicode

当然这是一种美好的愿景,人们也正在努力地去实现。但目前还有很多问题没有解决。

  •     历史遗留问题

    在Unicode这样的统一字符集出现之前和之后,很多系统、程序、文件,甚至硬件中,都使用不同的字符集。大多数情况下,修改它们使其支持Unicode是不现实的。

  •     根深蒂固的思想

    对于英语国家的多数人来说,让其放弃使用ASCII这样的简单而高效,并且能够很好地满足他们的需求的编码而转用Unicode可能有些困难。即使是非英语国家,为了最大程度地实现与其他系统的兼容,尤其是那些已运转多年的老系统(这似乎又回到了上一条),人们可能还是会优先选择本地化的字符集。

  •     固有的困难

    Unicode只能解决编码问题,但实际应用中还有很多问题存在。例如字体,大部分字体只支持有限的字符,例如中文字体通常不会包含日文、俄文或已经消失的文字字符(而且通常也没有必要)。实际上,有成千上万种字体,但能够包含大多数Unicode字符的字体寥寥可数。

但并不是只有坏消息,好消息也是有的。比如人们也正变得越来越容易接受并习惯Unicode;并且越来越多的新系统、新技术、新标准都采用了Unicode。例如Windows、Linux、还有Java等等,W3C也在新的标准中鼓励人们使用Unicode。

七、总结

不要将思维局限在计算机中。广义上讲,在信息传递的过程中,编码是必须的,并且是无处不在的,例如语言、文字、音乐、绘画,都是编码。实际上,你说的话、写的字,其实质都是为了表达你的想法,因此它们都是对你的想法的不同形式的编码。

 

接下来的内容计划包含以下几个部分:

第2篇讨论编码中的一些细节,以及编码之间的转换和产生乱码问题的原因

第3篇:TODO

 

 

 

posted @ 2013-09-11 00:12  Antineutrino  阅读(693)  评论(0编辑  收藏  举报