[转载]字符编码(理论篇)
0. 从ASCII码说起
学过电脑的人都听说过ASCII码,这是一种根据英文字符表设计的字符编码。严格意义上来讲,标准ASCII码只有7位(最高位为0),共128个字符,用十进制表示是:0-127。其中0-31和127为控制字符,而32-126为显示字符。国际标准化组织还发布了一些8位ASCII码扩展字符集,其中最流行的就是ISO 8859-1 (ISO Latin1)。它除了兼容标准的7位ASCII字符集以外,还利用高字节位扩充了一些西欧国家常用的字符和符号。尽管如此,由于ASCII码属于单字节编码(SBCS, or Single Byte Character Set),所以它最多只能表示256个字符。
显然,ASCII码“不懂中文"。那谁懂呢?
1. DBCS路过
记得99年我刚接触电脑的时候,还能看到UCDOS的身影。那会儿经常听老师说一句话:“一个汉字包含两个字节”——这其实就是DBCS (Double Byte Character Set,双字节字符集)使用的方法。DBCS是最常见的MBCS(Multi-Byte Character Set,多字节字符集),它保留了ASCII码的128个字符(0-127),并且对一般的非拉丁字符采用双字节编码。第一个字节(前导字节,Lead Byte)使用一个大于127的数字(即高字节位为1),和第二字节(尾随字节,Trailing Byte)一起组成一个字符。所以,在DBCS里面,ASCII字符仍占1个字节,而象中文、日文等字符则占2个字节。
我们常使用的DBCS编码有:
字符编码 | 第一字节范围 | 第二字节范围 | 第一字节最高位 | 第二字节最高位 | 字符数 | 说明 |
GB2312 | A1..F7 | A1..FE | 1 | 1 | 7445 | 简体字符集(基于区位码设计,高低字节各加了$A0) |
Big5 | 81..FE | 40..7E A1..FE | 1 | 0或1 | 13461 | 繁体字符集(香港、台湾、澳门等) |
GBK 1.0 | 81..FE | 40..FE 80..FE | 1 | 0或1 | 21886 | 兼容GB2312,支持Unicode 1.1中定义的CJK(简/繁体中文、日语和韩语) |
GB18030 | 27484 | 兼容GBK1.0,支持Unicode3.1 |
表1.1 常见的DBCS编码
备注:
- GB2312分为二级:一级为常用字,3755个,按照拼音排序;二级为次常用字,3008个,按照部首排序。
- GBK 1.0是Microsoft对GB2312的扩展。
说到这里,就要引入一个很重要的概念——代码页(Code Page, CP)。我们常说的代码页实际上是Windows为不同的字符编码方案所分配的一个数字编号。下面列举了一些常见的代码页:
代码页 | 描述 |
CP932 | 日语 (Shift-JIS) |
CP936 | 简体中文 (GBK) |
CP950 | 繁体中文 (Big5) |
CP1252 | Windows (ANSI) Codepage, 基于ISO-8859-1标准 |
CP1200 | UCS-2LE (Unicode little-endian) |
CP1201 | UCS-2BE (Unicode big-endian) |
CP12000 | UTF-32 LE |
CP12000 | UTF-32 BE |
CP54936 | GB18030 |
CP65001 | UTF-8 Unicode |
表1.2 常见的Windows代码页
DBCS编码总是和系统的代码页联系在一起的。在不同的代码页里面,同一串数据可能会被映射成 不同的字符。比如“BA BA D7 D6 41 42 43”,在简体中文环境下(GBK,CP936),它会被映射成成“汉字ABC”;若当前代码页为繁体中文 (Big5, CP950),它就变成了“犖趼ABC”;如果选择ISO8859-1代码页的话,那我们会看到“ººÓïABC”——这就是我们常说的乱码,即没有选择正确的编码。另外当某个字符编码在当前代码页中不存在时,系统会用一个特殊的符号来显示它(方框或问号)。比如将“©2007 Google”保存为ANSI文本文件,再打开时就变成了“?2007 Google”。
另外,不知道您发现了没有,对于DBCS编码的文本来说,如果其中有一个字节丢失或损坏,就可能造成整个文件出现乱码的情况。比如“.Net与字符编码(理论篇)”在内存中表示为:
“2E 4E 65 74 D3 EB D7 D6 B7 FB B1 E0 C2 EB A3 A8 C0 ED C2 DB C6 AA A3 A9”
其中D3 EB是“与”的内码(GBK),此时刚好前导字节和尾随字节都大于7F (127)。如果这时候我们把D3改成00,保存后重新打开。你猜会怎么样?它已经变得面目全非了:“.Net 胱址嗦耄ɡ砺燮 ”。
所以说,DBCS编码存储效率比较高,但可惜的是它包含的字符数量有限,最重要的是我们不能同时使用不同语言中的字符。而且从上面的例子可以看出,DBCS编码并不是很适合网络传输。
那我们该怎么办呢?
2. Unicode登场
字符是语言中最小的逻辑单位,而Unicode所做的就是为每个字符分配一个唯一的数字(Code Point,字位)并定义一系列规则,并且与程序、平台以及语言无关。使用Unicode编码,我们就可以用一种统一的方式来表示和处理不同语言中的字符,而这在以前是不可能做到的。(比如,“汉字ABCÖเอกรัตน์이설희“)。Unicode编码仅仅兼容了ISO-8859-1标准,比如U+0041表示'A',U+00A9表示'©'。像简体的"汉"字则是用U+6C49来表示的(其GBK内码是BABA)。
ISO10646标准曾经定义了两种Unicode字符集方案UCS (Universal Character Set):分别是UCS-2和UCS-4(2 字节编码和4字节编码)。采用UCS-4编码的字符有31位(最高位为0),理论上有0x80000000 (U-00000000-U-7FFFFFFF)个字位。整个编码空间(Code Space)分成128个组,每组有256个平面,每一平面包含256行,每行又有256个字位。其中00组的00平面(即第零平面)被称为基本多文种平面 (BMP, Basic Multilingual Plane)。BMP共含有256*256=65536个字位,这么大的空间已经包含了当今世界上绝大多数常用字符(Wikipedia上有一张关于BMP如何分配的Roadmap,有兴趣的话可以看看)。UCS-2就是对BMP中的字符进行编码,无论英文字符还是非拉丁字符,均用2个字节表示,显然UCS-2只是UCS-4的一个子集。
据资料显示,现存的汉字已经超过了9万。显然对于某些专业领域来说,BMP收录的2万多汉字是远远不够用的。2000年国家为了争取主动权,开始对我国境内销售的非嵌入式软件产品强制实行GB18030标准。该标准除了兼容GB2312和GBK以外,还收录了CJK扩展字符集A中的6千多汉字。它同时定义了4字节编码,为我国少数民族及以后的扩展保留了大量的可用编码空间。
在这样的环境下,Unicode组织推出了UTF-16和UTF-32编码方案,同时取代了原有的UCS-2和UCS-4。UTF(Unicode Transformation Format)编码格式保留了UCS-4中的前17个平面(U+000000 - U+10FFFF)作为有效编码空间。其中UTF-16是UCS-2的扩展:对于BMP内的字符,它和UCS-2的编码相同(2字节);对于BMP以外的字符则采用一对16位字组合(surrogate pairs)的方式进行编码(4字节)。组合编码的高字位(High Surrogate)在U+D800–U+DBFF,低字位(Low Surrogate)在U+DC00-U+DFFF。UTF-32则是在有效编码空间范围内,对所有字符全部使用4字节编码。
除了UTF-16和UTF-32,还有一种常用的Unicode编码UTF-8。接下来我们分析这几种编码有什么区别。使用系统自带的记事本和Debug工具,我们可以比较,在不同编码下将字符串"汉字ABC"保存到Text文件后在内存中的表示:
编码形式 | 编码结果(含BOM) |
UTF-8 | EF BB BF E6 B1 89 E8 AF AD 41 42 43 |
UTF-16 LE | FF FE 49 6C ED 8B 41 00 42 00 43 00 |
UTF-16 BE | FE FF 6C 49 8B ED 00 41 00 42 00 43 |
UTF-32 LE | FF FE 00 00 49 6C 00 00 ED 8B 00 00 41 00 00 00 42 00 00 00 43 00 00 00 |
UTF-32 BE | 00 00 FE FF 00 00 6C 49 00 00 8B ED 00 00 00 41 00 00 00 42 00 00 00 43 |
表2.1 Unicode编码形式
我们发现,在Windows的平面文件中,每种Unicode编码都用一串字节来标识自己。我们把这串连续的字节称为BOM (Byte-order Mark,字节顺序标识)。它使用一个特殊字符U+FEFF (ZERO WIDTH NO-BREAK SPACE)来表示编码形式和字节顺序。
为什么会有字节顺序呢?说道这里,就要补一补基本知识了。现代的计算机系统一般采用字节(Octet, 8 bit Byte)作为逻辑寻址单位。当物理单位的长度大于1个字节时,就要区分字节顺序(Byte Order, or Endianness)。常见的字节顺序有两种:Big Endian(High-byte first)和Little Endian(Low-byte first),这就是表2.1中的BE和LE。Intel X86平台采用Little Endian,而PowerPC处理器则采用了Big Endian。举例来说,整型数字$1234ABCD存储的时候就会有两种方式:
字节顺序 | 内存数据 | 备注 |
Big Endian (BE) | 0x12 0x34 0xAB 0xCD | 此时的0x12被称为most significant byte (MSB) |
Little Endian (LE) | 0xCD 0xAB 0x34 0x12 | 此时的0xCD被称为least significant byte (LSB) |
表2.2 字节顺序
需要注意的是,DBCS编码仍以字节作为基本编码单元,因而不会有字节顺序问题,而Unicode使用16位字(WORD)来编码,所以才会有LE和BE之分。另外从表2.1中我们可以看出UTF-8并没有字节顺序问题,使用BOM只是为了标识而已。
接下来我们一起看看UTF-8的编码方法:
编码范围 | UTF-8 |
U-00000000 – U-0000007F | 0xxxxxxx |
U-00000080 – U-000007FF | 110xxxxx 10xxxxxx |
U-00000800 – U-0000FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
U-00010000 – U-001FFFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
表2.3 UTF-8 编码方法
我们先看看“汉”字的UTF-8编码。“汉”字U+6C49用二进制表示是:0110 1100 0100 1001,对应U-00000800 – U-0000FFFF区间,将这些二进制位放入对应的位置后就变成了:11100110 10110001 10001001,再将其转换成十六进制是E6 B1 89。因此我们不难算出字符串“汉字ABC”用UTF-8编码后变为“E6 B1 89 E8 AF AD 41 42 43”,这也和表2.1中的数据一致。
分析完常见的几种Unicode编码格式,我们来总结一下这几种编码格式所需要的字节个数以及各自的优缺点。
编码范围 | UTF-8 | UTF-16 | UTF-32 | GB18030 * |
U+000000 – U+00007F | 1 | 2 | 4 | 1 |
U+000080 – U+00009F | 2 | 2 | 4 | 2 |
U+000800 – U+003FFF | 3 | 2 | 4 | 2 |
U+010000 – U+03FFFF | 4 | 4 | 4 | 4 |
表2.4 常见Unicode编码所需的字节数
备注:GB18030并不是真正意义上的Unicode编码,仅作参考
- UTF-8、UTF-16和UTF-32都可以表示有效编码空间(U+000000-U+10FFFF)内的所有Unicode字符。
- 使用UTF-8编码时ASCII字符只占1个字节,存储效率比较高,适用于拉丁字符较多的场合以节省空间。
- 对于大多数非拉丁字符(如中文和日文)来说,UTF-16所需存储空间最小,每个字符只占2个字节。
- Windows NT内核是Unicode(UTF-16),采用UTF-16编码在调用系统API时无需转换,处理速度也比较快。
- 采用UTF-16和UTF-32会有LE和BE之分,而UTF-8则没有字节顺序问题,所以UTF-8适合传输和通信。
- UTF-32采用4字节编码,一方面处理速度比较快,但另一方面也浪费了大量空间,影响传输速度,因而很少使用。