字符编码笔记(理论篇)

记录一下字符编码的有关事项,这篇文章先说说字符编码的一些历史和原理。

1.ASCII编码与ANSI标准

1字节(1B) = 8比特(8b) = 8个二进制位
1B的信息量 0b00000000 ~ 0b11111111 = 2^8 = 256个状态

标准ASCII编码使用1B的后7位,而第一位默认置0,即:
0b00000000 ~ 0b01111111 = 2^7 = 128个状态
这128个状态位存储了控制字符、数字、大小写字母和其他符号。详见ASCII编码表[]

渐渐地,128个状态不再能满足人们的需求,不同国家(语言区)都想把自己语言的字符编进ASCII编码中,于是人们纷纷自发的使用起了1B的另外128个状态,称为扩展ASCII编码,其使用1B的后7位,同时第一位默认置1,即:
0b10000000 ~ 0b11111111 = 2^7 = 128个状态
由于是自发行为,每个国家(语言区)都对扩展ASCII编码有自己独特的定义,因此彼此之间的扩展ASCII编码是不能通用的。(标准ACII编码区仍然通用)

麻烦还不止于此,部分国家(语言区)的字符数量众多(如汉字就是十万级别的数量),显然扩展ASCII编码的128个状态不能满足,于是有了下面的一种编码方式(ANSI标准编码):

  1. 保留标准ASCII编码的128个字符不变,称为半角
  2. 其余的字符用两个字节来表示,这样理论上能新增256*256=65536个编码,称为全角
    这也是大家最早所认知的下面这句话的由来

英文(半角)占一个字节,汉字(全角)占两个字节

全角字符中用于编码的两个字节被称为高位字节低位字节
全角字符 = 高位字节 + 低位字节
高位字节和低位字节都可以选择用三种策略编码:

  1. 标准ASCII编码的128个状态
  2. 扩展ASCII编码的128个状态
  3. 以上都用的256个状态

不难发现当这两个字节都使用标准ASCII编码的128个状态时,计算机是无法分辨这是两个半角字符,还是一个全角字符的。因此全角的编码区域实际上只有:
256*256 - 128*128 = 49152个
针对这个5万个左右的编码区,不同国家(语言区)制定了不同的编码标准,我们熟悉的有:

  • GB2312,大陆1980年标准(其中一级汉字3755个,二级汉字3008个,包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的全角字符682个。)
  • GBK,大陆1995年标准(GB2312的扩展,增加不常用汉字与字符,总编码量扩展到23940)
  • BIG-5,台湾标准

2. Unicode编码

随着互联网的发展,信息交流变得越来越频繁,这就促使一种“大一统”的编码方式出现。这种编码可以将世界上所有的字符都编入其中,且每个状态位和字符都唯一对应。正如它的名字代表的意思那样,Unicode编码做到了。
Unicode将编码存储这两个逻辑过程独立开来。

  • 世界上的每一个字符都有且只有一个Unicode编码方案即:字符S->Unicode(S)
  • 每一个Unicode码都可以通过的多种方案来存储,这些方案的名称即是我们常听到的UTF-8、UTF-16、UTF-32等等。

2.1 编码逻辑

下面先说一下Unicode的编码方式:
Unicode码的编码范围:0x000000 ~ 0x10FFFF
它的后四位称为一个plane:0x0000 ~ 0xFFFF = 2^16 = 65536个状态
前两位代表plane的编号,一共有:0x00 ~ 0x10 = 17个plane
所以Unicode编码一共有:17 * 65536 = 1114112个状态
这足以将世界上所有的字符都编码进去,而且还有很大的富余。同时别忘了,预留的空白plane可远远多于17个(16*16=256个)
事实上,在Unicode 5.0版本中只用到了0,1,2,14,15,16这几个编号plane中的238605个状态

2.2 存储逻辑

