35. 字符编码

由于计算机只能识别0和1,文字也只能以0和1的形式在计算机里存储,所以我们需要对文字进行编码才能让计算机处理, 编码的过程就是规定特定的01数字符串来表示特定的文字,最简单的字符编码例子是ASCII码。

35.1. ASCII编码

学习C语言时,我们知道在程序设计中使用ASCII编码表约定了一些控制字符、英文及数字。它们在存储器中, 本质也是二进制数,只是我们约定这些二进制数可以表示某些特殊意义,如以ASCII编码解释数字“0x41”时,它表示英文字符“A”。 ASCII码表分为两部分,第一部分是控制字符或通讯专用字符,它们的数字编码从0~31, 见表格 ASCII码中的控制字符和通讯专用字符 ,它们并没有特定的图形显示, 但会根据不同的应用程序,而对文本显示有不同的影响。ASCII码的第二部分包括空格、阿拉伯数字、标点符号、大小写英文字母以及“DEL(删除控制)”, 这部分符号的数字编码从32~127,除最后一个DEL符号外,都能以图形的方式来表示,它们属于传统文字书写系统的一部分, 见表格 ASCII码中的字符和数字

ASCII码中的控制字符和通讯专用字符 ASCII码中的字符和数字

后来,计算机引进到其它国家的时候,由于他们使用的不是英语,他们使用的字母在ASCII码表中没有定义, 所以他们采用127号之后的位来表示这些新的字母,还加入了各种形状,一直编号到255。 从128到255这些字符被称为ASCII扩展字符集。至此基本存储单位Byte(char)能表示的编号都被用完了。

35.2. 中文编码

由于英文书写系统都是由26个基本字母组成,利用26个字母组可合出不同的单词,所以用ASCII码表就能表达整个英文书写系统。 而中文书写系统中的汉字是独立的方块,若参考单词拆解成字母的表示方式,汉字可以拆解成部首、笔画来表示, 但这样会非常复杂(可参考五笔输入法编码),所以中文编码直接对方块字进行编码,一个汉字使用一个号码。

由于汉字非常多,常用字就有6000多个,如果像ASCII编码表那样只使用1个字节最多只能表示256个汉字,所以我们使用2个字节来编码。

35.2.1. GB2313标准

我们首先定义的是GB2312标准。它把ASCII码表127号之后的扩展字符集直接取消掉,并规定小于127的编码按原来ASCII标准解释字符。 当2个大于127的字符连在一起时,就表示1个汉字,第1个字节使用 (0xA1-0xFE) 编码,第2个字节使用(0xA1-0xFE)编码, 这样的编码组合起来可以表示了7000多个符号,其中包含6763个汉字。在这些编码里,我们还把数学符号、罗马字母、 日文假名等都编进表中,就连原来在ASCII里原本就有的数字、标点以及字母也重新编了2个字节长的编码, 这就是平时在输入法里可切换的“全角”字符,而标准的ASCII码表中127号以下的就被称为“半角”字符。

表格 GB2312兼容ASCII码的原理 说明了GB2312是如何兼容ASCII码的, 当我们设定系统使用GB2312标准的时候,它遇到一个字符串时,会按字节检测字符值的大小, 若遇到连续两个字节的数值都大于127时就把这两个连续的字节合在一起,用GB2312解码,若遇到的数值小于127,就直接用ASCII把它解码。

GB2312兼容ASCII码的原理

35.2.1.1. 区位码

在GB2312编码的实际使用中,有时会用到区位码的概念,见图 GB2312的部分区位码 。 GB2312编码对所收录字符进行了“分区”处理,共94个区,每区含有94个位,共8836个码位。而区位码实际是GB2312编码的内部形式, 它规定对收录的每个字符采用两个字节表示,第一个字节为“高字节”,对应94个区;第二个字节为“低字节”,对应94个位。 所以它的区位码范围是:0101-9494。为兼容ASCII码,区号和位号分别加上0xA0偏移就得到GB2312编码。在区位码上加上0xA0偏移, 可求得GB2312编码范围:0xA1A1-0xFEFE,其中汉字的编码范围为0xB0A1-0xF7FE,第一字节0xB0-0xF7(对应区号:16-87), 第二个字节0xA1-0xFE(对应位号:01-94)。

例如,“啊”字是GB2312编码中的第一个汉字,它位于16区的01位,所以它的区位码就是1601,加上0xA0偏移, 其GB2312编码为0xB0A1。其中区位码为0101的码位表示的是“空格”符。

GB2312的部分区位码

35.2.2. GBK编码

据统计,GB2312编码中表示的6763个汉字已经覆盖中国大陆99.75%的使用率,单看这个数字已经很令人满意了, 但是我们不能因为那些文字不常用就不让它进入信息时代,而且生僻字在人名、文言文中的出现频率是非常高的。 为此我们在GB2312标准的基础上又增加了14240个新汉字(包括所有后面介绍的Big5中的所有汉字)和符号, 这个方案被称为GBK标准。增加这么多字符,按照GB2312原来的格式来编码,2个字节已经没有足够的编码, 我们聪明的程序员修改了一下格式,不再要求第2个字节的编码值必须大于127, 只要第1个字节大于127就表示这是一个汉字的开始,这样就做到了兼容ASCII和GB2312标准。

表格 GBK兼容ASCII和GB2312的原理 说明了GBK是如何兼容ASCII和GB2312标准的, 当我们设定系统使用GBK标准的时候,它按顺序遍历字符串,按字节检测字符值的大小,若遇到一个字符的值大于127时, 就再读取它后面的一个字符,把这两个字符值合在一起,用GBK解码,解码完后,再读取第3个字符,重新开始以上过程, 若该字符值小于127,则直接用ASCII解码。

GBK兼容ASCII和GB2312的原理

35.2.3. GB18030

随着计算机技术的普及,我们后来又在GBK的标准上不断扩展字符,这些标准被称为GB18030, 如GB18030-2000、GB18030-2005等(“-”号后面的数字是制定标准时的年号),GB18030的编码使用4个字节, 它利用前面标准中的第2个字节未使用的“0x30-0x39”编码表示扩充四字节的后缀,兼容GBK、GB2312及ASCII标准。

GB18030-2000主要在GBK基础上增加了“CJK(中日韩)统一汉字扩充A”的汉字。加上前面GBK的内容, GB18030-2000一共规定了27533个汉字(包括部首、部件等)的编码,还有一些常用非汉字符号。

GB18030-2005的主要特点是在GB18030-2000基础上增加了“CJK(中日韩)统一汉字扩充B”的汉字。 增加了42711个汉字和多种我国少数民族文字的编码(如藏、蒙古、傣、彝、朝鲜、维吾尔文等)。 加上前面GB18030-2000的内容,一共收录了70244个汉字。

GB2312、GBK及GB18030是汉字的国家标准编码,新版向下兼容旧版,各个标准简要说明见表格 汉字国家标准 , 目前比较流行的是GBK编码,因为每个汉字只占用2个字节,而且它编码的字符已经能满足大部分的需求, 但国家要求一些产品必须支持GB18030标准。

汉字国家标准

35.2.4. Big5编码

在台湾、香港等地区,使用较多的是Big5编码,它的主要特点是收录了繁体字。而从GBK编码开始, 已经把Big5中的所有汉字收录进编码了。即对于汉字部分,GBK是Big5的超集,Big5能表示的汉字, 在GBK都能找到那些字相应的编码,但他们的编码是不一样的,两个标准不兼容, 如GBK中的“啊”字编码是“0xB0A1”,而Big5标准中的编码为“0xB0DA”。

35.3. Unicode字符集和编码

由于各个国家或地区都根据使用自己的文字系统制定标准,同一个编码在不同的标准里表示不一样的字符, 各个标准互不兼容,而又没有一个标准能够囊括所有的字符,即无法用一个标准表达所有字符。国际标准化组织(ISO)为解决这一问题, 它舍弃了地区性的方案,重新给全球上所有文化使用的字母和符号进行编号,对每个字符指定一个唯一的编号(ASCII中原有的字符编号不变), 这些字符的号码从0x000000到0x10FFFF,该编号集被称为Universal Multiple-Octet Coded CharacterSet, 简称UCS,也被称为Unicode。最新版的Unicode标准还包含了表情符号(聊天软件中的部分emoji表情), 可访问Unicode官网了解:http://www.unicode.org

Unicode字符集只是对字符进行编号,但具体怎么对每个字符进行编码,Unicode并没指定, 因此也衍生出了如下几种unicode编码方案(Unicode Transformation Format)。

35.3.1. UTF-32

对Unicode字符集编码,最自然的就是UTF-32方式了。编码时,它直接对Unicode字符集里的每个字符都用4字节来表示,转换方式很简单, 直接将字符对应的编号数字转换为4字节的二进制数。如表格 UTF-32编码示例 ,由于UTF-32把每个字符都用要4字节来存储, 因此UTF-32不兼容ASCII编码,也就是说ASCII编码的文件用UTF-32标准来打开会成为乱码。

UTF-32编码示例

