将网页数据传递给Beautiful Soup - 空列表
我检查了一遍我的代码,还对比了一些打开网址并把网页数据传给Beautiful Soup的操作,但不知道为什么我的代码就是不返回任何东西,尽管格式是正确的:
>>> from bs4 import BeautifulSoup
>>> from urllib3 import poolmanager
>>> connectBuilder = poolmanager.PoolManager()
>>> content = connectBuilder.urlopen('GET', 'http://www.crummy.com/software/BeautifulSoup/')
>>> content
<urllib3.response.HTTPResponse object at 0x00000000032EC390>
>>> soup = BeautifulSoup(content)
>>> soup.title
>>> soup.title.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'name'
>>> soup.p
>>> soup.get_text()
''
>>> content.data
a stream of data follows...
从上面可以看出,urlopen()返回了一个HTTP响应,这个响应被变量content捕获。这样理解是合理的,因为它可以读取响应的状态。但是,当这个响应传给Beautiful Soup后,网页数据并没有被转换成Beautiful Soup对象(变量soup)。你可以看到我尝试读取了一些标签和文本,get_text()却返回了一个空列表,这真是奇怪。
更奇怪的是,当我通过content.data访问网页数据时,数据是能显示出来的,但这没什么用,因为我无法用Beautiful Soup来解析它。我到底出了什么问题呢?谢谢。
4 个回答
我写的Beautiful Soup代码在一个环境下(我自己的电脑)运行得很好,但在另一个环境下(Ubuntu 14服务器)却返回了一个空列表。
我通过更改安装方式解决了这个问题。具体细节可以在另一个讨论中找到:
如图所示,urlopen() 返回的是一个 HTTP 响应,这个响应被存储在变量 content 中……
你所称的 content
其实并不是内容,而是一个像文件一样的对象,你可以从中读取内容。BeautifulSoup 可以处理这种对象,但直接打印出来对调试并没有太大帮助。所以,我们来实际读取一下内容,这样调试会更简单:
>>> response = connectBuilder.urlopen('GET', 'http://www.crummy.com/software/BeautifulSoup/')
>>> response
<urllib3.response.HTTPResponse object at 0x00000000032EC390>
>>> content = response.read()
>>> content
b''
这样应该能清楚地说明 BeautifulSoup
不是问题所在。不过继续往下看:
……但是在传入 Beautiful Soup 后,网页数据并没有转换成 Beautiful Soup 对象(变量 soup)。
其实是转换了的。你看到 soup.title
返回 None
而不是抛出 AttributeError
,这就很有说服力,但你也可以直接测试一下:
>>> type(soup)
bs4.BeautifulSoup
这绝对是一个 BeautifulSoup
对象。
当你给 BeautifulSoup
传入一个空字符串时,返回的内容会根据它使用的解析器而有所不同;如果它依赖于 Python 3.x 的标准库,你得到的将是一个 html
节点,里面有一个空的 head
和空的 body
,其他什么都没有。所以,当你查找 title
节点时,它并不存在,因此你得到 None
。
那么,怎么解决这个问题呢?
正如文档所说,你正在使用“最低级别的请求调用,所以你需要指定所有的原始细节。”这些原始细节是什么呢?老实说,如果你还不知道这些,你就不应该使用这个方法。在你连基础都不懂的情况下教你如何处理 urllib3
的底层细节,并不是在帮你。
实际上,你在这里根本不需要 urllib3
。只需使用 Python 自带的模块:
>>> # on Python 2.x, instead do: from urllib2 import urlopen
>>> from urllib.request import urlopen
>>> r = urlopen('http://www.crummy.com/software/BeautifulSoup/')
>>> soup = BeautifulSoup(r)
>>> soup.title.text
'Beautiful Soup: We called him Tortoise because he taught us.'
urllib3会返回一个响应对象,这个对象里面有一个叫.data
的部分,里面存的是已经加载好的内容。
根据上面这个快速入门的使用示例,我会这样做:
import urllib3
http = urllib3.PoolManager()
response = http.request('GET', 'http://www.crummy.com/software/BeautifulSoup/')
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.data) # Note the use of the .data property
...
其他部分应该能正常工作。
--
关于你原来代码出错的原因:
你传递了整个response
对象,而不是里面的内容。通常这样做是没问题的,因为response
对象像一个文件一样可以读取,但在这个情况下,urllib3已经把响应内容全部读取并解析了,所以里面没有东西可以再用.read()
去读取了。这就像你传递了一个已经读过的文件指针。而.data
则可以访问已经读取的数据。
如果你想把urllib3的响应对象当作文件来用,你需要关闭内容的预加载,像这样:
response = http.request('GET', 'http://www.crummy.com/software/BeautifulSoup/', preload_content=False)
soup = BeautifulSoup(response) # We can pass the original `response` object now.
现在应该能按你预期的那样工作了。
我知道这不是很明显的行为,作为urllib3的作者我对此表示歉意。:) 我们计划将preload_content=False
设为默认选项,也许很快就会实现(我在这里开了个问题)。
--
关于.urlopen
和.request
的小提示:
.urlopen
假设你会自己处理请求中传递的参数编码。在这种情况下,使用.urlopen
是可以的,因为你没有传递任何参数,但一般来说,.request
会为你做更多的额外工作,所以使用起来更方便。
如果有人愿意帮助我们改进文档,那将非常感谢。:) 请向https://github.com/shazow/urllib3提交一个PR,并把自己添加为贡献者!
如果你只是想抓取网页内容,使用 requests
就可以获取你需要的内容:
from bs4 import BeautifulSoup
import requests
r = requests.get('http://www.crummy.com/software/BeautifulSoup/')
soup = BeautifulSoup(r.content)
In [59]: soup.title
Out[59]: <title>Beautiful Soup: We called him Tortoise because he taught us.</title>
In [60]: soup.title.name
Out[60]: 'title'