Unicode编码总算是将全世界的字符都“装”下了,当然为了达到此目的,经过Unicode编码后的字符所占空间变的很大。本来只占一个字节的普通ASCII字符和占两个字节的ANSI字符统统都变成了占三个字节的Unicode字符,造成了空间的极大浪费。因此Unicode编码的编码方式存储方式分开独立实现,存储传输方案专注于实现Unicode编码如何节省存储空间。
常见的存储传输方案有:UTF-8,UTF-16,UTF-32等

UTF-8方案

UTF-8最大的一个特点,就是它是一种变长的存储方式。它可以使用1~4个字节存储一个Unicode码,根据不同的Unicode码而变化存储字节的长度。
UTF-8的存储规则很简单,只有二条:

  1. 对于小于0x7F的Unicode码,UTF-8编码只有一个字节,字节的第一位设为0,后面7位为Unicode码。这也是为了英语字母的UTF-8编码和普通ASCII码是相同的。
  2. 对于其他Unicode码,落入下表的相应的范围中。其中x代表空白位,用Unicode码补充。
Unicode符号范围(十六进制) UTF-8存储方式(二进制)
000000 ~ 00007F 0xxxxxxx
000080 ~ 0007FF 110xxxxx 10xxxxxx
000800 ~ 00FFFF 1110xxxx 10xxxxxx 10xxxxxx
010000 ~ 10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-16方案

UTF-16方案也是一种变长存储方式,它采用两个字节或四个字节来存储Unicode码。
当Unicode码位于编号为00的plane(即0x000000 ~ 0x00FFFF之间)时,Unicode码正好对应了两个字节(16位二进制)的长度。即使用两个字节顺序存储。
当Unicode码位于其他编号的plane(即0x010000 ~ 0x10FFFF)时,Unicode码减去0x10000后正好对应成20位二进制,依次填入以下四个字节的20个空位中:
110110xx xxxxxxxx 110111xx xxxxxxxx
前两个字节称为高位WORD,以110110开头;后两个字节称为低位WORD,以110111开头。

UTF-32方案

UTF-32方案是一种定长存储方案,它总是采用4个字节存储Unicode码,由于Unicode码只有24位,于是UTF-32方案不对Unicode码做任何变化直接存储。
有人会问,用3个字节不也是可以完全存储Unicode的所有编码吗?我个人的理解是,由于Unicode的前身:通用字符集(Universal Character Set, UCS)分为了2字节编码的UCS-2和4字节编码的UCS-4。为了使编码方案统一,UCS-4承诺不再向0x10FFFF之后编码,并由UCS-2作为编号为0的plane(Basic Multilingual Plane, BMP)共同组成Unicode编码。3者之间的关系为:
UCS-4编码中0x00000000 ~ 0x0010FFFF的部分组成了Unicode编码
Unicode编码中0x000000 ~ 0x00FFFF的部分组成了UCS-2编码
因此UTF-16作为UCS-2编码的存储方案,UTF-32作为UCS-4编码的存储方案使用4个字节,还是仍然保留着。

关于字节序(Byte Order Mark, BOM)

在UTF-16和UTF-32中存在着Little endian (LE)和Big endian (BE)两种传输字节流的方式(显然很蛋疼)
为了让机器识别出这两种传输方式,在Unicode编码规范中设定了一个叫做"ZERO WIDTH NO-BREAK SPACE"的状态位0x00FEFF,它不对应任何实际含义的字符,且总是出现在文件的开头,用于标志该文件是使用什么方式读取字节流的(LE or BE)。按照上文三种存储方案的规则:

编码方案 BOM(十六进制)
UTF-8 EF BB BF
UTF-16LE FF FE
UTF-16BE FE FF
UTF-32LE FF FE 00 00
UTF-32BE 00 00 FE FF

可见由于UTF-8不存在LE或BE的区分,因此它的BOM只有一种,可有可无。所以在UTF-8的方案下,有UTF-8 BOM和UTF-8 无BOM两种,而它们的区别仅仅是文件开头有没有EF BB BF 这三个字节罢了

推荐阅读更多精彩内容