最近在项目中发现了一个历史遗留而又埋的很深的关于编码的坑,涉及到中文编码的转换。在分析的过程中再次回顾了一下有关编码的知识。python版本为2.7。
在python中有两种字符串类型,分别是str和unicode
str类型与unicode类型之间的转换通过两个方法,分别是decode()和encode(),使用非常简单,如下所示:
str.decode(encoding) ---> unicode # encoding为str原来的编码类型
unicode.encode(encoding) ---> str # encoding为待生成str的编码类型
有两种实现方式:
先转换成unicode再转换成目标str
str.decode(src_encoding).encode(dst_encoding)
直接转换
str.encode(dst_encoding)
需要注意,这里有一个隐藏的调用,即先用默认编码对str进行解码。再转化为对应的编码。所以从理解层面上看,上面的代码等同于:
str.decode(sys.getdefaultencoding()).encode(dst_encoding)
这样看的话其实与2.2.1中的方法是一样的。
在背景中已经交代了python中有两种字符串类型:str和unicode,识别的方法也非常简单:
str_type_string = "this is str."
unicode_type_string = u"this is unicode"
type(str_type_string)
# <type 'str'>
type(unicode_type_string)
# <type 'unicode'>
识别字符串编码需要用到chardet模块的detect方法。上文中也说了只有str类型的字符串会有不同的编码,所以detect方法中只能传入str类型的字符串作为参数。可能是版本问题,有的版本貌似也能传入unicode的字符串。
import chardet
str_type_string = "this is str."
chardet.detect(str_type_string)
# {'confidence': 1.0, 'encoding': 'ascii'}
# 默认str的编码为ascii
字符编码在程序运行过程中如果处理不好一不小心就会出问题,下面就总结一些经典的场景。
脚本的内容需要被解释器读取,所以解释器需要知道脚本使用什么编码来写的。默认解释器也是使用ASCII编码来解码的,所以当脚本中使用了其他编码的文字,而没有指明编码类型的话,脚本执行就会报错,当然如果指定了错误的编码类型一样会执行错误。
示例:
country = "中国"
print country
执行结果如下:
# python test.py
SyntaxError: Non-ASCII character '\xe4' in file test.py on line 1, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
解决方法就是在脚本的在开头加上# -*- coding: utf-8 -*-
,即指定脚本所使用的编码为utf-8。将上面的代码修改后:
# -*- coding: utf-8 -*-
country = "中国"
print country
执行正常:
# python test.py
中国
解码需要指定原str的编码类型才能正确操作,否则执行报错,见下:
# -*- coding: utf-8 -*-
country = "中国"
u_conuntry = conuntry.decode()
执行结果:
# python test.py
Traceback (most recent call last):
File "test.py", line 3, in <module>
u_conuntry = country.decode()
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
原因分析:
decode的时候,如果不指定编码,会使用默认的ASCII编码,由于原字符串是utf-8编码,使用ASCII解码就会报错。
解码时指定的编码与原字符串编码类型不一致,也会出错:
# -*- coding: utf-8 -*-
country = "中国"
u_conuntry = conuntry.decode('cp936')
执行结果:
# python test.py
Traceback (most recent call last):
File "test.py", line 3, in <module>
u_conuntry = country.decode("cp936")
UnicodeDecodeError: 'gbk' codec can't decode bytes in position 2-3: illegal multibyte sequence
在进行字符串操作的时候,如果操作的字符串对象之间使用了不同的编码,python会进行自动的编码和解码。
一般而言,如果被操作的字符串对象组合中既有str又有unicode,python会自动将str解码为unicode。
一个简单的字符串格式化的例子:
# -*- coding: utf-8 -*-
people = "LiLei"
country = "中国"
format_str = "%s was born in %s" % (people, country)
print format_str
运行没有任何问题:
# python test.py
LiLei was born in 中国
但是如果我把people声明为一个unicode对象,问题就会发生,代码修改为:
# -*- coding: utf-8 -*-
people = u"LiLei"
country = "中国"
format_str = "%s was born in %s" % (people, country)
print format_str
执行的错误信息为:
# python test.py
Traceback (most recent call last):
File "test.py", line 4, in <module>
format_str = "%s was born in %s" % (people, country)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
为什么改了之后就会报错呢?这是因为改过之后,people为unicode对像,country为str对象,当他们进行字符串格式化操作的时候,python会将str类型转换成unicode类型。而且转换用的编码就是用的sys.getdefaultencoding()
显示的编码类型,默认都是ASCII。而country是用的utf-8编码,所以自然就会解码失败。
print 在打印unicode对象的时候会自动转码为str类型,但是此时用来转换的编码并不是系统默认编码sys.getdefaultencoding(),而是用的sys.stdout.encoding。sys.stdout.encoding的值优先使用python环境变量PYTHONIOENCODING的值,如果它为空则使用linux的环境变量LANG,如果LANG为空,最后才使用sys.getdefaultencoding()
的值。
举个例子:
# -*- coding: utf-8 -*-
import sys
print sys.stdout.encoding
country = u"中国"
print country
按下面的方法执行:
# unset PYTHONIOENCODING
# export LANG=en_US.C
# python test.py
ANSI_X3.4-1968
Traceback (most recent call last):
File "test.py", line 3, in <module>
print country
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
其中ANSI_X3.4-1968就是print自动编码用的编码类型,用这个编码是无法表示中文的,所以把“中国”两个字转换为这个编码时就发生了错误。
如果我们把优先级最高的PYTHONIOENCODING设置为能表示中文的编码,比如UTF-8,就能正常运行上面的代码了。
# export PYTHONIOENCODING='utf-8'
# export LANG=en_US.C
# python test.py
utf-8
中国
运行OK。