python2的编码问题整理

发表于 2017-10-31   |   分类于 技术

最近在项目中发现了一个历史遗留而又埋的很深的关于编码的坑,涉及到中文编码的转换。在分析的过程中再次回顾了一下有关编码的知识。python版本为2.7。

1. 背景

在python中有两种字符串类型,分别是str和unicode

  • str
    默认使用ANSCII编码,ANSCII编码只能表示英文数字和一些特殊字符,可以使用其他编码方式。
  • unicode
    也是一种编码,称为统一码,包含了全世界所有的字符。所有的编码都能转换为unicode。

2. 应用

2.1 str与unicode相互转换:

str类型与unicode类型之间的转换通过两个方法,分别是decode()和encode(),使用非常简单,如下所示:

str.decode(encoding) ---> unicode  # encoding为str原来的编码类型
unicode.encode(encoding) ---> str  # encoding为待生成str的编码类型

2.2 str类型不同编码之间相互转换

有两种实现方式:

  1. 先转换成unicode再转换成目标str

    str.decode(src_encoding).encode(dst_encoding)
  2. 直接转换

    str.encode(dst_encoding)

    需要注意,这里有一个隐藏的调用,即先用默认编码对str进行解码。再转化为对应的编码。所以从理解层面上看,上面的代码等同于:

    str.decode(sys.getdefaultencoding()).encode(dst_encoding)

    这样看的话其实与2.2.1中的方法是一样的。

2.3 字符串编码的识别

2.3.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'>

2.3.2 识别字符串编码类型

识别字符串编码需要用到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

3. 场景

字符编码在程序运行过程中如果处理不好一不小心就会出问题,下面就总结一些经典的场景。

3.1 指定脚本的编码

脚本的内容需要被解释器读取,所以解释器需要知道脚本使用什么编码来写的。默认解释器也是使用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 
中国

3.1 编码或解码未指定对应的编码类型

解码需要指定原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解码就会报错。

3.2 编码或解码指定了错误的编码类型

解码时指定的编码与原字符串编码类型不一致,也会出错:

# -*- 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

3.3 字符串自动编码解码

在进行字符串操作的时候,如果操作的字符串对象之间使用了不同的编码,python会进行自动的编码和解码。
一般而言,如果被操作的字符串对象组合中既有str又有unicode,python会自动将str解码为unicode。

3.3.1 字符串格式化

一个简单的字符串格式化的例子:

# -*- 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编码,所以自然就会解码失败。

3.3.2 print打印

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。

发表新评论

© 2017 Powered by Typecho
苏ICP备15035969号-3