字符编码与存储实现:ASCII、GB2312、Unicode、UTF

计算机处理信息分为两类,一类是数值处理,一类是文本(字符)处理。涉及到文本,就有文本编码的问题。字符处理包括字符表示、字符存储、字符显示(点阵显示)。字符的表示和存储就涉及到编码方案。对于拉丁语系国家来说,使用的字符较少,用8个二进制位的组合即可以表示全部字符了(如英文词就只是26个字母的组合而已)。对于一些非拉丁语系国家,如中国来说,字符就复杂了。所以字符的编码有一个逐步发展、完善和统一的过程。最先出现的是ASCII码,因为计算机最先在美国发展出来,ASCII就是符合美国字符处理需要的字符表示与存储方案,然后,各国因应自己国家的需要,制定的各自的字符编码方案(当然会考虑兼容ASCII,如GB2312),最后,当然会走上全球标准化、统一的方向,如Unicode编码方案及其存储实现UTF。

1 ASCII

ASCII ( American Standard Code for Information Interchange 美国信息交换标准代码)是基于拉丁字母的一套字符编码系统,主要用于显示现代英语和其他西欧语言,到目前为止共定义了128个字符(2^7),使用8个二进制位,首个二进制位为0。

1.1 控制字符

0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符),如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等;通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等;ASCII值为8、9、10 和13 分别转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响 。

1.2 可显示字符

32~126(共95个)是字符(32是空格)。

对于数字、字母的编码如果从二进制去看还是有一定规律的。

1.2.1 十个阿拉伯数字

48~57为0到9十个阿拉伯数字,48的二进制为110000。

1.2.2 大写英文字母

65~90为26个大写英文字母,65的二进制为1000001。

97~122号为26个小写英文字母,97的二进制为1100001。

所以大、小写字母编码相差100000(2^5)。

2 GB2312

ASCII对于欧美国家来说,其字符处理完全没有问题,但对于非拉丁语系国家来说,其字符处理遇到了大问题,对于中文汉字,有七万多个,常用的就有六千多个。

GB2312-80 是 1980 年制定的中国汉字编码国家标准。共收录 7445 个字符,其中汉字 6763 个。GB2312 兼容标准 ASCII码,采用扩展 ASCII 码的编码空间进行编码,一个汉字占用两个字节,每个字节的最高位为 1。具体办法是:收集了 7445 个字符组成 94*94 的方阵,每一行称为一个“区”,每一列称为一个“位”,区号位号的范围均为 01-94,区号和位号组成的代码称为“区位码”。区位输入法就是通过输入区位码实现汉字输入的。将区号和位号分别加上 20H,得到的 4 位十六进制整数称为国标码,编码范围为 0x2121~0x7E7E。为了兼容标准 ASCII 码,给国标码的每个字节加 80H,形成的编码称为机内码,简称内码,是汉字在机器中实际的存储代码GB2312-80 标准的内码范围是 0xA1A1~0xFEFE [7] 。

对于汉字,输入有输入码(如五笔、微软拼音输入法),存储有存储码,输出有输出码(如宋体)。

3 Unicode编码方案

为避免冲突和低效,全球统一一个标准总是一个最佳的选择,如通信标准、一些零部件的通用标准,字符编译方案也不例外,Unicode就是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。

UCS-2用两个字节编码,UCS-4用4个字节编码。

UCS-4使用4个字节(从右到左):

第4个字节:组(group),最高位为0,2^7=128个。

第3个字节:平面(plane),2^8=256个。

第2个字节:行 (row),2^8=256个。

第1个字节:码位(cell),2^8=256个。

如果UCS-4的前两个字节为全零,那么将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。

Unicode计划使用了17个平面(第3个字节,0x0-0x10,二进制:0b0-0b10000),一共有17×65536=1114112个码位(0x0-0x10FFFF)。在Unicode 5.0.0版本中,已定义的码位只有238605个,分布在平面0、平面1、平面2、平面14、平面15、平面16。其中平面15和平面16上只是定义了两个各占65534个码位的专用区(Private Use Area),分别是0xF0000-0xFFFFD和0x100000-0x10FFFD。所谓专用区,就是保留给大家放自定义字符的区域,可以简写为PUA。

平面0也有一个专用区:0xE000-0xF8FF,有6400个码位。平面0的0xD800-0xDFFF,共2048个码位,是一个被称作代理区(Surrogate)的特殊区域。代理区的目的用两个UTF-16字符表示BMP以外的字符。

在Unicode 5.0.0版本中,余下的99089个已定义码位(238605-65534*2-6400-2048=99089)分布在平面0、平面1、平面2和平面14上,它们对应着Unicode定义的99089个字符,其中包括71226个汉字。平面0、平面1、平面2和平面14上分别定义了52080、3419、43253和337个字符。平面2的43253个字符都是汉字。平面0上定义了27973个汉字。

中文范围 4E00-9FA5:CJK 统一表意符号19968-40869=20901

在Unicode 5.0的99089个字符中,有71226个字符与汉字有关。它们的分布如下: 如果不算兼容汉字,Unicode目前支持的汉字总数是20924+6582+42711=70217。

字符编码与存储实现:ASCII、GB2312、Unicode、UTF

1980年的GB2312一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。

这6763个汉字在Unicode中不是连续的,分布在CJK统一汉字字符区(0x4E00-0x9FA5)的20902个汉字中。

1995年的汉字扩展规范GBK1.0收录了21886个符号,包括21003个汉字和883个其它符号。

