py 爬中文网页,总是遭遇 UnicodeEncodeError

2015-01-17 17:12:01 +08:00
 KyL

觉得《码农周刊》挺有意思的,想写个脚本把内容抓下来。

import urllib
import httplib
def get_html_content(url):
    response = urllib.urlopen(url)
    html = response.read()
    print type(html)
    return html


if __name__ == '__main__':
    url = 'http://weekly.manong.io/issues/58'
    html = get_html_content(url)
    print html.decode('utf-8')

结果报错误

<type 'str'>
Traceback (most recent call last):
  File "E:\src\infra.py", line 32, in <module>
    print html.decode('utf-8')
UnicodeEncodeError: 'ascii' codec can't encode character u'\u201c' in position 44: ordinal not in range(128)
[Finished in 1.6s]

我真的查了许多,像是http://nedbatchelder.com/text/unipain.html 通读一遍,结果怎么都搞不定,求解。

6205 次点击
所在节点    Python
24 条回复
ihciah
2015-01-17 17:23:12 +08:00
我这里正常…不过是用的urllib2.urlopen
KyL
2015-01-17 17:29:03 +08:00
@ihciah 你是PY2还是PY3?
liqiu
2015-01-17 17:34:29 +08:00
aaaa007cn
2015-01-17 19:16:11 +08:00
如果只考虑这个场合
print html.decode('utf-8').encode('gb18030')
Delbert
2015-01-17 19:25:26 +08:00
.decode("utf-8", "ignore")
这样试试呢?
9hills
2015-01-17 19:39:44 +08:00
程序没问题,出问题的是你的环境。直接print Unicode的话,如果在Linux or OSX的默认UTF-8环境,是没有问题的。但是在Windows上就呵呵(4楼是windows上的解决办法)

两点:
1. 不要学3楼的reload sys哈,不是解决问题的办法。你reload前你懂它到底做了什么么,不懂的话就别写自己不懂的代码,如果你懂的话,那也不需要这个reload的办法
2. 直接输出Unicode是不好的行为,输出的时候应该encode下的,环境是utf-8就encode成utf-8,环境是gbk就encode成gbk
9hills
2015-01-17 19:42:12 +08:00
处理编码的主要一个原则,就是让程序的默认隐式转换变成显式的。

比如所有中文都写成 u"中文" 这样的Unicode,而不是让它的编码随文件编码变化而变化
比如输出的时候显式转换编码,而不是依赖系统的编码转换。。
pyKun
2015-01-17 21:02:33 +08:00
@9hills

处理编码的主要一个原则,就是让程序的默认隐式转换变成显式的。

+1
lerry
2015-01-17 21:39:23 +08:00
@KyL
我理解的,输出之前encode,处理之前decode,

另外,这样写,print的时候不会出错
print u"你妹".encode(sys.stdout.encoding)
lyhapple
2015-01-17 21:40:09 +08:00
用python的requests库试试.
pip install requests
9hills
2015-01-17 21:43:20 +08:00
@lerry 恩,这样适合写通用的CLI系统,可以兼容多种编码环境
icedx
2015-01-17 21:47:12 +08:00
print html.decode('utf8').encode('gbk','backslashreplace')
因为楼主用的是Windows
endoffight
2015-01-17 22:12:02 +08:00
建议安装chardect模块确定编码后再决定转不转码
EPr2hh6LADQWqRVH
2015-01-17 22:15:34 +08:00
不要assume数据的编码,拿到手保持Bytes的形态,然后自己尝试解码
Sylv
2015-01-18 13:44:25 +08:00
你这个问题其实是和 Sublime Text 有关的,我刚好在总结一篇相关的 Unicode 编码问题的分析,写好会分享出来。

而针对你这个问题的短答案是:
你在最后将 str 类型的 html decode 为了 unicode 类型的字符串,然后想要打印 print 出这个 unicode 字符串时,Python 又需要将其转换为 str 后才能输出。在正常情况下,Python 能判断出正确的输出编码来进行转换而并不会出错,这就是前面有人说在他们的环境能正常运行的原因。而你会出现 UnicodeEncodeError 的原因是,因为某种原因,在 Sublime Text 里运行的 Python 不能判断出这个正确的输出编码,于是它就使用了它的默认编码 ascii 来进行转换,于是就出错了。
在 Sublime Text 里想要 print 出一个 unicode 类型的字符串,正确的方法是显式地进行 encode('utf-8') 后再 print。而为什么我说要用的是 utf-8 编码,而不是前面说的 gbk 等编码呢?因为默认配置下的 Sublime Text 所接收的默认输出编码是 utf-8,你用其它编码会出现 [Decode error - output not utf-8] 错误。而如果你用 cmd 运行的话,可能就要用 gbk 等编码了。
而针对这种情况下的你这段代码,其实最后没必要 .decode('utf-8') 一遍,因为 html 原本就是 utf-8 编码的 str 了,可以直接 print html 了,否则你需要多此一举 print html.decode('utf-8').encode('uff-8')。

