Python 字符编码详解

  接着之前写过的字符编码详解,这里我们来讲一讲“Python”中的字符编码问题。由于笔者使用的是Python2.7,而且较之于Python3,前者的字符编码问题显得更为复杂一些,因此本文所讲的“字符编码”基于Python2.7(以下简称“Python”)。


1.str和unicode

  Python中有两种字符串,str和unicode,它们都是basestring的子类。如下:

class basestring(object)

class str(basestring)

class unicode(basestring)

  basestring是str和unicode的超类(父类),也是抽象类,因此不能被调用和实例化,但可以被用来判断一个对象是否为str或者unicode的实例,isinstance(obj, basestring)等价于isinstance(obj, (str, unicode))。如下:

>>> my_str="你好"
>>> my_unicode=u"你好"
>>> my_str
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>> my_unicode
u'\u4f60\u597d'
>>> isinstance(my_str, str)
True
>>> isinstance(my_str, unicode)       
False
>>> isinstance(my_unicode, unicode)
True
>>> isinstance(my_unicode, str)       
False
>>> isinstance(my_str, basestring)       
True
>>> isinstance(my_unicode, basestring)   
True

  严格来讲,str是字节字符串,由字符串的二进制字节组成(如果对str类型的字符串迭代的话,则会按照其在内存中的字节序依次迭代);unicode是文本字符串,由字符组成。如下:

>>> my_str="你好"
>>> my_unicode=u"你好"
>>> my_str
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>> my_unicode
u'\u4f60\u597d'
>>> len(my_str)#求的是字节数
6
>>> len(my_unicode)#求的是字符数
2
>>> my_str[1]
'\xbd'
>>> my_unicode[1]
u'\u597d'

上例中,对于同一个中文字符串“你好”,str的表示为:'\xe4\xbd\xa0\xe5\xa5\xbd',这是字符串“你好”的utf8编码(e4 bd a0 e5 a5 bd),至于为什么不是GBK编码或者别的什么编码,则与环境设置有关;unicode的表示为:u'\u4f60\u597d',这是字符串“你好”的Unicode表示(U+4f60 U+597d)。有一点需要说明的是,Python中,用转义序列\xhh表示十六进制值为hh的字符,用转义序列\uxxxx表示十六进制值为xxxx的字符(仅限Unicode)。

  str和unicode,二者之间是可以相互转换的,依靠decode()和encode()两个方法即可完成转换:


这里写图片描述

来看一段代码:

>>> my_str="你好"    
>>> my_unicode=u"你好"
>>> my_str
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>> my_unicode
u'\u4f60\u597d'
>>> my_str2unicode=my_str.decode("utf8")
>>> my_unicode2str=my_unicode.encode("utf8")
>>> my_str2unicode
u'\u4f60\u597d'
>>> my_unicode2str
'\xe4\xbd\xa0\xe5\xa5\xbd'

  当然,除了上图的标准转换方式外,我们还可以使用str.encode()和unicode.decode()方式进行转换,这就涉及到一个隐式转换的问题。当我们使用str.encode(encoding)时,实际上是在执行str.decode(sys.defaultencoding).encode(encoding);当我们使用unicode.decode(encoding)时,实际上是在执行unicode.encode(sys.defaultencoding).decode(encoding)。其中,defaultencoding的默认值为ASCII,如下:

>>> my_str="你好"
>>> my_str.encode("utf8")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>> import sys
>>> sys.getdefaultencoding()
'ascii'

显然,utf8编码的字符超出了ASCII的编码范围,所以抛出了异常(其中的codec是coder/decoder的首字母组合)。我们可以手动设置defaultencoding的值,使得decode()可以正确解码以执行str.encode(),如下:

>>> import sys
>>> reload(sys)
<module 'sys' (built-in)>
>>> sys.setdefaultencoding("utf8")
>>> sys.getdefaultencoding()      
'utf8'
>>> my_str="你好"
>>> my_str
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>> my_str.encode("gbk")
'\xc4\xe3\xba\xc3'

