您的位置:首页 > 编程语言 > Python开发

关于python中文乱码很好的一篇文章

2015-10-02 07:49 701 查看
http://in355hz.iteye.com/blog/1860787

首页
资讯
精华 论坛
问答 博客
专栏 群组
更多
招聘搜索

您还未登录 !
登录
注册

in355hz

博客
微博
相册
收藏
留言
关于我




in355hz

也谈 Python 的中文编码处理

博客分类:
Python

Python字符编码

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



很快,我就遇到了异常:



Python代码







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

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:



Python代码







# -*- 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 -*-
# 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 方法转换:



Python代码







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

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

# 从 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 会怎样?



Python代码







# 用 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'

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



Python代码







# -*- 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)

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



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



Python代码







# 正确,所有的字符串都是 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)

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



Python代码







# -*- 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'

# -*- 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 也设置成相同的字符集。)



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



Python代码







# -*- coding: utf-8 -*-
# file: example4.py
import string

# 这个是 str 的字符串
s = '关关雎鸠'

# 这个是 unicode 的字符串
u = u'关关雎鸠'

# 输出 str 字符串, 显示是乱码
print s # 鍏冲叧闆庨笭

# 输出 unicode 字符串,显示正确
print u # 关关雎鸠

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



Python代码







# -*- 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 # 关关雎鸠

# -*- 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代码







python -u example5.py | more

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

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 字符集不能用来表示中文字符,这里当然会编码失败。



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



Python代码







# -*- 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 # 关关雎鸠

# -*- 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 输出。



Python代码







# 这个是 str 的字符串
s = '关关雎鸠'

# 输出 str 字符串, 异常
print s # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)

# 这个是 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():



Python代码







# 使得 sys.getdefaultencoding() 的值为 'utf-8'
reload(sys) # reload 才能调用 setdefaultencoding 方法
sys.setdefaultencoding('utf-8') # 设置 'utf-8'

# 这个是 str 的字符串
s = '关关雎鸠'

# 输出 str 字符串, OK
print s # 关关雎鸠

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



(完)



分享到:






有关 SoftReference 的一些事实 |

Disruptor 2.0 - 所有的改变

2013-05-07 09:30
浏览 22063
评论(0)

分类:编程语言

相关推荐

评论


发表评论



您还没有登录,请您登录后再发表评论







in355hz

浏览: 67299 次



最近访客 更多访客>>




stxpons




shunai




winteen




searchfull

文章分类

全部博客 (31)
C/C++ (6)
WINDOWS/MFC (7)
Java (10)
MQ (1)
Beanstalkd (1)
Workqueue (1)
CPU (1)
cacheline (1)
Disruptor (8)
RingBuffer (8)
MySQL (3)
Python (1)
事务 (4)
分布式事务 (2)
ACID (4)
分布式系统 (2)

社区版块

我的资讯 (0)
我的论坛 (0)

我的问答 (0)

存档分类

2014-04 (1)
2014-03 (3)
2013-08 (1)
更多存档...

最新评论

xugangqiang: 很好很强大,强烈推荐。

漫谈事务与分布式事务(4)- 最终一致性

mike79: 有个问题,是关于gtid到底是在什么时候生成的。从refman ...

MySQL 5.6 全局事务 ID(GTID)实现原理(二)

supernavy: 翻译的非常好

Disruptor 2.0 - 所有的改变

in355hz: 检查一下 AttachThreadInput 的返回值? 另外 ...

利用 AttachThreadInput 改变其它进程的输入法状态

chengdu1113: 你好: 我正在开发一种npapi的插件,但是在输入框中 ...

利用 AttachThreadInput 改变其它进程的输入法状态



声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。若作者同意转载,必须以超链接形式标明文章原始出处和作者。

© 2003-2015 ITeye.com. All rights reserved. [ 京ICP证110151号 京公网安备110105010620 ]

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: