python中的字符编码

By | 11/09/2013
这篇文章写的

这段时间在接触Hadoop的stream MapReduce,结果用python的时候出现了各种奇奇怪怪的编码问题,比如使用pymongo操作数据库数据时,将值为中文的title字段调试打印时,在本地测试时正常,但当用crontab运行,并重定向stderr/stdout时,却频繁抛出编码错误。

 

阅读这篇文章建议先掌握unicode与字符编码
(如无特别说明,均指Python 2.7.3)

(Python)[cipher@Rose ~]$ python --version
Python 2.7.3

在python中,序列化类型包括几种:str, unicode, list, tuple, bytearray, buffer, xrange等。这里我主要讲讲strunicode.

在python中,字符串(string)包含两种类型:

  • str         使用单引号或双引号包含,如'你好', "world"
  • unicode     在字符串前加一个u"你好", u"world"

并且,str object 和 unicode object 是两种不同的对象。str 对象是由字符组成的序列,而unicode 对象是由Unicode单元组成的序列。
str 对象里的字符是有多种编码方式的(ASCII/GB2312/UTF-8等),要想解读str,必须首先知道str里的字符是用什么编码方式编码的。 而unicode对象呢?每一个unicode 单元是一个16-bit或32-bit的数值。(在python中,16-bit的unicode对应的是ucs2编码的,32-bit的unicode 对应的是ucs4编码的)
除了unicode对象的字符串,其它的字符串我们都称之为str对象。
可以理解为unicode是某个字符的全球唯一的表达,不带有任何编码信息
那么,这两者有什么不同呢?显而易见的区别就是unicode的字符串前面多了一个'u'字符。

>>> s = '你好'
>>> s
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>> u = u'你好'
>>> u
u'\u4f60\u597d'
>>> len(s)
6
>>> len(u)
2
>>> isinstance(s, str)
True
>>> isinstance(u, unicode)
True

由此,我们可以断定,

  1. unicode字符串在字符串前面有一个'u'字符,以表明它是unicode的字符串。
  2. len(str)返回的是str的字节数,len(unicode)返回的是unicode的字符

