在python编程中,常常会出现一些编码上的问题,大致包括这三类问题: SyntaxError: Non-ASCII character、UnicodeDecodeError 和 UnicodeEncodeError。下面我对日常遇到的编码问题做的一些小结,并解释上述三类问题出现的原因,并对日常编码做了一些规范化的建议,来避免上述这三类问题。
python2.x 的编码解码介绍
当对str进行编码(encode)时,会先用 默认编码 将str解码(decode)成unicode,然后再将unicode编码为你指定的编码。
那么由于python2.x的默认编码(通过 sys.getdefaultencoding()
获取)是ascii,那么在处理中文的时候,因为ascii能够表示的字符范围有限,所以解码中文会有问题,常常报如下错误,例如
string = "中国"
print(s.encode('gbk')) # 等价于 print(s.decode('ascii').encode('gbk'))
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
如果要想正常输出,可以指定python的默认编码样式为 utf-8,当然也可以不修改python的默认编码,而是在string的编码前面先显性的执行解码命令,并且指定解码样式为 utf-8,例如
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
string = "中国"
print(s.encode('gbk')) # 等价于 print(s.decode('utf-8').encode('gbk'))
所以使用 unicode
命令创建 unicode 对象时,如果不说明这个字符串的编码格式,那么程序会使用python的默认编码,例如如下代码,如果没有设置默认编码的话,也会出现上面的 UnicodeDecodeError 错误
string = unicode("中国") # 等价于 string = unicode("中国", sys.getdefaultencoding())
python2.x 的文件头声明编码
我们在写python2.x文件时,一般会在文件头声明编码,例如如下代码
# -*- coding: utf-8 -*-
# 或者是如下代码,注意下面 = 符号两边没有空格
# coding=utf-8
做这个编码声明,是因为文件中有中文字符(不论是注释内容,还是字符串赋值)时,python2.x才知道如何去解析这些字符串,否则会出现如下错误
SyntaxError: Non-ASCII character '\xe4' in file test.py on line 2, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
另外做这个编码声明,python2.x会解码初始化 u"中国" 为 unicode 对象
有人会发现,我按照这样在文件头部声明了编码,怎么我在执行 print(u"中国")
的时候,会报如下错误呢?
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 0-1: ordinal not in range(256)
出现这样的报错,是由于服务器的输出编码格式不能完成对 u"中国" 字符串的编码操作,通过shell命令 locale
查看到服务器的输出编码格式为 en_US
(或者是通过python命令 sys.stdout.encoding
查看到为 ISO-8859-1,该编码和ascii编码相似,且Latin1是ISO-8859-1的别名),而 ascii 能够表示的字符范围有限,不能对所有的unicode完成编码操作。
多重编码的字符串混合
如果我们将多重编码格式的字符串进行混合使用时,下面将介绍这种情况。
就像 int 和 float 类型的数据进行运算时,结果为 float,那么多重编码的字符串混合使用的时候,结果是什么格式的呢?
python2在处理unicode字符串和str类字符串时,是将str类字符串转化为unicode字符串。例如如下例子
string_1 = "Hello " + "中国" # "Hello" 为ascii编码,"中国" 为utf-8编码,得到的结果为 utf-8编码
string_2 = "Hello " + "中国" + u"美食" # u"美食"为unicode字符串,所以最终的结果应该是unicode编码,"Hello "容易decode为unicode,但是 "中国" 受制于python的默认编码(sys.getdefaultencoding())如果该值不是 utf-8 会报 UnicodeDecodeError 错误
string_3 = "Hello " + "中国" + "美食".decode('utf-8').encode('gbk') # ascii/utf-8/gbk 这三种不同编码的str类字符串相加,得到的结果是gbk编码的字符串
关于编码的字节长度
在计算字符串的字节长度的时候,使用命令 len
,我们知道ascii编码的字符,一个该字符由1个字节组成;utf-8编码的字符,一个该字符由3个字节组成;gbk编码的字符,一个该字符由2个字节组成。也正是由于gbk编码的字符串所占用的字节少,所以一些网站就使用gbk编码。
那我们对unicode的字符串使用 len
的话,得到的结果是字符数,不同于上面的字节数。
具体可以查看下面的例子:
# python2.x
string = "中国" # string编码为utf-8
print(len(string)) # 6个字节 (= 2 * 3)
print(len(string.decode('utf-8'))) # 2个字符 通过decode得到的是unicode字符串,通过len是在计算字符的数量
print(len(string.decode('utf-8').encode('gbk'))) # 4个字节 (= 2 * 2) 通过decode和encode得到的是gbk编码的字符串
string = u"中国" # string编码为unicode
print(len(string)) # 2个字符
print(len(string.encode('utf-8'))) # 6个字节 (= 2 * 3)将string编码为utf-8字符串
print(len(string.encode('gbk'))) # 4个字节 (= 2 * 2) 将string编码为gbk字符串
关于requests库
我们在爬虫或者是测试服务器响应数据的时候,常常使用requests库来操作,其中Request对象在访问服务器后返回一个Response对象,这个对象将返回的http响应字节码保存到content属性中,但是如果访问另外一个属性text时,会返回一个unicode对象,乱码就常常发生在这里,造成显示乱码。
因为Response对象会通过另外一个属性encoding来将字节码编码成unicode,而这个encoding属性是reponse通过chardet猜出来的,例如如果爬取一个 gbk 网页的内容,就会出现乱码,可以通过如下方法解决:
import requests
url = "https://pvp.qq.com"
response = requests.get(url)
response.encoding = 'gbk'
print(response.text)
编码维护习惯
基于以上的编码问题,这里列出一些我们在日常处理字符的一些规范,以此来统一我们的字符编码解码习惯,减少因为字符串编码的问题。
- 设置python的默认编码为 utf-8 (python2.x为 ascii)
- python代码文件的保存格式要与文件头部的声明编码保持一致,为 utf-8
- 如果是中文,程序内部尽量使用 unicode 而不是 str
- 保持程序内外的编码统一,即程序内只使用unicode,那么在从外部读取文件时,一定要将这些字节流转化为unicode,这样在后面的代码中只会去处理unicode,而不是str,例如如下的读取文件代码。
# python2.x
import codecs
with codecs.open('text.py', 'r', encoding='utf-8') as f:
for i in f:
print(i)
# python3.x
with open('text.py', 'r', encoding='utf-8') as f:
for i in f:
print(i)
python2.x 与 python3.x的编码区别
字符串的类型(type),在python2.x和python3.x中有不同的表示,python2.x的编码分为str和unicode,而python3.x的编码分为bytes和str,且他们是相互对应的,即python2.x的 str 对应的是python3.x的 bytes;python2.x的 unicode 对应的是 python3.x的 str。
python2.x中所有的 ascii/utf-8/gbk 等编码的字符都是str类字符串,而python2.x默认的字符编码格式是ascii,str类字符串转换时,都需要通过unicode作为媒介。
python3.x中所有的 ascii/utf-8/gbk 等编码的字符都是bytes类字符串,而python3.x默认的字符串编码格式是unicode,并且python3.x中str类字符串就是unicode类型的字符串。