返回介绍

3.4 抽取更多的 URL

发布于 2024-01-30 22:48:37 字数 8202 浏览 0 评论 0 收藏 0

到目前为止,我们使用的只是设置在爬虫的start_urls属性中的单一URL。而该属性实际为一个元组,我们可以硬编码写入更多的URL,如下所示。

start_urls = (
  'http://web:9312/properties/property_000000.html',
  'http://web:9312/properties/property_000001.html',
  'http://web:9312/properties/property_000002.html',
)

这种写法可能不会让你太激动。不过,我们还可以使用文件作为URL的源,写法如下所示。

start_urls = [i.strip() for i in
open('todo.urls.txt').readlines()]

这种写法其实也不那么令人激动,但它确实管用。更经常发生的情况是感兴趣的网站中包含一些索引页及房源页。比如,Gumtree就包含了如图3.7所示的索引页,其地址为 http://www.gumtree.com/flats-houses/london

图3.7 Gumtree的索引页

一个典型的索引页会包含许多到房源页面的链接,以及一个能够让你从一个索引页前往另一个索引页的分页系统。

因此,一个典型的爬虫会向两个方向移动(见图3.8):

图3.8 向两个方向移动的典型爬虫

· 横向——从一个索引页到另一个索引页;

· 纵向——从一个索引页到房源页并抽取Item。

在本书中,我们将前者称为水平爬取,因为这种情况下是在同一层级下爬取页面(比如索引页);而将后者称为垂直爬取,因为该方式是从一个更高的层级(比如索引页)到一个更低的层级(比如房源页)。

实际上,它比听起来更加容易。我们所有需要做的事情就是再增加两个XPath表达式。对于第一个表达式,右键单击Next Page按钮,可以注意到URL包含在一个链接中,而该链接又是在一个拥有类名next的li标签内,如图3.9所示。因此,我们只需使用一个实用的XPath表达式//*[contains(@class,"next")]//@href,就可以完美运行了。

图3.9 查找下一个索引页URL的XPath表达式

对于第二个表达式,右键单击页面中的列表标题,并选择Inspect Element,如图3.10所示。

图3.10 查找列表页URL的XPath表达式

请注意,URL中包含我们感兴趣的itemprop="url"属性。因此,表达式//*[@itemprop="url"]/@href就可以正常运行。现在,打开一个scrapy shell来确认这两个表达式是否有效:

$ scrapy shell http://web:9312/properties/index_00000.html
>>> urls = response.xpath('//*[contains(@class,"next")]//@href').extract()
>>> urls
[u'index_00001.html']
>>> import urlparse
>>> [urlparse.urljoin(response.url, i) for i in urls]
[u'http://web:9312/scrapybook/properties/index_00001.html']
>>> urls = response.xpath('//*[@itemprop="url"]/@href').extract()
>>> urls
[u'property_000000.html', ... u'property_000029.html']
>>> len(urls)
30
>>> [urlparse.urljoin(response.url, i) for i in urls]
[u'http://..._000000.html', ... /property_000029.html']

非常好!可以看到,通过使用之前已经学习的内容及这两个XPath表达式,我们已经能够按照自身需求使用水平抓取和垂直抓取的方式抽取URL了。

3.4.1 使用爬虫实现双向爬取

我们将之前的爬虫拷贝到一个新文件中,并命名为manual.py。

$ ls
properties scrapy.cfg
$ cp properties/spiders/basic.py properties/spiders/manual.py

在properties/spiders/manual.py文件中,通过添加from scrapy.http import Request语句引入Request模块,将爬虫的name参数改为'manual',修改start_urls以使用第一个索引页,并将parse()方法重命名为parse_item()。好了!现在开始编写一个新的parse()方法,来实现水平和垂直两种抓取方式。

def parse(self, response):
  # Get the next index URLs and yield Requests
  next_selector = response.xpath('//*[contains(@class,'
                  '"next")]//@href')
  for url in next_selector.extract():
    yield Request(urlparse.urljoin(response.url, url))

  # Get item URLs and yield Requests
  item_selector = response.xpath('//*[@itemprop="url"]/@href')
  for url in item_selector.extract():
    yield Request(urlparse.urljoin(response.url, url),
           callback=self.parse_item)

你可能已经注意到了前面例子中的yield语句。yield与return在某种意义上来说有些相似,都是将返回值提供给调用者。不过,和return不同的是,yield不会退出函数,而是继续执行for循环。从功能上来说,前面的例子与下面的代码大体相当:

next_requests = [] for url in...   next_requests.append(Request(...)) for url in...   next_requests.append(Request(...)) return next_requests

yield是Python“魔法”的一部分,它可以使日常的高效编程工作更加轻松。

我们现在已经准备好运行该爬虫了。不过如果让该爬虫以当前的方式运行的话,则会抓取网站完整的5万个页面。为了避免运行时间过长,可以通过命令行参数:-s CLOSESPIDER_ITEMCOUNT=90,告知爬虫在爬取指定数量(如90个)的Item后停止运行(更多细节参见第7章)。现在,我们可以运行了。

$ scrapy crawl manual -s CLOSESPIDER_ITEMCOUNT=90
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Crawled (200) <...index_00000.html> (referer: None)
DEBUG: Crawled (200) <...property_000029.html> (referer: ...index_00000.
html)
DEBUG: Scraped from <200 ...property_000029.html>
 {'address': [u'Clapham, London'],
 'date': [datetime.datetime(2015, 10, 4, 21, 25, 22, 801098)],
 'description': [u'situated camden facilities corner'],
 'image_urls': [u'http://web:9312/images/i10.jpg'],
 'price': [223.88],
 'project': ['properties'],
 'server': ['scrapyserver1'],
 'spider': ['manual'],
 'title': [u'Portered Mile'],
 'url': ['http://.../property_000029.html']}
DEBUG: Crawled (200) <...property_000028.html> (referer: ...index_00000.
html)
...
DEBUG: Crawled (200) <...index_00001.html> (referer: ...)
DEBUG: Crawled (200) <...property_000059.html> (referer: ...)
...
INFO: Dumping Scrapy stats: ...
  'downloader/request_count': 94, ...
  'item_scraped_count': 90,

如果仔细查看前面的输出,就会发现我们同时获得了水平抓取和垂直抓取的结果。第一个index_00000.html读取后,派生出了许多请求。当它们执行时,调试信息通过referer URL指出是谁发起的请求。比如,可以看到,property_000029.html、property_000028.html……及index_00001.html都有相同的referer(index_00000.html)。而property_000059.html及其他请求则是以index_00001.html为referer的,并且该过程还在持续。

从该示例中还可以观察到,Scrapy在处理请求时使用的是后入先出(LIFO)策略(即深度优先爬取)。用户提交的最后一个请求会被首先处理。在大多数情况下,这种默认的方式非常方便。比如,我们想要在移动到下一个索引页之前处理每一个房源页时。否则,我们将会填充一个包含待爬取房源页URL的巨大队列,无谓地消耗内存。另外,在许多情况中,你可能需要辅助的请求来完成单个请求,我们将会在后面的章节中遇到这种情况。你需要这些辅助的请求能够尽快完成,以腾出资源,并且让被抓取的Item能够稳定流动。

我们可以通过设置Request()的优先级参数修改默认顺序,大于0表示高于默认的优先级,小于0表示低于默认的优先级。通常来说,Scrapy的调度器会首先执行高优先级的请求,不过不要花费太多时间来考虑具体的哪个请求应该被首先执行。很可能在你的应用中,不会使用超过1个或2个请求优先级。此外还需要注意的是,URL还会被执行去重操作,这在大部分时候也是我们想要的功能。不过如果我们需要多次执行同一个URL的请求,可以设置dont_filter_Request()参数为true。

3.4.2 使用CrawlSpider实现双向爬取

如果感觉上面的双向爬取有些冗长,则说明你确实发现了关键问题。Scrapy尝试简化所有此类通用情况,以使其编码更加简单。最简单的实现同样结果的方式是使用CrawlSpider,这是一个能够更容易地实现这种爬取的类。为了实现它,我们需要使用genspider命令,并设置-t crawl参数,以使用crawl爬虫模板创建一个爬虫。

$ scrapy genspider -t crawl easy web
Created spider 'crawl' using template 'crawl' in module:
 properties.spiders.easy

现在,文件properties/spiders/easy.py包含如下内容。

...
class EasySpider(CrawlSpider):
  name = 'easy'
  allowed_domains = ['web']
  start_urls = ['http://www.web/']

  rules = (
    Rule(LinkExtractor(allow=r'Items/'),
callback='parse_item', follow=True),
  )

  def parse_item(self, response):
    ...

当你阅读这段自动生成的代码时,会发现它和之前的爬虫有些相似,不过在此处的类声明中,会发现爬虫是继承自CrawlSpider,而不再是Spider。CrawlSpider提供了一个使用rules变量实现的parse()方法,这与我们之前例子中手工实现的功能一致。

你可能会感到疑惑,为什么我首先给出了手工实现的版本,而不是直接给出捷径。这是因为你在手工实现的示例中,学会了使用回调的yield方式的请求,这是一个非常有用和基础的技术,我们将会在后续的章节中不断使用它,因此理解该内容非常值得。

现在,我们要把start_urls设置成第一个索引页,并且用我们之前的实现替换预定义的parse_item()方法。这次我们将不再需要实现任何parse()方法。我们将预定义的rules变量替换为两条规则,即水平抓取和垂直抓取。

rules = (
Rule(LinkExtractor(restrict_xpaths='//*[contains(@class,"next")]')),
Rule(LinkExtractor(restrict_xpaths='//*[@itemprop="url"]'),
     callback='parse_item')
)

这两条规则使用的是和我们之前手工实现的示例中相同的XPath表达式,不过这里没有了a或href的限制。顾名思义,LinkExtractor正是专门用于抽取链接的,因此在默认情况下,它们会去查找a(及area)href属性。你可以通过设置LinkExtractor()的tags和attrs参数来进行自定义。需要注意的是,回调参数目前是包含回调方法名称的字符串(比如'parse_item'),而不是方法引用,如Request(self.parse_item)。最后,除非设置了callback参数,否则Rule将跟踪已经抽取的URL,也就是说它将会扫描目标页面以获取额外的链接并跟踪它们。如果设置了callback,Rule将不会跟踪目标页面的链接。如果你希望它跟踪链接,应当在callback方法中使用return或yield返回它们,或者将Rule()的follow参数设置为true。当你的房源页既包含Item又包含其他有用的导航链接时,该功能可能会非常有用。

运行该爬虫,可以得到和手工实现的爬虫相同的结果,不过现在使用的是一个更加简单的源代码。

$ scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=90

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文