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')

下面例子筛选所有idmain的标签。

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">&rarr;</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参数。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。