本篇教程中主要介绍爬虫类spider如何分析下载到的页面,并从中解析出链接继续进行跟踪的框架。
源码分析(一)中流程图中讲到Crawler在创建执行引擎ExecutionEngin后,会从spider中获取初始请求列表,代码如下:
scrapy/cralwer.py:
@defer.inlineCallbacks def crawl(self, *args, **kwargs): assert not self.crawling, "Crawling already taking place" self.crawling = True try: self.spider = self._create_spider(*args, **kwargs) self.engine = self._create_engine() /*创建一个执行引擎*/ start_requests = iter(self.spider.start_requests()) /*获取spider的初始请求列表*/ yield self.engine.open_spider(self.spider, start_requests) /*执行引擎打开spider*/
我们先来看看start_requests的实现,代码很简单,可以看到start_requests,start_urls,make_requests_from_url都是公开的方法(这里公开指的是不是以下划线“_”开头),因此,scrapy允许(虽然私有方法也可以重写)我们通过在子类中重定义这些函数或变量来实现个性化。
scrapy/spiders/_init_.py:
def start_requests(self): for url in self.start_urls: /*从start_urls中依次获取url,并调用make_requests_from_url生成Request对象*/ yield self.make_requests_from_url(url)
def make_requests_from_url(self, url): return Request(url, dont_filter=True)
源码分析(二)中讲到执行引擎会以start_requests为起点开始主循环,不断的进行网页下载的爬取任务。因此start_requests就是整个爬取的起点。
源码分析(三)中讲到下载器在下载网页成功后,会将response传给scraper处理,scraper会优先调用request的callback,如果没有则调用spider的parse方法。
这里返回值需要是一个生成器,返回Request或者BaseItem,dict类型,如果返回的是Request则继续进行爬取,如果返回的是BaseItem则进行数据pipeline的处理,代码如下:
scrapy/core/scraper.py:
def _process_spidermw_output(self, output, request, response, spider): """Process each Request/Item (given in the output parameter) returned from the given spider """ if isinstance(output, Request): /*Request类型则交给执行引擎继续爬取*/ self.crawler.engine.crawl(request=output, spider=spider) elif isinstance(output, (BaseItem, dict)): /*如果是BaseItem或者dict则调用ItemPipelineManager处理*/ self.slot.itemproc_size += 1 dfd = self.itemproc.process_item(output, spider) dfd.addBoth(self._itemproc_finished, output, response, spider) return dfd elif output is None: /*返回空什么也不做*/ pass else: /*其它类型记录错误日志*/ typename = type(output).__name__ logger.error('Spider must return Request, BaseItem, dict or None, ' 'got %(typename)r in %(request)s', {'request': request, 'typename': typename}, extra={'spider': spider})
因此,从parse方法就开始了一个页面的解析操作,也是我们重点分析的流程。
spider中parse方法没有定义,需要子类实现,scrapy预定义了一些爬虫类,这里主要以CrawlSpider类讲解。
scrapy/spiders/crawl.py:
def parse(self, response): return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True)parse方法比较简单,只是对response调用_parse_response方法,并设置callback为parse_start_url,follow=True(表明跟进链接)
如果设置了callback,也就是parse_start_url,会优先调用callback处理,然后调用process_results方法来生成返回列表。前面讲到需要返回Request或者BaseItem,dict.
process_results方法默认返回空列表,也就是说如果我们不自己实现process_results,则什么数据也解析不出来,也不会有进一步的数据pipeline处理。
如果follow为True且_follow_links(这个默认是True,也可以通过配置'CRAWLSPIDER_FOLLOW_LINKS'设置。
def _parse_response(self, response, callback, cb_kwargs, follow=True): if callback: cb_res = callback(response, **cb_kwargs) or () cb_res = self.process_results(response, cb_res) for requests_or_item in iterate_spider_output(cb_res): yield requests_or_item if follow and self._follow_links: for request_or_item in self._requests_to_follow(response): yield request_or_item因此,对页面子链接的跟进主要由_requests_to_follow实现,Rule的实现后面详细介绍:
def _requests_to_follow(self, response): if not isinstance(response, HtmlResponse): /*首先确保response是HtmlResponse类型*/ return seen = set() /*用一个集合确保不跟踪重复链接*/ for n, rule in enumerate(self._rules): /*_rules是自定义的规则,用于定义跟踪链接的规则*/ links = [lnk for lnk in rule.link_extractor.extract_links(response) if lnk not in seen] /*从rule中定义的link_extractor中解析出希望跟进的链接*/ if links and rule.process_links: /*如果rule定义了process_links方法则用其进行过滤处理*/ links = rule.process_links(links) for link in links: seen.add(link) r = Request(url=link.url, callback=self._response_downloaded)/*跟进的Request使用_response_downloaded进行解析 ,前面讲了优先使用这个再使用spider.parse*/ r.meta.update(rule=n, link_text=link.text) yield rule.process_request(r) /*调用rule的process_request,默认原样返回*/ 我们再看下_response_downloaded的实现,可以看到只是使用rule中定义的callback,cb_kwargs和follow标志调用 _parse_response,也就是说我们对跟进的链接使用rule中定义的callback进行解析,如果规则允许follow则继续跟进 链接:
def _response_downloaded(self, response): rule = self._rules[response.meta['rule']] return self._parse_response(response, rule.callback, rule.cb_kwargs, rule.follow)