Qt与现有系统字符集的兼容问题研究

本文是假期完成的最后一篇文章,之后我将回到学校,届时更新频率将恢复为每周0-2篇左右。

众所周知,Qt在国际化方面的实现是非常方便的。但有时我们会面临一些问题,如在Windows系统中GB2312/GBK和unicode字符集的不兼容,以及在与现有网络程序实现通信的过程中发生的字符集不兼容问题。当然,此类问题可能比较罕见,而且事实上不只Qt面临这一棘手的情况。经过查阅一些资料并进行实践开发,我们相信Qt提供了目前最为便捷的字符集兼容解决方案。我们的最终应用是实现与Unix/Windows主机上的现有服务端程序进行通信,以实现如http、ftp等低级别应用层协议。

首先介绍GB2312-80、BIG-5、Unicode、GBK以及GB18030。

====================以下内容来自wikipedia====================

GB2312-80是中国国家标准简体中文字符集,全称《信息交换用汉字编码字符集-基本集》,又称GB0,由中国国家标准总局发布,1981年5月1日实施。GB2312中对所收汉字进行了“分区”处理,每区含有94个汉字/符号。这种表示方式也称为区位码。其中:01-09区为特殊符号;16-55区为一级汉字,按拼音排序;56-87区为二级汉字,按部首/笔画排序;10-15区及88-94区则未有编码。

在使用GB2312的程序通常采用EUC储存方法,以便兼容于ASCII。浏览器编码表上的“GB2312”,通常都是指“EUC-CN”表示法。每个汉字及符号以两个字节来表示。第一个字节称为“高位字节”,第二个字节称为“低位字节”。“高位字节”使用了0xA1-0xF7(把01-87区的区号加上0xA0),“低位字节”使用了0xA1-0xFE(把01-94加上0xA0)。由于一级汉字从16区起始,汉字区的“高位字节”的范围是0xB0-0xF7,“低位字节”的范围是0xA1-0xFE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。

Big5,又称为大五码或五大码,是使用繁体中文社区中最常用的电子计算机汉字字符集标准,共收录13,060个汉字。Big5码是一套双字节字符集,使用了双八码存储方法,以两个字节来安放一个字。第一个字节称为“高位字节”,第二个字节称为“低位字节”。“高位字节”使用了0x81-0xFE,“低位字节”使用了0x40-0x7E,及0xA1-0xFE。

Unicode是为了解决传统的字符编码方式的局限而产生的,例如ISO 8859所定义的字符虽然在不同的国家中广泛地使用,可是在不同国家间却经常出现不兼容的情况。很多传统的编码方式都具有一个共同的问题,即其容许电脑进行双语环境式的处理(通常使用拉丁字母以及其本地语言),但却无法同时支持多语言环境式的处理(指可同时处理混合多种语言的情况)。

大概来说,Unicode编码系统可分为编码方式和实现方式两个层次。

1、编码方式

Unicode的编码方式与ISO 10646的通用字符集(Universal Character Set,UCS)概念相对应,目前实际应用的Unicode版本对应于UCS-2,使用16位的编码空间。也就是每个字符占用2个字节。这样理论上一共最多可以表示216即65536个字符。基本满足各种语言的使用。实际上当前版本的Unicode尚未填充满这16位编码,保留了大量空间作为特殊使用或将来扩展。上述16位Unicode字符构成基本多文种平面(Basic Multilingual Plane,简称BMP)。最新(但未实际广泛使用)的Unicode版本定义了16个辅助平面,两者合起来至少需要占据21位的编码空间,比3字节略少。但事实上辅助平面字符仍然占用4字节编码空间,与UCS-4保持一致。未来版本会扩充到ISO 10646-1实现级别3,即涵盖UCS-4的所有字符。UCS-4是一个更大的尚未填充完全的31位字符集,加上恒为0的首位,共需占据32位,即4字节。理论上最多能表示231个字符,完全可以涵盖一切语言所用的符号。BMP字符的Unicode编码表示为U+hhhh,其中每个h 代表一个十六进制数位。与UCS-2编码完全相同。对应的4字节UCS-4编码后两个字节一致,前两个字节的所有位均为0。

2、实现方式

Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Translation Format,简称为UTF)。

