常见字符编码详解
一. 前言
在解决昨天的问题时,又引出了很多新的问题,如为什么要进行编码,这些编码的关系如何,如 ASCII,IOS-8859-1,GB2312,GBK,Unicode 之间的关系,笔者想要彻底理解字符编码背后的故事,遂进行了探索,具体笔记如下。如园友能读完本篇文章,我相信会解开很多疑惑。
二. 字符编码
2.1 为何需要字符编码
我们知道,所有的信息最终都表示为一个二进制的字符串,每一个二进制位(bit)有 0 和 1 两种状态。当我们需要把字符 A 存入计算机时,应该对应哪种状态呢,存储时,我们可以将字符 A 用 01000010(这个随便编的)二进制字符串表示,存入计算机;读取时,再将 01000010 还原成字符 A。那么问题来了,存储时,字符 A 应该对应哪一串二进制数呢,是 01000010?或者是 10000000 11110101?说白了,就是需要一个规则。这个规则可以将字符映射到唯一一种状态(二进制字符串),这就是编码。而最早出现的编码规则就是 ASCII 编码,在 ASCII 编码规则中,字符 A 既不对应 01000010,也不对应 1000 0000 11110101,而是对应 01000001(不要问为什么,这是规则)。
2.2 ASCII
这套编码规则是由美国定制,一共规定了 128 个字符的编码,比如空格 SPACE 是 32(十进制)(二进制 00100000),大写的字母 A 是 65(二进制 01000001)。这 128 个符号(包括 32 个不能打印出来的控制符号),只占用了一个字节(8 bit)的后面 7 位,最前面的 1 位统一规定为 0。总共才有 128 个字符编码,一个字节都没有用完,这好像似乎有点太少了。于是乎,就开始压榨最高位,对其为 1 时也进行编码,利用最高位进行编码的方式就称为非 ASCII 编码,如 ISO-8859-1 编码。
2.3 ISO-8859-1
这套编码规则由 ISO 组织制定。是在 ASCII 码基础上又制定了一些标准用来扩展 ASCII 编码,即 00000000(0) ~ 01111111(127) 与 ASCII 的编码一样,对 10000000(128) ~ 11111111(255)这一段进行了编码,如将字符§编码成 10100111(167)。ISO-8859-1 编码也是单字节编码,最多能够表示 256 个字符。Latin1 是 ISO-8859-1 的别名,有些环境下写作 Latin-1。但是,即使能够表示 256 个字符,对中文而言,还是太少了,一个字节肯定不够,必须用多个字节表示。
但是,由于是单字节编码,和计算机最基础的表示单位一致,所以很多时候,仍旧使用 ISO8859-1 编码来表示。而且在很多协议上,默认使用该编码。比如,虽然"中文"两个字不存在 ISO8859-1 编码,以 GB2312 编码为例,应该是 D6D0 CEC4 两个字符,使用 ISO8859-1 编码的时候则将它拆开为 4 个字节来表示:D6D0 CEC4(事实上,在进行存储的时候,也是以字节为单位进行处理)。而如果是 UTF 编码,则是 6 个字节 e4 b8 ad e6 96 87。很明显,这种表示方法还需要以另一种编码为基础才能正确显示。而常见的中文编码方式有 GB2312、BIG5、GBK。
2.4 GB2312
GB2312 其对所收录字符进行了"分区"处理,共 94 个区,区从 1(十进制)开始,一直到 94(十进制),每区含有 94 个位,位从 1(十进制)开始,一直到 94(十进制),共 8836(94 * 94)个码位,这种表示方式也称为区位码,GB2312 是双字节编码,其中高字节表示区,低字节表示位。各区具体说明如下:
01-09 区收录除汉字外的 682 个字符,有 164 个空位(9 * 94 - 682)。
10-15 区为空白区,没有使用。
16-55 区收录 3755 个一级汉字(简体),按拼音排序。
56-87 区收录 3008 个二级汉字(简体),按部首/笔画排序。
88-94 区为空白区,没有使用。
那么根据区位码如何算出 GBK2312 编码呢?区位码的表示范围为 0101 - 9494(包含了空的区位码)。 点击这里 ,查看中 GB2312 编码区位码。之后只需要按照如下规则进行转化即可。
- 将区(十进制)转化为十六进制。
- 将转化的十六进制加上 A0,得到 GB2312 编码的高字节。
- 将位(十进制)转化为十六进制。
- 将转化的十六进制加上 A0,得到 GB2312 编码的低字节。
- 组合区和位,区在高字节,位在低字节。
- 得到 GB2312 编码。
具体的流程图如下:
例如:'李'字的区位码为 3278(表示在 32 区,78 位)。1. 将 32(区)转化为十六进制为 20。2. 加上 A0 为 C0。3. 将 78(位)转化为十六进制为 4E。4. 加上 A0 为 EE。5. 组合区和位,为 C0EE。6. 得到 GB2312 编码,即'李'字的 GB2312 编码为 C0EE。
GB2312 用两个字节编码,采用分区编码,总共编码的中文个数为 6763(3755 + 3008)。这些汉字只是最常用的汉字,已经覆盖中国大陆 99.75%的使用频率。但是,还有一些汉字在 GB2312 中没有被编码,如'镕'字,在 GB2312 中就没有被编码,这样就导致了问题,随之就出现了主流的 GBK 编码。在讲解 GBK 编码之前,我们另外讲解一下 BIG5 编码。
2.5 BIG5
BIG5 采用双字节编码,使用两个字节来表示一个字符。高位字节使用了 0x81-0xFE,低位字节使用了 0x40-0x7E,及 0xA1-0xFE。该编码是繁体中文字符集编码标准,共收录 13060 个中文字,其中有二字为重复编码,即“兀、兀”(A461 及 C94A) 和“嗀、嗀”(DCD1 及 DDFC)。具体的分区如下:
8140-A0FE 保留给使用者自定义字符(造字区)
A140-A3BF 标点符号、希腊字母及特殊符号。其中在 A259-A261,收录了度量衡单位用字:兙兛兞兝兡兣嗧瓩糎。
A3C0-A3FE 保留。此区没有开放作造字区用。
A440-C67E 常用汉字,先按笔划再按部首排序。
C6A1-F9DC 其它汉字。
F9DD-F9FE 制表符。
点击这里 ,查看 BIG5 编码。注意,BIG5 编码与 GBK 编码没有什么关系。
2.6 GBK
GBK 编码扩展了 GB2312,完全兼容 GB2312 编码(如'李'字的 GBK、GB2312 编码均为 C0EE),但其不兼容 BIG5 编码('長'字的 BIG5 编码为 AAF8,GBK 编码为 E94C,'李'字的 BIG5 编码为 A7F5 不等于 C0EE),即如果使用 GB2312 编码,使用 GBK 解码是完全正常的,但是如果使用 BIG5 编码,使用 GBK 解码,会出现乱码。相比于 GB2312 编码,GBK 编码了更多汉字,如'镕'字。GBK 编码依然采用双字节编码方案,其编码范围:8140-FEFE,剔除 xx7F 码位,共 23940 个码位。能表示 21003 个汉字。 点击这里 ,查看 GBK 编码。 点击这里 ,可以查询中文的其他编码。在 GBK 之后又出现了 GB18030 编码,但是没有形成主流,故不做介绍,至此,中文编码的问题已经讲解完成。那么问题又来了,大陆网民与在海峡两岸网民交流时,若都使用 GBK 编码,则没有问题,若一方使用 GBK 编码,一方使用 BIG5 编码,那么就会出现乱码问题,这是在海峡两岸网民交流,如果漂洋过海进行交流呢?那就更容易出现乱码问题,这时候我们可能想,要是有一套全世界都通用的编码就好了,不要担心,这样的编码确实是存在的,那就是 Unicode。
2.7 Unicode
有两个独立的, 创立单一字符集的尝试. 一个是国际标准化组织(ISO) 的 ISO 10646 项目, 另一个是由多语言软件制造商组成的协会组织的 Unicode 项目. 在 1991 年前后, 两个项目的参与者都认识到, 世界不需要两个不同的单一字符集. 它们合并双方的工作成果, 并为创立一个单一编码表而协同工作. 两个项目仍都存在并独立地公布各自的标准, 但 Unicode 协会和 ISO/IEC JTC1/SC2 都同意保持 Unicode 和 ISO 10646 标准的码表兼容, 并紧密地共同调整任何未来的扩展。
Unicode 是指一张表,里面包含了可能出现的所有字符,每个字符对应一个数字,这个数字称为码点(Code Point),如字符'H'的码点为 72(十进制),字符'李'的码点为 26446(十进制)。Unicode 表包含了 1114112 个码点,即从 000000(十六进制) - 10FFFF(十六进制)。地球上所有字符都可以在 Unicode 表中找到对应的唯一码点。 点击这里 ,查询字符对应的码点。Unicode 将码空间划分为 17 个平面,从 00 - 10(十六进制,最高两位),即从 0 - 16(十进制),每个平面有 65536 个码点(2^16),其中最重要的是第一个 Unicode 平面(码位从 0000 - FFFF),包含了最常用的字符,该平面被称为基本多语言平面(Basic Multilingual Plane),缩写为 BMP,其他平面称为辅助平面(Supplementary Planes),在基本多文种平面內, 从 D800 到 DFFF 之间的码位区段是永久保留不映射到字符的, 因此 UTF-16 编码巧妙的利用了这保留下来的码位来对辅助平面内的字符进行编码,这点后面进行讲解。Unicode 只是一个符号集,只规定的字符所对应的码点,并没有指定如何存储,如何进行存储出现了不同的编码方案,关于 Unicode 编码方案主要有两条主线:UCS 和 UTF。UTF 主线由 Unicode Consortium 进行维护管理,UCS 主线由 ISO/IEC 进行维护管理。
简单来说 Unicode 码表的最大值是 0X10FFFF,也就是三个字节,但是 BMP 平面中包含着最常用的字符,它的最大值是 0XFFFF,也就只需要占用两个字节。在 Java 语言中 char 类型只占用两个字节,char 中存储的是字符的 Unicode 码点,char 只能表示 BMP 子符。
2.8 UCS
UCS 全称为"Universal Character Set",在 UCS 中主要有 UCS-2 和 UCS-4
2.8.1 UCS-2
UCS-2 是定长字节的,固定使用 2 个字节进行编码,从 0000(十六进制)- FFFF(十六进制)的码位范围,对应第一个 Unicode 平面。采用 BOM(Byte Order Mark) 机制,该机制作用如下:1. 确定字节流采用的是大端序还是小端序。2. 确定字节流的 Unicode 编码方案。
2.8.2 UCS-4
UCS-4 是定长字节的,固定使用 4 个字节进行编码。也采用了 BOM 机制。
2.9 UTF
UTF 全称为"Unicode Transformation Format",在 UTF 中主要有 UTF-8,UTF-16 和 UTF-32。
2.9.1 UTF-8
UTF-8 是一种变长编码方式,使用 1-4 个字节进行编码。UTF-8 完全兼容 ASCII,对于 ASCII 中的字符,UTF-8 采用的编码值跟 ASCII 完全一致。UTF-8 是 Unicode 一种具体的编码实现。UTF-8 是在互联网上使用最广的一种 Unicode 的编码规则,因为这种编码有利于节约网络流量(因为变长编码,而非统一长度编码)。关于 Unicode 码点如何转化为 UTF-8 编码,可以参照如下规则:
① 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
② 对于 n 字节的符号(n>1),第一个字节的前 n 位都设为 1,第 n+1 位设为 0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,全部为这个符号的 unicode 码。
总结的编码规则如下:
U+ 0000 ~ U+ 007F: 0XXXXXXX
00000000 00000000 ~ 00000000 01111111 #二进制范围
0 ~ 127 #十进制范围
U+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXX
00000000 10000000 ~ 00000111 11111111
128 ~ 2047
U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX
00001000 00000000 ~ 11111111 11111111
2048 ~ 65535
U+ 10000 ~ U+10FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
00000000 00000001 00000000 00000000 ~ 00000000 00010000 11111111 11111111
65536 ~ 1114111
说明:字符'A'的 Unicode 码点为 65(十进制),根据上表,在第一行范围,则字符'A'的 UTF-8 编码为 01000001,中文字符'李'的 Unicode 码点为 26446(十进制),二进制为 01100111 01001110,十六进制为 674E。根据上表,在第三行范围,则将'李'二进制代码从低位到高位依次填入 x 中,不足的填入 0。得到 UTF-8 编码为 11100110 10011101 10001110,即 E69D8E(十六进制)。
由上述编码规则可知,0000 0000 - 0000 FFFF(第一部分到第三部分)为 Unicode 第一个平面(基本多语言平面),而 0001 0000 - 10 FFFF(第四部分)为 Unicode 其他平面(辅助平面)。在基本多语言平面对应了绝大多数常用的字符。对于大于 65535(十进制)的码点,即在辅助平面上的码点,需要使用 4 个字节来进行 UTF-8 编码。
2.9.2 UTF-16
UTF-8 是不定长的编码,使用 1、2、3、4 个字节编码,而 UTF-16 则只使用 2 或 4 个字节编码。UTF-16 也是 Unicode 一种具体的编码实现。关于 Unicode 如何转化为 UTF-16 编码规则如下:
① 若 Unicode 码点在第一平面(BPM)中,则使用 2 个字节进行编码。
② 若 Unicode 码点在其他平面(辅助平面),则使用 4 个字节进行编码。
关于辅助平面的码点编码更详细解析如下:辅助平面码点被编码为一对 16 比特(四个字节)长的码元, 称之为代理对(surrogate pair), 第一部分称为高位代理(high surrogate) 或前导代理(lead surrogates),码位范围为:D800-DBFF. 第二部分称为低位代理(low surrogate) 或后尾代理(trail surrogates), 码位范围为:DC00-DFFF。注意,高位代理的码位从 D800 到 DBFF,而低位代理的码位从 DC00 到 DFFF,总共恰好为 D800-DFFF,这部分码点在第一平面内是保留的,不映射到任何字符,所以 UTF-16 编码巧妙的利用了这点来进行码点在辅助平面内的 4 字节编码。
说明:字符'A'的 Unicode 码点为 65(十进制),十六进制表示为 41,在第一平面。根据规则,UTF-16 采用 2 个字节进行编码。那么问题又来了,知道了采用两个字节编码,并且我们也知道计算机是以字节为单位进行存储,这两个字节应该表示为 00 41(十六进制)?或者是 41 00(十六进制)呢?这就引出了一个问题,需要用到之前提及的 BOM 机制来解决。
表示为 00 41 意味着采用了大端序(Big endian),而表示为 41 00 意味着采用了小端序。那么计算机如何知道存储的字符信息采用了大端序还是小端虚呢?这就需要加入一些控制信息,具体是采用大端序,则在文件前加入 FE FF,采用小端序,则在文件前加入 FF FE。这样,当计算开始读取时发现前两个字节为 FE FF,就表示之后的信息采用的是小端序,反之,则是大端序。
字符 (无法显示,只能截图显示),其 Unicode 码点为 65902(十进制),十六进制为 1016E,很显然,已经超出了第一平面(BMP)所能表示的范围。其在辅助平面内,根据规则,UTF-16 采用 4 个字节进行编码。然而其编码不是简单扩展为 4 个字节(00 01 01 6E),而是采用如下规则进行计算。
- 使用 Unicode 码位减去 100000(十六进制),得到的值扩展 20 位(因为 Unicode 最大为 10 FF FF(十六进制),减去 1 00 00(十六进制)后,得到的结果最大为 0FFF FF(十六进制),即为 20 位,不足 20 位的,在高位加一个 0,扩展至 20 位即可)。
- 将步骤一得到的 20 位,按照高十位和低十位进行分割。
- 将步骤二的高十位扩展至 2 个字节,再加上 D800(十六进制),得到高位代理或前导代理。取值范围是 D800 - 0xDBFF。
- 将步骤二的低十位扩展至 2 个字节,再加上 DC00(十六进制),得到低位代理或后尾代理。取值范围是 DC00 - 0xDFFF。
Unicode 转 UTF-16 规则流程图如下:
按照这个规则,我们计算字符的 UTF-16 编码,我们知道其码点为 1016E,减去 10000 得到 016E,扩展至 0016E,进行分割,得到高十位为 00 0000 0000,十六进制为 0000,加上 D800 为 D800;得到低十位为 01 0110 1110,十六进制为 016E,加上 DC00 为 DD6E;综合得到 D8 00 DD 6E。即 UTF-16 编码为 D8 00 DD 6E(也可为 D8 0 DD 6E)。
而对于 UTF-32 是使用 4 个字节表示,也采用 BOM 机制,可以类比 UTF-16,这里不再额外介绍。
三. 字符编码区别
3.1 UCS-2 与 UTF-16 区别
从上面的分析知道,UCS-2 采用的两个字节进行编码。在 0000 到 FFFF 的码位范围内,它和 UTF-16 基本一致,为什么说基本一致,因为在 UTF-16 中从 U+D800 到 U+DFFF 的码位不对应于任何字符,而在使用 UCS-2 的时代,U+D800 到 U+DFFF 内的值被占用。
UCS-2 只能表示 BMP 内的码点(只采用 2 个字节),而 UTF-16 可以表示辅助平面内的码点(采用 4 个字节)。
我们可以抽象的认为 UTF-16 可看成是 UCS-2 的父集。在没有辅助平面字符(surrogate code points)前,UTF-16 与 UCS-2 所指的意思基本一致。但当引入辅助平面字符后,想要表示辅助平面字符时,就只能用 UTF-16 编码了。
3.2 UCS -4 与 UTF-16 的区别
在 BMP 上,UTF-16 采用 2 个字节表示,而在辅助平面上,UTF-16 采用的是 4 个字节表示。对于 UCS-4,不管在哪个平面都采用的是四个字节表示。
3.3 为什么 UTF-8 编码不需要 BOM 机制
因为在 UTF-8 编码中,其自身已经带了控制信息,如 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx,其中 1110 就起到了控制作用,所以不需要额外的 BOM 机制。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: Java 策略枚举
下一篇: 谈谈自己对于 AOP 的了解
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论