Scrapy 参考文档

2018 年 10 月 18 日 • 阅读数: 124

Scrapy框架

Scrapy架构

概述

接下来的图表展现了Scrapy的架构,包括组件及在系统中发生的数据流的概览(绿色箭头所示)。 下面对每个组件都做了简单介绍,并给出了详细内容的链接。数据流如下所描述

scrapy_architecture.png

组件

Scrapy引擎(Scrapy Engine)

引擎负责控制数据流在系统中所有组件中流动,并在相应动作发生时触发事件

调度器(Scheduler)

调度器从引擎接受request并将他们入队,以便之后引擎请求他们时提供给引擎

下载器(Downloader)

下载器负责获取页面数据并提供给引擎,而后提供给spider

Spiders

Spider是Scrapy用户编写用于分析response并提取item(即获取到的item)或额外跟进的URL的类

每个spider负责处理一个特定(或一些)网站

Item Pipeline

Item Pipeline负责处理被spider提取出来的item

典型的处理有清理、 验证及持久化(例如存取到数据库中)

下载器中间件(Downloader middlewares)

下载器中间件是在引擎及下载器之间的特定钩子(specific hook),处理Downloader传递给引擎的response

其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能

Spider中间件(Spider middlewares)

Spider中间件是在引擎及Spider之间的特定钩子(specific hook),处理spider的输入(response)和输出(result)

其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能

数据流(Data flow)

Scrapy中的数据流由执行引擎控制,其过程如下:

  1. 引擎打开一个网站(open a domain),找到处理该网站的Spider并向该spider请求第一个要爬取的URL(s)
  2. 引擎从Spider中获取到第一个要爬取的URL并在调度器(Scheduler)以Request调度
  3. 引擎向调度器请求下一个要爬取的URL
  4. 调度器返回下一个要爬取的URL给引擎,引擎将URL通过下载中间件(请求(request)方向)转发给下载器
  5. 页面下载完毕,下载器生成一个该页面的Response,并将其通过下载中间件(返回(response)方向)发送给引擎
  6. 引擎从下载器中接收到Response并通过Spider中间件(输入方向)发送给Spider处理
  7. Spider处理Response并返回爬取到的Item及(跟进的)新的Request给引擎
  8. 引擎将(Spider返回的)爬取到的Item给Item Pipeline,将(Spider返回的)Request给调度器
  9. (从第二步)重复直到调度器中没有更多地request,引擎关闭该网站

事件驱动网络(Event-driven networking)

Scrapy基于事件驱动网络框架 Twisted 编写,因此,Scrapy基于并发性考虑由非阻塞(即异步)的实现

Scrapy入门

安装

在虚拟环境下直接通过 pipenv 安装

pipenv install scrapy

除此之外还有一些常用的第三方库

pypiwin32 = "*"
pillow = "*"
selenium = "*"

**注:**Windows下 pypiwin32 必须安装

创建项目

直接通过 startproject 指令来创建一个项目

scrapy startproject project_name

然后cd到项目目录下创建一个 Spider ,后面需要跟一个 spider 的名称和网站的根URL

scrapy genspider spider_name URL

使用 genspider -l 命令可以看到所有的内置模板

(Scrapy-AmnJqBH4) λ scrapy genspider -l
Available templates:
  basic
  crawl
  csvfeed
  xmlfeed

如果我们不指定模板,那么它将根据 basic 来创建Spider

Spiders

Spider

当我们根据默认模板新建了一个spider后,它会为我们自动生成以下数据

  • spider:核心类

  • name:spider的名称

  • allowed_domains:爬去的根域名

  • start_urls:起始地址

  • parse:解析数据的函数

import scrapy


class JobboleSpider(scrapy.Spider):
    name = 'jobbole'
    allowed_domains = ['blog.jobbole.com']
    start_urls = ['http://blog.jobbole.com/']

    def parse(self, response):
        pass

CrawSpider

如果要指定其他模板,可以参照以下命令(如下使用了 crawl 模板)

scrapy genspider -t crawl lagou www.lagou.com

CrawlSpider 是对 Spider 的进一步封装

