我们知道,计算机只能识别诸如 0101 这样的二进制数,于是人们必须以二进制数据与计算机进行交互,或者先将人类使用的字符按一定规则转换为二进制数。
那什么是字符呢?在计算机领域,我们把诸如文字、标点符号、图形符号、数字等统称为字符。而由字符组成的集合则成为字符集,字符集由于包含字符的多少与异同而形成了各种不同的字符集。
我们知道,所有字符在计算机中都是以二进制来存储的。那么一个字符究竟由多少个二进制位来表示呢?这就涉及到字符编码的概念了,比如一个字符集有 8 个字符,那么用 3 个二进制位就可以完全表示该字符集的所有字符,也即每个字符用 3 个二进制位进行编码。
我们规定字符编码必须完成如下两件事:
(1)规定一个字符集中的字符由多少个字节表示
(2)制定该字符集的字符编码表,即该字符集中每个字符对应的(二进制)值。
1. ASCII 码:
上个世纪 60 年代,美国制定了一套字符编码标准,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。
ASCII(American Standard Code for Information
Interchange),是一种字符编码标准,它的字符集为英文字符集,它规定字符集中的每个字符均由一个字节表示,指定了字符表编码表,称为 ASCII 码表。它已被国际标准化组织定义为国际标准,称为 ISO646 标准。
ASCII 码一共规定了 128 个字符的编码,比如空格“SPACE”是 32(二进制00100000),大写的字母 A是 65(二进制01000001)等。这 128 个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面 7 位,最前面的 1 位统一规定为 0。这种采用一个字节来编码 128 个字符的 ASCII 码称为标准 ASCII 码或者基础 ASCII 码。
但是,由于标准 ASCII 字符集字符数目有限,在实际应用中往往无法满足要求。为此,国际标准化组织又制定了 ISO 2022 标准,它规定了在保持与 ISO646 兼容的前提下将 ASCII 字符集扩充为 8 位代码的统一方法。 ISO 陆续制定了一批适用于不同地区的扩充 ASCII 字符集,每种扩充 ASCII 字符集分别可以扩充 128 个字符,这些扩充字符的编码均为高位为 1 的 8 位代码(即十进制数
128~255 ),称为扩展 ASCII 码。
但是需要注意,各种扩展 ASCII 码除了编码为 0~127 的字符外,编码为 128~255 的字符并不相同。比如,130 在法语编码中代表了 é,在希伯来语编码中却代表了字母 Gimel (?),在俄语编码中又会代表另一个符号。
2. ANSI编码标准
标准 ASCII 码和扩展 ASCII 码满足了西语国家的需求,但是,随着计算机在世界范围内的普及,对于亚洲国家,如中日韩等国来说,他们使用的符号很多,ASCII 字符编码标准远远不能满足其需要,于是这些国家便针对本国的字符集指定了相应的字符编码标准,如 GB2312、BIG5、JIS 等仅适用于本国字符集的编码标准。
这些字符编码标准统称为 ANSI 编码标准,这些 ANSI 编码标准有一些共同的特点:
(1)每种 ANSI 字符集只规定自己国家或地区使用的语言所需的'字符',比如简体中文编码标准
GB-2312 的字符集中就不会包含韩国人的文字。
(2)ANSI 字符集的空间都比 ASCII 要大很多,一个字节已经不够,绝大多数 ANSI 编码标准都使用多个字节来表示一个字符。
(3)ANSI 编码标准一般都会兼容 ASCII 码。
这里要特别提一下我国的几种字符编码标准:GB2312、GBK、GB18030。
字符必须编码后才能被计算机处理。计算机使用的默认编码方式就是计算机的内码。早期的计算机使用 7 位的 ASCII 编码(标准 ASCII 编码),为了处理汉字,程序员设计了用于简体中文的 GB2312 和用于繁体中文的 big5。
GB2312(1980年) 一共收录了 7445 个字符,包括 6763 个汉字和 682 个其它符号。汉字区的内码范围高字节从 B0-F7,低字节从 A1-FE,占用的码位是 72*94=6768。其中有 5 个空位是 D7FA-D7FE。
GB2312 支持的汉字太少。1995年的汉字扩展规范
GBO1.0 收录了 21886 个符号,它分为汉字区和图形符号区。汉字区包括 21003 个字符。2000 年的
GB18030 是取代 GBO1.0 的正式国家标准。该标准收录了 27484 个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。现在的 PC
平台必须支持 GB18030,对嵌入式产品暂不作要求。所以手机、MP3一般只支持 GB2312。
例如,在 Windows 中打开记事本,"另存为"对话框的"编码"下拉框中有一项 ANSI 编码,ANSI 是默认的编码方式。对于英文文件是 ASCII 编码,对于简体中文文件是 GB2312 编码(只针对Windows简体中文版,如果是繁体中文版会采用 Big5 码),在日文操作系统下,ANSI 编码代表 JIS 编码,其他语言的系统的情况类似。
3. Unicode、UCS 和 UTF
但是随着互联网的兴起,问题又出现了。由于 ANSI 码的第一个特点:各个国家或地区在编制自己的 ANSI 码时并未考虑到其他国家或地区的 ANSI 码,导致编码空间有重叠,比如:汉字'中'的编码是[0xD6,0xD0],这个编码在其他国家的 ANSI 编码标准中则不一定就是该编码了。于是,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。这样一来当在不同ANSI编码标准之间进行信息交换和显示的时候,乱码就不可避免了。
(1)Unicode
可以想象,如果有一种编码,将世界上所有的符号都纳入其中,每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是 Unicode,就像它的名字所表示的,这是一种所有符号的编码。
Unicode 是 Universal Multiple-Octet Coded Character Set 的缩写,中文含义是"通用多八位编码字符集"。它是由一个名为 Unicode 学术学会(Unicode。org)的机构制订的字符编码标准,Unicode 目标是将世界上绝大多数国家的文字、符号都编入其字符集,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求,以达到支持现今世界各种不同语言的书面文本的交换、处理及显示的目的,使世界范围人们通过计算机进行信息交换时达到畅通自如而无障碍。
由于一个 Unicode 字符用多个字节表示。这样
Unicode 编码在不同平台存储时就要注意其字节序了。比如:采用标准Unicode编码的'中'在 X86 平台上(big endian)的存储就是 '2D4E',而在 SPARC Solaris 上(little endian)的存储则是 '4E2D'。
(2) UCS
那什么又是 UCS 呢,它与 Unicode 有何关系?
历史上,有两个独立创立统一字符集的尝试。一个是国际标准化组织(ISO)的 ISO10646 项目, 另一个是由(一开始大多是美国的)多语言软件制造商组成的协会(unicode.org)组织的 Unicode 项目。 幸运的是, 1991 年前后, 两个项目的参与者都认识到,
世界不需要两个不同的统一字符集。 它们合并双方的工作成果, 并为创立一个统一编码表而协同工作。现在,两个项目仍都存在并独立地公布各自的标准, 但 Unicode 协会和 ISO/IEC JTC1/SC2 都同意保持 Unicode 和 ISO10646 标准的码表兼容, 并紧密地共同调整任何未来的扩展。
国际标准 ISO10646 定义了通用字符集 (Universal
Character Set) UCS。 UCS 是所有其他字符集标准的一个超集。
它保证与其他字符集是双向兼容的。 就是说, 如果你将任何文本字符串翻译到 UCS 格式, 然后再翻译回原编码,
你不会丢失任何信息。
ISO10646 定义了一个 31 位的字符集(4 个字节)。然而,在这巨大的编码空间中,迄今为止只分配了前 65534 个码位 (0x0000 到
0xFFFD)。 这个 UCS 的 16 位子集称为基本多语言面(Basic Multilingual Plane,BMP)。将被编码在 16 位 BMP 以外的字符都属于非常特殊的字符(比如象形文字),且只有专家在历史和科学领域里才会用到它们。按当前的计划,
将来也许再也不会有字符被分配到从 0x000000 到 0x10FFFF 这个覆盖了超过 100 万个潜在的未来字符的 21 位的编码空间以外去了。ISO10646-1 标准第一次发表于 1993 年, 定义了字符集与 BMP 中内容的架构。定义 BMP 以外的字符编码的第二部分 ISO 10646-2 正在准备中, 但也许要过好几年才能完成。 新的字符仍源源不断地加入到 BMP 中, 但已经存在的字符是稳定的且不会再改变了。
UCS 不仅给每个字符分配一个代码, 而且赋予了一个正式的名字。 表示一个 UCS 值的十六进制数,
通常在前面加上 "U+", 就象 U+0041 代表字符"拉丁大写字母A"。UCS
字符 U+0000 到 U+007F 与 US-ASCII(ISO646) 是一致的, U+0000 到 U+00FF 与
ISO 8859-1(Latin-1) 也是一致的。从
U+E000 到 U+F8FF, 已经 BMP 以外的大范围的编码是为私用保留的。
Unicode 字符编码标准与 ISO10646 的通用字符集(Universal Character Set,UCS)概念相对应,目前的用于实用的 Unicode 版本对应于 UCS-2,即使用 16 位来表示一个 Unicode 字符。也就是每个字符占用 2 个字节。这样理论上一共最多可以表示 65536(2 的 16 次方) 个字符。基本满足各种语言的使用。
实际上目前版本的 Unicode 尚未填充满这 16 位编码,保留了大量空间作为特殊使用或将来扩展。未来版本会扩充到 ISO 10646-1 实现级别 3,即涵盖 UCS-4 的所有字符。UCS-4 是一个更大的尚未填充完全的31位字符集,加上恒为 0 的首位,共需占据 32 位,即 4 字节。理论上最多能表示 2147483648(2的31次方)个字符,完全可以涵盖一切语言所用的符号。
由于Unicode 编码标准与 UCS 编码标准是相互兼容的,为了方便叙述,下面把二者作为一个统一编码标准来叙述。
(3)UTF
Unicode(UCS)只是一个字符集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
比如,汉字“严”的 Unicode(UCS)码是十六进制数 4E25,转换成二进制数足足有 15 位(100111000100101),也就是说这个符号的表示至少需要 2 个字节。表示其他更大的符号,可能需要 3 个字节或者 4 个字节,甚至更多。
这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
为了解决这些问题,就出现了 UTF。
UTF(Unicode Translation Format),它是 Unicode (UCS)的实现(或存储)方式,称为 Unicode 转换格式。Unicode 的实现方式不同于编码方式。一个字符的 Unicode 编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对 Unicode 编码的实现方式有所不同。
UTF 有三种实现方式:
UTF-16:其本身就是标准的 Unicode 编码方案,又称为 UCS-2,它固定使用 16 bits(两个字节)来表示一个字符。
UTF-32:又称为UCS-4,它固定使用32 bits(四个字节)来表示一个字符。
UTF-8:最广泛的使用的 UTF 方案,UTF-8 使用可变长度字节来储存 Unicode 字符,例如 ASCII 字母继续使用 1 字节储存,重音文字、希腊字母或西里尔字母等使用 2 字节来储存,而常用的汉字就要使用 3 字节。辅助平面字符则使用 4 字节。UTF-8 更便于在使用 Unicode 的系统与现存的单字节的系统进行数据传输和交换。与前两个方案不同:UTF-8 以字节为编码单元,没有字节序的问题。
UTF 有三种方案,那么如何在接收数据和存储数据时识别数据采用的是哪个方案呢?
Unicode(UCS)规范中推荐的标记字节顺序的方法是
BOM。BOM 是 Byte order Mark。
在 UCS 编码中有一个叫做“ZERO WIDTH
NO-BREAK SPACE”(零宽度非换行空格)的字符,它的编码是 FEFF。而 FFFE 在 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 来标记文本文件的编码方式的。
这样根据识别前面的“ZERO WIDTH NO-BREAK SPACE”字符即可识别编码方案,字节流中前几个字节所表示的编码方式如下:
EF BB BF |
UTF-8 |
FE FF |
UTF-16/UCS-2, little endian |
FF FE |
UTF-16/UCS-2, big endian |
FE FF 00 00 |
UTF-32/UCS-4, little endian |
00 00 FF FE |
UTF-32/UCS-4, big-endian |
在微软公司Windows XP操作系统附带的记事本中,“另存为”对话框可以选择的四种编码方式除去非 Unicode 编码的 ANSI 外,其余三种“Unicode”、“Unicode big endian”和“UTF-8”分别对应 UTF-16 小端(BOM)、UTF-16 大端(BOM)和 UTF-8 这三种实现方式。
另外,内码是指操作系统内部的字符编码。早期操作系统的内码是与语言相关的。目前 Windows 的内核已经支持 Unicode 字符集,这样在内核上可以支持全世界所有的语言文字。但是由于现有的大量程序和文档都采用了某种特定语言的编码,例如 GBK,Windows 不可能不支持现有的编码,而全部改用 Unicode。于是 Windows 就使用代码页(code page)来适应各个国家和地区不同的字符集。
而所谓代码页(code page)就是针对一种语言文字的字符编码。例如 GBK 的 code page 是
CP936,BIG5 的 code page 是 CP950,GB2312 的 code
page 是 CP20936。
微软一般将默认代码页指定的编码说成是内码。默认代码页指的是:默认用什么编码来解释字符。例如 Windows 的记事本打开了一个文本文件,里面的内容是字节流:BA、BA、D7、D6。Windows 应该去怎么解释它呢?
是按照 Unicode 编码解释、还是按照 GBK 解释、还是按照 BIG5 解释,还是按照 ISO8859-1 去解释?如果按 GBK 去解释,就会得到“汉字”两个字。按照其它编码解释,可能找不到对应的字符,也可能找到错误的字符。所谓“错误”是指与文本作者的本意不符,这时就产生了乱码。
答案是 Windows 按照当前的默认代码页去解释文本文件里的字节流。默认代码页可以通过控制面板的区域选项设置。记事本的另存为中有一项 ANSI,其实该项就是指按照默认代码页的编码方法保存。
注1:Unicode 编码转换为 UTF-8 编码的方法
UTF-8 就是以 8 位为单元对 Unicode 进行编码。下面是 Unicode 和 UTF-8 转换的规则:
Unicode |
UTF-8 |
0000 - 007F |
0xxxxxxx |
0080 - 07FF |
110xxxxx 10xxxxxx |
0800 - FFFF |
1110xxxx 10xxxxxx 10xxxxxx |
例如“汉”字的Unicode编码是 6C49。6C49 在
0800-FFFF 之间,所以肯定要用 3 字节模板了:1110xxxx
10xxxxxx 10xxxxxx。将 6C49 写成二进制是:0110 110001 001001, 用这个比特流依次代替模板中的 x,得到:11100110 10110001 10001001,即 E6 B1 89。
注2:当一个软件打开一个文本时,它要做的第一件事是决定这个文本究竟是使用哪种字符集的哪种编码保存的。软件一般采用三种方式来决定文本的字符集和编码:检测文件头标识,提示用户选择,根据一定的规则猜测。
最标准的途径是检测文本最开头的几个字节。
例如,当你在 Windows 的记事本里新建一个文件,输入“联通”两个字之后,保存,关闭,然后再次打开,你会发现这两个字已经消失了,代之的是几个乱码。当你新建一个文本文件时,记事本的编码默认是 ANSI(代表系统默认编码,在中文系统中是 GB 系列编码)。在这种编码下,"联通"的内码是:
c1 1100 0001
aa 1010 1010
cd 1100 1101
a8 1010 1000
注意,第一二个字节、第三四个字节的起始部分的都是"110"和"10",正好与 UTF-8 规则里的两字节模板是一致的。于是当我们再次打开记事本时,记事本就误认为这是一个 UTF-8 编码的文件,让我们把第一个字节的 110 和第二个字节的 10 去掉,我们就得到了“00001 101010”,再把各位对齐,补上前导的 0,就得到了“0000 0000 0110 1010”,不好意思,这是 UNICODE 的 006A,也就是小写的字母"j",而之后的两字节用 UTF-8 解码之后是 0368,这个字符什么也不是。这就是只有“联通”两个字的文件没有办法在记事本里正常显示的原因。
其实,如果记事本软件通过检测文件头标识来确定文件的编码方式就可避免该情况,即如果是 UTF-8 文件,则其文件前三个字节应该是 EF BB BF。
注3:地球人都知道,大多数Intel兼容机都采用小端法表示数据,而大多数IBM和Sun Microsystems的机器则采用大端法表示数据。例如 0x1234567 这个数,大端法在内存中按字节依次存放为:01 23 45 67,小端法在内存中按字节依次存放为:67 45 23 01。
很少有鱼油知道他们事实上是来源于Jonathan Swift的《格列佛游记》一书。
以下是Jonathan Swift 在1726 年关于大小端之争历史的描述:
我下面要告诉你的是,Lilliput 和Blefuscu 这两大强国在过去36 个月里一直在苦战。 战争开始是由于以下的原因:我们大家都认为,吃鸡蛋前,原始的方法是打破鸡蛋较大的一端, 可是当今皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋时碰巧将一个手指弄破了,因此他的父 亲,当时的皇帝,就下了一道敕令,命令全体臣民吃鸡蛋时打破鸡蛋较小的一端,违令者重罚。 老百姓们对这项命令极为反感。历史告诉我们,由此曾发生过6 次叛乱,其中一个皇帝送了命, 另一个丢了王位。这些叛乱大多都是由Blefuscu 的国王大臣们煽动起来的。叛乱平息后,流亡 的人总是逃到那个帝国去寻救避难。据估计,先后几次有11 000 人情愿受死也不肯去打破鸡蛋 较小的一端。关于这一争端,曾出版过几百本大部著作,不过大端派的书一直是受禁的,法律也 规定该派的任何人不得做官。(此段译文摘自网上蒋剑锋译的《格列佛游记》第一卷第4 章。)
在他那个时代,Swift 是在讽刺英国(Lilliput)和法国(Blefuscu)之间持续的冲突。Danny Cohen,一位网络协议的早期开创者,第一次使用这两个术语来指代字节顺序[25],后来这个术 语被广泛接纳了。