字符编码简介 一、字节 字节是计算机中存储数据的最小单位,一个字节有 8 个位(即二进制位,也叫 bit),可以表示 0~255 之间的任何一个数(即二进制的 00000000 到 11111111 之间)。你可以用字节表示任何东西,比如数字、字符、图像、音乐等,这取决于你如何解释这个字节。 二、ASCII 字符集 在标准 ASCII 码中,用一个字节来表示不同的字符,字节的最高位(也就是二进制代码的左边第一位)被用来做奇偶校验,所以只剩下 7 个位用来表示不同的字符,这样能表示的字符范围就变成了 0~127 之间。其中 0~31、127 这些数值被定义为控制字符,他们是不能显示的。32~126 这些数值被定义为下面的字符: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxy 如果要在计算机中存储 ASCII 字符串,只需要将这些字符所对应的数值(用来表示字符的数值也叫做码点)按字节依次存放即可,读取的时候也只要按字节依次读出即可。ASCII 字符集无法表示汉字。 三、Latin1 字符集 如果将标准 ASCII 码的最高位不用作奇偶校验,而也用来表示字符的话,那么就可以表示 256 个字符,在 ASCII 的基础上多出了 128 个字符,这个字符集叫做 Latin1 字符集,Latin1 是 ISO-8859-1 的别名,也可写作 Latin-1。其中 0~127 这些数值的定义与 ASCII 字符集一样,128~159 这些数值被定义为控制字符,160~255 这些数值被定义为下面的字符: ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ 如果要在计算机中存储 Latin1 字符串,只需要将这些字符所对应的码点(码点就是字符所对应的数值)按字节依次存放即可,读取的时候也只要按字节依次读出即可。Latin1 字符集无法表示汉字。 四、ANSI 字符集 由于 ASCII 字符集或 Latin1 字符集只能表示有限的字符,对于亚洲字符(例如中文、日文、韩文)则无法表示,所以为了使计算机支持更多语言,出现了 ANSI 字符集,ANSI 字符集中 0~127 这些数值的定义与 ASCII 字符集一样,而 128~255 这些数值被定义为双字节字符的一个编码,即“用其中的 2 个数值来表示 1 个字符”,这样的双字节可以表示的字符数量就达到了 1.6 万多个。 具体哪 2 个数值代表哪 1 个字符,则由各个国家自己去定义,不同国家的 ANSI 字符集是不同的,在中文系统下,ANSI 代表 GB2312 字符集,在日文系统下,ANSI 代表 JIS 字符集,不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。 由于 ANSI 字符串中的字符,有单字节和双字节之分,所以在存取的时候要判断当前字节是 0~127 范围的单字节,还是 128~255 范围的双字节,并做相应处理,才能得到正确的字符,这样的判断和处理就称为编码和解码。编码是将字符(码点)转换为字节(单字节或双字节)保存起来,解码是将字节(单字节或双字节)还原为字符(码点)显示出来。 ANSI 字符集存在的 BUG:当你在一个空文本文档中输入“联通”两个字,并保存为 ANSI 格式,再次打开的时候,内容将显示为乱码,这是因为,当文本文档中的所有的字符都满足“192 ≤ 第一字节 ≤ 223 且 128 ≤ 第二字节 ≤ 191”的时候,Windows 就无法正确识别文档的编码格式,错误的将 ANSI 格式识别成了 UTF-8 格式,造成解码错误,形成乱码。 所以在保存文本文档的时候,不推荐使用 ANSI 编码格式保存,而应该使用更好的 UTF-8 或 Unicode 编码格式。 五、UCS 和 Unicode 字符集 由于 ANSI 字符集的范围有限,不能包含世界上的所有字符,无法用于多语言环境(指可同时处理多种语言混合的情况),所以出现了 UCS 和 Unicode 字符集,UCS(Universal Character Set,通用字符集)使用 4 个字节来表示一个字符,其中最高字节的最高二进制位始终为 0,这样能够表示的字符数量就达到了约 20 亿个,足以容纳全世界所有的字符。在 UCS 字符集中,0~255 这些数值的定义与 Latin1 一样,只不过是用 4 个字节表示 1 个字符,除最低位字节外,其它 3 个字节全部用 0 填充。这样就保证了与 Latin1 字符集的兼容。 UCS 是由 ISO 制定的 ISO 10646(或称 ISO/IEC 10646)标准所定义的标准字符集。UCS 将最高字节定义为 2^7=128 个组(group),每个组再根据次高字节分为 256 个平面(plane),每个平面再根据次低字节分为 256 行(row),每行再根据最低字节分为 256 个码位(cell)。UCS 中第 0 组的第 0 平面被称为“基本多文种平面”,里面存放了包含世界各国的常用字符。除了第 0 平面,UCS 还定义了 16 个辅助平面,用来存放更多的字符。 Unicode 字符集是 UCS 字符集的子集,只包含 0x0~0x10FFFF 范围的字符。Unicode 3.0 及其以下版本只包含了“基本多文种平面”中的字符(即:0x0~0xFFFF),可以只使用 2 个字节来表示一个字符。Unicode 3.1 及其以上版本包含了 UCS 中的 16 个辅助平面(即:0x0~0x10FFF)。需要用 4 个字节来表示一个“基本多文种平面”以外的字符。 “基本多文种平面”中有一个专用区“0xE000~0xF8FF”,有 6400 个码位,用来存放用户自定义的字符。还有一个被称作代理区(Surrogate)的特殊区域“0xD800~0xDFFF”,共 2048 个码位,代理区的目的是用两个 UTF-16 编码表示“基本多文种平面”以外的字符。具体参照“UTF16 编码”的介绍。 历史上存在着两个独立的尝试创立单一字符集的组织,即国际标准化组织(ISO)和多语言软件制造商组成的统一码联盟。前者开发 ISO/IEC 10646(UCS)项目,后者开发 Unicode 项目。因此最初制定了不同的标准。1991 年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从 Unicode 2.0 开始,Unicode 采用了与 ISO 10646-1 相同的字库和字码;ISO 也承诺,ISO 10646 将不会给超出 U+10FFFF 的 UCS-4 编码赋值,以使得两者保持一致。两个项目仍都存在,并独立地公布各自的标准。由于 Unicode 这一名字比较好记,因而它使用更为广泛。 六、Unicode 编码方式 前面讲的 Unicode 字符集中的字符,只是单个的字符形式,如果要将多个 Unicode 字符保存到文件中,则需要一个编码方式,将 Unicode 码点转换为字节存放到文件中,目前常用的编码方式有:UTF-32、UTF-16、UTF-8 等。 UTF 是“Unicode/UCS Transformation Format”的首字母缩写,即“把 Unicode 字符转换为某种格式”之意。 UTF-32 对每一个 Unicode 码点使用 4 个字节表示。UTF-32 足以表示 Unicode 所有组所有平面中的所有字符,但是我们通常用不到那么多平面,我们常用的只有“基本多文种平面”,对于这个平面中的字符而言,只用 2 个字节来表示就足够了,对于欧洲国家而言,通常只使用 ASCII 字符,如果使用 UTF-32 来保存,将浪费很大的存储空间。所以,UTF-32 在实际中用的并不多。 UTF-16 对每一个 Unicode 码点使用 2 个或 4 个字节表示,“基本多文种平面”内的字符使用 2 个字节足以全部表示出来,而“基本多文种平面”外的字符,则需要用到“代理区域(0xD800~0xDFFF)”,“代理区域”中的数值永久保留,不与 Unicode 字符进行映射。UTF-16 就利用保留下来的 0xD800~0xDFFF 中的数值来对“基本多文种平面”以外的字符进行编码。UTF-16 的编码规则如下: 如果 Unicode 字符的码点在 0x0~0xFFFF 范围内,则直接用 Unicode 码点的最低 2 个字节作为其 UTF-16 的编码值。 如果 Unicode 字符的码点在 0x01FFFF~0x10FFFF 范围内,则将该码点的数值减去 0x10000,然后将结果的二进制值(共 20 位)从中间分开成两部分(高 10 位和低 10 位),高 10 位的值(值的范围为 0x0~0x3FF)被加上 0xD800 得到第一个码元(或称作前导代理,值的范围是 0xD800~0xDBFF,占用 2 个字节)。低 10 位值(值的范围也是 0x0~0x3FF)被加上 0xDC00 得到第二个码元(或称作后尾代理,值的范围是 0xDC00~0xDFFF,占用 2 个字节)。这两个码元合在一起就构成了一个 4 字节的 UTF-16 编码,这样 UTF-16 就可以表示整个 Unicode 范围内的所有字符了。由此可见,UTF-16 是一种变长的编码方式,它的编码长度不固定,有些字符用 2 个字节表示,有些字符用 4 个字节表示。UTF-16 的编码方式也叫 Unicode 编码方式。 UTF-8 对每一个 Unicode 码点使用 1、2、3、4 个字节表示,也是一种变长的编码方式。 对于 0x0~0x7F 之间的字符,使用 1 个字节表示,与 ASCII 字符集完全相同。 对于 0x80~0x7FF 之间的字符,使用 2 个字节表示,表示方法为,将该 Unicode 字符的二进制编码按顺序填入“110xxxxx 10xxxxxx”的 x 中即可得到 2 个字节的 UTF-8 编码。其中 110 是 UTF-8 编码的第一个字节标记,有了此标记,就说明该字节是一个 UTF-8 字符编码的开头字节,而且 110 中的 2 个 1 表示该 UTF-8 字符编码是由 2 个字节组成,后面 10 开头的字节表示 UTF-8 的后续字节。将这 2 个字节读出来,然后解码,就可以得到一个 Unicode 字符。 对于 0x800~0xFFFF 之间的字符,使用 3 个字节表示,表示方法为,将该 Unicode 字符的二进制编码按顺序填入“1110xxxx 10xxxxxx 10xxxxxx”的 x 中即可得到 3 个字节的 UTF-8 编码。其中 1110 是 UTF-8 编码的第一个字节标记,有了此标记,就说明该字节是一个 UTF-8 字符编码的开头字节,而且 1110 中的 3 个 1 表示该 UTF-8 字符编码是由 3 个字节组成,后面 10 开头的字节表示 UTF-8 的后续字节。将这 3 个字节读出来,然后解码,就可以得到一个 Unicode 字符。 对于 0x10000~0x10FFFF 之间的字符,使用 4 个字节表示,表示方法为,将该 Unicode 字符的二进制编码按顺序填入“11110xxx 10xxxxxx 10xxxxxx 10xxxxxx”的 x 中即可得到 4 个字节的 UTF-8 编码。其中 11110 是 UTF-8 编码的第一个字节标记,有了此标记,就说明该字节是一个 UTF-8 字符编码的开头字节,而且 11110 中的 4 个 1 表示该 UTF-8 字符编码是由 4 个字节组成,后面 10 开头的字节表示 UTF-8 的后续字节。将这 4 个字节读出来,然后解码,就可以得到一个 Unicode 字符。 七、BOM 在 UTF-32 和 UTF-16 编码中,有些系统将编码的高位字节放前面,低位字节放后面,而有些系统则刚好相反,这就造成不同系统之间,因为存放字节的顺序不同,而互相不兼容的情况。为了解决这个问题,UCS 建议在整个 UTF-32 或 UTF-16 编码流的起始位置添加一个标记,用来标识该编码流的字节存放顺序。 在 UCS 编码中有一个叫做“Zero Width No-Break Space”(中文译作“零宽无间断间隔”)的字符,它的编码是 FEFF。而另一个编码 FFFE 在 UCS 中是不存在的字符,所以不应该出现在实际传输中。UCS 规范建议我们在传输字节流前,先传输 BOM(Byte Order Mark,字节顺序标号)标记。这样如果接收者收到 FEFF,就表明这个字节流是 Big-Endian 格式(高位字节在后);如果收到 FFFE,就表明这个字节流是 Little- Endian 格式(低位字节在后)。因此像 FEFF、FFFE 这样的标记又被称作 BOM。 在 Windows 和 Unix 系统中,UTF-32 和 UTF-16 使用的是 Little-Endian 字节顺序,而在 Mac 系统中则使用 Big-Endian 字节顺序。 UTF-8 编码的字节顺序是固定的,在编码的时候就定好了。如果在 UTF-8 编码中使用 BOM(UTF-8 的 BOM 为 EFBBBF),则 BOM 只能用来标示它是一个 UTF-8 文件,而不用来说明字节顺序。 八、总结 字符集的概念比较简单,就是将数值与字符一一对映起来即可,该数值就是该字符的码点,比如 ASCII 字符集、Latin1 字符集、Unicode 字符集,一个数值对应一个字符。 要将字符集中的多个字符保存到文件中,就不那么容易了,对于 ASCII、Latin1 这样的单字节字符集而言,保存到文件是比较简单的,将码点按顺序写入文件即可。而对于 Unicode 这样的多字节字符集而言,要保存到文件,就要考虑效率和空间利用率问题,既要高效率,又不能浪费太多空间,所以就出现了各种各样的 UTF 编码方式。 对于欧洲国家而言,使用 UTF-8 编码保存文本比较合适,对于亚洲国家而言,使用 UTF-16 编码(即 Unicode 编码)保存文本比较合适。 对于比较特殊的 ANSI 字符集,由于它的字符集在不同的国家是不一样的,各国之间互不兼容,而且容量比较小,还是弃之不用比较好。