有心的读者可能已经注意到了,上述代码中似乎多加了一句reload(sys),这是因为,在sys加载后,setdefaultencoding函数被删除了,所以我们需要重新加载一遍。至于为什么要删掉,就是为了使用户无法在初始化后改变编码,以免出现一些未知的问题,针对这点,此处暂且按下不表。
  最后我们再提一下str()方法和unicode()方法。str(object)等同于object.encode(defaultencoding),unicode()则类似于decode(),具体如下:
  str()方法:

>>> my_unicode=u"你好"
>>> str(my_unicode)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> reload(sys)
<module 'sys' (built-in)>
>>> sys.setdefaultencoding("utf8")
>>> str(my_unicode)               
'\xe4\xbd\xa0\xe5\xa5\xbd'

  unicode()方法:

>>> my_str="你好"
>>> unicode(my_str, "utf8")
u'\u4f60\u597d'
>>> unicode(my_str)        
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>> import sys
>>> reload(sys)
<module 'sys' (built-in)>
>>> sys.setdefaultencoding("utf8")
>>> unicode(my_str)               
u'\u4f60\u597d'

2.#coding:utf8

  我们经常能在Python脚本的第一行或者第二行看见类似于#coding:utf8的一行代码,这叫编码声明,它主要有以下作用:

  1. 根据代码文件中的编码声明,Python解析器可以对其中包含的特定编码字符作正确解析;
  2. 比较高级的编辑器,会根据编码声明,将此作为代码文件的存储格式。

  有一点需要注意的是,在my_str="中文"这样的语句中,字节串的编码方式取决于代码文件的存储格式,而非编码声明,所以编码声明和代码的存储格式要一致,否则Python解析器很可能因无法正确解析而报错。我们来看一个例子:

#coding:gbk
my_str="中文"

以上是Python脚本test_str.py,其存储格式为utf8,执行后会报错:

  File "test_str.py", line 2
SyntaxError: 'gbk' codec can't decode bytes in position 10-11: illegal multibyte sequence

很明显,由于文件的存储格式为utf8,所以"中文"的编码为utf8,而文件的编码声明为gbk,所以Python解析器将以gbk的方式去解析"中文",二者不一致,所以抛出异常。

  另外,上一节出现了类似于my_unicode=u"中文"这样的语句,它的作用是完成str到unicode的转换,只要编码声明正确,Python便可以将str正确转换为unicode。

  关于编码声明,读者可以阅读PEP 263,这里我们只强调以下几点:

  1. 必须放在第一行或者第二行;
  2. 声明的通用格式:^[ \t\v]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)或者# vim: set fileencoding=<encoding name> :
  3. 如果你的代码文件本身编码是带BOM的UTF-8,即文件前三个字节是\xef\xbb\xbf,那么:a)、即使你没有附加编码声明,也会当做是UTF-8的编码;b)、如果你声明了文件编码,则必须是UTF-8,否则会报错;

3.setdefaultencoding()

  第一节中,提及了sys模块中的setdefaultencoding()函数,这一节,我们准备对它做一个详述。
  当我们启动Python解释器的时候,会自动加载库文件site.py,其中有这么一段代码:

# Remove sys.setdefaultencoding() so that users cannot change the
# encoding after initialization.  The test for presence is needed when
# this module is run as a script, because this code is executed twice.
if hasattr(sys, "setdefaultencoding"):
    del sys.setdefaultencoding

  从这段代码可以看到,当我们import sys后,其中的setdefaultencoding()函数就会被删除,这也就解释了为什么要reload(sys)才能使用该函数。那么,为什么要删除setdefaultencoding()函数?这是因为,随意更改默认编码会引发一些意想不到的问题。来看两个例子:

#coding:utf8
my_str="中文"
my_unicode=u"中文"
my_str==my_unicode
#coding:utf8
import sys
reload(sys)
sys.setdefaultencoding("utf8")
my_str="中文"
my_unicode=u"中文"
my_str==my_unicode

  执行第二段代码,正常;执行第一段代码,会抛出异常:

