一文搞清字符编码,彻底解决python2.x字符编码问题

       广义上,编码指的是把一个对象对应到另外一个对象,其实际上就是一个映射,不同的编码方案就是不同的映射规则。自然地,映射的两个集合元素也可以是任何对象,即我们可以对任何事物任何对象进行编码。编码的意义往往是在于让信息更加方便的储存、传递和交流,或者对信息进行加密。前者诸如我们汉字到拼音,这实际上也是一种编码,只是这种编码在识别上并不是一种好的编码方式,因为要严格识别和区分每个编码后的对象,那么这个映射必须得是单射,但是汉语拼音存在多音字,显然不是单射;但是拼音编码的首要目的不是为了严格识别和区分,更重要的是简洁,毕竟谁也不想为了在手机上打出一个字需要输入很多个符号(手写输入法就是一个例子,需要输入很多笔画,因为拼音更加简洁,所以使用手写输入的人会越来越少)。本文中,编码仅仅是指狭义上的对字符进行编码,并且编码的目的是为了方便计算机对信息进行处理、储存和传递。理论上,可以把字符映射到任何类型的集合上,比如可以是自然数,也可以是字节码。但是由于计算机的特性,计算机最底层只认识0和1,所以把字符编码成很容易转化为二进制流的字节码是一种比较好的方式,这也是目前各种编码方案对字符编码所采用的编码机制。由于这里对字符编码目的是为了方便计算机对信息进行处理、储存和传递,那么就需要计算机可以严格识别每个字符,即要求对字符的编码方案必须是单射,也就是在一套完整的编码方案中,一个字节码只能对应一个字符。

       在进一步阐述之前,先明确一下在字符编码中的字节码和二进制流的区别。字符编码后以字节码的形式储存信息,这里的字节码是以字节为基本单元,每个字节以\x标志,并后接十六进制数,这个十六进制数就是转化为一个字节的二进制数的依据。比如,中文的'你好',经过GBK编码后的字节码为‘\xe4\xe3\xba\xc3’,这个字节码由四个字节组成,每个\x标志一个字节,后接的e4,e3,ba,c3是十六进制数,当转化为二进制流时,只需要把每个字节的十六进制数转为二进制数即可,比如第一个字节e4对应二进制数11100100,将这个字节码的四个字节全部二进制转化后就得到了二进制流。由于一个字节为8比特,所以如果某个较小的十六进制数转为二进制数后,8比特没有填满,则前置位用0补充,直至8比特。要注意的是,编码只到字节码,不需要转为二进制流,将字节码转为二进制流,那是下一步的工作。

       目前对于字符编码具有多套编码方案,本文主要围绕ASCII、ISO、GBK和Unicode编码方案进行说明,借此来进一步理解字符编码以及这些编码方案之间的联系和区别。我们知道,计算机是由英语国家发明传播开来的,因此最初对于需要编码的字符集仅仅是英语世界里的常用字符集,比如阿拉伯数字、字母和一些常用字符,ASCII编码方案的编码对象就是这些英语世界的字符。ASCII字符共有127个,因此编码这些字符只需要7个比特就足够了(7比特可编码2^7=128个字符),但是由于是字节码,一个字节有8个比特,而且,为了方便后续可能的字符添加,用8个比特一个字节来储存编码后的字符也是更加合理的,因此,ASCII编码的编码对象就是英语世界字符,并且只用一个字节来储存编码信息。

       随着计算机在全世界的广泛使用,仅仅只有英语世界的字符集已经不够用了,比如我们自己国家,就需要有汉字的编码方案,对于欧洲的一些国家,也有其自己的一些独有的字符。因此,对于中国,我们自己就在ASCII字符集的基础上添加汉字字符,于是就出现了GB2312编码方案,这是针对汉语地区的编码方案,GBK是GB2312的扩展集,兼容GB2312,又增添了一些字符,windows中文系统默认的编码方案就是GBK,GBK和GB2312都是用两个字节储存信息;对于欧洲,其也在ASCII基础上,增添新字符,搞出了ISO系列编码方案,其用一个字节储存信息。GBK和ISO都兼容ASCII,因为其都是在ASCII的基础上增添新的字符和新的字节对应关系的。但是GBK和ISO编码方案之间除了对ASCII字符外,其他字符并不兼容。

       尽管不同的地区和国家自己有了自己的编码方案,但是由于国家编码方案之间并不兼容,这样如果是跨国家进行信息传输交流,那么对于编码后的信息解码时,就需要用对方的编码方案进行解码,而不是用自己默认的编码方案解码,而且,有时候并不那么容易就知道信息是用什么方案编码的,这样的话,解码就比较麻烦和困难。为了解决这种麻烦,就有组织搜集了全世界几乎所有的字符,对这些字符在ASCII的基础上统一排序,这个排序就是Unicode编码。但是这个组织也仅仅是搜集之后进行了排序而已,具体怎么编码,怎么映射到字节码,其并没有给出,而且这种映射也是不一定的。因为每个字符编码之后的字节码仅仅只是一种信息储存方式,而不是必然和原Unicode的排序对应,即比如一个字符的排序为10000,那么编码后的字节码十六进制数不需要也是10000,而且所用的字节数也是不定的,取决于具体的编码方案,所以存在着多种Unicode编码方案。显然,如果按照从字符到字节码的角度来看,Unicode编码实际上仅仅只是一种序号而已,并不是一般意义上的编码方案。Unicode编码的意义在于其搜集了世界上几乎所有的字符,并规范了排序,给一种世界编码(万国码)的形成做了最基本的工作,所以基本所有的万国码都是基于Unicode字符集的,这些编码方案因此也都叫做Unicode编码方案。

       始终记住,Unicode编码和Unicode编码方案的区别,前者只是字符的一个序号,后者才是真正的编码方案。编码对象应该是字符,或者Unicode编码,因为Unicode编码唯一对应着字符,所以知道了Unicode编码,就可以严格识别这个编码对应的字符,从而可以很简答的实现对Unicode编码进行编码。下面开始看字符编码在python2.7和python3之中的异同。

       在python2.7中,所有的str对象都可以进一步进行decode,得到对应字符的Unicode编码,而Unicode对象也有decode方法,但是实际上是通过先将对应Unicode的字符编码为bytes,然后再解码,所以对于非ASCII字符的Unicode对象,直接对其decode,是会报错的,因为默认的ASCII编码无法编码非ASCII字符,所以对于Unicode对象其也有decode这一点,实际上并不太合理,对于str对象的decode方法,我认为这更像是一种python处理正常str对象的底层机制,举个例子,对于方法len(str),如果str是字节码,那么得到的结果将是字节的个数,但是我们希望得到的是字符的个数,由于对于非ASCII字符,就可能用不止一个字节来储存,所以str对象就定义了decode方法,得到Unicode对象,由于Unicode对象的长度和字符个数完全对应,这样len(str)方法实际上在底层实现时,返回的是str.decode()的长度。但实际上,这种底层机制应该对用户封装,因为字符串对象有decode方法真的很奇怪;但是对于ASCII字符,由于python提供了默认的ASCII编码,对此很多的字符串方法的底层机制其是实现了封装的,所以看起来符合我们对正常字符串的预期。总体来说,python2.7中的字符编码时比较混乱的,对于初学者很容易在编码的问题上栽跟头。由于python2.7的字符编码本身就难以用一种合理的逻辑贯穿的去解释,因此这里我们将只从应用层面去统一解决python2.7中的编码问题,但不会进一步解释底层逻辑,因为没有统一贯穿的逻辑。

       从应用层面来看,python2.7中,两种字符对象很重要,分别是str和Unicode。关于str对象,需要区分ASCII字符str对象和非ASCII字符的str对象。当我们直接对一个对象s以s=str这样的方式赋值时,str实际上是需要被编码成字节码后再被保存在内存中的,然后在需要使用的时候,再通过ASCII编码将其解码回来,所以如果str是ASCII字符,那么一切正常,可以认为s保存的就是str字符的引用,但是如果str是非ASCII字符,由于2.7默认的编码方案是ASCII编码,这时默认编码便无法对其转化为字节码保存,因此这时python2.7就会调用系统默认的编码方式对其编码,在中文的windows系统中是GBK编码,在linux系统中是UTF8编码。所以2.7中,对于非ASCII字符,赋值后,实际上s的引用直接指向str经过系统默认的编码后的字节码,而在需要使用该对象的时候,也无法再通过ASCII编码自动解码,所以也将直接返回字节码。这一特点导致我们如果直接在脚本中定义中文字符串,那么其实际上将会是字节码,而这就和我们对字符串的很多预期就不一致了。举个例子,我们编码问题经常会发生在文件I/O中,比如我们从一个文件中读取了有中文字符的信息,在python2.7中,直接获取的是字节码对象,这个读取过程不存在解码问题,但是在处理读取后的信息时,由于需要让脚本中定义的字符编码和文件编码一致,这样其字节码才可以对应上,从而才可以正确识别。所以要么我们是在脚本中定义字符的时候,需要每次对定义的对象指定编码方式,这样是很繁琐的,更常用的方法是,对于读取后的对象,我们会指定正确的和文件对应的编码方式字节码进行解码,根据前述,2.7中对字符以及字节码的解码得到的对象都是Unicode对象,所以只要我们在脚本中定义中文字符的时候,每次将其定义为Unicode对象即可,这就是为什么常说需要在2.7脚本中定义中文字符时,不是直接s='你好',而应该是s=u'你好'的原因。所以在2.7中,关于中文编码问题最重要的是,要知道每次我们定义字符串或读取文件后得到的对象到底是什么,是字节码还是Unicode?这样我们才可以进行正确的处理。这里总结一下,对于中文字符,脚本中直接赋值得到的将是系统默认编码方式编码后的字节码;对字符串解码后得到的对象是Unicode对象,所以文件I/O中,读取得到的字符串是Unicode对象,以及对str对象decode后得到的对象也是Unicode对象,所以我们在脚本中定义中文字符串时,应该以u'string'的方式定义,以跟Unicode对象保持一致。对于字节码对象,我们应该将其解码得到Unicode对象,然后再运用各种字符串方法进行处理,要避免直接在字节码对象上做处理。最后还要强调的一点是,对于ASCII字符,其可以自动的在Unicode对象和str对象之间转化,即对于ASCII字符,其Unicode对象和str对象是等价的,比如'a'==u'a'返回的是True,这也是我们可以通过利用u'你好'方式定义中文字符串实现也包括ASCII字符在内比对识别的基础。

       在python3中,以上说到的不合理的地方,其都解决了。主要是因为在2中,默认编码是ASCII,其对ASCII外的字符无能为力,这就导致后续的2.x版本中为了可以解决其他字符的编码问题而逐渐添加一下不符合预期的功能和逻辑,可以说2.x的编码问题主要是因为默认编码为ASCII和字符串方法的一些底层机制没有对用户封装好造成的,而这又是一个历史的遗留问题。在3中,通过将默认编码设为Unicode编码方案---UTF8编码方案,以及将字符串的底层方法进行更好的封装来解决了这些不合理的地方,从而使得3中的字符编码逻辑清晰,很合理。比如,3中,字符串对象只有str一种形式,其不再有decode方法,而对于所有字符串(即不仅仅是ASCII字符)的内存保存形式和字符串方法的调用问题,其已经在底层封装。而且对于I/O,对于非ASCII字符,也可以通过指定encoding参数,直接以文本模式读取,而不仅仅是只可以对非ASCII字符读取字节码。

       所以对于python3字符编码的总结就很简单,由于其默认编码方式已经实现了所有字符的编码,这就导致底层可以很好的封装。所以3中,字符串对象只有一种形式,我们不必再纠结其到底是bytes,还是Unicode,抑或是str,我们只要认为其就是字符本身即可,然后只要注意在编码解码时,保证编码解码方案是同一套就好。

       写在最后:以上的论述,笔者并没有构建好论述结构,所以可能看起来有些混乱。但是没关系,对于解决问题,只要着重明白了标红部分的内容就可以了,其他部分内容可以不看。最后的建议,新的python项目就选择python3来写吧,这样就不存在这些编码问题了,何况,python2都快要不受官方支持维护了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值