zoukankan      html  css  js  c++  java
  • python2.7 的中文编码处理,解决UnicodeEncodeError: 'ascii' codec can't encode character 问题

    最近业务中需要用 Python 写一些脚本。尽管脚本的交互只是命令行 + 日志输出,但是为了让界面友好些,我还是决定用中文输出日志信息。

    很快,我就遇到了异常:

    UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)  

    为了解决问题,我花时间去研究了一下 Python 的字符编码处理。网上也有不少文章讲 Python 的字符编码,但是我看过一遍,觉得自己可以讲得更明白些。

    下面先复述一下 Python 字符串的基础,熟悉此内容的可以跳过。

     1.引入

    对应 C/C++ 的 char 和 wchar_t, Python 也有两种字符串类型,str 与 unicode:

    example1.py  

    # -*- coding: utf-8 -*-  
    # file: example1.py  
    import string  
      
    # 这个是 str 的字符串  
    s = '关关雎鸠'  
      
    # 这个是 unicode 的字符串  
    u = u'关关雎鸠'  
      
    print isinstance(s, str)      # True  
    print isinstance(u, unicode)  # True  
      
    print s.__class__   # <type 'str'>  
    print u.__class__   # <type 'unicode'>  

    前面的申明:# -*- coding: utf-8 -*- 表明,上面的 Python 代码由 utf-8 编码。

    为了保证输出不会在 linux 终端上显示乱码,需要设置好 linux 的环境变量:export LANG=en_US.UTF-8

    如果你和我一样是使用 SecureCRT,请设置 Session Options/Terminal/Appearance/Character Encoding 为 UTF-8 ,保证能够正确的解码 linux 终端的输出。

    两个 Python 字符串类型间可以用 encode / decode 方法转换:

    # 从 str 转换成 unicode  
    print s.decode('utf-8')   # 关关雎鸠  
      
    # 从 unicode 转换成 str  
    print u.encode('utf-8')   # 关关雎鸠  
    

      

    为什么从 unicode 转 str 是 encode,而反过来叫 decode? 

    因为 Python 认为 16 位的 unicode 才是字符的唯一内码,而大家常用的字符集如 gb2312,gb18030/gbk,utf-8,以及 ascii 都是字符的二进制(字节)编码形式。把字符从 unicode 转换成二进制编码,当然是要 encode。

    反过来,在 Python 中出现的 str 都是用字符集编码的 ansi 字符串。Python 本身并不知道 str 的编码,需要由开发者指定正确的字符集 decode。

    (补充一句,其实 Python 是可以知道 str 编码的。因为我们在代码前面申明了 # -*- coding: utf-8 -*-,这表明代码中的 str 都是用 utf-8 编码的,我不知道 Python 为什么不这样做。)

    如果用错误的字符集来 encode/decode 会怎样?

    # 用 ascii 编码含中文的 unicode 字符串  
    u.encode('ascii')  # 错误,因为中文无法用 ascii 字符集编码  
                       # UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)  
      
    # 用 gbk 编码含中文的 unicode 字符串  
    u.encode('gbk')  # 正确,因为 '关关雎鸠' 可以用中文 gbk 字符集表示  
                     # 'xb9xd8xb9xd8xf6xc2xf0xaf'  
                     # 直接 print 上面的 str 会显示乱码,修改环境变量为 zh_CN.GBK 可以看到结果是对的  
      
    # 用 ascii 解码 utf-8 字符串  
    s.decode('ascii')  # 错误,中文 utf-8 字符无法用 ascii 解码  
                       # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  
      
    # 用 gbk 解码 utf-8 字符串  
    s.decode('gbk')  # 不出错,但是用 gbk 解码 utf-8 字符流的结果,显然只是乱码  
                     # u'u934fu51b2u53e7u95c6u5ea8u7b2d'  

    这就遇到了我在本文开头贴出的异常:UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)

    现在我们知道了这是个字符串编码异常。接下来, 为什么 Python 这么容易出现字符串编/解码异常? 

    这要提到处理 Python 编码时容易遇到的两个陷阱。第一个是有关字符串连接的:

    example2.py  

    # -*- coding: utf-8 -*-  
    # file: example2.py  
      
    # 这个是 str 的字符串  
    s = '关关雎鸠'  
      
    # 这个是 unicode 的字符串  
    u = u'关关雎鸠'  
      
    s + u  # 失败,UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

    简单的字符串连接也会出现解码错误?

    陷阱一:在进行同时包含 str 与 unicode 的运算时,Python 一律都把 str 转换成 unicode 再运算,当然,运算结果也都是 unicode。

    由于 Python 事先并不知道 str 的编码,它只能使用 sys.getdefaultencoding() 编码去 decode。在我的印象里,sys.getdefaultencoding() 的值总是 'ascii' ——显然,如果需要转换的 str 有中文,一定会出现错误。

    除了字符串连接,% 运算的结果也是一样的:

    # 正确,所有的字符串都是 str, 不需要 decode  
    "中文:%s" % s   # 中文:关关雎鸠  
      
    # 失败,相当于运行:"中文:%s".decode('ascii') % u  
    "中文:%s" % u  # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  
      
    # 正确,所有字符串都是 unicode, 不需要 decode  
    u"中文:%s" % u   # 中文:关关雎鸠  
      
    # 失败,相当于运行:u"中文:%s" % s.decode('ascii')  
    u"中文:%s" % s  # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

    我不理解为什么 sys.getdefaultencoding() 与环境变量 $LANG 全无关系。如果 Python 用 $LANG 设置 sys.getdefaultencoding() 的值,那么至少开发者遇到 UnicodeDecodeError 的几率会降低 50%。

    另外,就像前面说的,我也怀疑为什么 Python 在这里不参考 # -*- coding: utf-8 -*- ,因为 Python 在运行前总是会检查你的代码,这保证了代码里定义的 str 一定是 utf-8 。

    对于这个问题,我的唯一建议是在代码里的中文字符串前写上 u。另外,在 Python 3 已经取消了 str,让所有的字符串都是 unicode ——这也许是个正确的决定。

    其实,sys.getdefaultencoding() 的值是可以用“后门”方式修改的,我不是特别推荐这个解决方案,但是还是贴一下,因为后面有用:

     example3.py  

    # -*- coding: utf-8 -*-  
    # file: example3.py  
    import sys  
      
    # 这个是 str 的字符串  
    s = '关关雎鸠'  
      
    # 这个是 unicode 的字符串  
    u = u'关关雎鸠'  
      
    # 使得 sys.getdefaultencoding() 的值为 'utf-8'  
    reload(sys)                      # reload 才能调用 setdefaultencoding 方法  
    sys.setdefaultencoding('utf-8')  # 设置 'utf-8'  
      
    # 没问题  
    s + u  # u'u5173u5173u96ceu9e20u5173u5173u96ceu9e20'  
      
    # 同样没问题  
    "中文:%s" % u   # u'u4e2du6587uff1au5173u5173u96ceu9e20'  
      
    # 还是没问题  
    u"中文:%s" % s  # u'u4e2du6587uff1au5173u5173u96ceu9e20'  

    可以看到,问题魔术般的解决了。但是注意! sys.setdefaultencoding() 的效果是全局的,如果你的代码由几个不同编码的 Python 文件组成,用这种方法只是按下了葫芦浮起了瓢,让问题变得复杂。

    另一个陷阱是有关标准输出的。

    刚刚怎么来着?我一直说要设置正确的 linux $LANG 环境变量。那么,设置错误的 $LANG,比如 zh_CN.GBK 会怎样?(避免终端的影响,请把 SecureCRT 也设置成相同的字符集。)

    显然会是乱码,但是不是所有输出都是乱码。

     example4.py  

    # -*- coding: utf-8 -*-  
    # file: example4.py  
    import string  
      
    # 这个是 str 的字符串  
    s = '关关雎鸠'  
      
    # 这个是 unicode 的字符串  
    u = u'关关雎鸠'  
      
    # 输出 str 字符串, 显示是乱码  
    print s   # 鍏冲叧闆庨笭  
      
    # 输出 unicode 字符串,显示正确  
    print u  # 关关雎鸠  

    为什么是 unicode 而不是 str 的字符显示是正确的? 首先我们需要了解 print。与所有语言一样,这个 Python 命令实际上是把字符打印到标准输出流 —— sys.stdout。而 Python 在这里变了个魔术,它会按照 sys.stdout.encoding 来给 unicode 编码,而把 str 直接输出,扔给操作系统去解决。

    这也是为什么要设置 linux $LANG 环境变量与 SecureCRT 一致,否则这些字符会被 SecureCRT 再转换一次,才会交给桌面的 Windows 系统用编码 CP936 或者说 GBK 来显示。

    通常情况,sys.stdout.encoding 的值与 linux $LANG 环境变量保持一致:

    example5.py  

    # -*- coding: utf-8 -*-  
    # file: example5.py  
    import sys  
      
    # 检查标准输出流的编码  
    print sys.stdout.encoding  # 设置 $LANG = zh_CN.GBK,  输出 GBK  
                               # 设置 $LANG = en_US.UTF-8,输出 UTF-8  
      
    # 这个是 unicode 的字符串  
    u = u'关关雎鸠'  
      
    # 输出 unicode 字符串,显示正确  
    print u  # 关关雎鸠  
    

      

    但是,这里有 陷阱二:一旦你的 Python 代码是用管道 / 子进程方式运行,sys.stdout.encoding 就会失效,让你重新遇到 UnicodeEncodeError。

    比如,用管道方式运行上面的 example4.py 代码:

    python -u example5.py | more  
      
    UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)  
    None  

    可以看到,第一:sys.stdout.encoding 的值变成了 None;第二:Python 在 print 时会尝试用 ascii 去编码 unicode.

    由于 ascii 字符集不能用来表示中文字符,这里当然会编码失败。

    怎么解决这个问题? 不知道别人是怎么搞定的,总之我用了一个丑陋的办法:

    example6.py  

    # -*- coding: utf-8 -*-  
    # file: example6.py  
    import os  
    import sys  
    import codecs  
      
    # 无论如何,请用 linux 系统的当前字符集输出:  
    if sys.stdout.encoding is None:  
        enc = os.environ['LANG'].split('.')[1]  
        sys.stdout = codecs.getwriter(enc)(sys.stdout)  # 替换 sys.stdout  
      
    # 这个是 unicode 的字符串  
    u = u'关关雎鸠'  
      
    # 输出 unicode 字符串,显示正确  
    print u  # 关关雎鸠  
    

      这个方法仍然有个副作用:直接输出中文 str 会失败,因为 codecs 模块的 writer 与 sys.stdout 的行为相反,它会把所有的 str 用 sys.getdefaultencoding() 的字符集转换成 unicode 输出。

    # 这个是 str 的字符串  
    s = '关关雎鸠'  
      
    # 输出 str 字符串, 异常  
    print s   # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

    显然,sys.getdefaultencoding() 的值是 'ascii', 编码失败。

    解决办法就像 example3.py 里说的,你要么给 str 加上 u 申明成 unicode,要么通过“后门”去修改 sys.getdefaultencoding():

    # 使得 sys.getdefaultencoding() 的值为 'utf-8'  
    reload(sys)                      # reload 才能调用 setdefaultencoding 方法  
    sys.setdefaultencoding('utf-8')  # 设置 'utf-8'  
      
    # 这个是 str 的字符串  
    s = '关关雎鸠'  
      
    # 输出 str 字符串, OK  
    print s   # 关关雎鸠  

    总而言之,在 Python 2 下进行中文输入输出是个危机四伏的事,特别是在你的代码里混合使用 str 与 unicode 时。

    有些模块,例如 json,会直接返回 unicode 类型的字符串,让你的 % 运算需要进行字符解码而失败。而有些会直接返回 str, 你需要知道它们的真实编码,特别是在 print 的时候。

    为了避免一些陷阱,上文中说过,最好的办法就是在 Python 代码里永远使用 u 定义中文字符串。另外,如果你的代码需要用管道 / 子进程方式运行,则需要用到 example6.py 里的技巧。

     2.python 自动解编码机制导致报错

    1.stirng 和 unicode 对象合并

    >>> s + u''
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
    >>>

    2.列表合并

    >>> as_list = [u, s]
    >>> ''.join(as_list)
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

    3.格式化字符串

    >>> '%s-%s'%(s,u)
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
    >>>

    4.打印 unicode 对象

    #test.py
    # -*- coding: utf-8 -*-
    u = u'中文'
    print u
    #outpt
    Traceback (most recent call last):
      File "/Users/zhyq0826/workspace/zhyq0826/blog-code/p20161030_python_encoding/uni.py", line 3, in <module>
        print u
    UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

    5.输出到文件

    >>> f = open('text.txt','w')
    >>> f.write(u)
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
    UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
    >>>

    1,2,3 的例子中,python 自动用 ascii 把 string 解码为 unicode 对象然后再进行相应操作,所以都是 decode 错误, 4 和 5 python 自动用 ascii 把 unicode 对象编码为字符串然后输出,所以都是 encode 错误。

    只要涉及到 unicode 对象和 string 的转换以及 unicode 对象输出、输入的地方可能都会触发 python 自动进行解码/编码,比如写入数据库、写入到文件、读取 socket 等等。

    到此,这两个异常产生的真正原因了基本已经清楚了: unicode 对象需要编码为相应的 string(字符串)才可以存储、传输、打印,字符串需要解码为对应的 unicode 对象才能完成 unicode 对象的各种操作,lenfind 等。

    string.decode('utf-8') --> unicode
    unicode.encode('utf-8') --> string

    3.如何避免这些的错误

    1.理解编码或解码的转换方向

    无论何时发生编码错误,首先要理解编码方向,然后再针对性解决。

    2.设置默认编码为 utf-8

    在文件头写入

    # -*- coding: utf-8 -*-

    python 会查找: coding: name or coding=name,并设置文件编码格式为 name,此方式是告诉 python 默认编码不再是 ascii ,而是要使用声明的编码格式。

    3.输入对象尽早解码为 unicode,输出对象尽早编码为字节流

    无论何时有字节流输入,都需要尽早解码为 unicode 对象。任何时候想要把 unicode 对象写入到文件、数据库、socket 等外界程序,都需要进行编码。

    4.使用 codecs 模块来处理输入输出 unicode 对象

    codecs 模块可以自动的完成解编码的工作。

    >>> import codecs
    >>> f = codecs.open('text.txt', 'w', 'utf-8')
    >>> f.write(u)
    >>> f.close()

    参考:

    http://in355hz.iteye.com/blog/1860787

    http://sanyuesha.com/2016/11/06/python-string-unicode/

  • 相关阅读:
    Educational Codeforces Round 67 D. Subarray Sorting
    2019 Multi-University Training Contest 5
    Educational Codeforces Round 69 (Rated for Div. 2) E. Culture Code
    Educational Codeforces Round 69 D. Yet Another Subarray Problem
    2019牛客暑期多校训练第六场
    Educational Codeforces Round 68 E. Count The Rectangles
    2019牛客多校第五场题解
    2019 Multi-University Training Contest 3
    2019 Multi-University Training Contest 2
    [模板] 三维偏序
  • 原文地址:https://www.cnblogs.com/huchong/p/9037142.html
Copyright © 2011-2022 走看看