字符编码

字符编码

  • 什么是字符编码

  • 字符集与字符编码

  • 常用字符编码

    在使用计算机的时候都有可能会碰到诸如ASCII、GB2312、GBK、UTF-8等标识,这些标识具体是什么,它的作用又是什么?

字符编码的产生和意义

字符在计算机中并不是被直接存储的,计算机只认识0、1所组成的串。如果想表示各种字符,那么就要有每个字符与这些0、1串的对应关系。字符编码就是表示这种关系的,它有两个作用:

  1. 对字符进行编码形成计算机认识的0、1串

  2. 把0、1串解码成字符

字符编码示意图

最早的编码——ASCII

计算机最早起源于以英文为母语的美国,英文中的字符比较少,用七个二进制位就足以表示,所以产生了单字节编码ASCII,如下图:
ASCII
ASCII是最早产生的编码方式,后来扩展的各种字符编码基本都是兼容ASCII编码的。

ASCII扩展

我们知道一个字节有8节,ASCII只用了低七位,还有一位后来用于对ASCII的扩展,IBM对于ASCII的扩展如下表所示,注意它是由IBM对ASCII的扩展,并非标准的ASCII表
ASCII扩展

还有一种扩展更常用一些。对于西欧国家的字符,使用最广泛的是ISO-8859-1编码,也被称为latin-1,它是早期8位编码方案的一种,现在大多数的8位编码方案都以它为基础,包括windows-1252。
ISO-8859-1

字符集与字符编码

通常所说的“某个字符串是以哪种形式编码的”这种说法是不恰当的,其实是字符串中的每个字符是以哪种形式编码的。

需要明确一个概念,字符集与字符编码是不一样的,字符集好比是一个字典,这个字典里记录了每个字符与表示该字符的代码(一般以16进制保存)的一一对应关系,但是这些代码在内存中是怎样存储的它不管。而字符编码更像是一种密钥,将内存中的0、1串解析成字符集里的代码。具体可由下图看出:
字符集与字符编码

Character:字符。计算机中所显示的单个符号,如’A’、’啊’等

Code Point:代码点。一个无符号的数字,通常以16进制保存。字符与代码点的一一对应关系就是字符集(Character Set),不同的字符集中的对应关系也是不一样的,所以有ASCII、GB2312、Unicode等字符集。

Bytes:字符在内存中的表示形式,以二进制存储,也就是0、1串。Bytes与Code Point的转换称为字符编码(Encoding)。

多字节编码

随着计算机在世界各地的普及,单字节编码远远不足以表示所有字符了,因此对于世界各地的语言(主要是亚洲)产生了多字节的编码。

GB2312

GB2312是我国国家标准总局在1980年发布的。它使用两个字节来表示字符码,理论上可最多可表示256X256 = 65536个符号。实际上GB2312里保存了94X94个汉字,GB2312使用“区位码”来表示字符集。

字符集

所有的国标汉字与符号组成一个94×94的矩阵。在此方阵中,每一行称为一个"区",每一列称为一个"位",因此,这个方阵实际上组成了一个有94个区(区号分别为1到94)、每个区内有94个位(位号 分别为1到94)的汉字字符集。一个汉字所在的区号和位号简单地组合在一起就构成了该汉字的"区位码"。在汉字的区位码中,高两位为区号,低两位为位号。
GB2312编码
由上图可看到GB2312对于可用空间并没有全部使用。

在区位码中:
01-09区为682个特殊字符
16-87区为汉字区,包含6763个汉字 。其中
16-55区为一级汉字(3755个最常用的汉字,按拼音字母的次序排列),
56-87区为二级汉字(3008个汉字,按部首次序排列)。
88-89区为信息交换用汉字编码字符集辅助集(103个汉字)。在GB/T 12345,并没有在GB2312中定义。
10-15区和90-94区为未分配区域。

汉字区位码(节选):
区位码

字符编码

实现GB2312字符集的编码主要是EUC-CN,该编码与ASCII码兼容,我们平时所说的GB2312编码指的就是EUC-CN编码。

GB2312的编码过程是这样的:

  1. 在字符集里找到与字符对应的区位码

  2. 将区和位分开,各加上160(0xA0)

  3. 再将新的区位合并得到机器码

示例

