字符编码方式


我们都知道,计算机只能处理数字,即0和1两种状态,如果要处理文本,就必须先把文本转换为数字才能处理。这种将信息从一种形式转换为另一种形式的过程叫做编码。最早的计算机在设计时采用8个比特作为一个字节,计算机存储的最小单位就是字节。一个字节可以组合出256种不同的状态,每一个状态对应一个符号,也就是256个符号

字符编码 = 字符集 + 编码规则

ASCII

由于计算机是美国人发明的,所以只出现大小写英文字母、数字和一些符号。因此,最早只有128个字符被编码到计算机里,编号0-127称为码位(码位代表对应字符的一个ID信息),而这128个字符集合称为ASCII字符集

有了字符集以后,就要考虑如何将字符集里的字符存储到计算机中。美国人直接将每个字符的码位转换成二进制信息进行存储,而转换后的二进制码就叫做ASCII码。所以ASCII码只能表示从0-127,共计128个字符。这128个字符只使用了8位二进制数中的后面7位,最前面的位统一规定为0

ASCII 字符集对照表
码位 字符 ASCII码
0 空字符 00000000
1 标题开始 00000001
2 正文开始 00000010
10 换行 00001010
13 回车 00001101
31 单元分隔符 00011111
32 空格 00100000
33 ! 00100001
48 0 00110000
57 9 00111001
65 A 01000001
90 Z 01011010
97 a 01100001
122 z 01111010
126 ~ 01111110
127 删除 01111111

欧洲编码

英语用128个符号编码足够使用了,但对于其他语言来说是不够的。于是欧洲就在原有的ASCII码基础上进行了扩展,利用字节中闲置的最高位编入新的符号(将原来的第一位0变成了1),这样一来就可以在采用单字节编码的同时表示256个符号。新增的这128个字符(码位128-255)叫做扩展ASCII字符集,对应的ASCII码就叫做扩展ASCII码

中国编码

当电脑来到中国以后发现,扩展后的编码也就256个,而中国文字多达上万,肯定是不够用的。既然一个字节只能表示256种符号,那就必须使用多个字节表达一个符号

GB2312字符集:采用分区管理,共计94个区,每个区包含94个位,共8836个码位

  • 01-09区收录除汉字外的682个字符
  • 10-15区为空白区,没有使用
  • 16-55区收录3755个一级汉字,按拼音排序(常用汉字)
  • 56-87区收录3008个二级汉字,按部首/笔画排序(不常用汉字)
  • 88-94区为空白区,没有使用

字符集举例

  • 英文字符a处于03区的第6行第5列,那它的码位就是0365
  • 中文字符"饼"处于17区第9行第3列,那它的码位就是1793
  • 中文字符“侃”处于57区第0行第9列,那它的码位就是5709

编码存储方式(这里以“侃”为例)

  1. 一个字符的码位按前两位和后两位分开:将5709分为57和09
  2. 把分开的两个数转换为十六进制:0x39和0x09
  3. 两个十六进制分别加上0xA0:0x39加上0xA0得到0xD9;0x09加上0xA0得到0xA9
  4. 最后将得到的这两个十六进制数合并,得到一个字符的GB2312码:0xD90xA9

计算机怎么知道正在处理的这个8位是ASCII码还是GB2312码?正是通过判定这个字符的大小,如果小于127就代表是ASCII码,如果碰到连续两个大于127的8位,那就代表这两个组成成一个GB2312码(一个字符的码位转换为十六进制后需要分别加上0xA0,是希望让它的高8位和低8位都大于127)

GB2312的高位和低位都大于127,一共6763个汉字。但后来发现中国的汉字实在太多了,还有很多汉字并不在GB2312字符集里面,于是对GB2312字符集进行扩充,并且不再规定它的低位一定要大于127,它可以小于127,但需要保证高位是大于127的,同时规定计算机只要碰到一个大于127的字节,就表示一个汉字的开始。通过这种方式新增了近2万个汉字和符号,将这样的字符集称作GBK字符集

后来很多少数民族也使用计算机,于是在GBK字符集的基础上,又新增了几千个少数民族的字符,对应的字符集就称作GB18030字符集,对应的编码就称作GB18030码

GB2312 GBK GB18030
高位和低位都大于127 不再规定低位大于127(在GB2312基础上扩展) 在GBK基础上扩展
6763个汉字 新增近2万个汉字和符号 新增几千少数名族字符

Unicode

那既然中国可以通过这样的方式实现字符编码,世界上那么多国家,每个国家都可以设计出一套属于自己国家的编码。世界上有很多编码,不同国家在进行信息交流的时候,就会发现存在乱码的现象,这时候ISO提出了一个Unicode标准,旨在收集全球所有的字符,并为每个字符分配唯一的字符编号即码点(Code Point)。在Unicode标准中,码点采用十六进制书写,并加上前缀 U+,例如U+0041就是拉丁字母A的码点

