1.字符集的基本概念
什么是字符集?什么是字符编码?
按照“Character set encoding basics”文中的定义,字符集的编码模型分为以下4个层次
1)抽象字符清单Abstract character repertoire (ACR),无序,无编码;
2)已编码字符集Coded character set (CCS),有序,有编码;
3)字符编码规则Character encoding form (CEF),有序,有编码;
4)字符编码方案Character encoding scheme (CES),有序,有编码,有传输和储存规则(字节序);
这种分层方式,比较偏于学术化,不太容易理解。按我个人的理解,GB2312/GBK/GB18030/ASCII这些字符集编码规则,由于都基于8-bit字节,是属于前三层的,可以认为是三层合一。如果拿Unicode来说明的话,Unicode中定义的所有字符的集合,是第一层;我们通常说的Unicode编码,是指的第二层;现在最常见的UTF-8,是指的第三层。当UCS-4在以8-bit为基础的计算机中存储和传输时,就要涉及字节序的问题,就是第四层,分为big-endian和little-endian。
借用“程序员趣味读物:谈谈Unicode编码”中举的一个记事本例子(内容不同):
1)打开记事本(windows自带的那个),输入“我”;
2)另存为 我_ansi.txt,注意,编码选择“ANSI”;
3)另存为 我_unicode.txt,注意,编码选择“Unicode”;
4)另存为 我_unicode_big.txt,注意,编码选择“Unicode Big Endian”;
5)另存为 我_utf8.txt,注意,编码选择“UTF-8”;
保存完以后,看一下4个文件的大小,很有意思吧,分别是2/4/4/6个字节,再用二进制方式(推荐使用ultraedit)查看一下其中的内容:(高位字节在前)
1)ansi:CE D2
2)unicode:FF FE 11 62
3)unicode_big:FE FF 62 11
4)UTF-8:EF BB BF E6 88 91
第一个文件,ansi,比较好解释,2字节,就是GB2312/GBK/GB18030编码,即简体中文windows的默认内码
第二个文件,unicode,就是Unicode编码,“我”的编码是0x62 0x11,不过前面多了2字节的前导符,FF FE,表示为little-endian
第三个文件,unicode_big,也是Unicode编码,不过前导符变为FE FF,表示big-endian
第四个文件,UTF-8,是在Unicode基础上的二次编码,分别将FE FF(big-endian)和62 11进行了二次编码,详细编码过程参见“程序员趣味读物:谈谈Unicode编码”
常见字符集(字符编码规则)
ASCII,读作阿斯克码,7bit表示,美国国家标准信息编码,是最常用英文字母和符号、数字的集合及编码;它的常见别名是ISO 8859-1 ,Latin1
EASCII,扩展ASCII码,完整的利用一个字节,在ASCII的基础上扩展了一些不常用字符
GB2312,国标中文字符编码,1980年制定并颁布;
GBK,国标码,1995年
GB18030,国标码,2000年
以上这三个编码标准都是向下兼容的,兼容的意思有两方面,其一是指字符的集合,其二是指编码。另外,在微软操作系统中(其实也影响到了Linux领域),经常出现“代码页”(code page)的概念,这些代码页,只是微软自己的定义,可以理解为CP936=GBK。
Unicode,UTF-8,我原来一直以为这两个东东是一回事,后来发现其实理解错了,UTF-8可以理解为是以Unicode为基础进行二次编码的,详见这篇文章:“程序员趣味读物:谈谈Unicode编码”,http://pcedu.pconline.com.cn/empolder/gj/other/0505/616631_1.html
2.关于乱码的思考
什么是乱码
个人认为,如果在储存或传输过程中,计算机中的信息不能被正常解析,从而导致在信息展示的时候出现无法被正确理解的情况,可以认为出现了“乱码”。常见的乱码有两种表现形式:
1)部分中文字符能够正常展示,另外的中文字符被展示为方框;
这种情况多数是由于缺少相应的字体支持,例如,在虚拟机上安装完linux之后,如果没有安装图形界面,默认的字符窗口其实是没有相应的字体支持的,这时的中文只能显示为方框,安装zhcon以后才能够正常展示GBK/UTF8的中文字符。
还有一个场景,部分网页上的字符,并不能被所有浏览器支持,或者该浏览器对某种编码方式的支持不完整,会出现部分字符展示为方框的情况。
另外,如果能够以GBK/GB2312正常展示的网页,如果手工将encoding变更为utf-8,则所有中文字符都会变成方框。
2)几乎所有字符都不能正常展示,许多字符被显示为“?”,或者被显示为一大堆不可理解的古怪字符;
这种情况很可能是由于字符编码不配套,需要具体分析。例如,在浏览器中能够正常显示的页面,如果将其编码更改为其他不兼容的编码,则很多会展示为“?”和乱七八糟字符的组合
乱码产生的原因
产生乱码的原因很复杂,也正是这个原因导致了对乱码问题的分析很难全面和彻底。但是,综合我目前遇到的乱码问题来看,只要将字符展示的过程剖析清楚,一段段的调整,总能找到解决的办法。
字符在计算机中,都是以二进制的方式进行存储的,而且文本本身是不能够标识它使用的编码方式的。也就是说,同一段二进制字节流,可以用很多种不同的编码方式去解码,然后根据解码后的结果(也是二进制字节流),在操作系统中按照预定义好的字体进行展示。所谓字体库,或者字库,其实就是数字和相应展示方式(点阵、truetype等)的组合。计算机本身是不会“体会”到“乱码”的发生的,它只是按照用户选定的字体,根据不同的数字进行展示而已,无论展示的结果如何,都只有人才能判断“乱码”与否。乱码的产生,其实只有两个原因,一是没有使用正确的解码规则来解释字节流,二是使用了错误的展示字体。实际应用当中,编码规则的问题居多。
单字节的编码通常情况下不会出现乱码的问题,特别是英文字符,而双字节由于多数情况下编码规则复杂,另外存在中间截断的问题,会比较复杂。从产生问题的渠道来看,常见的有以下几类:
1)网页展示乱码
多数情况下,可以通过更改页面编码方式来解决。少数情况下,浏览器本身处理多语言字符集有缺陷的时候,无论怎样修改编码方式,都不能彻底解决乱码问题。例如,截至本文定稿,IE9就存在部分UTF-8中文编码无法解析的问题,同样的网页在Chrome和firefox中都没有问题。
2)UNIX/LINUX终端显示乱码
2.1)终端的中文环境;
如果没有合适的中文环境(字库支持),无论解码方式如何正确,也不可能正常展示中文。在常用的终端工具中,例如:Xshell/Secure CRT/Putty,都可以设置终端的字符编解码方式,通常设置的值有两个系列:
其一,GB2312/GBK/GB18030/CP936/ANSI/Default等,其实都是兼容的编码,或者仅仅是名称不一样;
其二,UTF-8,这个是在互联网上最常见的编解码方式了;
另外,如果不是windows下的终端工具,而是系统自身的字符终端,则可以安装字符终端专用的中文环境,例如linux下的zhcon
2.2)cat显示文本文档内容
通过类似cat命令的方式显示纯文本文档的内容,通常只受一个因素的影响,即终端的工具的字符编码方式,常用工具中都可以进行设置。只要文本内容的编码方式与终端的编码方式一致(或兼容),则一定不会出现乱码。
2.3)命令行的中文提示(CLI)
命令行接口Command Line Interface的提示语言,是通过环境变量进行设置的,好几个变量都可以设置,但优先级有区别,其中LC_ALL > LC_XX > LANG,如果想用中文显示提示信息,可以这样设置:
export LC_ALL=zh_CN.gbk
其中zh表示使用中文输出提示信息,gbk表示使用GBK编码方式输出中文提示信息,这个编码方式要与终端的设置一致或者兼容才可以正常显示;
2.4)输入中文信息
从Shell环境输入中文,与vi/vim这种编辑器的情况稍有不同,编辑器的情况放到下一节说明。按照一般的理解(我原来就是这样理解的),只要能正常显示中文的地方,一定能够正常输入中文。但是,实测的情况略有不同,详见下面的表格。
输入中文信息,我暂时只考虑了以下三种情况:
a)在SHELL命令行中输入中文
这种场景下,如果终端字符集是GBK,LC_ALL为UTF-8时,输入的中文字节流乱序(第一个中文字符的高字节被放到字节流的末尾),无法正常展示。
b)使用cat等方式输入中文,并重定向到文件中
这种场景下,任何时候,都能够正常输入中文
c)使用文本编辑器,详见下一节描述
与仅仅显示中文信息不同,输入中文的时候实际上经历了更多的步骤。最开始从终端工具中输入中文编码字节流,然后经过网络协议传输到服务端,服务端收到字节流以后,根据终端设置的情况,再推送显示信息到终端工具,终端工具进行呈现。在SHELL命令行中输入中文不正常的情况,很有可能是由于服务端的处理逻辑不健全。
2.5)文本编辑器,例如vim
文本编辑器的种类很多,emacs/vi等,vi的版本也很多,各个主流UNIX平台的商业版本实现都不相同,还有vim。本文暂以vim为例子进行说明,其他编辑器的情况应该是类似的。可以参考:“让vim认识更多的编码”,http://blog.chinaunix.net/space.php?uid=20147410&do=blog&id=3018800。
遗憾的是,这篇文章并未给出vim处理这三个内部变量的顺序,经摸索,顺序应该为:
1)fileencoding
2)endocing
3)termencoding
这三个内部变量均可以通过set xxx=xxx的方式进行设置,并可以通过set和set all进行查看。另外,上面提到的博文中是使用LANG变量来改变vim的encoding变量值,但由于LANG的优先级最低,实际使用过程中,使用LC_ALL的效果最好,当然其实也可以直接在vim中使用set进行设置。
个人理解,如果仅仅从输入和输出(显示)的角度来看,其实vim等文本编辑工具并没有必要设置3个不同的变量来进行处理,这大概也是大多数商用unix平台的vi版本都没有类似设置的原因。提供3个变量的原因在于,vim试图提供一些编码转换的方式,例如,通过设置fileencoding变量,可以改变vim写入和读出使用的编码,而termencoding仅仅改变显示时使用的编码方式,而encoding其实只是提供缓冲。这与数据库的字符集(编码)处理方式是类似的。也可以这样理解,无论这三个变量设置为何值,其实并不见得不会影响数据的输入和展示。例如,在我们输入输出中文信息的时候,即便fileencoding=encoding=termencoding=iso8859(英文字符集),只要文本文件的编码方式与终端的编码方式一致(兼容),比如都是GB2312,文本信息都可以正常展示和输入。
附表:不同设置情况下的中文显示结果
服务端环境:ubuntu 11.10 服务器版
客户端环境:中文win7+Xshell 4
文件编码 | 终端字符集 | LC_ALL | cat输出 | SHELL输入 | VIM相关 | 备注 | ||||
vim显示 | vim输入 | encoding | fileencoding | termencoding | ||||||
GBK | GBK | GBK | 正常 | 正常 | 正常 | 正常 | GBK | GBK | ||
UTF-8 | GBK | GBK | 乱码 | 正常 | 正常 | GBK | UTF-8 | |||
GBK | UTF-8 | GBK | 乱码 | 正常 | 乱码 | 乱码 | GBK | 如果将encoding或termencoding改为utf-8,则可以正常显示 | ||
UTF-8 | UTF-8 | GBK | 正常 | 乱码 | 乱码 | GBK | UTF-8 | 如果将encoding或termencoding改为utf-8,则可以正常显示 | ||
GBK | GBK | UTF-8 | 正常 | 乱码 | 乱码 | 乱码 | UTF-8 | latin1 | 如果将encoding或termencoding改为latin1,则可以正常显示;或者将encoding和fileencoding改为GBK(cp936) | |
UTF-8 | GBK | UTF-8 | 乱码 | 乱码 | 乱码 | UTF-8 | utf-8 | 如果将encoding或termencoding改为cp936,则可以正常显示 | ||
GBK | UTF-8 | UTF-8 | 乱码 | 正常 | 乱码 | 乱码 | UTF-8 | latin1 | 如果将fileencoding改为cp936,则可以正常显示 | |
UTF-8 | UTF-8 | UTF-8 | 正常 | 正常 | 正常 | UTF-8 |
3)数据库乱码
数据库中与编码/字符集相关的设置主要有两个,一个是数据库本身的编码,另一个是客户端环境的编码。网上有很多关于数据库乱码问题的讨论,多数并没有涉及到问题的本质。数据库中保存的数据,其实与文件方式保存的数据没有什么两样,都只是字节流而已,而字节流本身通常是不能自我标识的,例如,如果仅仅根据二进制的编码,无法判断出它的内容是采用GB2312编码,还是EASCII编码,或者是一个图像信息。也许,正是由于字节流无法标识自己,因此需要有一个参数来标识数据库使用的文字编码。在客户端与服务端的编码设置统一的时候,无论在数据库的字段中存储什么样的数据,都是不影响数据的储存和展示的,原因是,不会发生编码转换。
例如,网上很多帖子讨论到乱码问题的时候,给出的建议都是,将数据库的字符集设置为utf-8,这当然不会有什么问题,utf-8编码是被最广泛使用的编码标准,所以支持也相当完备,特别是utf-8编码几乎可以被所有软件“识别”出来(特征码)。这样一来,实际上掩盖了编码的问题。其实,如果仅仅为了储存和展示中文信息,将数据库的字符集设置为iso-8859-1(单字节)编码,客户端的语言环境也设置为同样的编码方式,存取中文数据,也不会有任何乱码的情况发生。之所以产生乱码,是由于在某些地方出现了编码方式的不匹配。
比如,数据库的编码设置为GBK,但是客户端的设置为UTF-8,那么如果在客户端使用UTF-8的编码方式输入中文数据,当客户端软件发现这种不一致时,会执行从UTF-8到GBK的编码转换,然后通过网络插入到数据库的具体字段中。当这段数据被读取时,如果客户端的设置为UTF-8,那么同样要发生GBK到UTF-8的转换,最终以UTF-8的形式展示数据。但是,如果数据被读取时,客户端的设置为GBK,则数据无需转换就可以以GBK的形式直接呈现,然而,如果客户端是设置为UTF-8编码的网页,但使用GBK方式访问数据库,那么数据被最终呈现时就会出现乱码。
总之,数据库提供设置数据库和客户端编码方式的选项,只是为了更好的提供编码转换工作,并不是必需的,无论设置成何种编码方式,与实际存储在字段中的数据都没有必然联系,只是会在编码转换的时候提供方便,否则,这些转换工作就只能完全交给客户端来完成。
3.尚未解决的疑问
1)关于windows剪贴板的实现机制中,是否包括了编码转换
从现象上来看,当从一个ansi编码的文本中拷贝中文字符,再到utf-8编码的文本中进行粘贴,没有出现乱码,但是这两种编码方式是不同的,也就是说,必然在这个过程中出现了编码转换,个人怀疑是利用剪贴板进行复制的时候,进行了编码转换,将复制的文本保存为操作系统内码,然后粘贴的时候由应用程序进行内码到utf-8编码的转换,完成粘贴。
2)输入法输入不同编码的文本时,采取什么机制?
当打开一个cp936编码的文件进行编辑时,输入法的输出是cp936编码的,但打开一个utf-8编码文件进行编辑时,输入法的输出变成了utf-8的,输入法是如何知道什么时候应该使用什么编码的?个人猜想,有可能输入法的输出只是操作系统的内码,在文本编辑器中进行内码到其他编码的转换。
原出处:http://blog.chinaunix.net/uid-11187-id-3040030.html