Unicode in Python

用Python的时候,为下面这样的报错苦恼?

UnicodeDecodeError: ‘ascii’ codec can’t decode …… …… in position 10: ordinal not in range(128)……

嗯哼,本座在用lxml抓网页回来进行解析的时候,也遇到了类似的错误。从解决问题的过程来看,其实Python 2对unicode的支持已经很好了。你首先需要知道Unicode只是一种概念而不是一种实现(把字符表示到内存或者文件里面)。如果你还不清楚基本概念,可以先学习一下。然后,我们只需要了解python具体实现的一些细节:

encode/decode

在Python2中,有两种字符对象,str和unicode。你可以用type函数查看字符串对象

<<type 'basestring'> | +--<type 'str'> | +--<type 'unicode'>

str和unicode通过encode和decode方法可以互相转换(要确保encodin的正确)

s.decode(encoding)     <type 'str'> to <type 'unicode'>

u.encode(encoding)    <type 'unicode'> to <type 'str'>

完毕,python里面的unicode使用,就这么多点知识。

Debug Part I 单纯字符串操作

本座的开发工具是Eclipse3.3.1+pydev+python2.5,操作系统windows xp sp4。首先为了排除是开发环境的问题,写了一个utf8test.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# -*- coding: utf-8 -*-
"""
unicodetest.py
 
test if dev enviroment is ok
"""
if __name__ == '__main__':
    import sys
    reload(sys)
    sys.setdefaultencoding('utf8')
    ss ="全部"
    uu = u'全部
    CODEC = 'utf-8'
    FILE = 'unicodetest.html'
 
    f = open('archive.html', "r")
    bytes_out = f.read().decode(CODEC)
    bytes_in = bytes_out.encode(CODEC)
    f = open(FILE, "w")
    f.write(bytes_in)
    f.close()
 
    print repr(ss)
    print repr(uu)
 
    print("-------------------------------")
    print ss.decode(CODEC )
    print uu.encode(CODEC )
 
    print("-------------------------------")
    print repr(ss.decode(CODEC).encode('gbk'))
    print uu

这段程序里面,有三个地方是跟编解码有关的。

1. 声明代码用utf-8编码保存:因为我们的代码里面有中文。

# -*- coding: utf-8 -*-

这个声明必须在最开始的两行,在后面就没有用了。

2. 指明在console显示中sys的编码

import sys
reload(sys)
sys.setdefaultencoding(‘utf8′)

如果你的程序不需要在console打印中用utf-8编码,这个声明不必要(比如上面程序里没有那些print,只是写内容到文件的话)。

如果你指定了sys的encoding,但是在所用的console(如这里的Eclipse)里面没有设置成一致的选项,还是会报错。

console

3. 对字符串进行的编码解码

这里我们分别打印了str对象和unicode对象,并对它们进行了一些转换操作。程序的输出是这样的:

output1

可以看到,一切正常。python没有问题,本座的环境也是正常的。

很多的人在网上发帖的时候常说我在源文件加了coding: utf-8声明了,我的sys设置了defaultencoding了,我的console配置成xxx了,甚至还用了codec模块,还是乱码了。其实,是没有搞清楚这些步骤究竟是干啥用的表现。比如在很多地方本座都看到高手指导别人设置sys的编码。其实绝大多数的应用程序是不需要打印什么东西到console的,这样的声明反而会让你的程序在一些python安装包下面变得不可用。

Debug Part II lxml解析HTML

能够正常的打开和保存utf-8文件,那么错误可能就是出在lxml解析网页的过程中。本座一开始直接用了lxml.html里面那个parse方法,因为这个方法看起来很简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import urllib2
import lxml.html as H
from lxml.html.clean import Cleaner
 
if __name__ == '__main__':
    FILE = 'htmltest.html'
    stringUrl = 'http://lenciel.ycool.com/archive.html'
    req = urllib2.Request(stringUrl)
    req.add_header('User-agent', 'Ugrah/0.1')
    site = urllib2.urlopen(req)
    doc = H.parse(site)
    bytes_in = H.tostring(doc, pretty_print=True,encoding='utf-8')
    print(repr(bytes_in))
    f = open(FILE, "w")
    f.write(bytes_in)
    f.close()

但是这样在保存在本地的中文页面就会是乱码:

output2

代码打印了bytes_in的保存方式,我们可以看到“全部”这两个汉字的编码是:

output3

原来在序列化的时候,虽然指定了encoding是utf-8,但是两个汉字不知道为什么居然编出来了12个byte。本座也难得去下序列化的源代码看里面究竟做了什么操作。反正lxml提供了一个从字符串里面解析出html对象树的方法,叫做document_fromstring。所以把自己知道格式的字符串传进去让它解析就对了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import urllib2
import lxml.html as H
from lxml.html.clean import Cleaner
 
