Spider详解
Scrapy框架中,Spider是爬虫的核心组件,其中定义了爬取数据和数据提取的具体规则,这篇笔记我们将深入学习Spider的编写。
创建Spider类
前一篇笔记我们已经编写过基础的Spider了,这里我们再次回顾一下。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
urls = [
'https://quotes.toscrape.com/page/1/',
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
def parse(self, response, **kwargs):
page = response.url.split("/")[-2]
filename = 'quotes-%s.html' % page
with open(filename, 'wb') as f:
f.write(response.body)
self.log('Saved file %s' % filename)
Spider是一个Python类,它需要继承scrapy.Spider
基类,Spider类必须包含唯一的name
类属性,它是Spider的名字。Spider类还有几个重要的方法:
start_requests():爬虫的入口方法,通常用于返回初始请求对象(Request)的生成器。此外对于起始抓取的路径,如果没有特别复杂的生成逻辑,我们也可以直接用类属性start_urls
指定,不用具体编写start_requests()
方法。
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'https://quotes.toscrape.com/page/1/',
]
def parse(self, response, **kwargs):
# ...
parse():默认的解析回调函数,用于处理返回的响应信息,parse()
函数返回的也是生成器,如果生成器没有任何内容则代表运行结束,如果返回另一个请求对象则代表Scrapy需要继续发起下一个请求,而如果返回一个数据对象(Item),数据将被交由数据管线进行后续处理。
close():爬虫关闭时调用,接收一个reason
参数表示爬虫运行结束的原因,可以在这里做清理工作。
执行Spider
我们可以直接使用CLI工具执行Spider,其中指定的名字就是Spider类的name
属性。
scrapy crawl quotes
抽取数据
前面代码中我们并没有真的抽取数据,只是将服务端返回的HTML保存在了文件中,实际上Scrapy内置了一些用于抽取数据的辅助工具。
使用Scrapy Shell测试数据抽取
Scrapy有一个Shell功能,我们可以使用交互式命令行的形式测试网页或API接口的数据提取逻辑,执行以下命令即可进入交互式命令行。
scrapy shell 'http://quotes.toscrape.com/page/1/'
Shell中的提示信息列举了我们可以执行的操作。
CSS选择器
熟悉JQuery的前端开发者肯定对CSS选择器一点也不陌生,它能通过HTML标签的属性选择节点,这里我们介绍一些最常见的CSS选择器用法。
下面例子筛选所有<title>
标签。
response.css('title')
下面例子筛选所有CSS类包含quote
的标签。
response.css('.quote')
下面例子筛选所有id
为main
的标签。
response.css('#main')
下面例子筛选所有包含href="https://example.com"
的<a>
标签。
response.css('a[href="https://example.com"]')
下面例子筛选所有<a>
的href
属性。
response.css('a::attr(href)')
下面例子筛选所有CSS中包含author
的标签的文本信息。
response.css('.author::text')
XPath选择器
XPath是一种用于在XML或HTML文档中定位节点的表达式语言,对于CSS选择器难以定位的元素,我们也可以使用XPath选择器实现。
下面例子筛选title
节点的文本节点。
response.xpath('//title/text()')
有关XPath表达式具体可以参考软件工程/软件开发相关知识/表达式语言/XPath表达式
章节。
CSS选择器和XPath选择器组合链式调用
CSS和XPath选择器可以混合链式使用以组合出更强大的节点筛选能力,下面是一个例子,它先筛选所有CSS类中包含quote
的标签,然后以其为基础节点筛选其中的第一个<span>
的文本信息。
response.css('.quote').xpath('.//span[1]/text()')
执行数据抽取
无论CSS选择器、XPath选择器还是混合使用,上述操作返回的结果其实都是SelectorList
类型,它是Scrapy提供的一种列表对象,SelectorList
本身也是可遍历的,里面的每个元素都是Selector
对象,我们可以使用getall()
方法具体的文本提取出来,下面操作返回一个字符串列表。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'https://quotes.toscrape.com/page/1/',
]
def parse(self, response, **kwargs):
authors = response.css('.author::text').getall()
self.log(f'Authors: {authors}')
如果我们只需要第一个元素,则可以使用get()
方法抽取,如果列表为空返回None。
对于抽取得到的数据,我们这里仅仅是在控制台上打印日志,实际开发中通常需要将其存储到数据库或磁盘上,这需要用到Scrapy的数据模型和数据处理管线,有关如何持久化抓取结果将在后续章节详细介绍。
实现Follow Links
我们刚刚编写的Spider只能抓取指定URL上的信息,实际开发中这远远不够。比如我们抓取一个论坛的数据,初始页面可能是一个帖子列表,页面上还有翻页按钮,我们要抓取的是所有用户发帖信息,此时我们需要Spider能执行类似“从首页点进帖子里”这样的功能。
前面我们介绍过,parse()
方法返回的生成器中可以是Request
对象也可以是数据对象,只要我们返回页面超链接中的Request
对象,Scrapy就会自动为我们发起请求并抓取响应,但我们仍需要编写另一个解析方法来处理新的页面。还是以之前的网页为例,我们可以发现其中包含的分页链接HTML代码。
<li class="next">
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
</li>
所以我们可以采用如下方式提取翻页链接。
response.css('li.next a::attr(href)').get()
最终,我们实现的可“自动翻页”的爬虫如下。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
]
def parse(self, response, **kwargs):
quotes = response.css('div.quote').xpath('.//span[1]/text()').getall()
self.log(f'Quotes: {quotes}')
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
urljoin():URL链接可能是相对链接,urljoin()
方法会自动为相对链接添加主机名,最后返回一个绝对链接
代码中,我们先处理了起始页的数据,然后实例化Request
并通过生成器返回用于实现翻页,发出第二个HTTP请求后parse()
将再次被回调处理下一页数据,如此循环,直到判断分支next_page is not None
为假,整个循环结束。
此外,Scrapy还提供了一种简化的跟踪链接的方法,我们可以使用response.follow()
函数,其内部会自动调用urljoin()
处理链接。
yield response.follow(next_page, callback=self.parse)
处理异常和重试
创建Request
对象时我们可以指定一个errback
命名参数,其中可以包含对于异常的处理逻辑或是发起重试。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
urls = [
'https://quotes.toscrape.com/page/1/',
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse, errback=self.handle_error)
def handle_error(self, failure):
self.log(repr(failure))
传递状态变量
Scrapy中请求和响应对象支持一个叫meta
的命名参数,我们可以用它来传递状态变量,下面是一个例子。
def parse(self, response, **kwargs):
cnt = response.meta.get('cnt')
if cnt is None:
cnt = 0
self.log(f'cnt: {cnt}')
quotes = response.css('div.quote').xpath('.//span[1]/text()').getall()
self.log(f'Quotes: {quotes}')
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse, meta={'cnt': cnt + 1})
代码中我们维护了一个叫cnt
的状态变量,它递增的记录了Spider的parse()
方法被调用的序号。创建Request
时我们通过meta
命名参数传入,读取响应时我们通过response.meta
获取该变量。
去重过滤器
考虑这样一种情况,如果页面有两个完全相同的链接,我们提取的URL又没有加以区分,Spider会不会进入某个相同的链接两次?更进一步的,页面A包含页面B链接,页面B又包含页面A链接,这样抓取数据时不就死循环了吗?实际上,Scrapy默认已经考虑到了这一点,默认情况下Scrapy使用scrapy.dupefilters.RFPDupeFilter
作为去重过滤器,它会存储已处理请求的指纹以防止重复请求。
虽然settings.py
中默认没有明确配置,但它是默认就是开启的,配置字段如下。
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
RFPDupeFilter
本身支持Scrapy的断点续“爬”功能,如果Scrapy开启了JOBDIR
它会将数据缓存在磁盘上。此外我们也可以基于BaseDupeFilter
实现自己的去重过滤器,在分布式爬虫等场景我们可能需要将去重的结果存储在数据库或Redis中,此时就需要自己实现了。
此外,如果你刻意要关闭去重功能,创建请求对象时可以指定dont_filter
命名参数。
scrapy.Request(url, callback=self.parse, dont_filter=True)
Spider读取命令行参数
我们执行爬虫时可以传入命令行参数,下面是一个例子。
scrapy crawl quotes -o quotes-humor.json -a tag=humor
这些参数会传入__init__
变成Spider的实例属性,上面例子中我们可以直接使用self.tag
引用tag
参数。