例如,如果一个仅包含基本7位ASCII字符的Unicode文件,如果每个字符都使用2字节的原Unicode编码传输,其第一字节的8位始终为0。这就造成了比较大的浪费。对于这种情况,可以使用UTF-8编码,这是一种变长编码,它将基本7位ASCII字符仍用7位编码表示,占用一个字节(首位补0)。而遇到与其他Unicode字符混合的情况,将按一定算法转换,每个字符使用1-3个字节编码,并利用首位为0或1进行识别。这样对以7位ASCII字符为主的西文文档就大大节省了编码长度(具体方案参见UTF-8)。类似的,对未来会出现的需要4个字节的辅助平面字符和其他UCS-4扩充字符,2字节编码的UTF-16也需要通过一定的算法进行转换。再如,如果直接使用与Unicode编码一致(仅限于BMP字符)的UTF-16编码,由于每个字符占用了两个字节,在Macintosh (Mac)机和PC机上,对字节顺序的理解是不一致的。这时同一字节流可能会被解释为不同内容,如某字符为十六进制编码4E59,按两个字节拆分为4E和59,在Mac上读取时是从低字节开始,那么在Mac OS会认为此4E59编码为594E,找到的字符为“奎”,而在Windows上从高字节开始读取,则编码为U+4E59的字符为“乙”。就是说在Windows下以UTF-16编码保存一个字符“乙”,在Mac OS里打开会显示成“奎”。此类情况说明UTF-16的编码顺序若不加以人为定义就可能发生混淆,于是在UTF-16编码实现方式中使用了大端序(Big-Endian, 简写为UTF-16 BE)、小端序(Little-Endian,简写为UTF-16 LE)的概念,以及可附加的字节顺序记号解决方案,目前在PC机上的Windows系统和Linux系统对于UTF-16编码默认使用UTF-16 LE。

此外Unicode的实现方式还包括UTF-7、Punycode、CESU-8、SCSU、UTF-32等,这些实现方式有些仅在一定的国家和地区使用,有些则属于未来的规划方式。目前通用的实现方式是UTF-16小尾序(LE)、UTF-16大尾序(BE)和UTF-8。在微软公司Windows XP操作系统附带的记事本(Notepad)中,“另存为”对话框可以选择的四种编码方式除去非Unicode编码的ANSI(对于英文系统即ASCII编码,中文系统则为GB2312或Big5编码) 外,其余三种为“Unicode”(对应UTF-16 LE)、“Unicode big endian”(对应UTF-16 BE)和“UTF-8”。

目前辅助平面的工作主要集中在第二和第三平面的中日韩统一表意文字中,因此包括GBK、GB18030、Big5等简体中文、繁体中文、日文、韩文以及越南喃字的各种编码与Unicode的协调性被重点关注。考虑到Unicode最终要涵盖所有的字符,从某种意义而言,这些编码方式也可视作Unicode的出现于其之前的既成事实的实现方式,如同ASCII及其扩展Latin-1一样,后两者的字符在16位Unicode编码空间中的编码第一字节各位全为0,第二字节编码与原编码完全一致。但上述东亚语言编码与Unicode编码的对应关系要复杂得多。

GBK即汉字内码扩展规范,K为汉语拼音 Kuo Zhan(扩展)中“扩”字的声母。英文全称Chinese Internal Code Specification。字符有一字节和双字节编码,00–7F范围内是一位,和ASCII保持一致,此范围内严格上说有96个文字和32个控制符号。之后的双字节中,前一字节是双字节的第一位。总体上说第一字节的范围是81–FE(也就是不含80和FF),第二字节的一部分领域在40–FE,其他领域在80–FE。

GB18030,全称:国家标准GB 18030-2005《信息技术中文编码字符集》,是中华人民共和国现时最新的内码字集,是GB 18030-2000《信息技术信息交换用汉字编码字符集 基本集的扩充》的修订版。与GB 2312-1980完全兼容,与GBK基本兼容,支持GB 13000及Unicode的全部统一汉字,共收录汉字70244个。

GB 18030主要有以下特点:与 UTF-8 相同,采用多字节编码,每个字可以由1个、2个或4个字节组成。编码空间庞大,最多可定义161万个字符。支持中国国内少数民族的文字,不需要动用造字区。汉字收录范围包含繁体汉字以及日韩汉字。

GB 18030的字节结构如下:单字节,其值从0到0x7F。双字节,第一个字节的值从0x81到0xFE,第二个字节的值从0x40到0xFE(不包括0x7F)。四字节,第一个字节的值从0x81到0xFE,第二个字节的值从0x30到0x39,第三个字节从0x81到0xFE,第四个字节从0x30到0x39。

====================以上内容来自wikipedia====================