if __name__ == '__main__':
 
    FILE = 'htmltest.html'
    stringUrl = 'http://lenciel.ycool.com/archive.html'
    req = urllib2.Request(stringUrl)
    req.add_header('User-agent', 'Ugrah/0.1')
    site = urllib2.urlopen(req).read()
 
    doc = H.document_fromstring(site.decode('utf-8'))
 
    for child in doc:
        print(child.tag)
    bytes_in = H.tostring(doc, pretty_print=True,encoding=unicode)
    print cleaner.clean_html(bytes_in).encode('utf-8')
    f = open(FILE, "w")
    f.write(bytes_in.encode('utf-8'))
    f.close()

总结

1. 处理任何编解码问题时我们都要牢记,unicode是为世界上所有的字符分配了一个码位(code point)的概念,而不是实现(字符在内存或者文件中的存在方式)。unicode占16位是绝对错误的(世界上语言如此多,码位早就超过百万个了)。

2. 要对unicode对象进行保存或者打印前,你要对它进行编码(encode)才行。

3. 在python里面把str转化成unicode的操作时,如果你知道str的编码方式,显式的指定它。如果你不知道,python会试着去自动完成。这是很多第三方moudle出现编解码问题的根本原因。

4. 不要因为解决这样的问题随便使用sys.setdefaultencoding(‘utf-8′)设定系统的编码方式。这样有可能造成你的软件在别的平台上不能使用。

5. 正确的做法是,尽量早正确的decode一个str为unicode对象(如读入一个文件的内容,返回一个网页的内容等),并在你的程序里面全部使用unicode相关操作,直到你需要打印或者是写入文件时,再去encode它。

6. python提供了codec来减少我们的代码行数,它不是你乱码的救星:

f = open(‘small.html’, "r")
bytes_in=f.read()
unicode_in=bytes_in.encode(utf-8)

===> fileObj = codecs.open( "small.html", "r", "utf-8" )

7. BOM这东西对UTF-16和UTF-32(python不支持)是很关键的,但是对UTF-8而言可有可无,因为后者不需要大小端对齐(详情请看这里)。BOM在windows平台上见到得较多,长度2个bytes到4个bytes不等,codec提供了方法检验BOM:

sample.startswith(codecs.BOM_UTF16_LE)
sample.startswith(codecs.BOM_UTF16_BE)
sample.startswith(codecs.BOM_UTF8)

有时候我们是从文件读入内容进行解码,需要去除BOM部分。UTF-16的格式,python会自动去除BOM,UTF-8格式的需要显式调用:

s.decode(‘utf-8-sig’)

8. 文件或者网页使用的编码方式还没有很完美的方法进行检测。文件的话从BOM判断算是一个不错的选择。网页的话先查看header里面的Content-Type内容。另外,还有一个工具也可以试试。

9. 有些第三方库如果没有支持unicode功能的话,你要自己重写一部分wrapper。自己写的代码,在ut的时候一定要用unicode进行测试。



  1. learner 11.19.08 / 2下午

    :cheerful: …膜拜一下,我就是搞不清楚coding: utf-8声明,sys设置defaultencoding,console配置成gbk之类的啥是啥用途的人。

    lenciel

    :tongue:

  2. 66 2.14.09 / 8上午

    רҵ

  3. Rocky 3.23.09 / 1下午

    :angel:
    博主的操作系统主题好漂亮哎
    我也想要用用看。。

    lenciel

    又见到牛人一枚…看这么两张截图就看出操作系统theme了… :lol:

  4. 匿名 6.15.09 / 7下午

    不错,对编码解码问题,又有一些理解。

    lenciel

    :smile:

  5. oldma 7.9.09 / 8下午

    :shock: 像博主一样,是我的目标
    虽然没全看明白,至少懂了点,在此谢过

    lenciel

    np…have fun

  6. gcd0318 11.7.09 / 2上午

    unicode是可变长度的编码方式,常见的有单、双、三字节的编码长度,所以出现12字节也不奇怪,可能和具体的字有关。另外,排序的时候尤其注意,因为区分一/二级汉字,所以不是总按拼音顺序在排。再就是,gbk/2312/utf8之间,虽然有子集关系,但是code point不完全吻合,所以,传输上没问题,但是decode出来的东西,未必是一回事,也要提防

    lenciel

    感谢补充,不过我觉得,“unicode是可变长度的编码方式,常见的有单、双、三字节的编码长度,所以出现12字节也不奇怪,可能和具体的字有关。”是不是有点华丽了?

    两个汉字12字节,还是足够奇怪了,这个跟Unicode里面单、双、三字节编码好像没太大关系,不知道四爷是怎么推理出来的…我倒是觉得12个byte里面那些xc2有些可疑。这个肯定是和实现有关的,不是和具体的字有关。

Have your say

:want: :w00t: :tongue: :thrwon: :smile: :sick: :shock: :sad: :ninja: :getlost: :ermm: :dizzy: :devil: :cool: :boring: :blush: :blink: :angry: :angel:



Safari hates me


Douban Update

Get the Flash Player to see the slideshow.
去看看吧