3.4 抽取更多的 URL
到目前为止,我们使用的只是设置在爬虫的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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论