以“郸”字为例

  1. “郸“字的区位码为2106

  2. 21 + 160 = 181(0xB5) 06 + 160 = 166(0xA6)

  3. 在内存中的存储为0xB5A6

检测一下,在txt文件中写入了如下字符:
GB2312TXT

用UltraEdit编辑器打开,并切换到16进制模式:

到这里有一个疑问,我查的资料里都说GB2312是以两个字节来存储的,但是上图的中情形,凡是ASCII兼容的字符都是一个字节存储。所以GB2312的编码规则应该是对于ASCII里的字符以一个字节来存储,而ASCII里没有的就以两个字节来存储。至于怎么判断是否是ASCII里的字符,那就要判断每个字节的最高位,如果是0则证明是ASCII字符,以一个字节存储;如果是1则证明是其他字符,以两个字节存储。

扩展

GBK字符集是对GB2312的扩展,也是使用两个字符表示。GBK并没有官方的标准,现在使用最广的标准是微软在Windows 95中实现的版本——CP936编码。
GBK

Unicode

不同的地区可以有不同的编码,简体中文是GB2312、繁体中文Big5,此外还有日文、韩文等编码标准,这样会导致不同编码之间的信息无法交流,所以需要一个可以囊括世界上所有符合的编码方式,这就是Unicode编码,全称为universal character encoding。

字符集

Unicode字符集中的字符码以4个字节来表示,这么做的目的就是为了涵盖世界上所有的字符。
汉字Unicode编码表(节选):
Unicode字符集

字符编码

Unicode的存储形式一般称为UTF-*编码,其中UTF全称为Unicode Transformation Format。常见的有UTF-32、UTF-16、UTF-8。

UTF-32

UTF-32是Unicode最直接的编码方式,用4个字节来表示字符集中的4个字节,是UTF-*家族唯一的定长编码。它的优点是编码效率高。但是它的劣势却更明显,四个字节表示一个字符会占用大量的内存,况且常用的字符基本上不会用四个字节,因此这样的占用内存是毫无意义的,UTF-32在计算机中是很少用的。

UTF-16

UTF-16最少可以用两个字符来表示code point,它是一种变长编码,code point在65536之内的就两个字节来表示,超过65536的则使用4字节来表示。UTF-16的优势比UTF-32要节省一半的空间,如果用不到65536以上的字符的话,它的效率与UTF-32一样高效。

Unicode字符集范围(16进制)UTF-16编码方式(16进制)
0000-D7FF和E000-FFFF 0000-FFFF
D800-DFFF Unicode标准对于UTF-16编码方式保留这些代码点的值,它们不会被分配字符
10000-10FFFF D800 DC00-DBFF DFFF

0x10000转换为10进制就是65536,UTF-16对于超过0x10000(65536)的编码方式为:

  1. 将Code point减去0x10000,剩下一个20位的二进制数(范围在0-0x0FFFFF)

  2. 高10位(范围在0-0x3FF),加上0xD800构成前两个字节(0xD800-0xDBFF)

  3. 低10位(范围在0-0x3FF),加上0xDC00构成后两个字节(0xDC00-0xDFFF)

举例说明:
如下表,对U+10437( UTF-16特殊字符 )这个特殊节符进行编码

  1. 0x10437 – 0x10000 = 0x00437,转为二进制0000 0000 0100 0011 0111

  2. 将二进制数拆开为高十位0000 0000 01和低十位00 0011 0111

  3. 0xD800加在高十位,0xD800 + 0x0001 = 0xD801

  4. 0xDC00加在低十位,0xDC00 + 0x0037 = 0xDC37

UTF-16

UTF-16的解码是怎样识别这些0、1串是两个字节的一部分还是四个字节的一部分?注意到如果某两个字节的起始端为1101 10开头的,且后面跟随的两个字节的起始端是1101 11开头的,那么它就是以四个字节表示的字符,应当使用编码规则进行解码。

关于UTF-32、UTF-16还有一个字节序概念,Big-endian和Little-endian,以“勤”字为例,它的字符码是52 E4,那么在计算机中是以52 E4的顺序来存储还是以E4 52的顺序来存储。如果是以52 E4来存储就是Big-endian方式,如果是以E4 52来存储就是Little-endian方式。

