2.3 Spider 开发流程
有了前面知识的铺垫,现在回到本章的主题“编写Spider”。实现一个Spider子类的过程很像是完成一系列填空题,Scrapy框架提出以下问题让用户在Spider子类中作答:
爬虫从哪个或哪些页面开始爬取?
对于一个已下载的页面,提取其中的哪些数据?
爬取完当前页面后,接下来爬取哪个或哪些页面?
上面问题的答案包含了一个爬虫最重要的逻辑,回答了这些问题,一个爬虫也就开发出来了。
接下来,我们以上一章exmaple项目中的BooksSpider为例,讲解一个Spider的开发流程。为方便阅读,再次给出BooksSpider的代码:
# -*- coding: utf-8 -*- import scrapy class BooksSpider(scrapy.Spider): # 每一个爬虫的唯一标识 name = "books" # 定义爬虫爬取的起始点,起始点可以是多个,我们这里是一个 start_urls = ['http://books.toscrape.com/'] def parse(self, response): # 提取数据 # 每一本书的信息是在<article class="product_pod">中,我们使用 # css()方法找到所有这样的article 元素,并依次迭代 for book in response.css('article.product_pod'): # 书名信息在article > h3 > a 元素的title属性里 # 例如: <a title="A Light in the Attic">A Light in the ...</a> name = book.xpath('./h3/a/@title').extract_first() # 书价信息在 <p class="price_color">的TEXT中。 # 例如: <p class="price_color">£51.77</p> price = book.css('p.price_color::text').extract_first() yield { 'name': name, 'price': price, } # 提取链接 # 下一页的url 在ul.pager > li.next > a 里面 # 例如: <li class="next"><a href="catalogue/page-2.html">next</a></li> next_url = response.css('ul.pager li.next a::attr(href)').extract_first() if next_url: # 如果找到下一页的url,得到绝对路径,构造新的Request 对象 next_url = response.urljoin(next_url) yield scrapy.Request(next_url, callback=self.parse)
实现一个Spider只需要完成下面4个步骤:
步骤 01 继承scrapy.Spider。
步骤 02 为Spider取名。
步骤 03 设定起始爬取点。
步骤 04 实现页面解析函数。
2.3.1 继承scrapy.Spider
Scrapy框架提供了一个Spider基类,我们编写的Spider需要继承它:
import scrapy class BooksSpider(scrapy.Spider): ...
这个Spider基类实现了以下内容:
供Scrapy引擎调用的接口,例如用来创建Spider实例的类方法from_crawler。
供用户使用的实用工具函数,例如可以调用log方法将调试信息输出到日志。
供用户访问的属性,例如可以通过settings属性访问配置文件中的配置。
实际上,在初学Scrapy时,不必关心Spider基类的这些细节,未来有需求时再去查阅文档即可。
2.3.2 为Spider命名
在一个Scrapy项目中可以实现多个Spider,每个Spider需要有一个能够区分彼此的唯一标识,Spider的类属性name便是这个唯一标识。
class BooksSpider(scrapy.Spider): name = "books" ...
执行scrapy crawl命令时就用到了这个标识,告诉Scrapy使用哪个Spider进行爬取。
2.3.3 设定起始爬取点
Spider必然要从某个或某些页面开始爬取,我们称这些页面为起始爬取点,可以通过类属性start_urls来设定起始爬取点:
class BooksSpider(scrapy.Spider): ... start_urls = ['http://books.toscrape.com/'] ...
start_urls通常被实现成一个列表,其中放入所有起始爬取点的url(例子中只有一个起始点)。看到这里,大家可能会想,请求页面下载不是一定要提交Request对象么?而我们仅定义了url列表,是谁暗中构造并提交了相应的Request对象呢?通过阅读Spider基类的源码可以找到答案,相关代码如下:
class Spider(object_ref): ... def start_requests(self): for url in self.start_urls: yield self.make_requests_from_url(url) def make_requests_from_url(self, url): return Request(url, dont_filter=True) def parse(self, response): raise NotImplementedError ...
从代码中可以看出,Spider基类的start_requests方法帮助我们构造并提交了Request对象,对其中的原理做如下解释:
实际上,对于起始爬取点的下载请求是由Scrapy引擎调用Spider对象的start_requests方法提交的,由于BooksSpider类没有实现start_requests方法,因此引擎会调用Spider基类的start_requests方法。
在start_requests方法中,self.start_urls便是我们定义的起始爬取点列表(通过实例访问类属性),对其进行迭代,用迭代出的每个url作为参数调用make_requests_from_url方法。
在make_requests_from_url方法中,我们找到了真正构造Reqeust对象的代码,仅使用url和dont_filter参数构造Request对象。
由于构造Request对象时并没有传递callback参数来指定页面解析函数,因此默认将parse方法作为页面解析函数。此时BooksSpider必须实现parse方法,否则就会调用Spider基类的parse方法,从而抛出NotImplementedError异常(可以看作基类定义了一个抽象接口)。
起始爬取点可能有多个,start_requests方法需要返回一个可迭代对象(列表、生成器等),其中每一个元素是一个Request对象。这里,start_requests方法被实现成一个生成器函数(生成器对象是可迭代的),每次由yield语句返回一个Request对象。
由于起始爬取点的下载请求是由引擎调用Spider对象的start_requests方法产生的,因此我们也可以在BooksSpider中实现start_requests方法(覆盖基类Spider的start_requests方法),直接构造并提交起始爬取点的Request对象。在某些场景下使用这种方式更加灵活,例如有时想为Request添加特定的HTTP请求头部,或想为Request指定特定的页面解析函数。
以下是通过实现start_requests方法定义起始爬取点的示例代码(改写BooksSpider):
class BooksSpider(scrapy.Spider): # start_urls = ['http://books.toscrape.com/'] # 实现start_requests 方法, 替代start_urls类属性 def start_requests(self): yield scrapy.Request('http://books.toscrape.com/', callback=self.parse_book, headers={'User-Agent': 'Mozilla/5.0'}, dont_filter=True) # 改用parse_book 作为回调函数 def parse_book(response): ...
到此,我们介绍完了为爬虫设定起始爬取点的两种方式:
定义start_urls属性。
实现start_requests方法。
2.3.4 实现页面解析函数
页面解析函数也就是构造Request对象时通过callback参数指定的回调函数(或默认的parse方法)。页面解析函数是实现Spider中最核心的部分,它需要完成以下两项工作:
使用选择器提取页面中的数据,将数据封装后(Item或字典)提交给Scrapy引擎。
使用选择器或LinkExtractor提取页面中的链接,用其构造新的Request对象并提交给Scrapy引擎(下载链接页面)。
一个页面中可能包含多项数据以及多个链接,因此页面解析函数被要求返回一个可迭代对象(通常被实现成一个生成器函数),每次迭代返回一项数据(Item或字典)或一个Request对象。
关于如何提取数据、封装数据、提取链接等话题,我们在接下来的章节继续学习。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论