test_str.py:4: UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
  my_str==my_unicode

这是因为,Python在做字符串对比时,会先自动将所有的字符串decode为unicode,然后再进行比较。在第一段代码中,由于Python的默认编码是ASCII,无法将中文字符串decode为unicode,所以抛出异常;在第二段代码中,Python的默认编码被修改为utf8,可以将中文字符串decode为unicode,所以执行正常。
  通过上述例子,可以发现,修改Python的默认编码,会导致代码执行结果的不一致,这并非我们想看到的,故而Python进行了删除setdefaultencoding()函数的操作,以防止用户对Python默认编码的修改。

4.unicode-escape、string-escape、raw-unicode-escape

  这一节我们介绍几种用来“转义”的编码方式:unicode-escape、string-escape、raw-unicode-escape。话不多说,直接看代码:

#unicode-escape
>>> my_unicode=u"中文"
>>> my_unicode_escape="\u4e2d\u6587"
>>> my_unicode
u'\u4e2d\u6587'
>>> my_unicode_escape
'\\u4e2d\\u6587'
>>> my_unicode.encode("unicode-escape")
'\\u4e2d\\u6587'
>>> my_unicode_escape.decode("unicode-escape")
u'\u4e2d\u6587'
#string-escape
>>> my_str="中文"
>>> my_str_escape="\\xe4\\xb8\\xad\\xe6\\x96\\x87"
>>> my_str
'\xe4\xb8\xad\xe6\x96\x87'
>>> my_str_escape
'\\xe4\\xb8\\xad\\xe6\\x96\\x87'
>>> my_str.encode("string-escape")
'\\xe4\\xb8\\xad\\xe6\\x96\\x87'
>>> my_str_escape.decode("string-escape")
'\xe4\xb8\xad\xe6\x96\x87'
#raw-unicode-escape
>>> my_str1=u"\xe4\xb8\xad\xe6\x96\x87"
>>> my_str2="中文"
>>> my_str1
u'\xe4\xb8\xad\xe6\x96\x87'
>>> my_str2
'\xe4\xb8\xad\xe6\x96\x87'
>>> my_str1.encode("raw-unicode-escape")
'\xe4\xb8\xad\xe6\x96\x87'
>>> my_str2.decode("raw-unicode-escape")
u'\xe4\xb8\xad\xe6\x96\x87'

参考文献

[1] http://python.jobbole.com/82107/
[2] http://blog.csdn.net/trochiluses/article/details/16825269
[3] https://www.zhihu.com/question/31833164
[4] http://blog.csdn.net/xw_classmate/article/details/51933904
[5] https://www.python.org/dev/peps/pep-0263/
[6] http://blog.csdn.net/u012005313/article/details/48031281
[7] https://blog.ernest.me/post/python-setdefaultencoding-unicode-bytes
[8] http://blog.csdn.net/heybob/article/details/48373021
[9] http://python.usyiyi.cn/translate/python_278/reference/lexical_analysis.html
[10] http://kuanghy.github.io/2017/02/24/python-str-to-unicode-escape
[11] http://www.qmailer.net/archives/251.html
[12] http://blog.csdn.net/lrz4119/article/details/45247611
[13] https://mozillazg.github.io/2013/12/python-raw-unicode.html
[14] https://www.cnblogs.com/long2015/p/4090824.html
[15] http://python.jobbole.com/81244/
[16] https://www.2cto.com/kf/201411/355112.html
[17] http://www.cnblogs.com/harrychinese/archive/2012/01/19/change_python_default_encoding.html
[18] https://neo1218.github.io/reload/
[19] https://www.the5fire.com/why-need-reload-sys.html
[20] http://python-china.org/t/973
[21] http://www.jianshu.com/p/9686bfa7e81c
[22] http://blog.chinaunix.net/uid-29655675-id-4354608.html
[23] http://python-china.org/t/849
以上为本文的全部参考文献,对原作者表示感谢。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值