关于这个问题的详细解释和其它解决方法,请等我的长答案。
KyL
2015-01-18 14:44:29 +08:00
@Sylv 我确实是在用Sublime Text调试Py2的时候遇到这个问题的。str只可以hold1Byte字符的串,怎么会有所谓utf-8编码的str呢。期待着你的长答案。
Sylv
2015-01-18 15:16:23 +08:00
@KyL str 存储的是 bytes 字节序列,不是只有 1 byte。它当然就有不同的编码,见下例,同样表示的是 “中文” 的 str 字符串,在不同编码下,其内部存储的字节序列是不同的:
>>> text = u'中文'
>>> text.encode('utf-8')
'\xe4\xb8\xad\xe6\x96\x87'
>>> text.encode('gbk')
'\xd6\xd0\xce\xc4'
KyL
2015-01-18 16:33:41 +08:00
@Sylv py2中有两种字符串类型,str和unicode。 text = u'中文' 是Unicode类型,不是str类型。str只能储存ascii类型的字符吧。
Sylv
2015-01-19 04:10:37 +08:00
@KyL 你概念理解有错

首先,Python 2 unicode 和 str 转换方法要弄清楚:
str.decode('utf-8') -> unicode
unicode.encode('utf-8') -> str

我上例中 text.encode('utf-8') 后已经是 str 了。

然后你理解错了,str 并不是只能存储 ascii 类型的字符。str 存储的是一个一个的字节数据,也可以说就是一个一个字节的数字。然后这些数字代表的是什么字符,要看你用什么编码去解读它。

例如现在一个 str 用一个字节存储了数字 97,那么你用 ascii 编码去解读它,那么这个 str 就是 'a'。

然后现在有一个 str 用三个字节存储了三个数字:235、184 和 173,连起来用 16 进制表示也就是 '\xe4\xb8\xad'。然后你也可以用 ascii 编码去解读它,查 ascii 表后可以发现这三个数字都不在基本的 128 位 ascii 里,而是在扩展表里,都是一些很奇怪的字符,可见这个 str 用 ascii 编码去解读它对我们来说没有意义。
但是我们换一个编码 'utf-8' 去解读它,这个 str 就变得有意义了,在 utf-8 编码里是用三个字节来存储一个汉字字符的,而不是像 ascii 编码一个字节就对应了一个字符。那么 235、184 和 173 这三个字节的数字在 utf-8 编码里对应的就是一个汉字字符的 “中”。你可以用以下方法验证:
>>> char = u'中'
>>> print type(char)
<type 'unicode'>
>>> char = char.encode('utf-8')
>>> print type(char)
<type 'str'>
>>> print repr(char) # repr 方法可以将对象在 Python 内部的存储形式表现出来
'\xe4\xb8\xad'
>>> print char

>>> print '\xe4\xb8\xad'


在 print 这个 str 类型的 char 时,Python 只是把那三个数字直接发送给了用来显示的控制台(console)。console 就是用来输出的地方,例如 Sublime Text 的运行结果窗口,还有 Windows 下的 cmd。
然后决定用什么编码去解读它,是由 console 来决定的。在 Sublime Text 下这个编码默认是 utf-8,它用 utf-8 去解读 235、184 和 173 这三个数字,发现是 “中” 字,那么它就会去字库里找出 “中” 字这个字形给我们显示出来,因此我们就能看到 “中” 了。
而在 cmd 下,它用来解读的编码就不是 utf-8 了,而是 gbk 之类的。那它用 gbk 编码去解读这三个数字,可能它能找到另一些对应的字符,也有可能它完全找不到对应的字符,这时就产生了显示出乱码的情况。如果你想让它显示出 “中”,那么你就要让 Python 发送给它 gbk 编码下 “中” 所对应的数字,也就是 214 和 208,写成 16进制就是 '\xd6\xd0'。

因此你在 print 的时候想要显示正常,要看你现在的输出 console 用的是什么编码,然后就要发送给它对应编码的 str。
我最开始所说 html 是 utf-8 编码的 str,意思就是 html 里存储的字节序列,就是你想要的网页源码文字在 utf-8 编码下对应的一个个的数字,所以我们可以说它是 utf-8 编码的 str,因为它在 ascii 和 gbk 等编码下是没有意义的。Python 将它送给 Sublime Text, Sublime Text 也用 utf-8 编码去解读它,最后就能显示出你能看得懂的网页源代码文字。
Sylv
2015-01-20 12:44:26 +08:00
@KyL 之前说的长答案 /t/163786

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/163012

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX