原文链接:http://www.nicemxp.com/articles/14
背景:写python的时候,一旦涉及到中文字符串,总会遇到各种编码乱码问题,虽然总是可以通过Goggle,百度等解决,但是知其然而不知其所以然的感觉不太好,因此系统的学习,收集整理了关于字符串编码解码等知识。包括字符集,字符编码的概念。ASCII,GB2312,GBK字符集和ASCII,EUC-CN,CP936编码。UNICODE字符集和UTF-8等uft系列编码的关系。
一、字符存储到计算机的过程
一般写程序的时候比如写C/C++等,涉及到字符处理时我们有时会去查ascii码表,找到字符在ascii表中对应的值就可以算出字符在计算机存储的二进制值,这里就涉及到字符到计算机存储的二进制值的映射,这里面实际有两个步骤,第一步是字符到值(一般称为代码点或code point)的映射(通过ascii码表得到),第二步是code point到二进制存储的映射(就是所谓的编码),只不过ascii码表共128个字符,一个字节就可以存储,不需要复杂的编码,所以字符对应的code point直接转换成二进制就是这个字符在计算机中存储的二进制值了。这就是一个普通的ascii字符在计算机存储过程做的事情。
其实中文字符的存储也是一样的过程,ascii字符是从ascii码表中找到字符对应的code point,再通过ascii编码存储到计算机中,而中文字符是从包含中文的字符集中找到自己的code point,在通过特定的编码方式存储到计算机中。所以最上面说的GB2312,GBK,UNICODE都是包含中文的字符集,可以在其中找到中文字符的code point。但是中文字符太多了,一个字节是存不下的,需要两个或更多的字节,而且不同的字符集,同一个中文字符的code point不一样,所以就有了各种不同的编码方式,将code point编码成多个二进制字节从而存储到计算机中。
二、GB2312字符集和EUC-CN编码(GB2312编码)
GB2312是中国发布的一套标准字符集,用区码和位码表示,因此也称为区位码。整个字符集分成94个区,每个区有94个位,因此GB2312字符集共可以表示94*94个字符。
01-09区为特殊符号
16-55区为一级汉字,按拼音排序
56-87区为二级汉字,按部首/笔画排序
10-15区及88-94区未编码作拓展
举例来说,“啊”字是GB2312中的第一个汉字,它的区位码就是1601,即16区的第一位。
其实这里的区位码1601就是上面所说的code point,那么把这个code point的值以何种方式存储到计算机中,就是编码的事儿了。
GB2312字符集通常采用EUC-CN编码,而我们平时说的GB2312编码实际上就是EUC-CN编码,EUC-CN编码把每个汉字用两个字节表示,第一个字节称为高字节也称区字节,第二个字节称为低字节也称位字节,高字节使用了0xA1-0XF7(把01-87区的区号加上0xA0),低字节使用了0xA1-0xFE(把01-94加上0xA0)。因为一级汉字从16区开始,所以汉字的高字节的范围就是0xB0-0XF7,低字节的范围是0xA1-0XFE,所以一共可以表示72*94=6788个汉字。
举例,汉字“啊”的字符码是1601,他的区码是16(0x10),位码01(0x01),那么他的高字节(区字节) = 0x10 + 0xA0 = 0xB0,低字节(位字节) = 0x01 + 0xA0 = 0xA1,所以汉字“啊”在计算机中的存储就是0xB0 0xA1两个字节表示。
这里要说下EUC-CN编码是兼容ASCII编码的,所以刚刚看到的不管是高字节还是低字节的最高位都是1,当遇到ascii字符时,会将其编成一个字节来存储,最高位为0,其余7位来表示ASCII编码
三、GBK字符集和CP936编码(GBK编码)
GB2312只能表示6788个汉字,但是据我们的了解中文的汉字不止这些,这就会导致有很多人的人名,偏僻字在GB2312字符集中是找不到的,因此GBK对GB2312做了拓展,其编码范围从GB2312的0xB0A1-0xF7FE拓展到了0x8140-0xFEFE(剔除xx7F),共23940个码位。其编码使用最广的是CP936编码,也就是我们一般说的GBK编码,是完全兼容GB2312编码的。
四、UNICODE字符集和UTF-*系列编码
我们平时总会听到UNICODE编码,UTF-8编码,也总会有人问UNICODE和UTF-8是什么关系,实际UNICODE是一套字符集,UTF-8是这套字符集的一种编码方式。
UNICDOE用4个字节表示,写法一般为U+XXXX,XXXX为16进制数字,比如U+0041便是A,汉字“字”对应的数字是23383(十进制),所以UNICODE表示为U+5B57。有了字符的code point,那么如何将这个字符的code point存储到计算机中呢,这样就有了多种编码方式,如UTF-*系列,UTF-8,UTF-16,UTF-32编码。
UTF-8编码:
UTF-8编码以字节为单位对UNICODE进行编码,编码规则:
1、对于单字节的符号,字节的第一位设为0,后面的7位是这个符号的code point,所以英文字母,UTF-8的编码和ASCII码是相同的。
2、对于字节的符号,第一个字节的前n位都设为1,第n+1位设为0,后面的字节的前两位都为10,剩下的位数用来填充code point。
以上二图是UTF-8编码,可以看出UTF-8是一种变长编码。
UTF-16编码:
UTF-16也是一种变长编码,以16位无符号整数为单位,最少需要采用两个字节表示code point。编码规则:
我们把字符在UNICODE字符集中映射得到的code point记作U。
1、如果U < 0x10000,U的UTF-16编码就是U对应的16位无符号整数,因此,在UNICODE字符集中小于65535的code point,在UTF-16中采用2字节编码。
2、如果U ≥ 0x10000,我们先计算U' = U - 0x10000,然后将U'写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16二进制编码就是:110110yyyyyyyyyy,110111xxxxxxxxxx。这里为什么U'可以协程20个二进制位呢?因为UNICODE的最大码位是0x10FFFF,减去0x10000,U'的最大值是0xFFFFF,所以肯定可以用二十个二进制位表示。
UTF-32编码:
UTF-32编码就是直接将UNICODE的code point转化成二进制存到计算机中,占用4个字节,也是UTF-*编码家族中唯一的定长编码,但是每个字符都占用4个字节,应该不会有人用吧。
五、最后
写了一段测试代码用来演示下UNICODE,UTF-8,UTF-16编码,代码:
# -*- coding=utf-8 -*-
char = u'啊'
res_utf16 = char.encode('utf-16')
res_utf8 = char.encode('utf-8')
res_gbk = char.encode('gbk')
res_gb2312 = char.encode('gb2312')
print hex(ord(char)),
print '<---unicode code point'
print tuple(hex(ord(res)) for res in res_utf16),
print '<---utf-16 encode hex'
print tuple(bin(ord(res)) for res in res_utf16),
print '<---utf-16 encode bin'
print tuple(hex(ord(res)) for res in res_utf8),
print '<---utf-8 encode hex'
print tuple(bin(ord(res)) for res in res_utf8),
print '<---utf-8 encode bin'
print tuple(hex(ord(res)) for res in res_gbk),
print '<---gbk encode hex'
print tuple(bin(ord(res)) for res in res_gbk),
print '<---gbk encode bin'
print tuple(hex(ord(res)) for res in res_gb2312),
print '<---gb2312 encode hex'
print tuple(bin(ord(res)) for res in res_gb2312),
print '<---gb2312 encode bin'
输出:
根据输出结果我们可以看出,‘啊’字在UNICODE字符集中的code point表示为0x554A。
在UTF-16编码后打印出的结果是0xFF 0xFE 0x4A 0x55,去掉前两个字节0xFF 0xFE(因为UTF-16是以两个字节为编码单位,涉及到字节序的问题,前面这两个字节表示我的电脑上是小端字节序,计算机的低位地址存储数字的低位),后两个字节为0x4A 0x55,因为小端字节序的原因,实际的编码结果应为0x55 0x4A。
我们再来看下UTF-8编码后的结果是 0xE5 0x95 0x8A,二进制表示为:11100101 10010101 10001010 按照UTF-8的编码规则我们可以从这3个字节提取出code point,第一个字节去掉1110,第二个,三个字节去掉10,所以剩下的就是0101 010101 001010,将这16位二进制转化成16进制为:0x55 0x4A。
而gbk和gb2312的编码结果跟我们上面说的是一样的,这里就不一一说明了。
到此,应该对我们熟悉的字符串有一个新的理解了。