Unicode的码点可以按照使用上的频繁度划分为17个平面(编号为0-16),平面又称代码级别(Code plane)。码点范围是:0x000000-0x10FFFF

  • 基本的多语言平面(平面0,英文简写BMP):收集了使用最广泛的字符;码点从U+0000到U+FFFF,共有65536个字符,也就是2字节
  • 辅助平面(平面1-16,英文简写SMP):包括增补多语言平面(平面1)、增补象形平面(平面2)、保留平面(平面3-13)、增补专用平面等,每个增补平面有65536个码点;码点范围是:0x010000-0x10FFFF

Unicode字符集可以有不同的编码方式,如UTF-8,UTF-16,UTF-32,这里UTF指的是Unicode Transformation Format,即Unicode转换格式,即将Unicode编码空间中每个字符对应的码点,与字节顺序进行一一映射

USC-2、USC-4字符集

一开始Unicode使用的是UCS-2字符集,这个字符和ASCII码很像,它将所有用到的字符罗列在一起,并且按顺序给它标上对应的码位,然后它的存储方式也是直接将码位转换成对应的二进制信息进行存储即可。USC-2字符集一共可以表示2的16次方,也就是65536个字符

但是到后来发现这65536个字符还是无法表示世界上所有的字符,于是后面出现了UCS-4字符集。UCS-4字符集是用32位(4个字节)来表示一个字符,一共可以表示2的32次方,将近43亿个字符。这基本上就能够涵盖世界上所有的字符了,但这样的编码规则并没有世界各国很好的接受,因为它需要的存储空间比较大。ASCII编码本来只需要1个字节,现在变成4个字节,扩大了4倍;GB2312编码原本需要2个字节,现在需要4个字节,扩大了2倍,所以Unicode标准推出来以后很长时间并没有被广泛接受。直到后面互联网时代的来临,各国之间的信息交流愈加频繁,这时候不得不对编码进行重新思考,于是出现了UTF-8这样的编码规则

UTF-8 编码

UTF-8是一种变长编码方式,一般用1-4个字节来编码一个Unicode字符,是目前应用最广泛的一种编码方式

UTF-8编码方式:第一个字节提示了这个Unicode编码由几个字节组成

  • 首字节以0开头,表示单字节编码,对应的区间是0-7F
  • 首字节以110开头;表示双字节编码,后续字节以10开头,对应的区间是80-7FF
  • 首字节以1110开头,表示三字节编码,后续字节以10开头,对应的区间是800-FFFF
  • 首字节以11110开头,表示四字节编码,后续字节以10开头,对应的区间是10000-10FFFF
十六进制码点区间 二进制编码样式
0x00000000 ~ 0x0000007F(0-127) 0xxxxxxx
0x00000080 ~ 0x000007FF(128-2047) 110xxxxx 10xxxxxx
0x00000800 ~ 0x0000FFFF(2048-65535) 1110xxxx 10xxxxxx 10xxxxxx
0x00010000 ~ 0x0010FFFF(65536以上) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

举例说明如何得到UTF-8编码:
“王”这个字符在UCS-4字符集里面的码位是 0x0000738B,将这个码位转换为二进制 0000 0000 0000 0000 0111 0011 1000 1011,然后可以发现“王”这个字的码位属于第三区间,因为第三区间是从800到FFFF的。此时得到了“王”这个码位的二进制形式和它对应区间的编码样式,只需将二进制信息从后往前依次插入到编码样式中,得到 11100111 10001110 10001011,最后把它转换成十六进制,最终的UTF-8编码就是 0xe7 0x8e 0x8b

UTF-8解码方式

  • 当读取到一个字节的首位是0,表示这是一个单字节编码的ASCII字符
  • 当读取到一个字节的首位是1,表示这是一个多字节编码的字符;首字符是1之后继续读取直到遇到0为止,一共遇到多少个1就表示该字符为几个字节的编码
  • 当读到一个字节的首位是1,紧接着读取到一个0,则该字节就是多字节编码的后续字节

举例说明UTF-8编码方式:
中文的“黄”字,Unicode码点为 U+9EC4,转换为二进制表示则为 1001 1110 1100 0100,在UTF-8编码下,这个字符属于第三区间,应占用3个字节。于是将二进制按顺序从低位到高位插入到各个字节的有效位上,得到“黄”的UTF-8编码为 11101001 10111011 10000100,转为16进制则为 0xE9BB84

UTF-8编码方式的特点

  • 优点:变长,节省空间;自同步编码/自动纠错性能好,利于传输;完全兼容ASCII编码;无字节序
  • 缺点:不利于程序内部处理,如正则表达式检索

自同步:在传输过程中,若存在字节丢失或者存在错误的字节序列,也不会影响到其他字节的正常读取;如读取了一个10xxxx开头的字节,但是找不到首字节,就可以将该字节丢弃

UTF-16 编码

