社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

也谈 Python 的中文编码问题

编程派 • 7 年前 • 742 次点击  

作者:in355hz

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

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

很快,我就遇到了异常:

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

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

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

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

  1. # -*- coding: utf-8 -*-  

  2. # file: example1.py  

  3. import string  

  4. # 这个是 str 的字符串  

  5. s = '关关雎鸠'  

  6. # 这个是 unicode 的字符串  

  7. u = u'关关雎鸠'  

  8. print isinstance(s, str)      # True  

  9. print isinstance(u, unicode)  # True  

  10. print s.__class__   #  

  11. print u.__class__   #  

前面的申明:# -- 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 方法转换:

  1. # 从 str 转换成 unicode  

  2. print s.decode('utf-8')   # 关关雎鸠  

  3. # 从 unicode 转换成 str  

  4. 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 会怎样?

  1. # 用 ascii 编码含中文的 unicode 字符串  

  2. u.encode('ascii')  # 错误,因为中文无法用 ascii 字符集编码  

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

  4. # 用 gbk 编码含中文的 unicode 字符串  

  5. u.encode('gbk')  # 正确,因为 '关关雎鸠' 可以用中文 gbk 字符集表示  

  6.                 # '\xb9\xd8\xb9\xd8\xf6\xc2\xf0\xaf'  

  7.                 # 直接 print 上面的 str 会显示乱码,修改环境变量为 zh_CN.GBK 可以看到结果是对的  

  8. # 用 ascii 解码 utf-8 字符串  

  9. s.decode('ascii')  # 错误,中文 utf-8 字符无法用 ascii 解码  

  10.                   # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

  11. # 用 gbk 解码 utf-8 字符串  

  12. s.decode('gbk')  # 不出错,但是用 gbk 解码 utf-8 字符流的结果,显然只是乱码  

  13.                 # 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 编码时容易遇到的两个陷阱。第一个是有关字符串连接的:

  1. # -*- coding: utf-8 -*-  

  2. # file: example2.py  

  3. # 这个是 str 的字符串  

  4. s = '关关雎鸠'  

  5. # 这个是 unicode 的字符串  

  6. u = u'关关雎鸠'  

  7. 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 有中文,一定会出现错误。

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

  1. # 正确,所有的字符串都是 str, 不需要 decode  

  2. "中文:%s" % s   # 中文:关关雎鸠  

  3. # 失败,相当于运行:"中文:%s".decode('ascii') % u  

  4. "中文:%s" % u  # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

  5. # 正确,所有字符串都是 unicode, 不需要 decode  

  6. u"中文:%s" % u   # 中文:关关雎鸠  

  7. # 失败,相当于运行:u"中文:%s" % s.decode('ascii')  

  8. 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() 的值是可以用“后门”方式修改的,我不是特别推荐这个解决方案,但是还是贴一下,因为后面有用:

  1. # -*- coding: utf-8 -*-  

  2. # file: example3.py  

  3. import sys  

  4. # 这个是 str 的字符串  

  5. s = '关关雎鸠'  

  6. # 这个是 unicode 的字符串  

  7. u = u'关关雎鸠'  

  8. # 使得 sys.getdefaultencoding() 的值为 'utf-8'  

  9. reload(sys)                      # reload 才能调用 setdefaultencoding 方法  

  10. sys.setdefaultencoding('utf-8')  # 设置 'utf-8'  

  11. # 没问题  

  12. s + u  # u'\u5173\u5173\u96ce\u9e20\u5173\u5173\u96ce\u9e20'  

  13. # 同样没问题  

  14. "中文:%s" % u   # u'\u4e2d\u6587\uff1a\u5173\u5173\u96ce\u9e20'  

  15. # 还是没问题  

  16. u"中文:%s" % s  # u'\u4e2d\u6587\uff1a\u5173\u5173\u96ce\u9e20'  

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

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

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

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

  1. # -*- coding: utf-8 -*-  

  2. # file: example4.py  

  3. import string  

  4. # 这个是 str 的字符串  

  5. s = '关关雎鸠'  

  6. # 这个是 unicode 的字符串  

  7. u = u'关关雎鸠'  

  8. # 输出 str 字符串, 显示是乱码  

  9. print s   # 鍏冲叧闆庨笭  

  10. # 输出 unicode 字符串,显示正确  

  11. 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 环境变量保持一致:

  1. # -*- coding: utf-8 -*-  

  2. # file: example5.py  

  3. import sys  

  4. # 检查标准输出流的编码  

  5. print sys.stdout.encoding  # 设置 $LANG = zh_CN.GBK,  输出 GBK  

  6.                           # 设置 $LANG = en_US.UTF-8,输出 UTF-8  

  7. # 这个是 unicode 的字符串  

  8. u = u'关关雎鸠'  

  9. # 输出 unicode 字符串,显示正确  

  10. print u  # 关关雎鸠  

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

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

  1. python -u example5.py | more  

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

  3. None  

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

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

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

  1. # -*- coding: utf-8 -*-  

  2. # file: example6.py  

  3. import os  

  4. import sys  

  5. import codecs  

  6. # 无论如何,请用 linux 系统的当前字符集输出:  

  7. if sys.stdout.encoding is None:  

  8.    enc = os.environ['LANG'].split('.')[1]  

  9.    sys.stdout = codecs.getwriter(enc)(sys.stdout)  # 替换 sys.stdout  

  10. # 这个是 unicode 的字符串  

  11. u = u'关关雎鸠'  

  12. # 输出 unicode 字符串,显示正确  

  13. print u  # 关关雎鸠  

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

  1. # 这个是 str 的字符串  

  2. s = '关关雎鸠'  

  3. # 输出 str 字符串, 异常  

  4. 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():

  1. # 使得 sys.getdefaultencoding() 的值为 'utf-8'  

  2. reload(sys)                      # reload 才能调用 setdefaultencoding 方法  

  3. sys.setdefaultencoding('utf-8')  # 设置 'utf-8'  

  4. # 这个是 str 的字符串  

  5. s = '关关雎鸠'  

  6. # 输出 str 字符串, OK  

  7. print s   # 关关雎鸠  

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

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

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


题图:pexels,CC0 授权。

点击阅读原文,查看更多 Python 教程和资源。


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/MRqCkicdEq
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/6295
 
742 次点击