Fork me on GitHub

彻底研究Python的编码问题

Python的初学者(以及很多熟练工)相信都遇到过下面的运行时错误信息:

UnicodeDecodeError: ‘ASCII’ codec can’t decode byte 0xe4 in position 0: ordinal not in range(128)

然后百度一下发现说明这个问题的网页有一大把,基本无非是下面两种解决方案:

  1. 在Python文件的开头加一句#coding=utf-8
  2. 在代码中加入:
1
2
3
import sys
reload(sys)
sys.setdefaultencoding('utf8')

一般大家的做法是把两者都加上,然后问题一般也会得到解决。但很少有人会去深究过Python会什么会产生这样的编码问题,以及为什么通过加上面的代码可以解决这个问题。作为一个合格的程序员,遇到这种问题就应该追根究底,否则就只是以码谋生的码畜了 :)

string与Unicode string

Python的编码问题一般只在Python2.x中存在,在Python3.x中则基本不会遇到这样的问题。根本原因在于Python2.x中存在两种字符串:string和Unicode string,两者都继承自basic string。

string类似于C语言中的char*,或者C++中的std::string,本质上都是byte数组,本身不具备特定的encoding类型,而是由string的使用者决定的。例如向string中写入一个汉字,如果是UTF-8编码,则可能是三个char(或者更多),如果是GBK编码,则可能是两个char,但对于Python解释器,它并不清楚string用的是encoding,一切都是比特流。。。

而Unicode string则明确地存储了Unicode编码的字符串。在Python2.x中,a1="abc";,a1就是一个string(是什么编码还不确定,等下再谈)。而a2=u"abc";,a2就是一个Unicode string。

C/C++中的文件编码和字符串编码

C/C++不仅仅是一种编程语言,更是一个深入了解计算机底层原理的窗口和机会,一个只会Java或者Python的程序员,是不能称之为一个真正的程序员的(这跟月薪有没有上3w没有关系)。

我们先回过头来看看C/C++中是如何处理文件编码和字符串编码的问题的。以VC为例,VC下有两个编码的选项,分为Source encoding和Execution encoding。前者指定了源文件的编码(这比BOM更准确),后者是字符串常量等的编码。简单说,如果指定Source encoding为GBK,同时指定Execution encoding为UTF-8,则首先所有的源文件必须用GBK编码保存,否则VC在读入源文件时可能会无法解码其中的一些字符。同时,像const char* a3="中文";这样的常量字符串赋值语句,a3就是使用了UTF-8编码保存的字符串。在中文Windows系统下,如果不特意设置这两种编码,VC则一般使用的是system default encoding。

(参考 Char * encoding, /execution-charset (Set Execution Character Set), /source-charset (Set Source Character Set)。)

Python中的文件编码和字符串编码

对比C/C++处理编码的方法,我们再看看Python是怎么做的。

首先,类似于Source encoding,Python可以通过在文件开头加上#coding=utf-8这句来向Python解析器指明本py源文件的编码。然后,麻烦事情来了,与C/C++中的Execution encoding有很大的不同,对于Python2.x的string而言,对其用字符串常亮赋值时,string中存储的encoding,是跟#coding=utf-8挂钩的,而不是像Execution encoding一样有一个单独的选项来指定。也就是说,如果指定了Execution encoding,那么a1="中文"就是UTF-8编码,如果没有设置#coding=utf-8及其它任何#coding=xxx,那么就是用默认的ASCII编码,但显然ASCII是无法编码"中文"的,于是你就会看到下面的报错:

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

而关于sys.defaultencoding,这个在解码没有明确指明解码方式的时候使用。比如有如下代码:

1
2
3
4
#! /usr/bin/env python
# -*- coding: utf-8 -*-
s = '中文'
s.encode('gb18030')

这句代码将s(string类型)重新编码为gb18030的格式(仍然是string类型), Python会自动的先将s解码为Unicode string类型,然后再编码成gb18030。因为解码是Python自动进行的,我们没有指明解码方式,Python就会使用sys.defaultencoding指明的方式来解码。很多情况下sys.defaultencoding是ASCII,如果 s 不是这个类型就会出错。

拿上面的情况来说,我的sys.defaultencoding是ASCII,而s的编码方式和文件的编码方式一致,是UTF-8的,所以出错了:

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

但是sys.setdefaultencoding('utf8')有的时候也会导致一些意想不到的问题,所以目前网上主流意见是不推荐使用 。。。。。。
Why sys.setdefaultencoding() will break code

在Python3.x中,编码的问题基本不存在了,也没有了sys.setsystemencoding。因为Python3.x取消了string,只有Unicode string,也避免了很多编码转换的问题。

关于#coding:utf-8

#coding:utf-8指引Python解释器如何读取整个py文件,有些IDE也会根据这个comment来自动决定保存格式及显示格式,应该与py文件的保存格式一致。

根据PEP 263 – Defining Python Source Code Encodings中的说明,其实写成#coding=utf-8,’#coding:utf-8或者#-- coding:utf-8 --都是可以的,正则定义为coding[:=]\s*([-\w.]+)`。

encode/decode

encode是将Unicode string转化为指定encoding编码的string,而decode是将某种encoding编码的string转化为Unicode string。

print和sys.xxx.write在输出unicode-object string时会自动转换编码为sys.xxx.encoding,无需自己来转。
但是,如果utf8 string,而当前环境是win,则输出显然会乱码。

最佳实践


本文地址:http://xnerv.wang/all-truths-about-python-encoding-problem/