这21003汉字包括CJK统一汉字区的20902个汉字。余下的101个汉字包括:增补汉字和部首80个,包括28个部首和52个汉字。GBK编码是从FE50-FE7E,FE80-FEA0。

总结一下:GB2312有6763个汉字,GBK有21003个汉字,GB18030-2000有27533个汉字,GB18030-2005有70244个汉字。Unicode 5.0中,如果不算兼容区,目前有70217个汉字。

4 UTF:Unicode编码的实现方案

我们知道,C++对于整数有不同的类型,如short、int、long,根据不同的长度选择不同整数类型,以兼顾需要和内存节约的需要。

我们也知道,霍夫曼编码是一种变长的效率很高的编码方案,即有编码的唯一性,又能对使用频率高的字符使用较短的编码去实现。

Unicode只是编码方案,存储实现可以有不同的存储方案去实现,可以是定长编码实现,也可以是变长编码实现,或者两者结合。

UTF就是Unicode编码存储实现的一个方案,UTF是“Unicode Transformation Format”的缩写,可以翻译成Unicode字符集转换格式,即怎样将Unicode定义的数字转换成程序数据。

UTF有UTF-8(变长,1-4个字节)、UTF-16(变长,2个或4个字节)、UTF32(定长,4个字节)。

4.1 UTF-8

UTF-8的编码方式是变长的,它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度,这样就解决了存储浪费的问题,所以UTF-8是Unicode比较好的实现方式。

UTF-8以字节为单位对Unicode进行编码。从Unicode到UTF-8的编码方式如下:

Unicode编码(十六进制) UTF-8 字节流(二进制)

000000-00007F 0xxxxxxx

000080-0007FF 110xxxxx 10xxxxxx

000800-00FFFF 1110xxxx 10xxxxxx 10xxxxxx

010000-10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

字符编码与存储实现:ASCII、GB2312、Unicode、UTF

UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。从上表可以看出,4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位(2^20=1114111)。

如“汉”字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

例如,“汉字”对应的Unicode编码是0x6c49和0x5b57,在C语言中也可以使用数组来表示:

unsigned char data_utf8[]={0xE6,0xB1,0x89,0xE5,0xAD,0x97}; //UTF-8编码

4.2 UTF-16

我们知道Unicode的范围为0x0~0x10FFFF。
首先是BMP区间,也就是0x0~0xFFFF这段区间,正好16位就可以表示,也兼容,两全其美。
那么超过BMP区间的怎么办呢?
也就是0xFFFF~0x10FFFF这段,我们先看这段区间有多少个码位,0x10FFFF-0xFFFF=0x100000,那么这个十六进制表示的十进制也就是:1048576个码位。
我们既然16位存不下,那用32位存咯,因为计算机只能以2的倍数拓展,如果不这么设计,长短不一,解析起来的效率会非常低。
32位来存这些数字,那么我们需要怎么存下呢?简单的思考过后,大家认为应该分开存储,也就是将32位分开前16位和后16位,每个16位各存一半。


那么每一半存的就是1024(由来:√1048576=1024,也就是1024*1024=1048576),1024代表的是2的10次幂,也就是10位二进制数。
这样就知道了,32位二进制数字中,前后16位中各存10位就够用了,但是剩余的6位用来干什么呢?
和UTF-8的设计一样,为了让识别字符串变得容易,每个字节各用3位来做为识别码:

110110yyyyyyyyyy 110111xxxxxxxxxx(4个字节)。

54开头的为32位的前16位。
55开头的为32位的后16位。
其他开头的为单16位。
这样我们就能区分开这三个16位了,在读取文档中的任意位置,都能随意区分出间隔咯。
那么54开头的数据区间是多少呢,就是1101 10xx xxxx xxxx,区间就是D800~DBFF。
那么55开头的数据区间是多少呢,就是1101 11xx xxxx xxxx,区间就是DC00~DFFF。

为了配合UTF-16,Unicode中也将这两个区间屏蔽掉,不允许分配任何字符。

如果一个字符的UTF-16编码的第一个WORD在0xDB80到0xDBFF之间,那么它的Unicode编码在什么范围内?我们知道第二个WORD的取值范围是0xDC00-0xDFFF,所以这个字符的UTF-16编码范围应该是0xDB80 0xDC00到0xDBFF 0xDFFF。我们将这个范围写成二进制:

1101101110000000 11011100 00000000 - 1101101111111111 1101111111111111

按照编码的相反步骤,取出高低WORD的后10位,并拼在一起,得到

1110 0000 0000 0000 0000 - 1111 1111 1111 1111 1111

即0xe0000-0xfffff,按照编码的相反步骤再加上0x10000,得到0xf0000-0x10ffff。这就是UTF-16编码的第一个WORD在0xdb80到0xdbff之间的Unicode编码范围,即平面15和平面16。因为Unicode标准将平面15和平面16都作为专用区,所以0xDB80到0xDBFF之间的保留码位被称作高位专用替代。

字符编码与存储实现:ASCII、GB2312、Unicode、UTF

4.3 UTF-32

UTF-32 是固定长度的编码,始终占用 4 个字节,足以容纳所有的 Unicode 字符,所以直接存储 Unicode 编号即可,不需要任何编码转换。浪费了空间,提高了效率。

5 字节序和UTF编码BOM

字节序有两种,分别是“大端”(Big Endian, BE)和“小端”(Little Endian, LE)。

Unicode标准建议用BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为BOM的字符。

下表是各种UTF编码的BOM:

字符编码与存储实现:ASCII、GB2312、Unicode、UTF

-End-


分享到:


相關文章: