python编码问题

起因

今天让实习生用python写一个简单的流程,第一次问的时候说写好了执行没问题现在加到定时任务里面,隔了30分钟又问了一次说有点问题,手动执行结果正确,但是写到定时任务里面就有问题。What?

仔细问了一下,原来是程序原本没有问题,但是如果把文件输出重定向到文件之后执行结果就不正确了。我说:没有任何报错?他摇头说没有。抱着怀疑的态度我打开了他的代码。。

果然!全是try/except,更加无语的是except下面全都是跟的continue,我心想,这能报错才怪嘞。然后就是他说的另外一个问题为什么重定向到文件就会报错嘞,联想到他这个try/except,我猜应该是在循环内部有一个print语句,由于输出时候的编码问题导致报错,但是全都被try捕获到并且静悄悄的continue了,仔细一看果然有一行print。

其实这个问题的根源就在于python2版本中直接print字符串的情况下打印到终端没有问题,但是在含有中文一类的字符时如果是python code.py > debug这样来写是会出现编码问题的。

解决办法有两种,下面解释完原因会详细讲一下。

原因梳理

复现

这里首先对问题进行一下复现,考虑如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/env python
#coding:utf8
import sys


u1 = u"\u0001\u0001\u0001"
s1 = "这是中文"
u2 = u'这是中文'
print s1
try:
print u2
except Exception,e:
print str(e)

以上代码执行结果其实很简单,在终端中结果为这是中文,但是如果重定向到文件,那么文件中会有两行输出分别是

这是中文

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

错误复现了。

前置知识储备

其实错误的根源在于python2内部对于字符串的处理,默认python2中字符串使用的是str字符串类型(每个字符占用一个字节),而当我们在其前面写上u的时候表明这个字符串是一个unicode字符串,其与str类型就不再一样了变成了一个unicode类型。

将字符串写入文件其本质上是将二进制信息写入文本,所以这里理解一下就是我们最终写到文件里面的是二进制信息。所以最终到文件之前其实最终是要转化为byte类型的。

这里Stack Overflow上有个问题上面有很好的介绍

Encoding: characters → bytes

Decoding: bytes → characters

以上我们可以看出来通过调用Encoding以及Decoding对字符进行编码和解码。

问题原因

其实说了这些,问题的原因其实就在于python2在将字符串写入的时候发生了错误,ascii无法对字符串进行encode导致报错,看到这里意识到什么没有,报错的原因是ascii无法encode,这个是必然的,因为ascii只能表示0~128这么一个范围的字符,而汉字明显是不在这个区间的,所以本质上是由于python2在进行转化的时候使用了错误的编码导致。

那么为什么直接写入终端没有问题,而写入文件就会报错嘞?

这是由于在写入终端的情况下python2能获取到要写入的介质的编码类型,而重定向文件这种情况下是获取失败的,仍然是上代码

1
2
3
4
#!/bin/env python
#coding:utf8
import sys
print sys.stdout.encoding
1
2
3
4
5
6
#first
python code.py
#UTF-8
#second
python code.py > debug ; cat debug
# None

可以看到,写入终端的情况下编码为utf8而写入文件的时候变成了None,这种情况下python2会调用其内置默认的ascii进行encode于是就发生了开头所说的错误

问题的解决

问题的原因也知道了,那么解决方法也就很明了了,就是让字符串正确的decode就ok了,所以有如下几种方法:

  1. 在代码的开始调用reload(sys);sys.setdefaultencoding(‘utf8’)通过这种方式,我们制定了默认的encode字符集为utf8因此修正了以上错误
  2. 在print u1的地方改成print u1.decode(‘utf8’).encode(‘utf8’)由我们来指定调用的字符集防止其调用默认的ascii link

再深入一些

Python的字符串深入了解一下,考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/env python
#coding:utf8
import sys

u1 = u"\u0001\u0001\u0001"
s1 = '这是中文'
u2 = u'这是中文'
print s1
print 'start ord'
for c in u2:
print hex(ord(c))
print '------------'
for c in u2.encode('utf-8'):
print ord(c)
print 'end ord'
print sys.stdout.encoding
print 'str'
print u1
try:
print u2
except Exception,e:
print str(e)
print u2.encode('utf8').encode('hex')
print u2.encode('utf8')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这是中文
start ord
0x8fd9
0x662f
0x4e2d
0x6587
------------
232
191
153
230
152
175
228
184
173
230
150
135
end ord
None
str

'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)
e8bf99e698afe4b8ade69687
这是中文

这个例子从二进制的角度来对python2的字符串进行输出,例如对于unicode类型的字符串来说每个汉字表示一个元素,而一个汉字占用了三个字节,而对于python2内置的字符串类型,每一次遍历都是一个字节一个字节遍历的。同时需要注意的是并不是所有的unicode在使用默认ascii进行encode的时候都会报错,例如u1字符串在输出的时候就不会报错,因为1并没有超出ascii的编码范围 :)。

我觉得说到这里,python代码开头的coding这一行应该也不需要过多说明了吧?

参考资料

Stack Overflow:https://stackoverflow.com/questions/4545661/unicodedecodeerror-when-redirecting-to-file

字符集的介绍:http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

Python官方文档:https://docs.python.org/2/howto/unicode.html

关于python3

细心的同学一定发现了我上面说的全都是python2特意指明了是2才会这样,因为python 3是没有以上这些问题的,考虑到python混乱的字符问题,python 3默认使用的不再是str而是unicode,考虑如下代码,python3当中字符串已经不简单是str了,而是变成了class ‘str’ :)

1
2
3
4
5
6
7
#!/bin/env python
import sys

print('str')
print(type('str'))
print(u'这是中文'.encode('utf8'))
print(u'这是中文')
1
2
3
4
str
<class 'str'>
b'\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\xad\xe6\x96\x87'
这是中文

好啦,就到这里啦:)

转载请注明来源链接 http://just4fun.im/2017/11/30/python编码问题/ 尊重知识,谢谢:)