字符集和编码无疑是让很多人头痛的问题。当遇到各种字符集和编码时,问题的定位往往变得毫无头绪。本次我们就对字符集和编码做一个深层的分享,我将会介绍常用的字符集和编码原理,乱码的产生原因(包括可逆的乱码和不可逆的乱码),以及利用一些编码的特性来实现用字符流复制二进制文件。

目录:

1、字符集的由来

2、GB2312字符集

3、Unicode字符集

4、UTF-32编码

5、UTF-16编码

6、UTF-8编码

7、产生乱码的原因

8、可逆的乱码和非可逆的乱码

9、用字符流复制二进制文件

01 字符集的由来

1946年,第一台计算机”ENIAC”在美国被发明,处理符号信息就是计算机应该具备和基础能力。因为英语所使用的字符数量比较少,只有26个英文字母的大小写,数字以及一些打印时的控制字符,美国人就规定了一张表,叫ANSC II表,如图1.1。

图1.1

其中0-31是控制字符,32-127是可见字符。一共128个字符,7个二进位就可以表示,不到一个字节的长度。虽然只有128个字符,但是已经足够美国人平时的使用了。

后来,欧洲国家也开始使用计算机,这个时候128个字符显然就不够用了,于是将最高位也开始纳入编码,这时相比于ANSC II码,编码范围扩大了一倍。如图1.2。

图1.2

这个扩展后的编码,我们称之为ISO-8859-1,它是一种单字节编码,即编码范围正好是一个字节可以表示的最大范围,这是它的一个非常好的特性,后边我们还会讨论。

但是,故事还没有结束……

当电脑来到中国时,区区256个字符怎么够表示我大中华的汉字!一个字节不够,那就用多个,于是我们开始使用两个字节,也即16个二进位来表示一个字符,这样可以表示的范围成倍增加,理论上扩展到2的16次方,共可以表示65536个字符。

下一步,就是设计字符集了。

02 GB2312字符集

GB2312共收录其中一级汉字3755个,二级汉字3008个;同时,收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符。整个字符集分成94个区,每区有94个位。

01-09区为特殊符号。

16-55区为一级汉字,按拼音排序。

56-87区为二级汉字,按部首/笔画排序。

10-15区及88-94区则未有编码

图2.1

图2.1

GB2312规定对收录的每个字符采用两个字节表示,第一个字节为“高字节”,对应94个区;第二个字节为“低字节”,对应94个位。所以它的区位码范围是:0101-9494。区号和位号分别加上0xA0就是GB2312编码。例如最后一个码位是9494,区号和位号分别转换成十六进制是5E5E,0x5E+0xA0=0xFE,所以该码位的GB2312编码是FEFE。

GB2312编码范围:A1A1-FEFE,其中汉字的编码范围为B0A1-F7FE,第一字节0xB0-0xF7(对应区号:16-87),第二个字节0xA1-0xFE(对应位号:01-94)。

以“啊”和“半”为例,计算它们的GB2312编码:

GB2312出现的时间比较早,目前已经很少有用了。由于它的收录的字符比较少,当遇到一些生僻字的时候,往往就不能表示了,所以之后又出现了GBK,GB18030等字符集来扩充,它们间的兼容关系为GB18030兼容GBK,GBK兼容GB2312。

03 Unicode字符集

Unicode 为世界上所有字符都分配了一个唯一的数字编号,每个字符都有一个唯一的 Unicode 编号,这个编号一般写成 16 进制,在前面加上 U+。例如:“马”的 Unicode 是U+9A6C。

图3.1

Unicode是为整合全世界的所有语言文字而诞生的。任何文字在Unicode中都对应一个值,这个值称为代码点(code point)。代码点的值通常写成 U+ABCD 的格式。而文字和代码点之间的对应关系就是UCS-2(Universal Character Set coded in 2 octets)。顾名思义,UCS-2是用两个字节来表示代码点,其取值范围为 U+0000~U+FFFF。

为了能表示更多的文字,人们又提出了UCS-4,即用四个字节表示代码点。它的范围为 U+00000000~U+7FFFFFFF,其中 U+00000000~U+0000FFFF和UCS-2是一样的。

要注意,UCS-2和UCS-4只规定了代码点和文字之间的对应关系,并没有规定代码点在计算机中如何存储。规定存储方式的称为UTF(Unicode Transformation Format),其中应用较多的就是UTF-32、UTF-16和UTF-8了。下面就详细说一下这三种编码。

04 UTF-32编码

UTF-32中的码元由32位组成。UTF-32使用的32位码元足够大,目前Unicode字符集中所收录的每个字符的码点值都可直接映射为单个码元。即使是ASCII字符,同样需要占用32位(即四个字节)。这在三大UTF编码方式中无疑是最为浪费存储空间的;

不过,由于UTF-32是定长编码(UTF-8和UTF-16都是变长编码),因此在文本处理速度上又是三大UTF编码方式中最快的。

05 UTF-16编码

在了解UTF-16编码方式之前,先了解一下另外一个概念“平面”。

Unicode可以被理解为是一个很厚的字典,它将全世界所有的字符定义在一个集合里。这么多的字符不是一次性定义的,而是分区定义的。每个区中以存放65536个(2的16次方)个字符,称为一个平面(plane)。目前,一共有17个平面,65536*17=1114112也就是110多万,也就是说,整个Unicode字符集的大小现在是2的21次方。最前面的65535个字符位,称为基本面,它的码点范围是从0到65535。所有最常见的字符都放在这穿上平面,这是Unicode最先定义和公布的一个平面。剩下的字符都入在辅助平面,码点范围从U+010000到U+10FFFF。

了解了平面的概念后,再说回到UTF-16。UTF-16编码介于UF-32和UTF-8之间,同时结合了定长和变长两种编码方法的特点。它的编码规则很简单:基本平面的字符占用2个字节,辅助平面的字节占用4个字节.也就是说,UTF-16的编码长度要么是2个字节(U+0000-U+FFFF),要么是四个字节(U+010000-U+10FFFF)。那么问题来了,当我们遇到两个字节时,应该把这两个字节当成一个字符还是与后面的两个字节一起当做一个字符呢?为了将两个字节的UTF-16编码与四个字节的UTF-16编码区分开来,Unicode编码的设计者将0xD800-0xDFFF保留下来,并称为代理区(Surrogate).辅助平面的字符共有2的20次方个,因此表示这些字符至少需要20个二进制拉。UTF-16将这20个二进制位分成两半,前10位映射在U+D800到U+DBFF,称为高代理位(H),后10位映射在U+DC00到U+DFFF,称为低代理位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。

D800 – DBFF High Surrogates 高代理位

DC00 – DFFF Low Surrogates 低代理位

如果U>=0x10000,我们先计算U'=U-0x10000,然后将U'写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。

按照上述规则,Unicode编码0x10000-0x10FFFF的UTF-16编码有四个字节,前两个字节的高6位是110110,后两个字节的高6位是110111。可见,前两个字节的取值范围是11011000 00000000到11011000 11111111,即0xD800-0xDBFF。后两个字节取值范围是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。

因此,当我们遇到两个字节,发现它的码点在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-16 使用变长字节表示

一、对于编号在 U+0000 到 U+FFFF 的字符(常 用字符集),直接用两个字节表示。

二、编号在 U+10000 到 U+10FFFF 之间的字符,需要用四个字节表示。同样,UTF-16 也有字节的顺序问题(大小端),所以就有 UTF-16BE 表示大端,UTF-16LE 表示小端。

06 UTF-8编码

目前UTF-8是最常用的编码,它的编码复杂度处于UTF-32和UTF-16之间。它依赖一张编码范围表,可以用1-4个字节进行编码。如图6.1。

图6.1

当遇到一个字符,比如”C”,它的Unicode码值为67,则对应表中的第一行,这个时候它的UTF-8编码就是0100 0011,UTF-8编码值与ANSC II一致,所以呢UTF-8是兼容ANSC II编码的。当我们需要显示”C”这个字符的时候就需要将它转换为Unicode码(当然,这个过程不需要我们自己转换,但是需要了解是如何转换的),我们看图6.1,第一个字节,有多少个1,那就连续向后读几个字节,”C”的编码值第一位不是1,这时就只读一个字节,然后提取出X对应位上的值,此时就是”C”的Unicode码了。

我们换另外一个字,比如“马”,“马”的 Unicode 编号是:0x9A6C,整数编号是 39532,对应第三个范围(2048 – 65535),其格式为:

1110XXXX 10XXXXXX 10XXXXXX

39532 对应的二进制是:

1001 1010 0110 1100

将二进制填入进去就为:

11101001 10101001 10101100

编码过程结束,解码过程就是反着来一遍,提取出对应X位的值,就是其Unicode码值。

07 产生乱码的原因

一般来说,产生乱码的原因是编码和解码过程中使用了不同的方法。我们使用代码来演示一下:

@Test

输出结果为:ctripЯ��

上面这个例子呢,我们用GB2312来编码,用UTF-8来解码,英文部分是没有乱码的,但是中文部分乱码了,这里呢要说明的是,由于ANSC II编码是最早的编码,所以后来所有的编码都兼容ANSC II编码。

那是不是只要编解码过程用的方法不一样就会导致乱码呢?其实并不是,看下面的例子:

@Test

输结果为:ctrip携程

ctrip携程

这个例子,我们用GB2312来加密,分别用GBK和GB18030来解码,因为GB18030兼容GBK兼容GB2312,所以不会出现乱码的情况。

所以乱码出现的原因应该加上一个前提,那就是加解码过程中使用了不兼容的编码方式。

08 可逆的乱码和非可逆的乱码

某些情况下,乱码是可以恢复的,这种情况就是可逆的乱码;而有些情况下乱码,字符的码值被改变,从而永久地丢失信息,再也变不回原来的码值,这种情况就是乱码的非可逆。

可逆的情况:

@Test

最后为什么还可以输出原文呢?那是因为ISO-8859-1编码对于每一个字节都可以进行编码,从而信息不会丢失,所以最后还是可以还原。

再看一个不可逆的情况,其实只要改一改上边的编码就可以了,我们把ISO-8859-1换成GB2312

@Test

小结一下:

当用编解码使用不同方法时,尤其是在解码阶段,如果目标编码解不出编码中的字符时,会用默认的解码所用的编码的默认字符进行替换,这时会造成原来的信息被改变,从而造成不可逆的乱码。

09 用字符流复制二进制文件

在刚开始接触IO的时候,我们就被告知,字符流即可以处理文本文件,也可以处理二进制文件,而字符流只可以处理文本文件。这样的教条我还在还清晰的记得,但是万事没有绝对,下面我们将利用字符流来处理二进制文件。如果以后再有人搬教条,就请把后边的这段代码扔给他。

要实现这个功能,我们利用的编码是ISO-8859-1。前面我们有利用过它,它的特点是一个字节,共8个二进位全部都有对应的字符,因为利用它进行编解码,并不会丢失信息。上代码:

@Test

相关推荐