它可以自动的根据我们编写的 Rule 提取页面中的URL,并进行跟踪爬取

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class LagouSpider(CrawlSpider):
    name = 'lagou'
    allowed_domains = ['www.lagou.com']
    start_urls = ['https://www.lagou.com/']

    rules = (
        Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        i = {}
        #i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
        #i['name'] = response.xpath('//div[@id="name"]').extract()
        #i['description'] = response.xpath('//div[@id="description"]').extract()
        return i

Xpath

xpath简介

  • xpath使用路径表达式在xml和html中进行导航
  • xpath包含标准函数库
  • xpath是一个w3c标准

xpath语法

表达式 说明
article 选取所有 article 元素的所有子节点
/article 选取根元素 article
article/a 选取所有属于 article 的子元素的 a 标签
//div 选取所有 div 子元素(不论出现在文档的任何地方)
article//div 选取所有属于 article 元素的后代的 div 元素
//@class 选取所有名为 class 的属性

xpath语法-谓语

表达式 说明
/article/div[1] 选取属于 article 元素的第一个 div 元素
/article/div[last()] 选取属于 article 元素的最后一个 div 元素
/article/div[last()-1] 选取属于 article 元素的倒数第二个 div 元素
//div[@lang] 选取所有拥有 lang 属性的 div 元素
//div[@lang=‘eng’] 选取所有 lang 属性为 engdiv 元素

xpath语法-扩展

表达式 说明
/div/* 选取属于 div 元素的所有子节点
//* 选取所有元素
//div[@*] 选取所有带属性的 div 元素
/div/a | //div/p 选取所有 div 元素的 ap 元素
//span | //ul 选取文档中的所有 spanul 元素
article/div/p | //span 选取所有属于 article 元素的 div 元素的 p 元素,以及文档中所有 span 元素

CSS

css语法

表达式 说明
***** 选择所有节点
#container 选择 idcontainer 的节点
.container 选择所有 class 包含 container 的节点
li a 选择 li 下的所有 a 节点
ul + p 选择 ul 后面的第一个 p 元素
div#container > ul 选择 idcontainerdiv 的第一个 ul 子元素
ul ~ p 选择与 ul 相邻的所有 p 元素
a[title] 选取所有有 title 属性的 a 元素
a[href=“https://baidu.com”] 选取所有 href 属性为 baidu.com 值的 a 元素
a[href=“baidu”]* 选取所有 href 属性包含 baidua 元素
a[href^=“https”] 选取所有 href 属性值以 https 开头的 a 元素
a[href$=".jpg"] 选取所有 href 属性值以 .jpg 结尾的 a 元素
input[type=radio]:checked 选择选中的 radio 元素
div:not(#container) 选取所有 idcontainerdiv 属性
li:nth-child(3) 选取第三个 li 元素
tr:nth-child(2n) 第偶数个 tr

Shell

scrapy为我了使我们更好的进行调试,提供了一个shell模式,进入方式如下:

# 后面的参数需要跟一个URL地址
scrapy shell http://blog.jobbole.com/113659/

一旦进入了shell,实际上就已经获取到了完整的页面了,可以直接通过response对象来对页面进行提取

Selector

SelectorListxpath 或者 css 选择器提取页面后返回的对象,它是由 Selector 对象组成的一个数组

每个 Selector 对象都包含2个部分,前一部分是提取用的 xpath 后一部分是 data 也就是提取出来的数据

注:css 选择器实际上在Scrapy内部也会被转化成 xpath 进行提取

Selector 对象是可以再次调用 Xpath 或者 css 解析器进行内容提取的

要想解析出 dataSelector 提供了一个 extract() 方法,它会解析 SelectorList 然后返回一个 list

如果是单个的则直接返回 data (注意:通过 xpathcss 选择器提取出来的都是SelectorList

你也可以使用 extract_first() 直接解析第一个数据

Request

将提取出来的,并且需要进行进一步爬取的URL传给一个 Request 对象,并通过 yield 传递给调度器

Request 对象需要接收两个参数:

  • url 进一步爬取的地址
  • callback 解析函数(注意只传函数名,不能调用)
from scrapy import Request
from urllib import parse

post_urls = response.css('#archive .floated-thumb .post-thumb a::attr(href)').extract()
        for post_url in post_urls:
            yield Request(url=parse.urljoin(response.url, post_url),
                          callback=self.parse_detail)

Items

Item

Item 类似于Django中的 model ,我们可以定义字段,来对我们解析出来的数据进行结构化,但是需要注意的是,它的字段只有一种类型 Field ,因此它并不能对数据类型进行验证

定义一个 Item 它需要继承 scrapy.Item

class JobboleArticleItem(scrapy.Item):
    title = scrapy.Field()
    tags = scrapy.Field()
    url = scrapy.Field()
    url_object_id = scrapy.Field()
    create_data = scrapy.Field()
    content = scrapy.Field()
    comment_nums = scrapy.Field()
    praise_nums = scrapy.Field()
    fav_nums = scrapy.Field()
    front_image_path = scrapy.Field()
    front_image_url = scrapy.Field()

然后在 Spider 的解析函数 中创建一个实例,并且给它赋值,然后通过 yield 将它传递到 Pipline

from ArticleSpider.ArticleSpider.items import JobboleArticleItem


class JobboleSpider(scrapy.Spider):    
    ...
    
    def parse_detail(self, response):

        article_item = JobboleArticleItem
        ...
        article_item['title'] = title
        article_item['url'] = response.url
        article_item['create_date'] = create_date
        article_item['front_image_url'] = front_image_url
        article_item['tags'] = tags
        article_item['content'] = content
        article_item['praise_nums'] = praise_nums
        article_item['comment_nums'] = comment_nums
        article_item['fav_nums'] = fav_nums
        
        yield article_item

ItemLoader

在原始的 Item 中,我们需要将解析出来的数据,像字典赋值的方式一样,一个一个的赋给 Item 的字段

ItemLoader就是将 Item 的字段和 Selector 绑定在一起,解析出来的数据直接传递给对应的字段,最后调用它的 load_item() 方法,就可以返回一个 Item 对象

    def parse_detail(self, response):

        item_loader = ItemLoader(JobboleArticleItem(), response=response)
        item_loader.add_css('title', '.entry-header h1::text')
        item_loader.add_css('create_date', 'p.entry-meta-hide-on-mobile::text')
        item_loader.add_css('tags', 'p.entry-meta-hide-on-mobile a::text')
        item_loader.add_css('content', 'div.entry')
        item_loader.add_css('praise_nums', '.vote-post-up h10::text')
        item_loader.add_css('fav_nums', '.bookmark-btn::text')
        item_loader.add_css('comment_nums', 'a[href="#article-comment"] span::text')
        item_loader.add_value('url', response.url)
        item_loader.add_value('url_object_id', get_md5(response.url))
        item_loader.add_value('front_image_url',
                              response.meta.get('front_image_url', None))

        article_item = item_loader.load_item()

使用 ItemLoader 我们可以在定义 Item 的时候,给它传递一些方法,这些方法可以帮助我们对数据进行处理

常用方法:

  • input_processor :输入处理器
  • output_processor :输出处理器

input_processor 可以接收一个 MapCompose() 方法,该方法里面可以传入多个自定义方法,这些方法会在输入的时候依此调用

output_processor 也可以接收一个MapCompose 方法,用法和上面相同,它还可以接受 TakeFirst() 表示只返回第一个数据,Join 表示将列表连接成一个字符串

from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose, TakeFirst, Join


def date_convert(value):
    try:
        value = value.strip().replace(' ·', '')
        value = datetime.datetime.strptime(value, '%Y/%m/%d')
    except Exception as e:
        print(e)
        value = datetime.datetime.now().date()
    return value


def get_nums(value):
    if len(value) == 2:
        value = int(value[0])
    else:
        value = 0
    return value

def remove_comment_tags(value):
    if value.endswith('评论'):
        pass
    else:
        return value


class JobboleArticleItem(scrapy.Item):
    title = scrapy.Field()
    tags = scrapy.Field(
        input_processor=MapCompose(remove_comment_tags),
        output_processor=Join(','))
    url = scrapy.Field()
    url_object_id = scrapy.Field()
    create_date = scrapy.Field(
        input_processor=MapCompose(date_convert))
    content = scrapy.Field()
    comment_nums = scrapy.Field(
        input_processor=MapCompose(get_nums))
    praise_nums = scrapy.Field(
        input_processor=MapCompose(get_nums))
    fav_nums = scrapy.Field(
        input_processor=MapCompose(get_nums))
    front_image_url = scrapy.Field(
        output_processor=MapCompose(lambda x: [x]))
    front_image_path = scrapy.Field()

我们还可以定义一些 ItemLoader 的默认输入输出处理,只需要自定义一个类继承 ItemLoader

如下图,只需要在创建实例的时候,实例化我们自己定义的这个类,就可以具有一些默认行为

class ArticleItemLoader(ItemLoader):

    default_output_processor = TakeFirst()

Model

在Scrapy中我们也可以使用ORM框架,方便我们对数据库进行操作,并且可以做数据验证

peewee 为例,我们可以定义自己的Model,然后在Pipeline中进行存储的时候就方便多了

from peewee import *


db = MySQLDatabase('scrapyproject', host='127.0.0.1', port=3306,
                   user='root', passwd='77895zlp')

class JobboleArticleModel(Model):
    title = CharField(max_length=255)
    tags = CharField(max_length=255)
    url = CharField(max_length=255)
    url_object_id = CharField(max_length=255)
    create_date = DateTimeField()
    content = TextField()
    comment_nums = IntegerField()
    praise_nums = IntegerField()
    fav_nums = IntegerField()
    front_image_url = CharField(max_length=255)
    front_image_path = CharField(max_length=255)

    class Meta:
        database = db
        db_table = 'jobbole_article'

Pipelines

Pipeline简介

Pipelines可以理解为一连串的管道,而 Item 就是流经这条管道的数据,管道可能由很多小节组成,每一个小节都可以对 Item 进行操作,可以是添加数据,删除数据,也可以是保存数据库等

由于每一个管道的小节都需要 Item 这个数据,所以在前面的Pipeline处理完数据后,都需要将 Item 给返回出来,后面的才能接受到数据,当然,如果是最后一个可以不返回数据

在配置文件中有一个 ITEM_PIPELINES 配置,所有的Pipeline都需要配置在里面才会生效

自定义Pipeline

如下是一个保存 Item 到数据库的操作,前面我们定义了Model,需要在这里引入

自定义的pipeline的类里面不需要继承任何类,但是需要实现一些方法

常用方法:

  • process_item (item, spider)

    每个item pipeline组件都需要调用该方法,这个方法必须返回一个 Item (或任何继承类)对象, 或是抛出 DropItem 异常,被丢弃的item将不会被之后的pipeline组件所处理

    **注:**该方法必须实现,后面的可根据情况使用

  • open_spider(spider)

    当spider被开启时,这个方法被调用

  • close_spider(spider)

    当spider被关闭时

from .items import JobboleArticleModel

class MysqlPipeline(object):

    def process_item(self, item, spider):
        if JobboleArticleModel.table_exists() is False:
            JobboleArticleModel.create_table()
        try:
            JobboleArticleModel.create(title=item['title'],
                                       tags=item['tags'],
                                       url=item['url'],
                                       url_object_id=item['url_object_id'],
                                       create_date=item['create_date'],
                                       content=item['content'],
                                       comment_nums=item['comment_nums'],
                                       praise_nums=item['praise_nums'],
                                       fav_nums=item['fav_nums'],
                                       front_image_url=item['front_image_url'],
                                       front_image_path=item['front_image_path'])
        except Exception as e:
            print(e)

**注:**定义了Pipeline后,都需要将它加入到配置文件中,后面的数字是它执行的一个顺序,越小越优先

ITEM_PIPELINES = {
    'ArticleSpider.pipelines.MysqlPipeline': 2,
    'ArticleSpider.pipelines.ArticleImagePipeline': 1,
}

在对性能要求较高的情况下,我们可以采用异步的方式保存数据库

可以看到在下面的代码中我们处了实现了 process_item() 方法外,还实现了一个类方法 from_settings 这个方法也是内置的固定写法,它的作用是获取 settings 中的配置

用这些配置文件中配置的内容,我们使用 adbapi.ConnectionPool() 方法新建了一个异步的数据库连接池,然后在初始化对象的时候传入实例变量中,在下面处理 Item 的时候,就可以直接使用该连接池来保存数据

from twisted.enterprise import adbapi

class MysqlTwistedPipeline(object):

    def __init__(self, dbpool):
        self.dbpool = dbpool

    @classmethod
    def from_settings(cls, settings):
        dbparms = dict(
            host=settings['MYSQL_HOST'],
            user=settings['MYSQL_USER'],
            password=settings['MYSQL_PASSWORD'],
            db=settings['MYSQL_DBNAME'],
            charset='utf8',
            use_unicode=True)

        dbpool = adbapi.ConnectionPool('MySQLdb', **dbparms)

        return cls(dbpool)

    def process_item(self, item, spider):
        query = self.dbpool.runInteraction(self.do_insert, item)
        query.addErrback(self.handle_error)

    def handle_error(self, failure):
        print(failure)

    def do_insert(self, cursor, item):
        insert_sql = """
            insert into jobbole_article(title, tags, url, url_object_id)
            value (%s, %s, %s, %s)
                     """
        cursor.execute(insert_sql, (item['title'], item['tags'],
                                    item['url'], item['url_object_id'])

内置Pipeline

我们除了可以自定义Pipeline以外,Scrapy还为我们提供了一些内置的Pipeline,我们可以直接在配置中添加它们,也可以继承它们,来完成一些定制操作


ImagesPipeline 用来下载图片的一个Pipeline

使用方法如下:

# 声明使用,所有定义的Pipeline都需要添加到这里面
ITEM_PIPELINES = {
    'scrapy.pipelines.images.ImagesPipeline': 1,
}
# 指定对应Item中的字段,指向图片的URL地址
IMAGES_URLS_FIELD = 'front_image_url'
# 获取配置文件所在目录
project_dir = os.path.abspath(os.path.dirname(__file__))
# 创建一个和配置文件同级的目录,用来存放下载下来的图片
IMAGES_STORE = os.path.join(project_dir, 'images')

**注:**该Pipeline依赖一个第三方的库 pillow

还有指定的字段(这里的 front_image_url )必须是一个迭代器

扩展 ImagesPipeline

这里我们以获取下载图片的保存地址为例:

我们继承 ImagesPipeline ,并且重写它的 item_completed() 方法

results 参数中实际上就保存了下载图片的实际路径,我们将它取出来赋给 itemimage_file_path 字段,最后还要记得将 item 返回回去,给下一个Pipeline使用

from scrapy.pipelines.images import ImagesPipeline

class ArticleImagePipeline(ImagesPipeline):
    def item_completed(self, results, item, info):
        for res, value in results:
            image_file_path = value['path']
        item['front_image_path'] = image_file_path

        return item

Spider中间件

使用说明

Spider中间件是介入到Scrapy的spider处理机制的钩子框架,您可以添加代码来处理发送给Spiders的response及spider产生的item和request

声明spider中间件

要启用spider中间件,您可以将其加入到 SPIDER_MIDDLEWARES 设置中,该设置是一个字典,键为中间件的路径,值为中间件的顺序(越小越优先)

SPIDER_MIDDLEWARES = {
    'myproject.middlewares.CustomSpiderMiddleware': 543,
}

默认中间件

SPIDER_MIDDLEWARES设置会与Scrapy定义的 SPIDER_MIDDLEWARES_BASE 设置合并(但不是覆盖), 而后根据顺序(order)进行排序,最后得到启用中间件的有序列表: 第一个中间件是最靠近引擎的,最后一个中间件是最靠近spider的

如果您想禁止内置的(在 SPIDER_MIDDLEWARES_BASE 中设置并默认启用的)中间件, 您必须在项目的 SPIDER_MIDDLEWARES 设置中定义该中间件,并将其值赋为 None, 例如,如果您想要关闭off-site中间件:

SPIDER_MIDDLEWARES = {
    'myproject.middlewares.CustomSpiderMiddleware': 543,
    'scrapy.contrib.spidermiddleware.offsite.OffsiteMiddleware': None,
}

**注:**有些中间件需要通过特定的设置来启用,更多内容请查看相关中间件文档

自定义spider中间件

spider中间件简介

编写spider中间件十分简单,每个中间件组件是一个定义了以下一个或多个方法的Python类:

class ArticlespiderSpiderMiddleware(object):
    
    @classmethod
    def from_crawler(cls, crawler):
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_spider_input(self, response, spider):
        return None

    def process_spider_output(self, response, result, spider):
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
        pass

    def process_start_requests(self, start_requests, spider):
        for r in start_requests:
            yield r

    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)

spider中间件方法详解

from_crawler(cls, crawler)

  • 类方法,被 Middlewaremanager 调用( DownloaderMiddleware 中同理)
  • 默认情况下它发送了一个信号,调用了 spider_opened 方法
参数 说明
crawler ( Crawler 对象) - 当前的爬虫对象

process_spider_input(self, response, spider)

  • 当response通过spider中间件时,该方法被调用,处理该response

  • 该方法应该返回 None 或者抛出一个异常

  • 如果其返回 None ,Scrapy将会继续调用所有其他的中间件直到spider处理该response

  • 如果其抛出一个异常,Scrapy将不会调用任何其他中间件,并调用request的errback

  • errback的输出将会以另一个方向被重新输入到中间件链中,使用 process_spider_output()方法来处理

  • 当其抛出异常时则带调用 process_spider_exception()

参数 说明
response ( Response 对象) – 生成该输出的response
spider ( Spider 对象) – 该response对应的spider

process_spider_output(self, response, result, spider)

  • 当Spider处理response返回result时,该方法被调用
  • 该方法必须返回包含 RequestItem 对象的可迭代对象(iterable)
参数 说明
response ( Response 对象) – 生成该输出的response
result (包含 RequestItem 对象的可迭代对象(iterable)) – spider返回的result
spider ( Spider 对象) – 其结果被处理的spider

process_spider_exception(self, response, exception, spider)

  • 当spider或(其他spider中间件的) process_spider_input() 抛出异常时, 该方法被调用
  • 该函数必须返回 None , 或者返回一个包含 ResponseItem 对象的可迭代对象(iterable)
  • 如果其返回 None ,Scrapy将继续处理该异常,调用中间件链中的其他中间件的process_spider_exception()方法,直到所有中间件都被调用,该异常到达引擎
  • 如果其返回一个可迭代对象,则中间件链的 process_spider_output() 方法被调用, 其他的 process_spider_exception() 就不会再被调用
参数 说明
response ( Response 对象) – 异常被抛出时被处理的response
exception ( Exception 对象) – 被抛出的异常
spider ( Spider 对象) – 抛出该异常的spider

process_start_requests(self, start_requests, spider)

  • 该方法以spider 启动的request为参数被调用,执行的过程类似于 process_spider_output()
  • 只不过其没有相关联的response并且必须返回request(不是item)
  • 其接受一个可迭代的对象( start_requests 参数)且必须返回另一个包含 Request 对象的可迭代对象
参数 说明
start_requests (包含 Request 的可迭代对象) – start requests
spider ( Spider 对象) – start requests所属的spider

spider_opened(self, spider)

  • 默认只是打印日志的功能,在创建类的时候被调用
参数 说明
spider ( Spider 对象) – 当前的spider

内置Spider中间件

DepthMiddleware

DepthMiddleware是一个用于追踪每个Request在被爬取的网站的深度的中间件

其可以用来限制爬取深度的最大深度或类似的事情

配置 说明
DEPTH_LIMIT 爬取所允许的最大深度,如果为0,则没有限制
DEPTH_STATS 是否收集爬取状态
DEPTH_PRIORITY 是否根据其深度对requet安排优先级

HttpErrorMiddleware

过滤出所有失败的HTTPresponse,因此spider不需要处理这些request

默认情况下返回值为200-300之间的值为成功的resonse

如果您想处理在这个范围之外的response,您可以通过 spider的 handle_httpstatus_list 属性或HTTPERROR_ALLOWED_CODES 设置来指定spider能处理的response返回值

Request.meta 中的 handle_httpstatus_list 键也可以用来指定每个request所允许的response code

不过请记住,除非您知道您在做什么,否则处理非200返回一般来说是个糟糕的决定

OffsiteMiddleware

该中间件过滤出所有主机名不在spider属性 allowed_domains 的request

为了避免记录太多无用信息,其将对每个新发现的网站记录一次

如果spider没有定义 allowed_domains 属性,或该属性为空, 则offsite 中间件将会允许所有request

如果request设置了 dont_filter 属性, 即使该request不在允许列表里,offsite中间件也会允许该request

Downloader中间件

使用说明

下载器中间件是介于Scrapy的request/response处理的钩子框架

是用于全局修改Scrapy request和response的一个轻量、底层的系统

声明Download中间件

要激活下载器中间件组件,将其加入到 DOWNLOADER_MIDDLEWARES 设置中,该设置是一个字典(dict),键为中间件类的路径,值为其中间件的顺序

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.CustomDownloaderMiddleware': 543,
}

默认的下载中间件

DOWNLOADER_MIDDLEWARES 设置会与Scrapy定义的 DOWNLOADER_MIDDLEWARES_BASE 设置合并(但不是覆盖), 而后根据顺序(order)进行排序,最后得到启用中间件的有序列表: 第一个中间件是最靠近引擎的,最后一个中间件是最靠近下载器的

如果您想禁止内置的(在 DOWNLOADER_MIDDLEWARES_BASE 中设置并默认启用的)中间件, 您必须在项目的 DOWNLOADER_MIDDLEWARES 设置中定义该中间件,并将其值赋为 None

例如,如果您想要关闭user-agent中间件:

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.CustomDownloaderMiddleware': 543,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
}

自定义下载中间件

下载中间件简介

编写下载器中间件十分简单,每个中间件组件是一个定义了以下一个或多个方法的Python类

class ArticlespiderDownloaderMiddleware(object):

    @classmethod
    def from_crawler(cls, crawler):
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_request(self, request, spider):
        return None

    def process_response(self, request, response, spider):
        return response

    def process_exception(self, request, exception, spider):
        pass

    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)

下载中间件方法详解

from_crawler(cls, crawler)

  • 类方法,被 Middlewaremanager 调用( DownloaderMiddleware 中同理)
  • 默认情况下它发送了一个信号,调用了 spider_opened 方法
参数 说明
crawler ( Crawler 对象) - 当前的爬虫对象

process_request(self, request, spider)

  • 当每个request通过下载中间件时,该方法被调用
  • process_request()必须返回其中之一: 返回 None 、返回一个 Response 对象、返回一个 Request 对象或raise IgnoreRequest
  • 如果其返回 None ,Scrapy将继续处理该request,执行其他的中间件的相应方法,直到合适的下载器处理函数(download handler)被调用, 该request被执行(其response被下载)
  • 如果其返回 Response 对象,Scrapy将不会调用任何其他的 process_request()process_exception() 方法,或相应地下载函数; 其将返回该response,已安装的中间件的 process_response() 方法则会在每个response返回时被调用
  • 如果其返回 Request 对象,Scrapy则停止调用 process_request() 方法并重新调度返回的request,当新返回的request被执行后, 相应地中间件链将会根据下载的response被调用
  • 如果其raise一个 IgnoreRequest 异常,则安装的下载中间件的 process_exception() 方法会被调用,如果没有任何一个方法处理该异常, 则request的errback(Request.errback)方法会被调用,如果没有代码处理抛出的异常, 则该异常被忽略且不记录(不同于其他异常那样)
参数 说明
request (Request 对象) – 处理的request
spider (Spider 对象) – 该request对应的spider

process_response(self, request, response, spider)

  • process_request() 必须返回以下之一: 返回一个 Response 对象、 返回一个 Request 对象或raise一个 IgnoreRequest 异常
  • 如果其返回一个 Response (可以与传入的response相同,也可以是全新的对象), 该response会被在链中的其他中间件的 process_response() 方法处理
  • 如果其返回一个 Request 对象,则中间件链停止, 返回的request会被重新调度下载,处理类似于 process_request() 返回request所做的那样
  • 如果其抛出一个 IgnoreRequest 异常,则调用request的errback(Request.errback),如果没有代码处理抛出的异常,则该异常被忽略且不记录(不同于其他异常那样)
参数 说明
request (Request 对象) – response所对应的request
response (Response 对象) – 被处理的response
spider (Spider 对象) – response所对应的spider

process_exception(self, request, exception, spider)

  • 当下载处理器(download handler)或 process_request() (下载中间件)抛出异常时, Scrapy调用 process_exception()
  • process_exception() 应该返回以下之一: 返回 None 、 一个 Response 对象、或者一个 Request
  • 如果其返回 None ,Scrapy将会继续处理该异常,接着调用已安装的其他中间件的process_exception() 方法,直到所有中间件都被调用完毕,则调用默认的异常处理
  • 如果其返回一个 Response 对象,则已安装的中间件链的 process_response() 方法被调用,Scrapy将不会调用任何其他中间件的 process_exception() 方法
  • 如果其返回一个 Request 对象, 则返回的request将会被重新调用下载,这将停止中间件的process_exception() 方法执行,就如返回一个response的那样
参数 说明
request ( Request 对象) – 产生异常的request
exception (Exception 对象) – 抛出的异常
spider (Spider 对象) – request对应的spider

内置下载中间件

CookiesMiddleware

该中间件使得爬取需要cookie(例如使用session)的网站成为了可能,其追踪了web server发送的cookie,并在之后的request中发送回去, 就如浏览器所做的那样

配置 说明
COOKIES_ENABLED 是否启用cookies middleware,如果关闭,cookies将不会发送给web server
COOKIES_DEBUG 如果启用,Scrapy将记录所有在request发送的cookies及response接收到的cookies

DefaultHeadersMiddleware

该中间件设置 DEFAULT_REQUEST_HEADERS 指定的默认request header

DownloadTimeoutMiddleware

该中间件设置 DOWNLOAD_TIMEOUT 指定的request下载超时时间

Scrapy进阶

自动限速

下面是控制AutoThrottle扩展的设置:

  • AUTOTHROTTLE_ENABLED:是否启用AutoThrottle扩展
  • AUTOTHROTTLE_START_DELAY:初始下载延迟(单位:秒)
  • AUTOTHROTTLE_MAX_DELAY:在高延迟情况下最大的下载延迟(单位秒)
  • AUTOTHROTTLE_DEBUG:起用AutoThrottle调试(debug)模式,展示每个接收到的response
  • CONCURRENT_REQUESTS_PER_DOMAIN:对单个网站进行并发请求的最大值
  • CONCURRENT_REQUESTS_PER_IP:对单个IP进行并发请求的最大值
  • DOWNLOAD_DELAY:下载器在下载同一个网站下一个页面前需要等待的时间

暂停与重启

Scrapy提供了一种暂停和重启的机制,我们可以启动一个爬虫后,在一段时间后,将它暂停,然后过一段时间后,重启爬虫,让它继续上一次的任务

要暂停爬虫,我们需要一个文件夹来存放爬虫的状态,可以在配置文件中定义一个 JOBDIR = 'info' 来指定一个文件夹存放数据,一般的我们还需要在这个文件夹下在定义一个文件夹如 001 它表示,我们第一次爬取的状态

除了通过配置文件的方式,我们也可以直接通过命令行的方式,来指定目录

scrapy crawl jobbole -s JOBDIR=info/001

在爬虫运行的过程中,使用 Ctrl + c 可以使爬虫暂停,注意只按一次,它不会马上停止,还会执行一些收尾工作,两次的话会强制关闭进程,保存状态就会失败

再次使用上面的命令就可以重启爬虫

scrapy crawl jobbole -s JOBDIR=info/001

如果要重头开始重新爬的话就需要重新定义一个文件夹如 002

当然,不同的 spider 也不能使用同一个文件夹,注意区别开来

Telnet终端

Scrapy提供了内置的telnet终端,以供检查,控制Scrapy运行的进程,telnet仅仅是一个运行在Scrapy进程中的普通python终端,因此您可以在其中做任何事

telnet终端监听设置中定义的 TELNETCONSOLE_PORT ,默认为 6023

所以直接在命令行中输入(windows下需要开启以下telnet服务)

telnet localhost 6023

telnet为了方便提供了一些常用的变量:

快捷名称 描述
crawler Scrapy Crawler ( scrapy.crawler.Crawler 对象)
engine Crawler.engine属性
spider 当前激活的spider
slot the engine slot
extensions 扩展管理器(Crawler.extensions属性)
stats 状态收集器(Crawler.stats属性)
settings Scrapy设置对象(Crawler.settings属性)
est 打印引擎状态的报告
prefs 针对内存调试
p pprint.pprint 函数的简写
hpy 针对内存调试

数据收集器

Scrapy提供了方便的收集数据的机制。数据以key/value方式存储,值大多是计数值

该机制叫做数据收集器(Stats Collector),可以通过 Crawler API 的属性 stats 来使用

数据收集器对每个spider保持一个状态表,当spider启动时,该表自动打开,当spider关闭时,自动关闭

基本使用方法如下:

我们可以通过spider的 crawler.stats 来使用数据收集器,如下

class ExtensionThatAccessStats(object):

    def __init__(self, stats):
        self.stats = stats

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.stats)

在Spider的类中,我们也可以直接使用 self.crawler.stats 来使用该对象

方法 说明
set_value('key', value) 设置数据
inc_value('key') 增加数据值
max_value('key', value) 当新的值比原来的值大时设置数据
min_value('key', value) 当新的值比原来的值小时设置数据
get_value('key') 获取数据
get_stats() 获取所有数据

除了基本的 StatsCollector ,Scrapy也提供了基于 StatsCollector 的数据收集器

可以通过 STATS_CLASS 设置来选择,默认使用的是 MemoryStatsCollector

举个例子:我们允许接收 404 页面的返回,在处理函数中,判断到页面的状态码为 404 的时候,我们写入一个数据到 stats

class MySpider(scrapy.Spider):
    ...
    handle_httpstatus_list = [404]

    def parse(self, response):
        if response.status == 404:
            self.crawler.stats.inc_value('failed_url')

信号

Scrapy使用信号来通知事情发生,信号机制是基于 Twisted deferreds 实现的异步通信

可以在Scrapy项目中捕捉一些信号(使用extension)来完成额外的工作或添加额外的功能,扩展Scrapy

内置信号介绍

信号 说明
engine_started 当Scrapy引擎启动爬取时发送该信号
engine_stopped 当Scrapy引擎停止时发送该信号
item_scraped 当item被爬取,并通过所有 Item Pipeline 发送该信号
item_dropped 当item通过 Item Pipeline 时,抛出 DropItem 异常时,发送该信号
spider_closed 当某个spider被关闭时,该信号被发送
spider_opened 当spider开始爬取时发送该信号
spider_idle 当spider进入空闲状态时该信号被发送
spider_error 当spider的回调函数产生错误时,该信号被发送
request_scheduled 当引擎调度一个 Request 对象用于下载时,该信号被发送
response_received 当引擎从downloader获取到一个新的 Response 时发送该信号
response_downloaded 当一个 HTTPResponse 被下载时,由downloader发送该信号

使用方式

  • connect(receiver, signal)

    链接一个接收器函数(receiver function) 到一个信号(signal)。signal可以是任何对象,虽然Scrapy提供了一些预先定义好的信号

    参数 说明
    receiver (可调用对象) – 被链接到的函数
    signal (对象) – 链接的信号
  • send_catch_log(signal, **kwargs)

    发送一个信号,捕获异常并记录日志

    关键字参数会传递给信号处理者(signal handlers)(通过方法 connect()关联)

  • send_catch_log_deferred(signal, **kwargs)

    send_catch_log() 相似但支持返回 deferreds 形式的信号处理器

    返回一个 deferred ,当所有的信号处理器的延迟被触发时调用,发送一个信号,处理异常并记录日志

    关键字参数会传递给信号处理者(signal handlers)(通过方法 connect() 关联)

  • disconnect(receiver, signal)

    解除一个接收器函数和一个信号的关联

    这跟方法 connect() 有相反的作用, 参数也相同

  • disconnect_all(signal)

    取消给定信号绑定的所有接收器

    参数 说明
    signal (对象) – 要取消绑定的信号

举个例子:

在初始化的时候我们绑定了一个 spider_closed 的信号,回调函数为 handle_spider_closed

当spider关闭的时候就会调用 handle_spider_closed 这个函数

from scrapy import Request, signals
from scrapy.xlib.pydispatch import dispatcher

class MySpider(scrapy.Spider):
    ...
    def __init__(self):
        self.fail_urls = []
        dispatcher.connect(self.handle_spider_closed, signals.spider_closed)

    def handle_spider_closed(self, spider, reason):
        self.crawler.stats.set_value('failed_urls', ','.join(self.fail_urls))
标签: Scrapy爬虫

召唤伊斯特瓦尔