你是否对字符编码的问题还是不了解,是否遇到过文件乱码的问题呢,看到 ANSI,GB2312,GBK,latin-1,cp936,euc-cn,GB18030,big5。这么多的会不会头晕呢?
先看一个很早看到的一个奇怪现象:在 Windows 的记事本里新建一个文本文件,输入 "联通" 两个字,保存,关闭,再次打开,会发现文本已经不是 "联通" 了,而是几个乱码。这原因下面就会揭开。
ASCII(ISO 646) 编码大家都应该熟悉,是用一个 8 位的字节来表示空格、标点符号、数字、大小写字母或者控制字符的,其中最高位为 "0",其他位可以自由组合成 128 个字符了,这就是 ASCII 的编码。
latin-1 又称 ISO/IEC 8859-1,是国际标准化组织 ISO 字符编码标准 ISO/IEC 8859 的一部分,它在 ASCII 编码空置的 0xA0-0xFF 的范围内加入了其他符号字母以供西欧来使用,所以也叫 "西欧语言",另外 ISO 的编码还有其他的一些语言编码,这些都是单字节 8 位编码。
ANSI 是美国的国家标准协会,ANSI 的编码也是在 ASCII 的标准上扩展而来的,但 ANSI 的编码是双字节 16 位的编码,在简体中文的操作系统中 ANSI 就指的是 GB2312,而在日文的操作系统就指的是 JIS,这些编码之间互相不兼容,但所有的 ANSI 编码都兼容 ASCII 编码。
GB2312 是对 ANSI 的简体中文扩展。GB2312 的原型是一种区位码,这种编码把常见的汉字分区,每个汉字有对应的区号和位号,例如:"我" 的区号是 46,位号是 50,这种区位码在上初中时还用一个小册子查过。GB2312 因要与 ASCII 相兼容,所以每个字的区号和位号都加上 0xA0 得到两个最高位都是 "1" 的 8 位字节(0xCED2, 11001110 11010010),这两个字节组合而成就是一个汉字的 GB2312 编码,GB2312 编码中小于 127 的字符与 ASCII 的相同。与区位码常提到的另一个词是 "内码",字面意思就是计算机内部使用的二进制编码,也就是区位码加上 0xA0 得到的。因为 GB2312 无法对繁体中文编码,所以与之对应的繁体中文编码方式为 BIG5。
GB2312 共收录了七千个字符,由于 GB2312 支持的汉字太少而且不支持繁体中文,所以 GBK 对 GB2312 进行了扩展,对低字节不再做大于 127 的规定,以支持繁体中文和更多的字符,GBK 共支持大概 22000 个字符,GB18030 是在 GBK 的基础上又增加了藏文、蒙文、维吾尔文等主要的少数民族文字。
由于各国之间的编码不同造成的交流传输不便,ISO 打算废除所有的地区性编码方案,重新建立一个全球性的编码方案把所有字母和符号都统一编码进去,称之为 "Universal Multiple-Octet Coded Character Set",简称为 UCS(ISO10646),UCS分为 UCS-2 和 UCS-4 两个方案,UCS-2 采用 2 个字节来存储一个字符,共可以编码 216 个字符(即 65536),这大概可以覆盖完世界上所有的字符,如果不够还可以采用 UCS-4 来编码,UCS-4 采用 31 位来编码,最高位为 "0",大概有 21 亿个字符。UCS-4 高两个字节为 0 的码位被称作BMP(Basic Multilingual Plane, 基本多语言面),即将 UCS-4 的 BMP 去掉前面的两个零字节就得到了 UCS-2。在 UCS-2 的两个字节前加上两个零字节,就得到了 UCS-4 的 BMP。
在同时代又有 unicode.org 这个组织也制定了自己的全球性编码 unicode,unicode 1.0 的编码统一采用双字节编码,也可以编码 65536 个字符,unicode2.0 采用 20 位编码,编码范围为 0 到 0x10FFFF,由于这两种编码采用了不同的编码,也阻碍了交流,但自从 unicode2.0 开始,unicode 采用了与 USC 相同的字库和字码,ISO 也承诺将不会给超出 0x10FFFF 的 UCS-4 编码赋值,使得两者保持一致。现阶段主要采用的是 UCS-2/unicode 16 位的编码,这种定长编码便于计算机的处理,ASCII 在这种编码下就统一变成了高字节全是 "0",低字节来编码,这种编码在英文存储中会浪费一倍的空间,但这些浪费在在现在存储器极度便宜的时代也算不得什么。
UCS 不仅给每个字符分配一个代码,而且赋予了一个正式的名字. 表示一个 Unicode/UCS 值的十六进制数,通常在前面加上 "U+",就象 U+0041 代表字符 "拉丁大写字母A"。
UTF(Unicode/UCS Transfer Format),UCS 变长存储的编码方式,主要用来解决 UCS 编码的传输问题的。分为 UTF-7,UTF-8,UTF-16,UTF-32 等。
UTF-8 是一次传输 8 位 (一个字节) 的 UTF 编码方式,一个字符可能会经过 1-6 次传输,具体的跟 unicode/UCS 之间的转换关系如下:
unicode | 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 |
U+00200000 - U+03FFFFFF: | 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
U+04000000 - U+7FFFFFFF: | 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
比如: "我" 的 unicode/UCS 编码为 "U+6211"(01100010 00010001),在 U+00000800 - U+0000FFFF 之间,所以采用三字节编码,按规则分段为:0110 001000 010001,再分别替换上表中的x,得到11100110 10001000 10010001,即为 "E6 88 91",这就是 "我" 的 UTF-8 编码。
UTF-8 的传输与字节顺序无关,可以在不同平台之间交流,并且容错能力高,任何一个字节损坏后,最多只会导致一个编码码位损失,不会链锁错误 (如 GB 码少一个字节就会整行乱码),所以建议在保存文件时尽量采用 UTF-8 的编码来保存文件。
再来看开头提到的那个奇怪现象,就不难解释了,当使用记事本新建文件时,默认的编码是 ANSI,输入中文就是 GB 系列的编码,"联通" 两字的编码为:
注意到了吗?第一二个字节、第三四个字节的起始部分的都是 "110" 和 "10",正好与 UTF-8 规则里的两字节模板是一致的,于是再次打开记事本时,记事本就误认为这是一个 UTF-8 编码的文件,让我们把第一个字节的 110 和第二个字节的 10 去掉,我们就得到了 "00001 101010",再把各位对齐,补上前导的 0,就得到了 "0000 0000 0110 1010",这是 UNICODE 的 006A,也就是小写的字母 "j",而之后的两字节用 UTF-8 解码之后是 0368,这个字符什么也不是。这就是只有 "联通" 两个字的文件没有办法在记事本里正常显示的原因。
而如果你在 "联通" 之后多输入几个其他字,其他的字的编码不见得又恰好是 110 和 10 开始的字节,这样再次打开时,记事本就不会坚持这是一个 UTF-8 编码的文件,而会用 ANSI 的方式解读之,这时乱码又不出现了。
UTF-16 是一次传输两个字节的 UTF 编码方式,现如今 Unicode/UCS 也主要采用 16 位编码,所以 UTF-16 的存储方式和 Unicode/UCS 的编码方式也相同。确切的说是和 UCS-2/unicode 16 的编码方式相同。
在 UTF-16 或者 UCS 的编码中经常遇到这两个选项,big endian 和little endian 是 CPU 处理多字节数的不同方式。例如“汉”字的 Unicode/UCS 编码是 6C49。那么写到文件里时,究竟是将 6C 写在前面,还是将 49 写在前面?如果将 6C 写在前面,就是 big endian。还是将 49 写在前面,就是 little endian。
这两个词语出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头 (Big-Endian) 敲开还是从小头 (Little-Endian) 敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。
我们一般将 endian 翻译成 "字节序",将 big endian 和 little endian 称作 "大尾" 和 "小尾" 。
BOM 称为 "Byte Order Mark"。UTF-8 以字节为编码单元,没有字节序的问题。而 UTF-16 以两个字节为编码单元,在解释一个 UTF-16 文本前,首先要弄清楚每个编码单元的字节序。例如收到一个 "奎" 的 Unicode/UCS 编码是 594E,"乙" 的 Unicode/UCS 编码是 4E59。如果我们收到 UTF-16 字节流 "594E",那么这是 "奎" 还是 "乙"?
在 Unicode/UCS 编码中有一个叫做 "ZERO WIDTH NO-BREAK SPACE" 的字符,它的编码是 FEFF。而 FFFE 在 Unicode/UCS 中是不存在的字符,所以不应该出现在实际传输中。UCS 规范建议我们在传输字节流前,先传输字符 "ZERO WIDTH NO-BREAK SPACE"。这样如果接收者收到 FEFF,就表明这个字节流是 Big-Endian 的;如果收到 FFFE,就表明这个字节流是 Little-Endian 的。因此字符 "ZERO WIDTH NO-BREAK SPACE" 又被称作 BOM。
UTF-8 不需要 BOM 来表明字节顺序,但可以用 BOM 来表明编码方式。字符 "ZERO WIDTH NO-BREAK SPACE" 的 UTF-8 编码是 EF BB BF。所以如果接收者收到以 EF BB BF 开头的字节流,就知道这是 UTF-8 编码了。Windows 就是使用 BOM 来标记文本文件的编码方式的。
UCS 的组合字符指的是一些单个字符不是一个完整的字符,它是一个类似于重音符或其他指示标记,加在前一个字符后面。比如说重音符号,汉语拼音的音调,还比如 "菊花文" (我҉是҉菊҉花҉文҉) 中的菊花 (U+0489),组合字符机制允许在任何字符后加上重音符或其他指示标记, 这在科学符号中特别有用,比如数学方程式和国际音标字母,可能会需要在一个基本字符后组合上一个或多个指示标记。
不是所有的系统都需要支持象组合字符这样的 UCS 里所有的先进机制. 因此 ISO 10646 指定了下列三种实现级别:
Unicode 标准额外定义了许多与字符有关的语义符号学,一般而言是对于实现高质量的印刷出版系统的更好的参考。Unicode 详细说明了绘制某些语言 (比如阿拉伯语) 表达形式的算法,处理双向文字 (比如拉丁与希伯来文混合文字) 的算法和排序与字符串比较所需的算法,以及其他许多东西。
另一方面,UCS(ISO-10646) 标准,只不过是一个简单的字符集表。它指定了一些与标准有关的术语。定义了一些编码的别名。并包括了规范说明。指定了怎样使用 UCS 连接其他 ISO 标准的实现,比如 ISO-6429 和 ISO-2022。还有一些与 ISO 紧密相关的,比如 ISO-14651 是关于 UCS 字符串排序的。
考虑到 Unicode 标准有一个易记的名字,且在任何好的书店里的 Addison-Wesley 里有,只花费 ISO 版本的一小部分,且包括更多的辅助信息,因而它成为使用广泛得多的参考也就不足为奇了。然而,一般认为,用于打印 ISO-10646-1 标准的字体在某些方面的质量要高于用于打印 Unicode 2.0 的。专业字体设计者总是被建议说要两个标准都实现,但一些提供的样例字形有显著的区别。ISO-10646-1 标准同样使用四种不同的风格变体来显示表意文字如中文,日文和韩文 (CJK),而 Unicode 2.0 的表里只有中文的变体。这导致了普遍的认为 Unicode 对日本用户来说是不可接收的传说,尽管是错误的。
所谓代码页 (codepage) 就是各国的文字编码和 Unicode 之间的映射表。例如 GBK 和 Unicode 的映射表就是 CP936,所以也常用 cp936 来指代 GBK。
写了这么多了,举一个例子吧
打开 "记事本" 程序 Notepad.exe,新建一个文本文件,内容就是一个 "我" 字,依次采用 ANSI,Unicode,Unicode big endian 和 UTF-8 编码方式保存。
然后,用文本编辑软件 UltraEdit 中的 "十六进制功能",观察该文件的内部编码方式。
1)ANSI:文件的编码就是两个字节 "CE D2",这正是 "我" 的 GB2312 编码,这也暗示 GB2312 是采用大尾方式存储的。
2)Unicode:编码是四个字节 "FF FE 11 62",其中 "FF FE" 表明是小头方式存储,真正的编码是 U+6211。
3)Unicode big endian:编码是四个字节 "FE FF 62 11",其中 "FE FF" 表明是大头方式存储。
4)UTF-8:编码是六个字节 "EF BB BF E6 88 91",前三个字节 "EF BB BF" 表示这是 UTF-8 编码,后三个 "E6 88 91" 就是 "我" 的具体编码,它的存储顺序与编码顺序是一致的。