美文网首页
[转载]理解和解决Python2中的编码问题

[转载]理解和解决Python2中的编码问题

作者: 卡尔是正太 | 来源:发表于2018-01-29 20:50 被阅读0次

    原文地址 http://blog.csdn.net/u010223750/article/details/56684096 侵删.

    前言

    经常处理一些文本,处理英文语料没什么问题,但是到了中文这儿就让人抓狂了,稍微不注意就会窜出各种乱码错误,平时出现几个小错误试试调调也能过去,但是对于编码这个问题还是畏惧,这几天好好整理了一下python的编码问题,感谢万能的Google和万能的StackOverflow,算是解决了我当前对编码问题的一些困惑

    编码的前世今生

    从unicode说去

    在计算机中,所有的东西都是以字节形式储存的,但是字节对于我们来说是没有具体意义的,因此我们需要把二进制字节转换成人们能够理解的字符。最早的是使用ASCII码将字符对应到二进制字节上,如下图


    给定一个ASCII码65(十六进制41),我们知道它对应的是字符A,但是ASCII码仅仅能够表示有限的字符,于是ISO Latin1 或者8859-1,比ASCII码多拓展了96个字符,之后也有一些字符集进行了更多的拓展,但是世界范围内的字符是很多的,东亚的中日韩还有俄文等字符也需要对应的编码方式,这个时候Unicode的编码方式出来了,Unicode的想法是统一天下编码方式,Unicode涵盖了上数以千计的字符码点方法,而且Unicode留有还有很多未码点空间,所以Unicode能完全满足现在和之后若干年的人类编码需求。Unicode是一个以’U+’字符开头,后面跟着4位或者5位或者6位十六进制数字,下图展示了6种字符的Unicode码点方式:


    那么接下来的问题就是如何把Unicode码点映射到二进制字节上,常用的码点映射方式有UTF-16、UTF-32,UTF-8。下图是UTF-8映射的一个实例:


    可以看出,单字节的ASCII码字符仍然映射的是单字节,而且ASCII编码方式是UTF-8的子集。

    理解python2的encode和decode

    python的str和Unicode类型

    python2的编码解码方式和python3不一样,这里暂且先表述python2 中的encode和decode。
    在python2中,有两种不同的字符串数据类型,一种是 “str”对象,存储着字节,如果在字符串前使用一个’u’的前缀,表示的是这个字符的Unicode码点:

    >>> my_string="Hello World"
    >>> type(my_string)
    <type 'str'>
    >>> my_unicode=u"Hello World"
    >>> type(my_unicode)
    <type 'unicode'>
    

    其中第一个是“str”另一个是“unicode”,这是不一样的,这点需要尤其注意,这两种都可以叫string,但是有时不一样的东西 这点会在之后介绍两者的区别。

    python的encode和decode

    首先明白一件事情,之前说过Unicode将所有的字符都对应上了相应的码点,而UTF-8或者ASCII码不过是对应从Unicode到字节的映射方式,既然有映射方式,那么就有映射方向。我们把从Unicode到字节码(byte string)称之为encode,把从字节码(byte string)到Unicode码称之为decode
    看个小例子:

    >>> s='中国'
    >>> len(s)
    6
    >>> s.decode('utf-8')
    u'\u4e2d\u56fd'
    >>> s_decode=s.decode('utf-8')
    >>> len(s_decode)
    2
    >>> s_decode.encode('utf-8')
    '\xe4\xb8\xad\xe5\x9b\xbd'
    

    这个例子中,s表示的是byte string,其长度有6个字节,将该byte string decode到s_decode,显然,s_decode是一个unicode字符串,其长度变成了2,也就是两个Unicode码点,然后我将s_decode这个Unicode string再次encode到byte string,可以看到这个字节的十六进制编码方式。

    python2 的encode和decode errors解析

    python2经常出现的错误就是,decode和encode的error问题,来具体看一下。
    接着上面的例子:

    >>> s_decode.encode('ascii')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1:
    

    将s_decode这个Unicode码点使用ascii码的编码方式映射到字节码的时候,出现了问题,问题报错也很显然,ascii码不能讲这个Unicode码点映射到字节上,造成这种原因很简单,ascii码并没有对汉字进行编码,所有这个肯定是有问题的

    接着看下面的例子:

    >>> s.decode('ascii')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: or
    

    将s 使用ascii编码方式,将其代表的字节码decode到Unicode码,出现了问题,问题也很显然,ASCII码不包含汉字的映射关系

    python2 的encode和decode errors处理

    既然有错误,自然有解决的办法,看下面的例子

    >>> s_decode.encode('ascii','replace')
    '??'
    

    这个例子中,是上一节的encode错误,我们在其后面的参数加上了’replace’这个选项,然后就没有报错,取而代之的是问号

    >>> s_decode.encode('ascii','ignore')
    ''
    

    这里例子中,参数改成了’ignore’,这个的意思是忽略编码错误,所以结果是个空字符
    这些处理方法同样适用于decode 的错误解决

    >>> s.decode('ascii','replace')
    u'\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd'
    >>> s.decode('ascii','ignore')
    u''
    

    当然这并不是最好的解决方法,这些只是防止一些不能编码的问题出现造成程序的exception,所以在编写程序的时候,特别是使用python2处理中文的时候,还是得小心翼翼的写,说多了都是泪。

    python2的潜在的编码转换

    Python2默认的编码解码方式是ascii码这点需要牢记于心,接下来看几个潜在的编码转换方式:

    >>> ss ='china'
    >>> type(ss)
    <type 'str'>
    >>> type(s_decode)
    <type 'unicode'>
    >>> s_decode + ss
    u'\u4e2d\u56fdchina'
    

    上面这个例子中,s_decode是Unicode字符,ss是字节字符(byte string),两者相加最后的结果仍然是Unicode字符,这个例子中,处理过程其实是这样的:首先将ss按照ascii码decode到unicode字符,然后再将两个Unicode字符拼起来。之所以是按照ascii方式decode,就是因为python2默认的编码方式是ascii。这就造成了一些问题,我们看下面例子:

    >>> s_decode+s
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: or
    

    很显然,在先把s这个byte_string转换成Unicode的过程中,系统使用了ascii编码来decode,然而s这个字节码并不能按照ascii编码来decode,原因也是因为s表示的是汉字的字节码。

    有没有好的解决方案呢,当然有:

    >>> import sys
    >>> reload(sys)
    <module 'sys' (built-in)>
    >>> sys.setdefaultencoding('utf-8')
    >>> s_decode+s
    u'\u4e2d\u56fd\u4e2d\u56fd'
    

    这个代码将Python的编码默认编码方式由ascii码换成了utf-8,编码,因此s_decode+s这个就不像上面一样报错了。

    另外说一点,print可以直接将unicode和byte string打印出来,这点我还有点疑惑

    >>> import sys
    >>> reload(sys)
    <module 'sys' (built-in)>
    >>> sys.setdefaultencoding('ascii')
    >>> print s
    中国
    >>> print s_decode.encode('utf-8')
    中国
    >>> print '\xe4\xb8\xad\xe5\x9b\xbd'
    中国
    

    理解python2的文件读写编码问题

    上面说了这么多,基本上都是在命令行里进行的,但是平时我们写的最多的还是文件的读写,而文件的读写中文的乱码问题也是让python2变得很让人沮丧的一件事,好好写程序的心情全让这些乱七八糟玩意儿破坏了,有时候也想,如果全世界都是用中文或者英文一种语言多好,没有该死的编码问题。
    吐槽就到这儿,还得看看具体的问题:

    Python程序开头写#coding=utf-8的作用

    Python文件编译最终还是要转换成字节码,Python程序开头写#coding=utf-8的作用其实就是把这个Python程序文件按照utf-8编码的方式映射到字节码,如果不加这个开头,程序里面的中文会按照Python默认的ascii码方式encode,这个肯定是要报错的,大家都知道,如果程序里写了中文但是没有加这个开头,那么pycharm会在一开始就报错,也是这个道理。加了这个开头之后,程序里面的字符都将会使用utf-8编码的方式被映射到字节码,也就是上一个大节里面的byte string,值得注意的是,程序中的汉字将会以utf-8的形式编码成为字节码,因此如果需要将其decode到Unicode字符,也是需要使用utf-8方式decode。
    还有一个问题是,写了这个文件之后,Python的系统默认编码方式有没有变?
    当然没有,在程序中,它将还是会使用ascii码来encode和decode,举个例子:
    首先创建个test.py

    vi test.py
    

    该文件内容为:

    #coding=utf-8
    
    s='中国'
    s_decode=s.decode('utf-8')
    ps=s_decode+s
    print ps                                                                                                        
    

    我们执行该文件:

     python test.py 
    Traceback (most recent call last):
      File "test.py", line 5, in <module>
        ps=s_decode+s
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
    

    会发现,仍然出现了这个问题,所以说这个开头并没有改变Python的系统编码方式,如果想改变,还是按照之前说的,加上sys.setdefaultencoding()这个方法。
    将test.py的内容改为:

    #coding=utf-8
    s='中国'
    s_decode=s.decode('utf-8')
    ps=s_decode+s.decode('utf-8')
    print ps              
    

    执行结果为:

    python test.py 
    中国中国
    
    

    由此可见Python默认仍然是使用ascii码方式来编码解码的

    Python2读文件的编码问题

    先来看Python2的读文件的问题,英文就不说了,怎么弄都是对的,主要说说中文文件的读写。
    创建一个文件test.txt

    vi test.txt
    

    内容为:

    我爱中国
    

    我们使用最简单的读文件方式:

    >>> f=open('test.txt','rb')
    >>> s=f.read()
    >>> s
    '\xe6\x88\x91\xe7\x88\xb1\xe4\xb8\xad\xe5\x9b\xbd\n'
    >>> s.decode('utf-8')
    u'\u6211\u7231\u4e2d\u56fd\n'
    >>> print s.decode('utf-8')
    我爱中国
    >>> print s
    我爱中国
    

    显然f.read()读出来的是字节码,然后和上文一样,这个结果都能print.
    下面换一种方法读文件:

    >>> import codecs
    >>> f=codecs.open('test.txt','rb','utf-8')
    >>> f.read()
    u'\u6211\u7231\u4e2d\u56fd\n'
    >>> f=codecs.open('test.txt','rb','utf-8')
    >>> s=f.read()
    >>> print s
    我爱中国
    

    上面这段代码使用了codesc这个包,并给出了编码方式为utf-8,注意python2的open()方法里面没有encoding这个参数,python3才有,千万不要搞混了。可以看出,这次f.read()读出的是Unicode码。这两种不同的形式可以会在后面有不用的结果。

    首先看下使用open遇到的问题
    看下面的例子:
    创建一个test.py文件:

    #coding=utf-8
    f=open('test.txt','rb')
    s=f.read()
    print s+u'hello'
    

    执行得到:

     python test.py 
    Traceback (most recent call last):
      File "test.py", line 4, in <module>
        print s+u'hello'
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 0: ordinal not in range(128)
    

    错误原因当然很明显。还是ascii编码问题。

    然后看使用codecs遇到的问题:
    修改test.py为:

    #coding=utf-8
    import codecs
    f=codecs.open('test.txt','rb','utf-8')
    s=f.read()
    ss=s.split('国')
    print ss
    

    执行test.py文件:

     python test.py 
    Traceback (most recent call last):
      File "test.py", line 5, in <module>
        ss=s.split('国')
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)
    

    遇到的问题来了,原因是因为split()方法中的这个’国’是从Unicode码经过encode(‘utf-8’)得来的字节码,而s读的是Unicode码,自然这个会报错,修改test.py为:

    #coding=utf-8
    import codecs
    f=codecs.open('test.txt','rb','utf-8')
    s=f.read()
    ss=s.split('国'.decode('utf-8'))
    print ss
    

    执行结果为:

    python test.py 
    [u'\u6211\u7231\u4e2d', u'\n']
    

    这次执行成功!

    由此可见在文件读写过程中,时刻要保持高度警惕,清醒的知道什么是byte string 什么又是Unicode类型,这点在编写程序的时候要牢记在心。

    Python2写文件的编码问题

    和上面对应的,使用f=open()这个方法打开待写文件时,使用f.write()或者f.writelines()切记把文本encode转换成字节形式,使用codecs.open()这个方法打开文件,codesc.write()需要写入的是unicode字符。

    其他

    今天碰到个问题,我觉得可以写下,就是Python2将list中的unicode转换成中文显示,方法如下,感谢作者muge:的解答

    list = [{'channel_id': -3, 'name': u'\u7ea2\u5fc3\u5146\u8d6b'}, {u'seq_id': 0, u'name_en': u'Personal Radio', u'channel_id': 0, u'abbr_en': u'My', u'name': u'\u79c1\u4eba\u5146\u8d6b'}]
    
    s = str(self.channel_list).replace('u\'','\'')
    print s.decode("unicode-escape")
    
    [{'channel_id': -3, 'name': '红心兆赫'}, {'seq_id': 0, 'name_en': 'Personal Radio', 'channel_id': 0, 'abbr_en': 'My', 'name': '私人兆赫'}, ]
    
    

    结语

    今天系统梳理了一下python2那坑爹的编码问题,吃了多次亏也算是久病成医了,其实关键就是一点铭记于心,Python2的字符串有两种类型:byte string 和unicode string,这才是解决python2的编码问题的关键所在。于北京,25/2/2017

    相关文章

      网友评论

          本文标题:[转载]理解和解决Python2中的编码问题

          本文链接:https://www.haomeiwen.com/subject/rfyfzxtx.html