最近业务中须要用 Python 写一些脚本。尽管脚本的交互只是命令行 + 日志输出,可是为了让界面友好些,我仍是决定用中文输出日志信息。html
很快,我就遇到了异常:python
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)
为了解决问题,我花时间去研究了一下 Python 的字符编码处理。网上也有很多文章讲 Python 的字符编码,可是我看过一遍,以为本身能够讲得更明白些。linux
下面先复述一下 Python 字符串的基础,熟悉此内容的能够跳过。数据库
对应 C/C++ 的 char 和 wchar_t, Python 也有两种字符串类型,str 与 unicode:json
example1.py socket
# -*- 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-8spa
若是你和我同样是使用 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 字符集表示 # '\xb9\xd8\xb9\xd8\xf6\xc2\xf0\xaf' # 直接 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'\u934f\u51b2\u53e7\u95c6\u5ea8\u7b2d'
这就遇到了我在本文开头贴出的异常: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'\u5173\u5173\u96ce\u9e20\u5173\u5173\u96ce\u9e20' # 一样没问题 "中文:%s" % u # u'\u4e2d\u6587\uff1a\u5173\u5173\u96ce\u9e20' # 仍是没问题 u"中文:%s" % s # u'\u4e2d\u6587\uff1a\u5173\u5173\u96ce\u9e20'
能够看到,问题魔术般的解决了。可是注意! 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 里的技巧。
>>> 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) >>>
>>> 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)
>>> '%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) >>>
#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)
>>> 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 对象的各类操做,len
、find
等。
string.decode('utf-8') --> unicode unicode.encode('utf-8') --> string
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/