事实上,上述wiki内容是没有必要细究的:),下面进入正题。

Qt中的内置字符串类为QString,它是以16位unicode编码(unicode 4.0/unicode-16)存储的。QString被应用于所有涉及字符串操作的API中,也就是说,如果你要用Qt构建应用程序,那么研究QString是不可避免的。

下面是一种比较自由的字符集转换方式:

[code] QByteArray encodedString = "mp77的技术交流频道"; //为什么要用QByteArray? QTextCodec *codec = QTextCodec::codecForName("gb2312"); QString string = codec->toUnicode(encodedString); [/code]

QTextCodec为我们提供了针对超过30种常用字符集间互相转换的机制,但这种简单实现代码量较大,不适合字符串操作较多的情况。那么下面这种方案则受到广大程序员的欢迎:

[code] QTextCodec::setCodecForTr(QTextCodec::codecForName("gb2312")); //beginning of code …… QString string = QObject::tr("mp77的技术交流频道"); [/code]

QTextCodec提供了一种类似国际化接口的操作,所有的字符集规定在代码行首部完成,之后只需要借助国际化的方法初始化任意字符串即可(尽管这与真正的国际化功能有所区别)。现在我们解释首段代码中为何使用QByteArray接收中文字符串,如果不做任何处理直接运行:

[code] QString string = "mp77的技术交流频道"; [/code]

由于系统采用ANSI编码(包括Multi-Byte Chactacter System,MBCS字符集),那么上述语句执行后,字符串string在内存中的内容实际上是以ANSI编码。由于对ansi的操作与unicode完全不兼容,那么此后QT中所有的字符串操作都会受到影响,最终输出时就会出现所谓的“乱码”。因此,类似的直接赋值形式应当尽量避免。

我们注意到在vc6时代经常采用以下赋值方式:

[code] const char* string = "mp77的技术交流频道"; [/code]

这一句包括了申请内存、写入内容并以“\0”结尾等操作,其在内存中的字符编码形态自然也是ANSI的。Qt特别提供的QByteArray类就是为了兼容这种情况,而且官方手册声称它比const char *的声明初始化方式要好很多(如采用implicit sharing,简单地说就是COW技术),而且使用上也方便许多。

这里介绍一个我们在实际应用中遇到的例子。题目是实现FTP协议,当然,要想实现一个稳定的FTP协议难度并不小,这主要是因为相关RFC文档并没有提供足够的协议细节,导致目前几乎没有绝对通用的FTP服务端或客户端。当然,目前仍有能够兼容大多数平台的客户端/服务端程序,且在日常使用中似乎没有发现什么问题。

Qt实现FTP客户端很容易——只要使用现成的QFtp即可,但关键的服务端需要手动完成了。我们在使用cuteFTP和FlashFXP这种常用客户端软件进行测试时立即发现了一个严重问题,即服务端返回的目录list中出现汉字乱码。我们检查了服务端生成目录list的函数,并未对任何字符串进行编码操作。于是直接在服务端socket的数据信息中添加了toUtf8的操作。事实表明这一修改没有解决任何问题。

继续检查后我们注意到类似如下一行关键代码:

[code]

QString string;

string = "…";

string += local.toString(fileinfo.lastModified()," MMM dd yyyy ") + filename+"\r\n";

[/code]

上面实际上利用了QString对+/+=的重载进行字符串的简单构建,其中filename是出现乱码的地方——也就是唯一存在中文可能的文件或目录名称。由于字符串常量的存在,我们对string进行直接赋值实际上就导致内存信息为ansi编码,而filename来自于fileinfo.completeBaseName(),是Qt有关目录操作函数返回的文件名信息,其编码自然是utf-16。查阅QString对+/+=的重载说明知,Qt为了提升字符串操作的性能,只是在目标字符串的尾部申请了一块额外的内存空间,并直接将源字符串所在的内存块复制到新空间中。由此可见,string中实际上是ansi和utf-16两种编码的字符串混合,无论我们今后采用何种编码转换方式,都无法实现完整内容的正确转换。

一种解决方案是:

[code]

QString string;

string = "…";

string += local.toString(fileinfo.lastModified()," MMM dd yyyy ") + filename.toLocal8Bit()+"\r\n";

[/code]

toLocal8Bit函数返回的是QByteArray类型的本地字符集串,如此就能保证string保持唯一的字符集了。

近期的文章中我们将介绍如何使用Qt构建网络应用程序,并提供一个代码示例。