计算机是怎么判断字符编码的存储是大端模式还是小端模式?Unicode编码中有一个“ZERO WIDTH NO-BREAK SPACE”(零宽度非换行空格,也被称为BOM)的字符,它的编码是FEFF,它被放在文件或字符流的头两个字节,FE FF表示大端模式,FF FE表示小端模式。(UTF-32以四个字节来表示字节序,00 00 FE FF表示小端,FF FE 00 00表示大端)。

Big-endian和Little-endian的命名源自于《格列佛游记》,在小人世界里有一个国家,这个国家里的人认为吃鸡蛋要从较大的一端打破吃,但是皇帝小时候有一次吃鸡蛋的时候从大端打破不小心把手划破了,非常愤怒,命令全国所有的人以后吃鸡蛋打破较小的一端吃,违者重罚。可是老百姓却不愿这样,为此发生过六次叛乱,一个皇帝送了命,一个皇帝丢了王位,好多人流亡到了其他国家,有些人情愿受死也不愿打破鸡蛋较小的一端吃。

举例来说,将”勤”字输入到txt文本文件里,并保存为Unicode格式
UTF-16TXT

用UltraEdit编辑器打开,并切换到16进制模式:
UTF-16UEDIT
注意红线部分,它是以小端模式来编码的。

UTF-8

UTF-8是比UTF-16更省空间的编码方式,它也是一种变长编码,可使用1~4个字节表示一个符号,比如对于ASCII字符使用一个字节,对于拉丁字符使用两个字节,对于汉字使用三个字节,对于使用频率极少的字符采用四个字节。UTF-8的不足之处是在效率上不及UTF-16。

UTF-8的编码规则有以下两条

  1. 对于单字节的符号,字节的第一位设为0,后面7位是这个字符的codepoint。因此对于英文字母,UTF-8和ASCII编码是相同的。

  2. 对于n字节的符号,第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位全部是这个字符的code point。

如果要表示Unicode字符集里的所有字符,那么UTF-8理论上的范围如下图所示:
UTF-8R1
可以看到最后两行竟然以5、6个字节来表示一个字符了!为了匹配UTF-16的范围,规定UTF-8的最大值为U+10FFFF,因此删除了以5字节、6字节和部分4字节表示的code point序列。
最终的UTF-8范围是:
UTF-8R2

举例来说明,还是以汉字“勤”为例,它的code point是52 E4(101 0010 1110 0100),它需要15位,第三行可以满足要求,即格式是“1110xxxx 10xxxxxx 10xxxxxx”,将“勤”字的code point从后向前依次填入“x”的位置,多出的补0,就得到“11100101 10001011 10100100”,转换成16进制就是E58BA4。
将”勤”字输入到txt文本文件里,并保存为UTF-8格式,并用UltraEdit打开:
UTF-8UEDIT

图中前三个字符EF BB BF是什么意思?还记得的UTF-16的字节序吧,UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符”ZERO WIDTH NO-BREAK SPACE”的UTF-8编码是EF BB BF。如果在文件和字节流的起始位置是EF BB BF,就知道这是以UTF-8形式编码的。

总结

到这里,总算对字符编码这个一直似懂非懂的知识点有了更深入、更清晰的认识。平常遇到关于字符编码方面的问题,网上搜索一下总能找到解决方案。但这样的方式就好比是“借刀杀人”,下次遇到时还需再“借”。而在学习了这方面知识后变得更加自信了。所以,如果遇到自己疑惑的问题,可以上网寻求解决方案,但最好能知其所以然,这样才能真正的变成自己的“武器”。

[参考]
http://www.tuicool.com/articles/VvmeUz3

http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

https://en.wikipedia.org/wiki/ISO/IEC_8859-1

https://en.wikipedia.org/wiki/GB_2312

http://baike.baidu.com/link?url=OyTEfo8CZv1gKcfJn1ywD_o1j0ljg0A7eGgFm7dUXZNFgRT9Nuvr5UgkOcutJ09TvYEboKMMPSRCm27yUpw43q

https://en.wikipedia.org/wiki/UTF-16#U.2B10000_to_U.2B10FFFF

https://en.wikipedia.org/wiki/UTF-8

posted @ 2016-01-04 22:43  如是逆旅行人  阅读(349)  评论(0编辑  收藏  举报