编码

前言

我们都知道,我们交给计算机处理的信息,与计算机实际处理的信息其实并不一样,这里的信息转换,就涉及到了编码的内容。我们现实中交给计算机处理的信息可以简单地理解为一个个的字符,数字也可以认为是字符吧。但计算机并不能理解字符,计算机能够处理的是一个个二进制值,每一个二进制位(bit)可以表示0和1两种状态。于是就变成了怎么讲字符转变成了二进制的问题。

简单字符集

字符编码的思路是先把字符映射到一个数字,然后再将数字映射到存储的二进制。映射字符到数字的表格,被称作字符集(Character Set)。

通常来说,人们认为字符编码和字符集是同义词,因为使用同样的标准来定义提供什么字符并且这些字符如何编码到一系列的代码单元(通常一个字符一个单元)。
由于历史的原因,MIME和使用这种编码的系统使用术语字符集来表示用于将一组字符编码成一系列八位字节数据的整个系统。

现代编码模型

但是,由统一码和通用字符集所构成的现代字符编码模型则没有跟从简单字符集的观点。
它们将字符编码的概念分为:有哪些字符、它们的编号、这些编号如何编码成一系列的“码元”(有限大小的数字)以及最后这些单元如何组成八位字节流。
Unicode Technical Report (UTR) #17中,现代编码模型分为5个层次,所用的术语列在下面:

抽象字符表

抽象字符表(Abstract character repertoire),是一个系统支持的所有抽象字符的集合,解释了有哪些字符
字符表可以是封闭的,即除非创建一个新的标准(ASCII和多数ISO/IEC 8859系列都是这样的例子),否则不允许添加新的符号;字符表也可以是开放的,即允许添加新的符号(统一码和一定程度上代码页是这方面的例子)。

特定字符表中的字符反映了如何将书写系统分解成线性信息单元的决定。
例如拉丁、希腊和斯拉夫字母表分为字母、数字、变音符号、标点和如空格这样的一些少数特殊字符,它们都能按照一种简单的线性序列排列(尽管对它们的处理需要另外的规则,如带有变音符号的字母这样的特定序列如何解释——但这不属于字符表的范畴)。
为了方便起见,这样的字符表可以包括预先编号的字母和变音符号的组合。其它的书写系统,如阿拉伯语和希伯莱语,由于要适应双向文字和在不同情形下按照不同方式交叉在一起的字形,就使用更为复杂的符号表表示。

编码字符集

编码字符集(CCS:Coded Character Set)是将字符集C中每个字符映射到1个坐标(整数值对:x, y)或者表示为1个非负整数 N。字符集及码位映射称为编码字符集,编码字符集解释了字符对应的编号。
例如,在一个给定的字符表中,表示大写拉丁字母“A”的字符被赋予整数65、字符“B”是66,如此继续下去。多个编码字符集可以表示同样的字符表,例如ISO-8859-1IBM的代码页037和代码页500含盖同样的字符表但是将字符映射为不同的整数。由此产生了编码空间(encoding space)的概念:简单说就是包含所有字符的表的维度。可以用一对整数来描述,例如:GB 2312的汉字编码空间是94 x 94。可以用一个整数来描述,例如:ISO-8859-1的编码空间是256。也可以用字符的存储单元尺寸来描述,例如:ISO-8859-1是一个8比特的编码空间。编码空间还可以用其子集来表述,如行、列、面(plane)等。

编码空间中的一个位置(position)称为码位(code point)。一个字符所占用的码位称为码位值(code point value)。1个编码字符集就是把抽象字符映射为码位值。

字符编码表

字符编码表(CEF:Character Encoding Form),也称为”storage format”,是将编码字符集的非负整数值(即抽象的码位)转换成有限比特长度的整型值(称为码元code units)的序列。

这对于定长编码来说是个到自身的映射(null mapping),但对于变长编码来说,该映射比较复杂,把一些码位映射到一个码元,把另外一些码位映射到由多个码元组成的序列。

例如,使用16比特长的存储单元保存数字信息,系统每个单元只能够直接表示从0到65,535的数值,但是如果使用多个16位单元就能够表示更大的整数。这就是CEF的作用,它可以把Unicode从0到140万的码空间范围的每个码位映射到单个或多个在0到65,5356范围内的码值。最简单的字符编码表就是单纯地选择足够大的单位,以保证编码字符集中的所有数值能够直接编码(一个码位对应一个码值)。这对于能够用使用八比特组来表示的编码字符集(如多数传统的非CJK的字符集编码)是合理的,对于能够使用十六比特来表示的编码字符集(如早期版本的Unicode)来说也足够合理。但是,随着编码字符集的大小增加(例如,现在的Unicode的字符集至少需要21位才能全部表示),这种直接表示法变得越来越没有效率,并且很难让现有计算机系统适应更大的码值。
因此,许多使用新近版本Unicode的系统,或者将Unicode码位对应为可变长度的8位字节序列的UTF-8,或者将码位对应为可变长度的16位序列的UTF-16。

字符编码方案

字符编码方案(CES:Character Encoding Scheme),也称作”serialization format”。将定长的整型值(即码元)映射到8位字节序列,以便编码后的数据的文件存储或网络传输。

在使用Unicode的场合,使用一个简单的字符来指定字节顺序是大端序或者小端序(但对于UTF-8来说并不需要专门指明字节序)。然而,有些复杂的字符编码机制(如ISO/IEC 2022)使用控制字符转义序列在几种编码字符集或者用于减小每个单元所用字节数的压缩机制(如SCSU、BOCU和Punycode)之间切换。

传输编码语法

传输编码语法(transfer encoding syntax),用于处理上一层次的字符编码方案提供的字节序列。

一般其功能包括两种:一是把字节序列的值映射到一套更受限制的值域内,以满足传输环境的限制,例如Email传输时Base64或者quoted-printable,都是把8位的字节编码为7位长的数据;另一是压缩字节序列的值,如LZW或者行程长度编码等无损压缩技术。

字符集、代码页,与字符映射

术语字符编码(character encoding)字符映射(character map)字符集(character set)或者代码页,在历史上往往是同义概念,即字符表(repertoire)中的字符如何编码为码元的流(stream of code units)–通常每个字符对应单个码元。

码元(Code Unit,也称“代码单元”)是指一个已编码的文本中具有最短的比特组合的单元。对于UTF-8来说,码元是8比特长;对于UTF-16来说,码元是16比特长;对于UTF-32来说,码元是32比特长。码值(Code Value)是过时的用法。

代码页通常意味着面向字节的编码,但强调是一套用于不能语言的编码方案的集合.著名的如”Windows”代码页系列,”IBM”/“DOS”代码页系列.

字符集

我们对字符编码的一些概念有了大概的了解后,接下来介绍下一些我们常见的字符集。

在上面的介绍中,映射字符到数字的表格,被称作字符集(Character Set)。但是许多国家有着自己的语言,于是也就衍生出了针对各种语言的字符集

常见的一些有:

  • GB2312、GBK、GB18030 - 中文
  • BIG5 - 中文繁体
  • JIS X 0208 - 日文

这里就有一个问题了,这些字符集会不会在表示各自文字的时候用到相同的码点?答案是肯定的,这也是乱码的一个原因。

1
2
3
4
5
>>> c = '诗'
>>> print c.decode('gbk')

>>> print c.decode('big5')

ASCII编码

ASCII(American Standard Code for Information Interchange)编码是美国制定的一套字符编码方案,字符集中有0~127共计128个码点,每一个码点都可以使用7位二进制表示。编码方式非常直接,就是码点对应的二进制。

查看一个英文字符“a”对应的ASCII字符集的码点和ASCII编码:

1
2
3
4
>>> ord(u'a'.encode('ascii'))
97
>>> bin(ord(u'a'.encode('ascii')))
'0b1100001'

因为计算机是按字节分配内存的,尽管ASCII字符集的码点只需要7位,实际上要占用一个字节,于是最前面的一位统一规定为0。所以,有2^8-2^7=128个字符空间是没有用到的。

后来,为了解决部分西欧语言的问题,有人想到利用这剩下的128来表示更多的字符,这些方法统称为EASCII(Extended ASCII),其中著名的有ISO 8859-1(又称作Latin 1)。

