前几天看文初的《精武门之Web安全研讨会首日感受》,说到利用字符集攻击时提到以前宝宝写的一篇有关国际化的文章,趁机再次拜读了宝宝的这篇大作,不得不感慨宝宝的写作功底,无敌!这么好的文章不分享出来实在是太可惜了,在此将宝宝的大作转帖于此;
在我开发Java程序的几年中,遇到得最多,也是别人向我提问最多的问题,就是各种各样看似稀奇古怪的中文乱码问题了。网上也有许多解释和解决Java中文问题的文章,但水平参差不齐,有一些文章甚至是错误的。
此外,我们公司自己的Java程序从一开始就采用了错误的方式处理中文问题,虽能解一时之急,却引出了越来越多的深远的问题。每当我听到有的同事还在讨论如何特殊处理双字节的中文GB码,就感慨他们思路的狭隘。试问,今天我们可以用特殊的方式处理我们所熟悉的中文编码,可是今后我们怎样才能应付日文版、韩文版、或世界其它国家语言的产品开发呢?
在我看来,与其说这些问题是“中文化问题”,不如说是“国际化问题”。所谓的“汉化”这种说法已经随时代远去了。想想看,这个词带有明显的小农经济的色彩:自家汉化自家用,哪管世界变化多。经过汉化的软件,常常意味着:版本落后、不兼容、不稳定。为什么会这样呢?根本原因是,从软件的设计阶段,就没有考虑国际用户的需要,没有采用国际通用的标准。事后要弥补自然难上加难。
所以让我们把眼光放开,想一想“国际化”。当然国际化的目的还是生产出“汉化”的软件,但我们可以用同样的方法“韩化”、“日化”、“阿拉伯化”,统称为“本地化” —— 这就是“国际化”的目的。国际化和本地化有两个很体面的英文缩写:I18n(Internationalization)和L10n(Localization)。
想要开发出国际化的软件产品,首先要了解国际标准,而不是使用东拼西凑的权宜之计。本文首先从相关国际标准的讨论切入,相信正确地理解和应用这些标准,所有的“中文化问题”或“国际化问题”都会迎刃而解。
从学计算机的那天开始,老师就告诉我们在计算机里面,所有的英文字母都对应到一个数字编码,这就是ASCII码(American Standard Code for Information Interchange)。ASCII码是很久很久以前(1968年)制定的。它只使用了一个8位字节中的低7位,总共是127个编码位。这样的方案很快就不够使用了。
在80年代早期,一些现在流行的标准(如ISO 8859和Unicode)还未出现。那时为了支持多种地区的语言,各大组织机构或IT厂商开始发明它们自己的编码方案,以便弥补ASCII编码的不足。一时间,各种互不相容的字符编码方案成百花齐放之势。
为了避免混乱,ISO组织在1998年之后,陆续发表了一系列代号为8859的标准,作为ASCII编码的标准扩展,终于统一了单字节的西方字符的编码。ISO是设在瑞士的国际标准化组织的简称(International Organization for Standardization)。
ISO-8859-1(Latin1 - 西欧字符)
ISO-8859-1覆盖了大多数西欧语言,包括:法国、西班牙、葡萄牙、意大利、荷兰、德国、丹麦、瑞典、挪威、芬兰、冰岛、爱尔兰、苏格兰、英格兰等,因而也涉及到了整个美洲大陆、澳大利亚和非洲很多国家的语言。
此外,ISO-8859-1后来被采纳为ISO-10646标准(后面会讲到)的首页,换句话说,Unicode的最开头256个字符编码和ISO-8859-1是一一对应的。正是由于这个特殊性,使很多人产生了对ISO-8859-1编码的误用。
ISO-8859标准还包括:
但是ISO 8859系列标准的字符编码,还是互不相容,不可能同时使用的。毕竟它们只是单字节的编码方案。而且,它们和多字节的编码方案如中文编码GB2312和BIG5也是不相容的。那些欧洲字符(最高位为1的字符),在GB2312和BIG5中被认为是双字节汉字编码的首字节。
单字节编码只有256个码位(28=256),而中文字符何止千千万,单字节编码不可能满足中文编码的需要。于是为了适应东方文字信息处理的需要,ISO又制定了ISO 2022标准(Character code structure and extension techniques),提供了七位与八位编码字符集的扩充方法的标准。我国根据ISO 2022制定了国家标准GB2311 ——《信息交换用七位编码字符集的扩充方法》,并根据该标准制定了国家标准GB2312-80编码。其他东方国家和地区也制定了各自的字符编码标准,如日本的JIS0208,韩国的KSC5601,台湾地区的CNS11643等。
BIG5
BIG5是从CNS11643的早期版本发展而来的,虽然没有包括CNS11643的全部内容,但却是目前台湾、香港地区普遍使用的一种繁体汉字的市场标准,包括440个符号,一级汉字5401个、二级汉字7652个,共计13060个汉字。
GB2312-80
全称是《信息交换用汉字编码字符集 基本集》,1980年发布,是中文信息处理的国家标准,在大陆及海外使用简体中文的地区(如新加坡等)是强制使用的唯一中文编码。
他由6763个常用汉字和682个全角的非汉字字符组成。其中汉字根据使用的频率分为两级。一级汉字3755个,二级汉字3008个。由于字符数量比较大,GB2312采用了二维矩阵编码法对所有字符进行编码。首先构造一个94行94列的方阵,对每一行称为一个“区”,每一列称为一个“位”,然后将所有字符依照下表的规律填写到方阵中。这样所有的字符在方阵中都有一个唯一的位置,这个位置可以用区号、位号合成表示,称为字符的区位码。如第一个汉字“啊”出现在第16区的第1位上,其区位码为1601。因为区位码同字符的位置是完全对应的,因此区位码同字符之间也是一一对应的。这样所有的字符都可通过其区位码转换为数字编码信息。GB2312字符的排列分布情况如下:
分区范围 | 符号类型 |
第01区 | 中文标点、数学符号以及一些特殊字符 |
第02区 | 各种各样的数学序号 |
第03区 | 全角西文字符 |
第04区 | 日文平假名 |
第05区 | 日文片假名 |
第06区 | 希腊字母表 |
第07区 | 俄文字母表 |
第08区 | 中文拼音字母表 |
第09区 | 制表符号 |
第10-15区 | 无字符 |
第16-55区 | 一级汉字(以拼音字母排序) |
第56-87区 | 二级汉字(以部首笔画排序) |
第88-94区 | 无字符 |
区位码 | 区码转换 | 位码转换 | 存储码 |
1001H | 10H+A0H=B0H | 01H+A0H=A1H | B0A1H |
· 双字节编码,范围:B0A0 ~ F7FE(首字节在B0-F7 之间,尾字节在A0-FE 之间)。
GBK
汉字内码扩展规范(GBK)是国家技术监督局1995年为中文Windows 95所制定的新的汉字内码规范。
· 双字节编码,GB2312-80的扩充,在码位上和GB2312-80兼容。
· 范围:8140 ~ FEFE(首字节在81-FE 之间,尾字节在40-FE 之间,剔除xx7F)共23940个码位。
· 包含21003个汉字,包含了ISO 10646中的全部中日韩汉字,简、繁体字融于一库。
严格说,GBK不能算是国家标准,最多算是一个商业标准。而GB18030才是真正的国家标准。
GB18030-2000
全称是《信息交换用汉字编码字符集》,是我国的强制标准,所有不支持GB18030标准的软件将不能作为产品出售。
· 单字节、双字节、四字节编码。
· 向下与GB2312编码兼容。
· 支持GB 13000.1-1993中的全部中、日、韩(CJK)统一汉字字符和全部CJK统一汉字扩展A的字符。
虽然GB18030标准非常强大,但它是一个中国大陆的标准。在编码上,除了和GB2312以外,还是不能和世界上其它任何一种字符编码统一。
前面所讲的一切字符编码方案,都是针对局部地区或少数语言文字的,没有办法同时表达所有的语言文字,或在多种语言平台上交换。这对今天极其频繁的国际信息交流是不相称的。
为了提高计算机的信息处理和交换功能,使得世界各国的文字都能在计算机中处理,从1984年起,ISO组织就开始研究制定一个全新的标准:通用多八位编码字符集(Universal Multiple-Octet Coded Character Set),简称UCS。标准的编号为:ISO 10646。这一标准为世界各种主要语言的字符(包括简体及繁体的中文字)及附加符号,编制统一的内码。
统一码(Unicode)是Universal Code的缩写,是由另一个叫“Unicode学术学会”(The Unicode Consortium)的机构制定的字符编码系统。Unicode与ISO 10646国际编码标准从内容上来说是同步一致的。
Unicode是Java语言和XML的基础,所以我们要稍微详细地介绍一下Unicode以及ISO 10646标准。
注意:不够耐心的读者可以跳过本章的余下部分。但显然了解本章所描述的Unicode及相关编码的技术细节,有利于你更好地理解和应用Unicode。
在1991年,Unicode学术学会与ISO国际标准化组织决定共同制订一套适用于多种语言文本的通用编码标准。Unicode与ISO 10646国际编码标准于1992年1月正式合作发展一套通用编码标准。自此,两个组织便一直紧密合作,同步发展Unicode及ISO 10646国际编码标准。
ISO 10646(UCS) |
Unicode |
1993年,ISO组织发表ISO 10646国际编码标准的第一个版本,全名是ISO/IEC 10646-1:1993。它收录了20902个表意字符(ideograph,中日韩文均属表意字符)。 |
同年,Unicode学术学会根据ISO/IEC 10646-1:1993修订了Unicode 1.0,发布Unicode 1.1。 |
不断改善和修订ISO 10646标准。 |
1996年发表Unicode 2.0,1998年发表Unicode 2.1,根据ISO 10646做了一些改善和修订,新增了欧元符号。 |
2000年10月发表了ISO 10646第二版的第一部分:ISO/IEC 10646-1:2000,新增收了6,582个表意字符于扩展区A中(CJK Unified Ideographs Extension A)。 |
2000年2月,发表Unicode 3.0,也包含了同样的CJK Ext A。 |
2001年,发表了ISO/IEC 10646的第二部分,增收了42711个表意字符于扩展区B里。 |
2001年,Unicode发表3.1版,将CJK Ext B纳入新版Unicode中。 |
虽然两个组织保持如此密切的合作关系,但Unicode和ISO 10646还是有区别的。ISO 10646着重定义字符编码,而Unicode则在此基础上,为这些字符及编码数据提出应用的方法以及对语义数据作补充。
UCS的结构是一个四维的编码空间,每一维由一个字节(八位二进制位)组成,范围是00到FF。总体上分为128个群组(Group 00-7F),每一群组由256个平面(Plane 00-FF)组成,每一平面有256行(Row 00-FF),每一行256个编码位(Cell 00-FF)。所以,每一平面包括65,536个字符位(Character Position 0000-FFFF)。
整个编码字符集的每个字符都由4个字节,按“组-面-行-列”的顺序表示。所以UCS的可编码空间为:128 × 256 × 256 × 256 = 231。
UCS将其第一个平面(00群组中的00平面)称作基本多语种平面(Basic Multilingual Plane,BMP)。
在UCS中,目前只有00组是重要的,Unicode学术学会断言,在可以预见的将来,甚至不可能用完00组中的前17个平面(00平面到10平面)。因此,Unicode只定义了ISO 10646的第00组的前17个平面。事实上,目前绝大多数字符,都分配在第00平面BMP中。
下表中列出了BMP中的字符分配情况:
区间 |
描述 |
(0000-1FFF)基本拼音字符区 |
包括所有拼读文字的字母拼音和音标。它的字符集一般较小,如:拉丁文、西里尔文、希腊文、希伯来文、阿拉伯文、泰文、天成文书(梵文)等。 |
(2000-28FF)符号区 |
包括许多种用于标点、数学、化学、科技及其它特殊用途上的“符号”和“丁贝符”(示意图形符号)。 |
(2E80-33FF)中日韩语音及符号区 |
包括用于中国、日本、韩国语言中的标点、符号、字根(笔画)及发音等字符。 |
(3400-9FA5)中日韩汉字字符区 |
由27,484个中日韩(越)的统一汉字组成。 |
(A000-A4C6)彝族字符区 |
由1,165个中国南方彝族音节和50个其字根组成。 |
(AC00-D7A3)韩字符拼音区 |
由11,172个预先组合的韩字符拼音音节组成。 |
(D800-DFFF)代理区 |
这个区被平分为1024个“高半代理区”(D800-DBFF)码位和1024个“低半代理区”(DC00-DFFF)码位,用来形成代理对,可以得到超过一百万个扩充编码位。 |
(E000-F8FF)私人专用区 |
包含6,400个编码位,用于用户或开发商自行定义的字符编码。 |
(F900-FA2D)兼容字符区 |
包括一些被许多行业协会和国家标准广泛使用的字符,但在Unicode编码中有不同的表现形式。包含一些专用字符。 |
UCS有两种方式来表示一个字符编码:四字节正规形式(UCS-4,Four-octet canonical form)和双字节基本平面形式(UCS-2,Two-octet BMP form)。
UCS-4 —— 四字节正规形式
UCS-4用4个字节来表示一个字符。第一个字节表示组(Group),第二表示平面(Plane),第三表示行(Row),第四表示单元号或列(Cell)。
UCS-2 —— 双字节基本平面形式
当系统只使用BMP的字符码时,可以省略群组和平面中的八位,将字符码由32个位缩短为16个位(2个字节)。标记为UCS-2。
Unicode和UCS-2同样采用16位编码。所以一般可以把Unicode和UCS-2看作是同一样东西。
代理对(Surrogate Pair)
UCS-4定义了4个字节表示一个字符,用来应付将来的扩展是绰绰有余。可是Unicode和UCS-2只定义了2个字节,却很容易用尽。代理对(Surrogate Pair)的设计在这种背景下应运而生。
UCS-2在BMP中开辟了一个特殊的区间(D800 - DFFF) -- 代理区,并平分成两个区,分别称为高半代理区(High-half Zone,D800 - DBFF),和低半代理区(Low-half Zone,DC00 - DFFF),各有1024个码位。使用时,从高低两个代理区中各取一个编码组成一个四字节的代理,来表示一个在BMP以外平面上的编码字符位。这样一来,总共可以多表示1024×1024个字符,映射到00群组中的01到10平面(共16个平面)。
代理对提供了用BMP的2字节编码来表示在基本多文种平面(BMP)之外的16个平面编码的机制。一些不常用的字符可以用代理对表示。目前,只有ISO/IEC 10646-2:2001和Unicode 3.1才使用到代理对。
高半代理区和低半代理区的划分,使编码位相互区分开。非代理区字符一定不会在这个区里。因为高半代理区和低半代理区不相交,所以很容易决定字符值的边界。一个完好的文本中,高半代理码和低半代理码总是按先后成对出现。
如果在实现上没有删除代理码或在代理码对中插入字符,数据的完整性就可得到保证。即使数据有残损,也只是局部的。一个残缺的码只影响一个字符。因为高半代理区和低半代理区不相交,且成对出现,错码不会传到文本的其它部分。
具体来说,一个代理对(H,L)由码值为D800-DBFF 的高半代理码H和码值为 DC00-DFFF低半代理码L组成。将一个字符映射到UCS-4码位中。假设N是UCS-4码值,则有:(以下所有数字均为16进制)
N = (H - D800) × 0400 + (L - DC00) + 01 0000
于是得到N的码值为01 0000到10 FFFF。
注意
Unicode 3.0没有用到代理对,直到3.1才增加了CJK Ext B,用到了02平面,需要使用代理对才能访问。但99.99%的情况下,根本用不到那些字。此外,JDK1.4只支持到Unicode 3.0,所以目前Java还不能应用代理对。
UTF为UCS Transformation Format的缩写,意为“UCS转换格式”。UCS只是一个字形和内码上的标准,并没有定义实际在计算机上存取的方法,而UTF便定义了一整套的计算机存取UCS编码的转换格式,并考虑了与其它编码方式兼容。常用的格式有UTF-8和UTF-16。有时也用到UTF-7来进行7位数据传输。
UTF-16
UTF-16是用定长16位(2字节)来表示的UCS-2或Unicode转换格式。它将Unicode的编码值变成2字节的Big-endian(高位字节在前,低位字节在后)或Little-endian(低位字节在前,高位字节在后)编码。UTF-16利用代理对来访问BMP之外的字符编码。
Java使用Big-endian系统,而Intel系列处理器内部使用Little-endian系统(学汇编语言和C语言的人都知道)。
例如:“中国”两字,Unicode是4E2D 56FD,在Windows上用UTF-16编码,结果为四个字节:2D 4E FD 56;如果使用Java输出,结果为:4E 2D 56 FD。
使用UTF-16有什么缺点呢?很显然,
1. 所有原本1个字节就可以表示的西方字符,现在要用2个字节来表示,体积大了一倍。
2. 学过C的人都知道,0x00代表C字符串的结尾。但是用UTF-16来表示单字节字符(ISO-8859-1)时,高位字节为0x00。这样就会使C语言库函数发生误判。用UTF-16表示文件名、网址等,全引出无数的问题。
3. 字符的边界不好找。程序处理时必须从字符串的头部开始扫描,才可能正确地找出一个字符的边界,效率较低。此外,万一坏掉一个字节,这个字节之后的字符都会错位,坏掉一片。
所有的这些问题,在UTF-8中都不存在。
但是,UTF-16也有其天然的优点:它直接表现了字符编码的整数值。所以UTF-16是最直接的Unicode表示法。此外,它是定长的,这大大简化了字符串的操作。Java语言就是用UTF-16格式将字符存储在内存中的。正是这样,才使Java的Unicode字符串的操作格外简单高效。
UTF-8
UTF-8使用了变长技术,在每一个编码区域有不同的字码长度:
1. 对UCS-2,由1字节至3字节构成;
2. 如果UCS-2使用了代理对,则UTF-8最长可到4字节;
3. 对UCS-4,由1字节至6字节构成。
因为以字节(8位)为组成单元,故称为“UTF-8”。对于英文文本,UTF-8的文件大小比其它转换格式都小。
在UTF-8内,字符由1个至6个字节为组合。下表列举出了不同范围的UCS码转换成UTF-8的规则。英文字母“x”代表可以用来记录 Unicode 码值的区域。
UCS-4 区域(十六进制) |
UTF-8字节组合(二进制) |
0000 0000 —— 0000 007F |
0xxxxxxx |
0000 0080 —— 0000 07FF |
110xxxxx 10xxxxxx |
0000 0800 —— 0000 FFFF |
1110xxxx 10xxxxxx 10xxxxxx |
0001 0000 —— 001F FFFF |
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
0020 0000 —— 03FF FFFF |
111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
0400 0000 —— 7FFF FFFF |
1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
在UTF-8内,
1. 如果一个字节,最高位(第8位)为0,表示这是一个ASCII字符(00 - 7F)。可见,所有ASCII编码已经是UTF-8了。
2. 如果一个字节,以11开头,连续的1的个数暗示这个字符的字节数,例如:110xxxxx代表它是双字节UTF-8字符的首字节。
3. 如果一个字节,以10开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节。
可见UTF-8可以有效地保证数据的完整性,避免出现编码的错位。即使偶然出现“坏字”,也不会影响到后续的文本。
那么UTF-8有什么缺点呢?显然,对于在BMP中的中文字来说,需要用3个字节才能表示,比使用UTF-16或直接使用双字节的GB2312编码大了0.5倍。
上文说了一大通,总结一下,其实很简单: