计算机作为人类制造出的最为强大的工具,将计算机变得更加强大和好用,是近百年来人类科技发展的主旋律和主要动力。
为了充分利用计算机的工具属性,让人更简单的理解计算机和更简单的操作计算机是至关重要的,于是,字符编码成为了计算机发展中非常非常重要的一环。
现在的日常使用中,我们很少会再关心编码的问题,但实际上,在计算机的发展历程中,字符编码也经历的几个不同阶段的发展。
BCD 码
本来这篇文章是以 ASCII 码为开端的,但是在翻阅文章时偶然看到,在计算机真正出现之前,最早在打孔卡上面,就已经存在使用二进制的编码方式了,这种编码方式就是 BCD 码。如今可能只有嵌入式和电子行业的人员还会遇到 BCD 编码。
BCD 编码使用四个二进制位表示一个字符,四个二进制位一共可以表示 16 中不同的状态,BCD 编码一般会选取其中的 10 种状态来表示 0-9 这十个十进制位,然后参与运算。
严格意义上来说,BCD码并不属于真正的字符编码,因为它至多只能表示 16 种状态,并且多数情况下只用来进行数学运算。
ASCII 码
ASCII 码应该是最为程序员所熟知的一种编码,它最初由由美国国家标准学会(ANSI)制定,后来被国际标准化组织(ISO)定为国际标准。
ASCII 码使用一个字节即八个二进制位表示一个字符,基础版本的 ASCII 码规定最高位固定为 0,剩余的七位分别表示 127 个不同的字符:
0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符)
32~126(共95个)是字符(32是空格)
- 48~57 为 0 到 9 十个阿拉伯数字
- 65~90 为 26 个大写英文字母
- 97~122 为 26 个小写英文字母
- 其余为一些标点符号、运算符号等
对于英语国家而言,ASCII 码可以覆盖大部分的使用场景,但是当计算机开始进入欧洲,ASCII 码 127 个码位开始不能满足需求,比如法语中带有注音符号的字符 é,俄语中的字母 й,希伯来语中的 א 等。
ASCII 拓展码
为了解决 ASCII 码对于不同的国家不能满足需求的情况,拥有独特语言体系的国家决定利用 ASCII 码闲置的最高位对 ASCII 码进行拓展,将 ASCII 码能够表示的字符数量拓展至 256 个。
不同的语言体系开始开始制定其独特的 ASCII 拓展码标准,不同的 ASCII 拓展码标准之间 0~127 表示的意义是相同的,128~255 表示的意义可能是不同的。比如,130 在法语编码中代表了 é,在希伯来语编码中却代表了字母 Gimel (ג),在俄语编码中又会代表另一个符号。
这种拓展虽然可以解决一部分的需求,但是也引入的新的问题,由于不同语言体系各自制定的编码标准不同,在一种语言体系进行编码的文件流通到其他语言体系地区时,极大的可能会存在不能解码的问题,并不利于互相之间的交流和流通。
而且,对于不同语言体系中的一些国家,即便是 128 个 ASCII 拓展码也远远不能满足实际的需求。比如,在我国,仅常用的汉字数量就有 6000 多个,日本和韩国等国家也拥有自己独特的文字系统。
GB2312
对于文字数量或者字母数量用 ASCII 码或者 ASCII 拓展码远远不能满足需求的国家和地区而言,单字节的编码方式显然不再适用。双字节字符集开始出现,本文仅以我国编码标准的发展为例进行展开。
为了满足需求,我国的科技工作者制定了 GB2312 标准,在这个标准中,一个汉字适用两个字节进行编码,分为高字节和低字节。GB2312 规定这两个字节都要大于 127 以便能够兼容 ASCII 码而不产生混淆,其中,高字节的范围为 0xA1 ~ 0xF7,低字节的范围为 0xA1 ~ 0xFE。使用二进制表示 GB2312 的编码范围为:
1010 0001 1010 0001 (0xA1A1)
到
1111 0111 1111 1110 (0xF7FE)
理论上,GB2312 标准共有 22109 个码位可供使用,但在实际的应用过程中,为了方便国人理解和使用,GB2312 标准采用了分区的方式来对编码进行划分。GB2312 标准一共分为了 94 个区,每区含有 94 个位,每个区位表示一个汉字/符号。分区定义如下:
- 01 - 09 区为特殊符号
- 10 - 15 区为用户自定义符号区(未编码)
- 16 - 55 区为一级汉字,按拼音排序
- 56 - 87 区为二级汉字,按部首/笔画排序
- 88 - 94 区为用户自定义汉字区(未编码)
按照分区定义,共有 94 * 94 共 8836 个码点可用,GB2312 标准中收录了汉字 6763 个和非汉字图形字符 682 个,预留了可供用户自定义的码点 1391 个。
GB2312 标准中区位号和二进制之间的转换也十分的简单,一个汉字字符的双字节编码的高字节为汉字的区号加上 0xA0,低字节则为位号加上 0xA0。
举例说明,汉字 ”啊“ 的双字节编码为 0xB0 0xA1,区位码为 1601,计算方式如下:
1010 0000 1010 0000 (0xA0 0xA0)
+
0001 0000 0000 0001 (0x10 0x01) (16 01)
=
1011 0000 1010 0001 (0xB0 0xA1)
了解了区位码之后,可以更好的理解国标码,GB2312 标准中的 GB 就表示国家标准代码,简称国标码,亦被新加坡采用。国家标准强制标准冠以“GB”。
区位码和国标码的转换规则为把换算成十六进制的区位码加上0x2020,就得到国标码,国标码加上0x8080,就得到常用的双字节十六进制表示。
GBK
随着计算机的不断发展,逐渐发现 GB2312 标准中仍旧存在大量没有收录的特殊汉字和少数民族文字。于是在 GB2312-80 标准上拓展出了 GBK 标准。
相较于 GB2312 标准,GBK 标准的编码范围进行了拓展,变为了 0x8140 ~ 0xFEFE,剔除了原本低字节需要小于 127 的限制,同时对高字节范围也进行了拓展。
经过拓展后的 GBK 标准共23940个码位,收录了21003个汉字,并且可以完全向下兼容 GB2312 标准。此后的 GBK 标准还经历过几次的拓展,本文不再赘述,有兴趣的同学可以自行查阅。
类似于 GB2312 标准和 GBK 标准的这种使用两个字节表示一个字符的编码集,通称它们叫做 “DBCS“(Double Byte Charecter Set 双字节字符集)
自此,中文在计算机中有了较为成熟的编码和显示规则。但是,不论是 GB2312 标准还是 GBK 标准都属于中国的国家标准而非国际标准,相当于在计算机中,中国地区拥有了自己独特的方言,而对于有着其他方言的国家或者地区,互相间的交流仍旧是十分困难的。
此时,就需要一个统一的国际化标准来统一不同国家和地区间的字符编码。
Unicode
为了解决不同国家和地区标准字符集不同的问题,为每种语言中的每个字符设定统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。国际标准化组织(ISO)于1990年开始研发一种统一编码,于1994年正式发布 Unicode 1.0 版本。
Unicode 被称为统一码、万国码或单一码,学名是 "Universal Multiple-Octet Coded Character Set",简称为UCS。Unicode用数字 0 ~ 0x10FFFF 来映射这些字符,最多可以容纳 1114112 个字符,或者说有 1114112 个码位。
Unicode 本意上是制定一个类似于字典的字符集,规定了每个符号对应的数字,至于这个数字如何存储和传输则没有任何规定。它的想法很简单,就是为每个字符规定一个用来表示该字符的数字,仅此而已。
既然 Unicode 只规定了字符对应的码点数字, 没有规定这个数字如何存储和传输,那么对于不同的字符对应的数字如何存储和传输,以及计算机应该按照什么样的规则去拆分和理解连续的二进制数据,便又衍生出了不同的 UTF 标准。
UFT
上文提及,Unicode 本身对于不同的字符对应的数字如何存储和传输并没有任何规定。存储和传输 Unicode 码点的工作由 UTF 标准完成。
简单来讲, UTF 标准规定了在存储和传输过程中,需要将一个字节、两个字节还是四个字节解析为一个 Unicode 码点。
为何需要 UTF?
有这样一个字符串:”a啊😀“,其中包含了三个不同的符号
”a“ 在 Unicode 中的码点为 U+0041
”啊“ 在 Unicode 中的码点为 U+554A
”😀“ 在 Unicode 中的码点为 U+1F600
可以看出,”a“ 仅需要一个字节就可以表示,”啊“ 需要两个字节表示,”😀“ 则至少需要三个字节。那么如何确定 ”a啊😀“ 这个字符串的二进制编码,以及在连续的二进制编码中如果正确的解读出这三个符号的 Unicode 码点?
最简单的方式便是将字符串中码点最大的字符作为标准,”a啊😀“ 中每个字符都用三个字节存储,二进制编码便是 ”0x000041 0x00554A 0x01F600“。
但是明显的,这种方式会造成极大程度的浪费,如果码点最大的字符需要占用 4 个字节,而其他字符 1 个字符就可以表示的话,这种浪费将是不可接受的。
而如果使用固定的一个字节或者两个字节来表示一个码点,对于需要字节数量更多的码点则需要一个规则组合表示,而 UTF 就是这个规则的制定者。
本文主要介绍几种常见 UTF 编码格式:UTF-8、UTF-16、UTF-32
UTF-8
UTF-8 是一个非常惊艳的编码方式,它规定一个字节为一个单位,一个码点可能由 1 - 4 个单位来组合表示。由于它可以表示单字节码点,完美的实现了对 ASCII 码的向后兼容,保证了 Unicode 可以被大众接受。
像 UTF-8 这种一个码点可能由不同字节数表示的编码方式被称为可变长编码。
而对于一个字节不能表示的码点,UTF-8 规定使用如下的编码规则:
码点: 0x0000 - 0x007F
码点二进制: 0xxxxxxx
UTF-8 编码:0xxxxxxx
码点: 0x0080 - 0x07FF
码点二进制: 00000aaa aabbbbbb
UTF-8 编码:110aaaaa 10bbbbbb
码点: 0x0800 - 0xFFFF
码点二进制: 00000000 aaaabbbb bbcccccc
UTF-8 编码:1110aaaa 10bbbbbb 10cccccc
码点: 0x10000 - 0x10FFFF
码点二进制: 00000000 000aaabb bbbbcccc ccdddddd
UTF-8 编码:11110aaa 10bbbbbb 10cccccc 10dddddd
UTF-8 的编码解析只需要根据每个字节中第一个 0 之前 1 的数量来确定这个字节和其他字节的组合方式即可。
UTF-16
在了解 UTF-16 编码方式之前,可能需要先了解一下另外一个概念 —— "平面"
根据上文,我们知道 Unicode 是一本很厚的字典,其中定义了世界上所有的字符,为了给这些不同的字符进行分类。Unicode 使用了分区定义的方式,每个区可以存放 65536 个(2^16)字符,称为一个平面(plane)。目前,一共有 17 个(2^5)平面。划分如下:
0x0000~0xFFFF:第0平面,基本多文种平面(Basic Multilingual Plane, BMP)
0x10000~0x1FFFF:第1辅助平面,多文种补充平面(Supplementary Multilingual Plane, SMP)
0x20000~0x2FFFF:第2辅助平面,表意文字补充平面(Supplementary Ideographic Plane, SIP)
0x30000~0x3FFFF:第3辅助平面,表意文字第三平面(Tertiary Ideographic Plane, TIP)
0x40000~0xDFFFF:第4-13辅助平面,尚未使用
0xE0000~0xEFFFF:第14辅助平面,特别用途补充平面(Supplementary Special-purpose Plane, SSP)
0xF0000~0xFFFFF:第15辅助平面,保留作为私人使用区(Private Use Area, PUA)
0x100000~0x10FFFF:第16辅助平面,保留作为私人使用区(Private Use Area, PUA)
了解了平面的概念后。UTF-16 编码方式就很好理解了,它结合了定长和变长两种方式的特点,规定:基本平面的字符占用 2 个字节,辅助平面的字符占用 4 个字节。
但是,这里仍旧会存在一个问题,当我们遇到两个字节时,到底是把这两个字节当作一个字符还是与后面的两个字节一起当作一个字符,为了解决这个问题,UTF-16 规定了一种巧妙的映射方式。
实际上,在基本平面内,0xD800 ~ 0xDFFF 中间的码点没有对应任何字符,UTF-16 利用了这个空段来映射辅助平面的码点。目前,辅助平面的可以容纳的所有码点需要 20 bits 来表示,UTF-16 将这 20 bits 拆分为了前 10 bits 和后 10 bits。前 10 bits 被映射到了 0xD800 ~ 0xDBFF,后 10 bits 被映射到了 0xDC00 ~ 0xDFFF。
映射完成后,前两个字节作为高位,后两个字节作为低位,四个字节共同表示一个码点,计算过程如下:
- 对于小于 0x10000 的码点,直接使用码点的十六进制编码
- 对于 0x10000 ~ 0x10FFFF 之间的码点,使用下面的步骤对码点 U 进行编码:
1、U‘ = U - 0x10000
将码点减去 0x10000,得到一个小于 0xFFFFF 的结果,这个结果可以用 20 bits 表示
2、U’ = xxxx xxxx xxyy yyyy yyyy
将上一步的结果用二进制表示为 20 bits
3、W1 = 1101 10xx xxxx xxxx
W2 = 1101 11yy yyyy yyyy
将 U‘ 的前 10 bits 映射到 U+D800 到 U+DBFF 之间得到 W1
将 U‘ 的后 10 bits 映射到 U+DC00 到 U+DFFF 之间得到 W2
4、W1 + W2
W1 和 W2 进行拼接即为最终的编码结果
所以,当遇到两个字节的码点在 0xD800 ~ 0xDBFF 之间,就可以确定,后面两个字节的码点,应该在 0xDC00 ~ 0xDFFF 之间,而这四个字节必须放在一起进行解读。
UTF-32
由于 Unicode 的码点范围只截止到 0x10FFFF,所以 32 bits 足够表示 Unicode 的所有码点,因此 UTF-32 也属于定长编码,每个码点都使用四个字节进行表示。
网友评论