UCS-2将字符码点直接映射为字符编码,采用固定的2字节编码,只覆盖了BMP的码点,对于SMP的码点,2字节的16位二进制数是不足以表示的。而UTF-16扩展了原来的UCS-2,解决了辅助平面码点的字符无法表示的问题。

它的编码规则很简单:BMP中的字符需要一个字节,其他的SMP中需要两个字节

  • BMP中的有效码点,用固定2字节16位编码,数值等于对应的码点,和UCS-2一样
  • 辅助平面中的有效码点,使用代理进行编码。在BMP中有一个范围的码点是未定义的,被称为代理区,其码点范围是 0xD800~0xDFFF,共211个码点。代理区又被分为高代理码点和低代理码点。其中高代理码点范围是 0xD800~0xDBFF,低代理码点范围是 0xDC00~0xDFFF。高代理码点和低代理码点结合在一起,就表示一个辅助平面中的字符。由于SMP中的字符共有 220 个,高代理码点和低代理码点皆有 210 个取值,二者结合,恰好有 220 种不同的组合,可以表示完Unicode中的字符

因此,当我们遇到两个字节,发现它的码点在U+D800到U+DBFF之间,就可以断定紧跟在后面的两个字节的码点,应该在U+DC00到U+DFFF之间,这四个字节必须放在一起解读

UTF-16编码举例说明:
汉字“?”的Unicode码点为 0x20BB7,该码点显然超出了基本平面的范围(0x0000-0xFFFF),因此需要使用四个字节表示。首先用 0x20BB7-0x10000 计算超出的部分,然后将其用20个二进制位表示(不足前面补0),结果为 0001000010 1110110111。接着,将前10位映射到 U+D800 到 U+DBFF之间,后10位映射到 U+DC00 到 U+DFFF即可。U+D800对应的二进制数为 1101100000000000,直接填充后面的10个二进制位即可,得到 1101100001000010,转换成16进制数则为 0xD842。同理可得,低位为 0xDFB7。因此得出汉字"?"的UTF-16编码为 0xD842 0xDFB7

UTF-32 编码

UTF-32固定以4字节来编码,ISO 10646中称UTF-32是UCS-4的一个子集
若用UTF-32来编码,程序处理会比较简单,但是所有字符皆占4个字节,比较浪费空间

名词解释

  • 码点(Code Point):是指与一个编码表中的某个字符对应的代码值
  • 代码单元(Code Unit):指已编码的文本中,最短的比特组合单元。对UTF-8来说,代码单元是8比特;对UTF-16来说是16比特;对UTF-32来说是32比特。即UTF-8以1个字节为最小单位,UTF-16以2个字节为最小单位
  • 字节序(Byte Order Mark,BOM):出现在文件头部,表示字节顺序。在USC编码中,有一个叫"ZERO WIDTH NO-BREAK SPACE"字符,其码点为 0xFEFF.UCS规范建议,在传输字节流时,线传输这个字符。这样,如果接收者收到 FEFF,表明字节流是 Big-Endian(大端字节序),如果接收者收到 FFFE,表明字节流是 Little-Endian(小端字节序)。对UTF-8来说,不需要BOM字节序,但是可以用BOM来表明是UTF-8编码。0xFEFF 的UTF-8编码为 EF BB BF,若接收者收到 EF BB BF 开头的字节流,就知道这是UTF-8的字节流了
  • 大端序/小端序:大端序就是将高位字节放到低地址处;小端序就是将低位字节放到高地址处。如果不分大小端的话,那么就会出现解读错误,比如我们一次要处理四个字节 12 34 56 78,这四个字节是表示 0x12 34 56 78 还是表示 0x78 56 34 12?不同的解释最终表示的值不一样

三种编码方式比较

编码方式 UTF-8 UTF-16 UTF-32
编码字节数 变长;1-4字节;代码单元为8位(1字节) 2字节或4字节;代码单元为16位(2字节) 4字节;代码单元为32位(4字节)
优点 兼容ASCII码;节省空间;纠错能力强,利于网络传输 最早的编码方式,适合内存中的Unicode处理,很多语言中作为String类的编码方式 固定字节编码;简单,利于程序处理;Unicode码点和编码一一对应
缺点 变长方式不利于程序内部处理 不兼容ASCII,增补平面使用代理对,较为复杂,扩展性差 不兼容ASCII,浪费存储空间和网络带宽,扩展性差
BOM字节序 无字节序(可用BOM来表示UTF-8编码) 有字节序(UTF-16LE小端序FFFE,UTF-16BE大端序FEFF) 有字节序(UTF-32LE小端序FFFE,UTF-32BE大端序FEFF)

具体语言中的使用

Java中的char类型,内部编码采用的utf-16(历史原因),以定长方式,2个字节存储

对于BMP中的字符,一个char就能表示;对于SMP内的字符,一个char可不够用,需要由2个char来存储,或者用String来表示