对UTF-32数据进行解码的时候,以4个字节为单位进行解析即可,根据编码可直接找到Unicode字符集中对应编号的字符。

UTF-32的优点是编码简单,解码也很方便,读取编码的时候每次都直接读4个字节,不需要加其它的判断。它的缺点是浪费存储空间, 大量常用字符的编号只需要2个字节就能表示。其次,在存储的时候需要指定字节顺序,是高位字节存储在前(大端格式),还是低位字节存储在前(小端格式)。

35.3.2. UTF-16

针对UTF-32的缺点,人们改进出了UTF-16的编码方式,如表格 UTF-16编码示例 它采用2字节或4字节的变长编码方式(UTF-32定长为4字节)。 对Unicode字符编号在0到65535的统一用2个字节来表示,将每个字符的编号转换为2字节的二进制数,即从0x0000到0xFFFF。 而由于Unicode字符集在0xD800-0xDBFF这个区间是没有表示任何字符的,所以UTF-16就利用这段空间, 对Unicode中编号超出0xFFFF的字符,利用它们的编号做某种运算与该空间建立映射关系,从而利用该空间表示4字节扩展, 感兴趣的读者可查阅相关资料了解具体的映射过程。

UTF-16编码示例

注:𧗌 五笔:TLHH(不支持GB18030码的输入法无法找到该字,感兴趣可搜索它的Unicode编号找到)

UTF-16解码时,按两个字节去读取,如果这两个字节不在0xD800到0xDFFF范围内,那就是双字节编码的字符, 以双字节进行解析,找到对应编号的字符。如果这两个字节在0xD800到 0xDFFF之间,那它就是四字节编码的字符, 以四字节进行解析,找到对应编号的字符。

UTF-16编码的优点是相对UTF-32节约了存储空间,缺点是仍不兼容ASCII码,仍有大小端格式问题。

35.3.3. UTF-8

UTF-8是目前Unicode字符集中使用得最广的编码方式,目前大部分网页文件已使用UTF-8编码, 如使用浏览器查看百度首页源文件,可以在前几行HTML代码中找到如下代码:

网页字符的编码方式UTF-8
1
<meta http-equiv=Content-Type content="text/html;charset=utf-8">

其中“charset”等号后面的“utf-8”即表示该网页字符的编码方式UTF-8。

UTF-8也是一种变长的编码方式,它的编码有1、2、3、4字节长度的方式,每个Unicode字符根据自己的编号范围去进行对应的编码, 见表格 UTF-8编码原理_x的位置用于填充Unicode编号 。它的编码符合以下规律:

  • 对于UTF-8单字节的编码,该字节的第1位设为0(从左边数起第1位,即最高位),剩余的位用来写入字符的Unicode编号。 即对于Unicode编号从0x0000 0000 - 0x0000 007F的字符,UTF-8编码只需要1个字节, 因为这个范围Unicode编号的字符与ASCII码完全相同,所以UTF-8兼容了ASCII码表。

  • 对于UTF-8使用N个字节的编码(N>1),第一个字节的前N位设为1,第N+1位设为0, 后面字节的前两位都设为10,这N个字节的其余空位填充该字符的Unicode编号,高位用0补足。

UTF-8编码原理_x的位置用于填充Unicode编号

注:实际上utf-8编码长度最大为四个字节,所以最多只能表示Unicode编码值的二进制数为21位的Unicode字符。 但是已经能表示所有的Unicode字符,因为Unicode的最大码位0x10FFFF也只有21位。

UTF-8解码的时候以字节为单位去看,如果第一个字节的bit位以0开头,那就是ASCII字符,以单字节进行解析。 如果第一个字节的数据位以“110”开头,就按双字节进行解析,3、4字节的解析方法类似。

UTF-8的优点是兼容了ASCII码,节约空间,且没有字节顺序的问题,它直接根据第1个字节前面数据位中连续的1个数决定后面有多少个字节。 不过使用UTF-8编码汉字平均需要3个字节,比GBK编码要多一个字节。

35.4. BOM

由于UTF系列有多种编码方式,而且对于UTF-16和UTF-32还有大小端的区分,那么计算机软件在打开文档的时候到底应该用什么编码方式去解码呢? 有的人就想到在文档最前面加标记,一种标记对应一种编码方式,这些标记就叫做BOM(Byte Order Mark),它们位于文本文件的开头, 见表格 BOM标记 。注意BOM是对Unicode的几种编码而言的,ANSI编码没有BOM。

BOM标记

但由于带BOM的设计很多规范不兼容,不能跨平台,所以这种带BOM的设计没有流行起来。Linux系统下默认不带BOM。