昨天正好碰到跟字符编码相关的问题,就查了点资料。总算是把几个概念搞的比较清楚了,在这儿写下来分享一下。

计算机内部使用二进制来存储内容,对于我们使用的书写符号就涉及到了如何将书写字符与二进制数进行对应的问题。这个过程就是字符编码。字符编码从最早的ASCII起,到现在的Unicode,经历了相当大的变迁……什么?“极大的方便了人们的生产和学习生活”?完全不是啦……是给人们的生产和学习生活带来了非常多的麻烦=。=


I. ASCII

American Standard Code for Information Interchange,美国信息交换标准码。是一种单字节字符编码,也是迄今为止最为通用的字符编码。关于它的来龙去脉不管是百度还是维基甚至大计基的老师讲的恐怕都比我详细的多,我也就不再赘述了。

值得注意的是,ASCII码初期只有0-127共128个字符/控制符,只占用一个字节8bit中的7bit。后来128以上的部分也被利用起来,在部分标准中填充了一些特殊的图形符号和制表符等,用于在字符界面下绘制表格之类的共用。老美觉得很满足:你看不仅英文和数字表现的很好,我们还能画出表格。

而对于其它语言和地区的计算机用户来说就完全不够了,先不提字符量更大的中日韩等语言,也放过拉丁语系里字幕存在上下标志的情况,仅就其它的英语国家而言就不乐意:就连货币单位ASCII码里也是只有$一个啊!英国用户用的可是英镑(£)。

所以后来对ASCII码进行扩充的时候存在了各种各样的方法和标准,也就是下面所说的,字符编码最为混乱的一个时期:ANSI。

II. ANSI与DBCS

ANSI本身指美国国家标准协会。它指定了一个ASCII的扩充标准(0-255),里面包含了一些欧洲国家的字母。但是通常我们在提到ANSI的时候,实际上指Windows里面一种被叫做ANSI的字符编码方式,“Windows代码页”。而这个……其实跟ANSI这个组织半毛钱关系没有。

重新谈回ASCII的扩充问题上。原始的ASCII码只占用了一个字节中的低7位,最高位为0。当计算机引入非英语国家,面临新的字符编码需求的时候,我们就开始在ASCII码最高位为1,数字范围为128-255的部分做手脚,做出了如下规定:一个小于127的字符的意义与原来相同,但是大于127的字符将和后面一个字符连在一起,组成一个双字节共同表示一个字符。这样一下子就将字符能够表示的范围大大扩展了。这种方式就称作Double Byte Character Set,DBCS。

以国内为例,早期制定的标准是连续两个大于127的字节合并表示一个字符,这个标准被称作GB2312。日常使用的语言基本够用,但是遇上人名等就比较麻烦。于是后来又进行了扩充,不再要求后一个字节是大于127的,这样又能使用更多的空间了,这个标准被称作GBK。

DBCS的一个重要特点是,在编码存储的文件中,英文字符依然保持单字节编码,而非英文字符才会呈现出双字节编码的状态。而这一点也就是众多程序员所熟悉的“一个汉字等于两个英文”的来历。这种情况下,用单字节表示出的字符被称作半角字符,而双字节表示的字符被称作全角字符。所以听好了!全角字符不是因为长得宽才叫全角字符的!

问题在于,DBCS这种方式各个国家和地区均有自己的标准。在不同标准下,同样的两个字节所表示的字符是不同的。仅以汉字为例,就有TW制订的繁体Big5标准和大陆的GB系列标准。日语使用的是Shift-JIS,韩语是EUC-Kr。在同一个文档中,无法同时使用两种或以上的编码方式:一篇中日夹杂的文章,要么中文正常日文乱码,要么中文乱码日文正常。

在Windows当中,就有一个叫做Code page代码页的设置。代码页会指明字符的编码方式是什么。在运行非Unicode编码的应用程序的时候,需要指定代码页来使程序中的文字得到正确的解析和显示。常见的代码页有:CP936指中文简体,CP950为中文繁体。Windows默认的代码页是CP1252,一种以西欧拉丁字符为扩展对象的编码,这个编码标准据说曾经被提交给ANSI作为ASCII的标准扩充标准,但是后来没有采用。不过CP1252的“ANSI Latin-1”的名字却留下来了。

