在上一章中,我们构建了一个爬虫,可以通过跟踪链接的方式下载我们所需的网页。虽然这个例子很有意思,却不够实用,因为爬虫在下载网页之后又将结果丢弃了。现在,我们需要让这个爬虫从每个网页中抽取一些数据,然后实现某些事情,这种做法也被称为抓取(scraping)。
首先,我们会介绍了一个叫做Firebug Lite的浏览器扩展,用于检查网页内容,如果你有一些网络开发背景的话,可能已经对该扩展十分熟悉了。然后,我们会介绍了三种抽取网页数据的方法,分别是正则表达式/Beautiful Soup和lxml。最后,我们将对比这三种数据抓取方法。
分析网页
想要了解一个网页的结果如何,可以使用查看源代码的方法。在大多数浏览器中,都可以在页面上右键单击选择View page source选项,获取网页的源代码。
我们可以在HTML的代码中找到我们感兴趣的数据。
三种网页抓取方法
现在我们已经了解了该网页的结果,下面将要介绍三种抓取其中数据的方法。首先是正则表达式,然后时流行的BeautifulSoup模块,最后是强大的lxml模块。
正则表达式
如果你对正则表达式还不熟悉,或是需要一些提示时,可以查阅https://docs.python.org/2/howto/regex.html获得完整介绍。
下例为正则抓取国土面积
import urllib.request
import re
def scrape(html):
html = html.decode('utf-8')
area = re.findall('<tr id="places_area__row">.*?<td\s*class=["\']w2p_fw["\']>(.*?)</td>', html)[0]
return areahtml.decode('utf-8')
if __name__ == '__main__':
html = urllib.request.urlopen('http://example.webscraping.com/view/United-Kingdom-239').read()
print(scrape(html))
获得结果244,820 square kilometres
。
正则表达式为我们提供了抓取数据的快捷方式,但是该方法过去脆弱,容易在网页更新后出现问题。
Beautiful Soup
Beautiful Soup是非常流行的Python模块。该模块可以解析网页,并提供定位内容的便捷接口。如果你还没有安装模块,可以使用下面的命令安装其最新版本:pip install beautifulsoup4
使用Beautiful Soup的第一步是将已下载的HTML内容解析为soup文档。由于大多数网页都不具备良好的HTML格式,因此Beautiful Soup需要对其实际格式进行确定。例如,在下面这个简单网页的列表中,存在属性值两侧引号缺失和标签未闭合的问题。
<ul class=country>
<li>Area
<li>Population
</ul>
如果Population列表项被解析为Area列表项的子元素,而不是并列的两个列表项的话,我们在抓取时就会得到错误的结果。下面让我们看一下Beautiful Soup 是如何处理的。
>>> from bs4 import BeautifulSoup
>>> broken html = ’ <ul class = country> <li>Area<li>Population</ul> ’
>>> # parse the HTML
>>> soup = BeautifulSoup(broken_html , ’ html.parser’)
>>> fixed html = soup.prettify( )
>>> print(fixed_html)
<html>
<body>
<ul class = ” country ”>
<li>Area</li>
<li>Population</li>
</ul>
</body>
</html>
从上面的执行结果中可以看出,Beautiful Soup能够正确解析缺失的引导关闭合标签,此外还添加了<html>
和<body>
标签使其成为完整的HTML文档。现在可以使用find()和find_all()方法来定位我们需要的元素了。
>>> ul = soup.find('ul', sttrs=('class', 'country'))
>>> ul.find('li') # returns just the first match
<li>Area</li>
>>> ul.find_all('li') # returns all matches
[<li>Area</li>, <li>Population</li>]
下面是使用该方法抽取示例国家面积数据的完整代码
import urllib.request
from bs4 import BeautifulSoup
def scrape(html):
soup = BeautifulSoup(html, "lxml")
tr = soup.find(attrs={'id': 'places_area__row'}) # 找到区域行
# 'class'是一个特殊的Python属性,所以使用‘class_’替换
td = tr.find(attrs={'class':'w2p_fw'}) # 找到区域标签
area = td.text # 从该标签提取区域内容
return area
if __name__ == '__main__':
html = urllib.request.urlopen('http://example.webscraping.com/view/United-Kingdom-239').read()
print(scrape(html))
输出为244,820 square kilometres
Lxml
Lxml是基于libxml2这一XML解析库的Python封装。该模块使用C语言编写,解析速度比Beautiful Soup更快,不过安装过程也更复杂。最新的安装说明可以参考http://Lxml.de/installation.html。
和Beautiful Soup一样,使用lxml模块的第一步也是将有可能不合法的HTML解析为统一格式。下面是使用该模块解析同一个不完整的HTML的例子。
>>> import lxml.html
>>> broken html = ’<ul class=country><li>Area<li>Population</ul>’
>>> tree = lxml.html.fromstring(broken_ html) #parse the HTML
>>> fixed html = lxml.html.tostring( tree , pretty_print=True )
>>> print(fixed_html)
<ul class=”country">
<li>Area</li>
<li>Population</li>
</ul>
同样地,lxml也可以正确解析属性两侧缺失的引号,并闭合标签,不过该模块没有额外添加<html>和<body>标签。
解析完输入内容之后,进入选择元素的步骤,此时lxml有几种不同的方法,比如XPath选择器和类似Beautiful Soup的find()方法。不过,在本例和后续示例中,我们将会使用CSS选择器,因为它更加简洁,并且能够在解析动态内容时得以复用。此外,一些拥有jQuery选择器相关经验的读者也会对其更加熟悉。
下面是使用lxml的CSS选择器抽取面积数据的示例代码。
import urllib.request
import lxml.html
def scrape(html):
html = html.decode('utf-8')
tree = lxml.html.fromstring(html)
td = tree.cssselect('tr#places_area__row > td.w2p_fw')[0]
area = td.text_content()
return area
if __name__ == '__main__':
html = urllib.request.urlopen('http://example.webscraping.com/view/United-Kingdom-239').read()
print(scrape(html))
CSS选择器首选会找到ID为places_area__row的表格行元素,然后选择class为w2p_fw的表格数据子标签。
需要注意的是,lxml在内部实现中,实际上是将CSS选择器转换为等价的XPath选择器。
如果你的爬虫瓶颈时下载网页,而不是抽取数据的话,那么使用较慢的方法(如Beautiful Soup)也不成问题。如果只需抓取少量数据,并且想要避免依赖的话,那么正则表达式可能更加适合。不过,通常情况下,lxml是抓取数据的最好选择,这是因为该方法即快速又健壮,而正则表达式和Beautiful Soup只在默写特定场景下有用。
为链接爬虫添加抓取回调
我们将抓取的国家数据集成到链接爬虫当中,要想复用这段爬虫代码抓取其他网站,我们需要添加一个callback参数处理抓取行为。callback是一个函数,在发生某个特定事件之后调用该函数(在本例中,会在网页下载完成后调用)。该抓取callback函数包含url和html两个参数,并且可以返回一个待爬取的URL列表。下面是添加回调之后的链接爬虫。
def link_crawler(seed_url, link_regex=None, delay=5, max_depth=-1,
max_urls=-1, headers=None, user_agent='wswp', proxy=None,
num_retries=1, scrape_callback=None):
"""从指定的种子网址按照link_regex匹配的链接进行抓取"""
crawal_queue = queue.deque([seed_url]) # 仍然需要抓取的网址队列
seen = {seed_url: 0} # 已经看到的网址以及深度
num_urls = 0 # 跟踪已下载了多少个URL
rp = get_robots(seed_url)
throttle = Throttle(delay)
headers = headers or {}
if user_agent:
headers['User-agent'] = user_agent
while crawal_queue:
url = crawal_queue.pop()
depth = seen[url]
# 检查网址传递的robots.txt限制
if rp.can_fetch(user_agent, url):
throttle.wait(url)
html = download(url, headers, proxy=proxy, num_retries=num_retries)
links = []
if scrape_callback:
links.extend(scrape_callback(url, html) or [])
if depth != max_depth:
# 仍然可以进一步爬行
if link_regex:
# 过滤符合我们的正则表达式的链接
links.extend(link for link in get_links(html) if re.match(link_regex, link))
for link in links:
link = normalize(seed_url, link)
# 检查是否已经抓取这个链接
if link not in seen:
seen[link] = depth + 1
# 检查链接在同一域内
if same_domain(seed_url, link):
# 成功! 添加这个新链接到队列里
crawal_queue.append(link)
# 检查是否已达到下载的最大值
num_urls += 1
if num_urls == max_urls:
break
else:
print("Blocked by robots.txt:", url) # 链接已被robots.txt封锁
在上面的代码片段中,我们增加了抓取callback函数代码。
现在,我们只需要传入的scrap_callback函数定制化处理,就能使用该爬虫抓取其他网站了。下面对lxml抓取示例的代码进行了修改,使其能够在callback函数中使用,并且我们将得到的结果保存到CSV表格中。
import csv
import re
import urllib.parse
import lxml.html
from link_crawler import link_crawler
class ScrapeCallback:
def __init__(self):
self.writer = csv.writer(open('countries.csv', 'w'))
self.fields = ('area', 'population', 'iso', 'country',
'capital', 'continent', 'tld', 'currency_code',
'currency_name', 'phone', 'postal_code_format',
'postal_code_regex', 'languages', 'neighbours')
self.writer.writerow(self.fields)
def __call__(self, url, html):
if re.search('/view/', url):
tree = lxml.html.fromstring(html)
row = []
for field in self.fields:
row.append(tree.cssselect('table > tr#places_{}__row > td.w2p_fw'.format(field))[0].text_content())
self.writer.writerow(row)
if __name__ == '__main__':
link_crawler('http://example.webscraping.com/', '/(index|view)', scrape_callback=ScrapeCallback())
为了实现该callback,我们使用了会掉泪,而不在是回调函数,以便保持csv中writer属性的状态。csv的writer属性在构造方法中进行了实例化处理,然后在__call__
方法中执行了多次写操作。请注意:__call__
是一个特殊方法,在对象作为函数被调用时会调用该方法,这也是链接爬虫中的cache_callback
的调用方法。也即是说,scrape_callback(url, html)
和调用scrape_callback.__call__(url html)
是等价的。
现在,当我们运行这个使用了callback的爬虫时,程序就会将结果写入一个CSV文件中,我们可以使用类似Excel或者LibreOffice的应用查看该文件。
网友评论