Unicode(basedstring, encoding) String在io中使用,还有呢?。
json.loads(stringReadFromFile) 会自动编码成unicode。
The principle is that when str and unicode instances meet, the result is a unicode instance.
Allow str() to return unicode strings [http://www.python.org/dev/peps/pep-0349/]

下面再来讲讲老生长谈的问题,unicode对象和str对象之间如何进行转换。

依旧是先明确概念,概念总是在闭幕时才被抽象出来,理解后再看又发现概念如此的简洁。(这个过程就像是用推论去证明假设,总是正确的 🙂 )

  • 编码    unicode -> str
  • 解码    str -> unicode

时刻记住,utf-8 为 unicode的一种实现。

[cipher@Eleanor ~]$ python
Python 2.7.3 (default, Aug 9 2012, 17:23:57) [GCC 4.7.1 201
20720 (Red Hat 4.7.1-5)] on linux2 Type "help", "copyright", "credits" or "license" for more information.
>>> from chardet import detect
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
# python cli 默认的编码方式

>>> a = "中国"
>>> a
'\xe4\xb8\xad\xe5\x9b\xbd'
>>> detect(a)
{'confidence': 0.7525, 'encoding': 'utf-8'}
>>> isinstance(a, str)
True 
# a 为utf-8编码的字符串(str类型)

>>> b = unicode(a, "utf-8")
>>> b
u'\u4e2d\u56fd'
# 将a转换成unicode对象,需要知道str对象是如何编码的才能转换成功。
>>> detect(b)
{'confidence': 1.0, 'encoding': 'ascii'}
>>> isinstance(b, unicode)
True
# b为unicode字符串

>>> a.encode("utf-8")
Traceback (most recent call last): File "", line 1, in UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>> b.encode("utf-8")
'\xe4\xb8\xad\xe5\x9b\xbd'
# str对象已经经过编码,不能再进行编码
# unicode对象可以进行编码

>>> a.decode("utf-8")
u'\u4e2d\u56fd'
>>> b.decode("utf-8")
Traceback (most recent call last): File "", line 1, in File "/home/note/Development/Python/lib64/python2.7/encodings/utf_8.py", line 16, in decode return codecs.utf_8_decode(input, errors, True) UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
# 可以将str类型先进行解码,转换成unicode类型
# 而unicode类型不包含任何编码,不能解码

>>> str(a)
'\xe4\xb8\xad\xe5\x9b\xbd'
>>> str(b)
# 在你的python shell 中试试:help(str)
# str()返回对象的str对象表示。如果对象本身就是一个str,返回该对象本身。

>>> unicode(a)
Traceback (most recent call last): File "", line 1, in UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>> unicode(b)
u'\u4e2d\u56fd'

# 在你的python shell 中试试:help(unicode)
# unicode()从指定编码的string类型对象,返回一个unicode对象。
# 关键字参数encoding,若未指定,默认使用当前编码(文件/当前环境指定)。
## 文件:默认为“ascii”编码,当文件中出现非"ascii"编码时,需要手工指定文件的编码方式。
## cli:通过sys.getdefaultencoding()查看。
>>> sys.getdefaultencoding()
'ascii'

>>> a.encode('utf-8')
Traceback (most recent call last): File "", line 1, in UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
# str.encode(“utf-8”) 会先将str以默认的编码方式解码(这里为"ascii")
# ascii并不是unicode的编码形式之一。所以无法进行这种类型的转换。

>>> sys.setdefaultencoding("utf-8")
# 将cli的默认编码设为"utf-8"
>>> sys.getdefaultencoding()
'utf-8'
>>> a.encode("utf-8")
'\xe4\xb8\xad\xe5\x9b\xbd'
# 已将默认编码设为"utf-8",再次走一遍str.encode()的流程,先按默认解码,再按指定编码。
>>> len(a)
6
>>> a.decode("utf-8")
u'\u4e2d\u56fd'
>>> a.decode("utf-8") == b
True
# 留个小疑问,如果对unicode.decode("utf-8")会发生什么事呢?

总结

  1. str()unicode()并不是互相转换对应的函数。
  2. str()/unicode()对于各自的类型,不会抛出异常。
  3. str()调用对象的.__str__()方法。
  4. str.encode()sys.getdefaultencoding()先将str解码 再按指定/默认的编码方式编码 str.decode() 按指定的默认/解码方式解码。
  5. unicode.encode() 按指定的默认/解码方式编码 unicode.decode()sys.getdefaultencoding()先将unicode编码 再按指定/默认的编码方式解码。
  6. 在程序内部,总是使用unicode编码字符串。对所接收的字符串进行解码,对所发送的字符串进行编码。
  7. pymongo查询出的数据默认为unicode编码。

Encoding of Python stdout 中也可以看到,“python 根据 LC_CTYPE 的值决定 stderr/stdout输出的编码方式,但stdout必须为tty。如果输出到终端(terminal),LC_CTYPE(或者 LC_ALL)会决定编码方式。 然而,当输出是以管道(pipe)方式输出到文件或者其它进程时,编码是未定义的。默认是‘ascii’”。
至此,已经可以解决开篇提出的疑问了:为什么直接输出到stderr/stdout没有任何问题,而对stderr/stdout重定向到文件却会抛出异常了。

题外话:
事实上,python2.7.3版本中,已经决定了stdout/stderr的编码方式,可以通过sys模块查询:

>>> sys.stdout.encoding
'UTF-8'
>>> sys.stderr.encoding
'UTF-8'

但当python的输出编码方式不对时,我们却不能指定其编码方式。
>>> sys.stderr.encoding = "utf-8"
Traceback (most recent call last): File "
", line 1, in TypeError: readonly attribute

TODO: python re unicode

Reference
setting-the-correct-encoding-when-piping-stdout-in-python
Encoding_of_Python_stdout
howto/ unicode
diveintopython unicode

发表评论

电子邮件地址不会被公开。 必填项已用*标注