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

很快,我就遇到了异常:

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

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

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

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

对应 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__   # <type 'str'>
  11. 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 方法转换:

  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 里的技巧。

也谈 Python 的中文编码处理的更多相关文章

  1. 【Python五篇慢慢弹(4)】模块异常谈python

    模块异常谈python 作者:白宁超 2016年10月10日12:08:31 摘要:继<快速上手学python>一文之后,笔者又将python官方文档认真学习下.官方给出的pythondo ...

  2. 浅谈Python时间模块

    浅谈Python时间模块 今天简单总结了一下Python处理时间和日期方面的模块,主要就是datetime.time.calendar三个模块的使用.希望这篇文章对于学习Python的朋友们有所帮助 ...

  3. 浅谈Python在信息学竞赛中的运用及Python的基本用法

    浅谈Python在信息学竞赛中的运用及Python的基本用法 前言 众所周知,Python是一种非常实用的语言.但是由于其运算时的低效和解释型编译,在信息学竞赛中并不用于完成算法程序.但正如LRJ在& ...

  4. 浅谈python字符串存储形式

    http://blog.csdn.net/zhonghuan1992 钟桓 2014年8月31日 浅谈python字符串存储形式 记录一下自己今的天发现疑问而且给出自己现有知识有的回答. 长话短说,用 ...

  5. 开发技术--浅谈Python函数

    开发|浅谈Python函数 函数在实际使用中有很多不一样的小九九,我将从最基础的函数内容,延伸出函数的高级用法.此文非科普片~~ 前言 目前所有的文章思想格式都是:知识+情感. 知识:对于所有的知识点 ...

  6. 开发技术--浅谈python数据类型

    开发|浅谈python数据类型 在回顾Python基础的时候,遇到最大的问题就是内容很多,而我的目的是回顾自己之前学习的内容,进行相应的总结,所以我就不玩基础了,很多在我实际生活中使用的东西,我会在文 ...

  7. 开发技术--浅谈python基础知识

    开发|浅谈python基础知识 最近复习一些基础内容,故将Python的基础进行了总结.注意:这篇文章只列出来我觉得重点,并且需要记忆的知识. 前言 目前所有的文章思想格式都是:知识+情感. 知识:对 ...

  8. 1-浅谈 python变量

    浅谈 python变量 python变量概念 程序执行的过程中,很多数据都在变化的过程,我们需要一种机制把这种变化体现出来,变量是我们记录这种变化的方式. python以及其它各种语言的变量 ,其作用 ...

  9. [转]浅谈Python web框架

    说到web framework,Ruby的世界Rails一统江湖,而Python则是一个百花齐放的世界,各种micro-framework.framework不可胜数,不完全列表见:http://wi ...

随机推荐

  1. TestNG关键字和testNG.xml结构学习

    转自官网:http://testng.org/doc/documentation-main.html#test-results TestNG关键字 @BeforeSuite@AfterSuite@Be ...

  2. ios-简单算法

    #import "ViewController.h" @interface ViewController () @end @implementation ViewControlle ...

  3. 基于redis 内存数据库简单使用

    在ecplise中使用内存数据的客端户,前提要准备要下载两个jar包 commons-pool2-2.0.jar jedis-2.4.2.jar 前提准备做好了,那我们就开启redis的服务,打开一个 ...

  4. JBoss EAP6/AS7/WildFly: How to Use Properties Files Outside Your Archive--reference

    Introduction I’d like to preface this post with, really, all properties files should be inside your ...

  5. 亲测 安装 mysql5.5 64位

    1.选择 customn 更改安装目录2.选择 develop 模式,默认,占用内存较小. server machine 中等内存 dedicate mysql server machine 专用服务 ...

  6. uva 1391 Astronauts(2-SAT)

    /*翻译好题意 n个变量 不超过m*2句话*/ #include<iostream> #include<cstdio> #include<cstring> #inc ...

  7. PHP解析xml

    <?xml version="1.0" encoding="UTF-8"?> <ZIP_result> <result name= ...

  8. C# 实现文件夹的复制以及删除

    代码来源:http://blog.163.com/u_tommy_520/blog/static/20406104420147493933662/ http://www.cnblogs.com/lov ...

  9. Wpf TextChanged事件导致死循环,事件触发循环问题

    1.实例: 说明:当TextBox控件的Text内容发生变化时,TextChanged事件触发,并且会立即同步执行. 基于这个特点,设置一个全局变量标识,ChangeTxtB,如果是正在修改txtB的 ...

  10. 最短路径算法——Dijkstra,Bellman-Ford,Floyd-Warshall,Johnson

    根据DSqiu的blog整理出来 :http://dsqiu.iteye.com/blog/1689163 PS:模板是自己写的,如有错误欢迎指出~ 本文内容框架: §1 Dijkstra算法 §2 ...