举一个更为好多人所熟悉的例子,许多爱玩GalGame的同学在运行日文原版游戏的时候会遇到乱码。这时候就要借用AppLocale这个工具。这个工具的实际作用是以指定的Code page来运行程序,这样日文程序就能在非日文系统里以正确的方式进行解码显示了。

ANSI DBCS标准的多样性直接导致了跨编码交流的困难。随着国际交流的增强、因特网的出现等多种因素,新的字符编码标准被提出来了,那就是Unicode。

III. Unicode与UTF

Unicode由ISO主持制定,将完全使用两个字节来进行编码。即使是单字节的英文字符,也会在高位上填充上0。多达65535的字符空间,已经足够将现有的所有文化字符包括在内进行显示了。即便如此,ISO还准备了UCS-4的方案,使用4个字节进行编码以防将来不够用。相对的,现有的双字节Unicode标准被称作UCS-2。

Unicode虽然编码了世界上的各个语言的字符,但是很显然,它不会与现有的任何编码标准直接兼容。而Unicode在编排字符顺序的时候,似乎也没有参考现有的编码方式。因此GBK之类的编码方式向Unicode转化的时候,只能用查表的方式,而无法使用简单的算术运算进行。

Unicode是一种双字节编码方式,但是缺点在于,当它处理目前使用最广泛的英文字符的时候,会有一个字节全部为0。这是对空间的极大的浪费。尤其在传输的时候,前后相差一倍的速度。而且对于很多程序来说,0x00有着其他重大的意义,直接使用UCS-2进行存储的时候,里面大量的0x00会导致严重的错误。而这种错误只能通过重写程序来解决。

因此人们制定了其他的Unicode编码标准,居于ASCII和Unicode之间,用于存储和传输等用途。这些标准被称作Unicode Transformation Format,即UTF格式。目前最常用的,就是UTF-8格式了。

UTF-8使用1-6个不等的字节来编码Unicode。具体的编码方式不再赘述(其实是要画表格好麻烦我好懒我会乱说么),只要知道UTF-8的一些特点就行了:它和DBCS运作的方式非常相似,英文字符会保持与ASCII编码一致的编码方式,只占用一个字节。而高于128的字符会根据特定的方式进行转换表示。

UTF-8有效解决了UCS-2在实际应用中的一些问题。而且UCS-2因为采用双字节存放,需要考虑字节序的约定问题,而UTF-8则不需要字节顺序的标志。


但是你真的天真的以为Unicode一出就解决了编码混乱的问题么?才不是呢……Unicode再大统一,也是新的编码方式,只是在当前多种多样的编码方式上又加了一种罢了。很多老的站点依然采用GBK等编码,而新的站点也不是家家都用UTF。所以乱码依然存在。

值得注意的是,编码方式本身并不规定存储或传输时如何标明自己。很多纯文本编辑器只能通过字节的特征来判断所采用的编码方式。比如如果一个文件完全是小于128的字符,则可以放心大胆的直接用ASCII解码。如果出现了大于128的字符,就考虑使用DBCS系列的编码方式——而且这种情况下即使错了也不能怪我对吧!如果出现了一个110开头的字节紧接着一个10开头的字节,则有可能是UTF-8编码的文件。(这一点和UTF-8的编码方式有关)

一个很著名的实验就是:打开记事本,输入“联通”两个字,然后简单的保存退出。然后再重新打开这个文件,你会发现原本的联通两个字神奇的变成了乱码!这就是因为两个字符在GBK编码下的字节特征与UTF-8相像,而记事本在打开时选择了错误的解码方式的缘故。想解决这个问题只要多打几个字,让记事本有足够的判断条件就可以了。


P.S. UTF-8与BOM

BOM是Byte Order Mark的缩写。很多玩WordPress的童鞋或者入门PHP开发的同学都会被提醒千万不要使用记事本编辑php的源码文件。为什么呢?因为记事本在保存UTF-8的格式的文件的时候,会在文件头部添加三个字节的BOM文件头标志,以表示这是一个UTF编码的文件并提供字节序的信息。问题在于这件事情只有微软在做,并不是一个关于纯文本文件的统一标准(好吧纯文本文件哪来的标准……)。因此当这种带有BOM标记的文件放在其他非M$平台的程序里使用的时候就会发生解析错误的问题。

——等等,你不是说UTF-8不需要字节序标记么?
——……我怎么知道这是为什么=。=!