GB XXX编码

对于汉字来说,ASCII剩下的128个字符显然是不够用的。于是,GB2312率先出场。GB2312收录了共7445个图形字符,其中汉字占6763个。显然码点的个数已经超出了1个字节的表示范围,所以使用两个字节来编码一个汉字。

考虑到与ASCII的兼容,规定码点在0~127范围内的字符和ASCII相同,还是1个字节表示1个字符。

1
2
>>> ord(u'a'.encode('gb2312'))
97

规定两个码点大于127的字符连在一起时,表示一个汉字。首字节需要在0xA10xF7之间,尾字节在0xA10xFE之间,这样可以组合出8000多种码点。

1
2
>>> print u'诗'.encode('gb2312')
'\xca\xab'

由于GB2312只收录6763个汉字,还有不少汉字并未有收录。于是微软基于GB2312扩展出GBKGBK共收录21886个汉字和图形符号,其中汉字21003 个。GBK的首字节在0x810xFE 之间,尾字节在0x400xFE 之间,一共有2万多个码点。

机智的你有没有猜到,GB是Guo Biao(国标)的意思,K是Kuo Zhan(扩展)的意思…

再后来,GB18030推出,兼容GB2312,基本兼容GBK,共收录汉字70244个。GB18030采用了多字节编码方案,支持更大的编码空间。

UNICODE

ASCII作为一个开始,后面出现了各种字符集,各种编码方案。貌似,我们在自己的世界里玩得很好。

事实上,如果把字符集比作是一部字典,那现在的情况是,我的字典里未必有你,你的字典里未必有我。

如果能有一部大而全的字典,我们都用同一个,世界大同,再也不用担心乱码,多好!

于是,UNICODE来了。
UNICODE是一个世界级的字符集,全世界的每一个字符都有唯一的码点。码点具有这样的形式U+[XX]XXXX,其中,X是一个十六进制数字。UNICODE的范围目前是U+0000~U+10FFFF,超过100万。感觉好任性,还有哪个字符没收录进来~

因为码点太多了,为了方便管理,把每65536个码点归为一组,称为一个平面(Plane),共有17个平面。

第一个平面,即Plane 0,又叫做BMP(Basic Multilingual Plane,基本多语言平面),码点范围是U+0000~U+FFFF,日常用到的绝大多数字符都在这个平面。

1
2
>>> u'诗'
u'\u8bd7'

参考ASCII的编码方法,UNICODE的编码方法也可以如此直接,即直接存储码点对应的二进制。
额,因为UNICODE的码点最少也要4个字节,所以,意味着曾经在ASCII只需要1个字节的字符们要占用4个字节了,并且,这还和ASCII不兼容。

UTF-XXX

UTF(Unicode Transformation Format),即UNICODE的码点转换为最终的编码的格式。出于不同的目的,有多种UTF编码方法:UTF-8UTF-16UTF-32

其中,UTF-32便是直接将码点转换为4字节二进制的方法。

考虑到大多数人都在BMP(U+0000~U+FFFF)里玩,只需要使用两个字节编码即可。万一用到了BMP以外的字符,再使用4字节。这便是UTF-16
由于UTF-16UTF-32使用多个字节来存储一个数字(码点),所以都需要考虑字节序的问题。

UTF-8不同于UTF-16UTF-32UTF-8是一种变长字节的编码方式,无需考虑字节序。对于汉字“诗”,其UTF-8编码计算方式如下。

utf

对于字符“a”,对应的UNICODE码点、UTF-XXX编码为:

1
2
3
4
5
CHAR   : a
UNICODE: u+0061
UTF-8 : 0x61
UTF-16 : 0x0061
UTF-32 : 0x00000061

对于字符“诗”,对应的UNICODE码点、UTF-XXX编码为:

1
2
3
4
5
CHAR   : 诗
UNICODE: u+8bd7
UTF-8 : 0xe8af97
UTF-16 : 0x8bd7
UTF-32 : 0x00008bd7

参考:
维基百科 · 字符编码
字符编码那些事儿