聊一聊Python2的编码坑

Py2的编码问题真是历史悠久,下边这个报错更是经常见到。

1
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe9 in position 2: ordinal not in range(128)

在避开Py2的编码坑之前,我们需要了解一下一些常见的编码。

常见编码

ASCII

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是现今最通用的单字节编码系统。

Unicode

Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

UTF-8

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,又称万国码。由Ken Thompson于1992年创建。现在已经标准化为RFC 3629。UTF-8用1到4个字节编码Unicode字符。用在网页上可以统一页面显示中文简体繁体及其它语言(如英文,日文,韩文)。

GB2312

GB2312编码由中国国家标准总局1980年发布,1981年5月1日开始实施的一套国家标准,适用于汉字处理、汉字通信等系统之间的信息交换,通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB2312。基本集共收入汉字6763个和非汉字图形字符682个。

GBK

GB2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率,不过 GB2312 还是不能100%满足中国汉字的需求,对一些罕见的字和繁体字 GB2312 没法处理,后来就在GB2312的基础上创建了一种叫GBK 的编码,GBK不仅收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。

编码问题

背景介绍完毕。回到Python2当中,Python2默认编码是ASCII。

1
2
3
>>> import sys
>>> sys.getdefaultencoding()
'ascii'

如果.py文件中包含中文字符(严格的说是含有非anscii字符),则需要在第一行或第二行指定编码声明:

1
# -*- coding: UTF-8 -*-

否认将会看到这样的报错

1
SyntaxError: Non-ASCII character '\xe4' in file C:/github/sgauto/test.py on line 4, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

在Python2中有两种字符串类型,分别是str和unicode,他们都是basestring的派生类。

对unicode进行指定格式的编码后,就是str。
对str进行指定格式的解码后,就是unicode。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding: UTF-8 -*-
hi = '今天天气不错'
print type(hi)
print type(hi.decode('utf-8'))
print "-"*50
hello = u'今天天气不错'
print type(hello)
print type(hello.encode('utf-8'))
output:
<type 'str'>
<type 'unicode'>
--------------------------------------------------
<type 'unicode'>
<type 'str'>

也就是说,utf-8,gb2312,gbk编码的字符串,都可以通过decode函数,转换为unicode,而unicode字符串,可以通过encode函数指定编码,转换为utf-8,gb2312,gbk等任意编码的字符串。

unicode就是一个编码转换的中间人。

避开编码坑,最重要的就是清楚源字符串的编码和输出环境的编码。最好是在Python处理过程中,都先转换为unicode,在最后输出的时候,再进行编码。

举个乱码的例子。

1
2
3
# -*- coding: UTF-8 -*-
print '今天天气不错'

在IDE里边,一切正常,可是当你在Windows的CMD下运行的时候,乱码鸟

1
浠婂ぉ澶╂皵涓嶉敊

这是为什么呢?CMD的默认输出编码是GB2312,而我们提供的是UTF-8编码的字符串,于是解析的时候,出现了乱码。

解决方案一:

修改CMD的默认编码为UTF-8,在CMD里边输入 CHCP 65001即可。

解决方案二:
在Py文件里边,将字符串的编码转换成Unicode,这样可以有几种写法。

1
2
3
# -*- coding: UTF-8 -*-
print '今天天气不错'.decode('utf-8')
1
2
3
# -*- coding: UTF-8 -*-
print u'今天天气不错'
1
2
3
4
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
print '今天天气不错'

最推荐的方式是第三种,通过__future__模块引入Py3的一个新特性,字符串默认使用unicode编码。

而在文章最开始列出来的报错,有两种常见情况。

一是unicode和str混用,进行了运算

1
2
3
4
5
6
7
8
# -*- coding: UTF-8 -*-
hi = u"今天" + "天气真好"
output:
Traceback (most recent call last):
File "C:/github/sgauto/test.py", line 5, in <module>
hi = u"今天" + "天气真好"
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)

二是对字符串进行编码解码的时候,指定了错了编码类型。

1
2
3
4
5
6
7
8
# -*- coding: UTF-8 -*-
print "今天天气真好".encode('gb2312')
output:
Traceback (most recent call last):
File "C:/github/sgauto/test.py", line 5, in <module>
print "今天天气真好".encode('gb2312')
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

结论

在Python2中,进行编码转化时候,需要先转为unicode编码,再进行下一步的转化。如:utf-8—>unicode—>gb2312

推荐实践

  • 从__future__模块引入unicode_literals

  • 使用chardet检测编码格式,chardet是一个检测字符串编码的第三方库,必备神器,通过 pip install chardet 安装。

1
2
3
4
5
6
7
# -*- coding: UTF-8 -*-
import chardet
hi = '今天天气不错'
print chardet.detect(hi)
output:
{'confidence': 0.99, 'encoding': 'utf-8'}
  • 使用codecs打开文件

上文提到,推荐在Py内处理文本之前,都先转为unicode编码,自带的open函数,并不具备这个功能,推荐使用codecs模块的open函数来打开文件,同时指定文件编码,将读取的文本转化为unicode编码。

1
2
3
4
import codecs
with codecs.open("a.txt",'r',encoding='utf-8') as f:
print f.read()

说点别的

经常会看到有人问这样一个问题:打印字典和列表的时候,怎么显示成中文?

1
2
3
4
5
6
7
# -*- coding: UTF-8 -*-
hello = {'hi': "今天天气真好"}
print hello
output:
{'hi': '\xe4\xbb\x8a\xe5\xa4\xa9\xe5\xa4\xa9\xe6\xb0\x94\xe7\x9c\x9f\xe5\xa5\xbd'}

解决方法是:

1
2
3
4
5
6
7
8
# -*- coding: UTF-8 -*-
import json
hello = {'hi': "今天天气真好"}
print json.dumps(hello,ensure_ascii=False)
output:
{"hi": "今天天气真好"}