scrapy学习笔记


入坑爬虫,学习scrapy查看 官方文档时的一些笔记。

初入爬虫

Scrapy是一个用于爬网网站和提取结构化数据的应用程序框架,可用于各种有用的应用程序,例如数据挖掘,信息处理或历史档案。

结构概览

数据流向
scrapy中的engine控制着数据的流动方向。

  1. enginespiders中获取到需要爬取的Request对象
  2. engine将这个Request对象放到Scheduler中进行任务调度,并且要求scheduler返回下一个要爬取的Request对象
  3. scheduler将待爬取的Request对象传递给engine
  4. engine经过download middlewatesprocess_request()方法后,到达downloader
  5. downloader开始下载Request对应的网页,然后将返回的内容生成一个Response对象。这个Response对象将经过download middlewatesprocess_response()方法后,到达engine
  6. engine收到downloader传递的response对象以后,会将其通过spider middleawareprocess_spider_input()传递到spider中进行处理
  7. spider再处理完response对象以后,将提取处理的items对象和下一个将要爬取的Request对象通过spider middleawareprocess_spider_output()传递到engine
  8. engine将处理好的items对象传递到item pipeline中,然后将Request对象传递给scheduler,并询问下一个需要下载的Request对象
  9. 然后就开始重复1-8,直到所有的链接都被爬取完毕

安装

scrapy官网建议使用虚拟环境,避免与其他库冲突。

这里选用anaconda环境。conda install -c conda-forge scrapy

基本命令

scrapy提供了许多命令官方文档,分为全局可用和项目可用两大类。所有全局可用的在项目中也可以使用,但是可能会有不同的表现形式
scrapy 命令

1.全局可用命令

  • 所有命令scrapy
  • 创建新项目 scrapy startproject tutorial
  • 创建爬虫模块 scrapy genspider example example.com
  • 请求网页资源 scrapy fetch <url> 在项目中执行该命令,会使用程序运行的上下文环境,比如项目中的user-agent,session, cookie等信息
  • 打开网页 scrapy view <url>
  • 使用shell工具 scrapy shell [url]
  • 查看配置信息 scrapy settings [options] 在项目中使用,会包含项目中的settings.cfg
  • 直接运行一个爬虫,不用创建项目scrapy runspider <spider_file.py>

2.项目可用命令

  • 可执行爬虫列表 scrapy list
  • 开始爬虫scrapy crawl myspider
  • 检测scrapy check myspider
  • 请求页面,并解析结果scrapy parse <url> [options]

简单的爬虫项目

1. 创建新项目

在终端执行 scrapy startproject tutorial

项目结构

2. 创建爬虫模块

example: 爬虫名 项目唯一 example.com: 需要爬取的网站 scrapy genspider example example.com

3. 编写代码

quotes_spider.py

 	import scrapy
	class QuotesSpider(scrapy.Spider):
    # 当前爬虫的名称,日志、启动时需要使用
    name = "quotes"

    # 当需要爬取的链接比较少,可以省略start_requests方法,选择start_urls。爬虫启动时,会爬取start_urls中的链接
    # start_urls = [
    #     'http://quotes.toscrape.com/page/1/',
    #     'http://quotes.toscrape.com/page/2/',
    # ]

    def start_requests(self):
        """
        :return: 可迭代对象,元素为爬虫将要爬取数据的网站
        """
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        """
        下载完成以后,对获取到内容以后进行处理
        :param response: TextResponse对象,包含了网页信息
        :return:
        """
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('save file {}'.format(filename))

4. 开始爬虫

scrapy crawl example

5. 开始爬虫,并保存数据

执行命令 scrapy crawl example -o data.jl会将程序中yield的数据保存在指定文件中

scrapy默认的将输出的数据追加到已存在的文件上。如果使用json格式,将会导致文件出错。因此这里选择jl格式的数据。

6. 深度爬取数据

获取到的网页中,含有许多超链接,里面可能包含需要的数据,因此需要将这些链接提取出来,继续进行数据爬取。
修改quotes_spider.py文件。

    def parse(self, response):
        """
        下载完成以后,对获取到内容以后进行处理
        :param response: 网页信息
        :return:
        """
        for quote in response.css("div.quote"):
            yield {
                # ::text 获取文本
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags::text').getall()
            }
        # 提取网页中的URL,进行深度爬虫
        next_page = response.css('li.next a::attr(href)').get()
        # next_page = response.css('li.next a').attrib['href']
        if next_page:
            # 拼接下一个uri的地址,并发起请求
            # yield response.follow(next_page, callback=self.parse)
            next_url = response.urljoin(next_page)
            yield scrapy.Request(url=next_url, callback=self.parse)
  • 创建链接的快捷方式
    scrapy提供了response.follow()response.follow_all()方法,用来快速创建链接对象。
    ps: 需要自己使用yield返回。使用follow_all()时,需要使用yield from
  1. 可以将合并链接、发起请求简化为一个方法
yield response.follow(next_page, callback=self.parse)
  1. 传入选择器
for href in response.css('ul.pager a::attr(href)'):
    yield response.follow(href, callback=self.parse)
  1. 对于使用了a标签的超链接,可以直接传入一个选择器或选择器的筛选条件
for a in response.css('ul.pager a'):
    yield response.follow(a, callback=self.parse)
  1. 批量创建链接
# 选择器
anchors = response.css('ul.pager a')
yield from response.follow_all(anchors, callback=self.parse)
# 也可以传入css,xpath等筛选条件
yield from response.follow_all(css='ul.pager a', callback=self.parse)

7. 爬虫时传递参数

在开始爬虫时,使用-a key=value传递参数。类在初始化时在__init__方法中将key设置为实例属性,
eg: scrapy crawl quotes -o quotes-humor.json -a tag=humor

6. 使用pycharm进行debug

scrapy.cmdline模块中提供了execute函数,执行这个函数即可启动爬虫

  1. project.cfg同级目录下,新建一个debug.py文件
  2. 加入以下代码
# -*- coding: utf-8 -*-

from scrapy.cmdline import execute


if __name__ == '__main__':
	# execute()
    execute(['scrapy', 'crawl', 'quotes-simple'])

  1. 多个爬虫程序时,可以省略execute参数,然后在pycharm运行时,指定要进行的操作
    pycharm debug传参

settings.py配置文件

官方文档

  • DUPEFILTER_CLASSURL过滤器
    scrapy默认使用scrapy.dupefilters.RFPDupeFilter,用来过滤已经爬过的网站

提高自我-核心概念

1.spiders

一个spider类对应着将要爬取数据的一个网站,在这个类中需要定义爬虫的名称,将要爬取的URL,以及对爬取数据的解析等。
通常来说,爬虫在进行着如下循环:

  1. 首先调用类中的start_requests()方法,这个方法中定义了一开始要爬取的地址start_urls,并指定了对爬取回数据的处理函数call_back_function。ps:start_requests()方法只会调用一次
  2. call_back_function中,用Selectors、BeautifulSoup、lxml等工具解析TextResponse对象中的数据,将解析好的数据传递出去。还可以返回新的Request对象
  3. 最后通过item Pipeline或者FeedExport将数据保存下来

基类 Spider

所有的爬虫是scrapy.Spider的子类

通用爬虫CrawlSpider

CrawlSpider可以通过提供的rules来提取网站中的链接,因此通常用来爬取URL格式比较规范的网站。
构造Rule的核心参数为LinkExtractor,通过正则来过滤并返回需要继续爬取的URL,然后将爬取到的数据通过callback方法来解析。

注意:

  1. callback可以是一个可以找到对应方法的字符串
  2. callback不要用parse
  3. 当callback不为空时,follow默认为False
import scrapy
from scrapy.spiders import Rule, CrawlSpider
from scrapy.linkextractors import LinkExtractor


class QuotesSpiderExtendCrawl(CrawlSpider):
    name = "quote-crawl"
    allow_domains = ['quotes.toscrape.com']
    start_urls = ['http://quotes.toscrape.com']
	# callback函数不要用parse,不然会怀疑人生
	# follow为True时,会从已爬取的链接中再次使用如下规则进行过滤,有点儿递归的感觉
    rules = (Rule(LinkExtractor(allow=(r"page/\d",)), callback='parse_list', follow=True),
             Rule(LinkExtractor(allow=(r"author/.*",)), callback='parse_author'))

    def parse_list(self, response):
        """
        下载完成以后,对获取到内容以后进行处理
        :param response: 网页信息
        :return:
        """
        for quote in response.css("div.quote"):
            yield {
                # ::text 获取文本
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags::text').getall()
            }

    def parse_author(self, response):
        """
        提取作者信息
        :param response:
        :return:
        """
        author_dict = dict(Name=response.css(".author-title::text").get(),
                           BornDate=response.css(".author-born-date::text").get(),
                           BornLocation=response.css(".author-born-location::text").get(),
                           Desc=response.css(".author-description::text").get()[:20])
        self.log(json.dumps(author_dict))
        yield author_dict

2. selectors

通常爬取的网页中信息量很大,而我们需要用的只是其中的一部分。因此需要从HTML中提取数据,BeautifulSoupxml都可以实现。这里主要记录scrapy所提供的xpathcss这两种简单方式
Response对象中的selector属性为Selector实例,提供了xpath()css()方法。后来因为这两个方法使用太频繁,就在response中提供了这两个方法的快捷方式

选择器用于获取dom节点,支持链式语法。获取节点时,数据处理需谨慎,获取不到数据也最好不要让程序崩溃

There’s a lesson here: for most scraping code, you want it to be resilient to errors due to things not being found on a page, so that even if some parts fail to be scraped, you can at least get some data.

css选择器

  • ::text获取dom节点上的文字信息
  • ::attr(href)获取指定的属性
  • .get()获取一个元素
  • .getall()获取所有选中元素
  • .re()使用正则过滤元素
  • .attrib['href']获取指定的属性
scrapy shell "http://quotes.toscrape.com/page/1/"
>>> response.css('title')
# 返回选择器对象
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
# ::text获取dom节点上的文字信息
# .get()获取一个元素
>>> response.css('title::text').get()
'Quotes to Scrape'
# .getall()获取一个list
>>> response.css('title::text').getall()
['Quotes to Scrape']
# .re()使用正则过滤数据
>>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape']
>>> response.css('title::text').re(r'Q\w+')
['Quotes']
>>> response.css('title::text').re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']
# ::attr(itemprop)获取指定的属性
>>> response.css('div.quote')[0].css('span.text::attr(itemprop)').get()
'text'
# .attrib('itemprop')获取指定的属性
>>> response.css('div.quote')[0].css('span.text').attrib['itemprop']
'text'

xpath选择器

scrapy抽取数据时,支持使用cssxpath进行数据选择。但是css的底层还是转化为xpath,因此建议使用xpath
详细用法参考

  • /使用绝对路径,从顶级开始
  • //使用的是相对路径
  • .当前路径
  • ..父类路径
  • nodename所有指定节点
  • @获取节点属性

使用技巧

  1. 当根据class筛选节点时,使用CSS选择器
    如果选择xpath,使用@class=A会错过那些同时拥有A B的节点 ,使用contains(@class, 'A')会捕获到C-B-A这种类名里包含的节点。
>>> from scrapy import Selector

>>> t = Selector(text='<div class="ABC">ABC</div><div class="A B C">A B C</div><div class="A">A</div>')
>>> t.xpath('//div[@class="A"]/text()').getall()
['A']
>>> t.xpath('//div[contains(@class,"A")]/text()').getall()
['ABC', 'A B C', 'A']
>>> t.css('div.A::text').getall()
['A B C', 'A']

  1. //node[1](//node)[1]的区别
    //node[1]获取所有node节点下的第一个节点
    (//node)[1]获取第一个node节点下的第一个节点
  2. 使用文本内容筛选节点,用.来代替.//text()
  3. xpath筛选时传入参数
# 使用$param作为占位符,然后通过关键字参数传参
>>> t.xpath('//div[contains(@class, $val)]/text()', val="A").getall()
['ABC', 'A B C', 'A']

3. items

scrapy将spider返回的数据称为item,是一个键值对的Python对象。以下为几种常用item类型

字典dict

scrapy定义的item对象scrapy.item.Item

scrapy的Item对象是dict的加强版,提供了copydeepcopy方法,并且可以将key定义为属性,使用时更加规范

from scrapy.item import Item, Field
from scrapy.loader.processors import MapCompose, Join, TakeFirst
from w3lib.html import remove_tags

class QuotesAuthorItem(scrapy.Item):
	# input_processor和output_processor分别为输入输出处理函数
    Name = scrapy.Field(input_processor=MapCompose(remove_tags),
    					output_processor=TakeFirst())
    BornDate = scrapy.Field()
    BornLocation = scrapy.Field()
    Desc = scrapy.Field()
    Remark = scrapy.Field()

Dataclass 装饰的对象

使用dataclass装饰的对象,也可以将key定义为属性

from dataclasses import dataclass

@dataclass
class CustomItem:
    one_field: str
    another_field: int

attr.s 装饰的对象

使用attr.s装饰的对象,也可以将key定义为属性

import attr

@attr.s
class CustomItem:
    one_field = attr.ib()
    another_field = attr.ib()

4. Items Loader

Items看做一个容器,那么items loader就是用来决定数据进入那个容器的机制。

实例

导入QuotesAuthorItem,重写parse_author方法

form ..items import QuotesAuthorItem

def parse_author(self, response):
	# 默认返回的数据都是列表形式
    item = ItemLoader(item=QuotesAuthorItem(), response=response)
    item.add_css('Name', ".author-title::text")
    item.add_css('BornDate', ".author-born-date::text")
    item.add_css('BornLocation', ".author-born-location::text")
    item.add_value('Desc', response.css(".author-description::text").get()[:20])
    item.add_xpath('Remark', '//h1/text()')
    yield item.load_item()

输入输出处理器

每一个item Loader对于每一个属性,都有对应的输入处理器和输出处理器。当使用add_css(), add_xpath(), add_value()时输入处理器会被调用,当数据提取完毕,调用item.load_item()时输出处理器会被调用。item最终获取的值,是经历过输出处理器处理过的值。

输入和输出处理器都必须接收一个可迭代对象作为其第一个参数。
这些函数的输出可以是任何东西。
输入处理器的结果将被添加到内部列表中(在Loader中),该列表包含(针对该字段)收集的值。
输出处理器的结果是最终将分配给该item的值。

常用处理器函数

  • w3lib.html.remove_tags 去除htmlxml的标签
  • scrapy.loader.processors.TakeFirst 取可迭代对象中的第一个元素
  • scrapy.loader.processors.Join 拼接数据,默认空格
  • scrapy.loader.processors.Compose(*functions, **default_loader_context) 输入值由第一个函数处理,输出值由第二个函数处理
  • scrapy.loader.processors.MapCompose(*functions, **default_loader_context)可以同时传入多个处理函数,然后每一个经过处理的元素都会被加入到一个列表中,然后会将这个列表作为参数,传入下一个处理函数中,直到所有函数都处理完以后才会返回结果。
  • scrapy.loader.processors.SelectJmes(json_path)声明的时候需要传入一个字符串,然后就可以在json格式的数据中搜索到这个字符串对应的值

自定义Items Loader

scrapy中提供了需要通用的处理器,使用时直接实例化对应类即可。也可以自定义处理器,通过在item loader类属性后加_in_out来指定对应的输入处理器和输出处理器。

ps:自定义输入处理器后,一定要记得返回处理结果,否则输出处理器会获取不到值
自定义处理器函数,需要作为参数实例化scrapy.loader.processors.MapCompose

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

def value_in(value):
    if isinstance(value, str):
        print('{} in'.format(value))
    return value


class TutorialAuthorLoader(ItemLoader):
    default_output_processor = TakeFirst()
    Name_in = MapCompose(value_in)
    Name_out = Join()

嵌套——nested_xpath和nested_css

item loader提供了nested_xpath()nested_css()方法,当需要提取同一个父节点下的数据,并且这个父节点隐藏的很深的时候。可以通过这两个方法,先选中父节点,然后子节点的选择只需要相对父节点即可。类似于使用xpath选择器时使用的相对路径。

# 通常写法
loader = ItemLoader(item=Item())
# load stuff not in the footer
loader.add_xpath('social', '//footer/a[@class = "social"]/@href')
loader.add_xpath('email', '//footer/a[@class = "email"]/@href')
loader.load_item()

# 嵌套写法
loader = ItemLoader(item=Item())
# load stuff not in the footer
footer_loader = loader.nested_xpath('//footer')
footer_loader.add_xpath('social', 'a[@class = "social"]/@href')
footer_loader.add_xpath('email', 'a[@class = "email"]/@href')
# no need to call footer_loader.load_item()
loader.load_item()

5. scrapy shell

加强版的python shell。可以用来尝试获取选择器,提取数据 scrapy shell http://www.baidu.com

shell

跳一跳

在编写程序时,经常需要对代码进行调试,但是依赖pycharm的调试框,每次只能展示一个结果,结果对比比较麻烦。贴心的是,scrapy提供了一个inspect_response()的方法,程序执行到这个方法时,会自动跳到终端,并且携带了上下文环境,当退出终端时,还可以继续执行刚才的代码,非常因吹斯汀。

6. Item Pipline

当数据从spider中抛出(yield)以后,会依次进入指定的item Pipline中进行数据筛选,每一个Pipline都可以决定数据继续处理,还是被舍弃掉。

常用场景

  1. 清洗html数据
  2. 校验数据是否是自己想要的
  3. 检查数据是否有重复
  4. 将数据存放到数据库中

自定义Pipeline

每一个Pipeline组件都是一个独立的python类,必须实现一些必要的方法:

  1. 必须实现process_item(self, item, spider)
    其中spider就是对应的爬虫类,item则为爬虫返回的数据。这个方法必须在返回item object、返回defererdraise DropItem exception中三选一。··
    process_item() must either: return an item object, return a Deferred or raise a DropItem exception.
  2. 可选实现方法
  • open_spider(self, spider) 爬虫开始时调用
  • close_spider(self, spider) 爬虫结束后调用
  • from_crawler(cls, crawler)

If present, this classmethod is called to create a pipeline instance from a Crawler. It must return a new instance of the pipeline. Crawler object provides access to all Scrapy core components like settings and signals; it is a way for pipeline to access them and hook its functionality into Scrapy.
from_crawler方法存在的时候,将以crawer为媒介创建并返回一个Pipeline实例。crawler对象提供了访问所有scrapy组件(比如settings,signals)访问的入口。Pipeline管道可以通过这种方式访问scrapy的方法,并且将自己的方法挂到scrapy中,等待数据处理时被调用。

PipeLine 实例

  1. 在Pipeline.py中实现自定义管道
import json
from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem
from .items import QuotesItem


class TutorialPipeline:
	"""
    过滤内容为空的数据
    """
    def process_item(self, item, spider):
        adapter = ItemAdapter(item)
        print(item, type(item))
        if isinstance(item, QuotesItem):
            if adapter.get("Text"):
                return item
            else:
                raise DropItem("没有内容%s" % item)
        return item


class TutorialStorePipeline:
	"""
    数据存储
    """
    collection_name = 'scrapy_items'

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def close_spider(self, spider):
        self.client.close()

    def process_item(self, item, spider):
        self.db[self.collection_name].insert_one(ItemAdapter(item).asdict())
        return item

  1. 在配置文件settings.py中,注册要使用的管道
ITEM_PIPELINES = {
	# key为Pipeline所在包,value是数据流通的顺序,值范围通常在0-1000,数据会先进入值最小的管道,然后进入值大的管道
   'tutorial.pipelines.TutorialPipeline': 300,
   'tutorial.pipelines.TutorialStorePipeline': 301,
}

在管道中使用协程

当在管道中处理数据需要进行一些耗时的操作时,可以通过使用协程来分配内存资源,降低程序耗时。

class ImageDownloadPipeline:
    async def process_item(self, item, spider):
        adapter = ItemAdapter(item)
        if adapter.get("Url"):
            request = scrapy.Request(adapter.get("Url"))
            response = await spider.crawler.engine.download(request, spider)

            if response.status != 200:
                return item

            url = adapter["url"]
            url_hash = hashlib.md5(url.encode("utf8")).hexdigest()
            filename = "{}.png".format(url_hash)
            with open(filename, "wb") as f:
                f.write(response.body)
            # Store filename in item.
            adapter["screenshot_filename"] = filename
        return item

7.Feed Exports

通过爬虫爬取的数据,通常需要导出到文件存储系统中(数据库,文件),以供其他系统使用。scrapy通过Feed Exports实现这个功能,允许数据以多种格式存放到多种存储系统中。

支持数据序列化格式

  • json
  • json line
  • csv
  • xml
  • pickle
  • Marshal

支持的存储引擎

  • Local filesystem
  • ftp
  • S3 (requires botocore)
  • Standard output

相关配置

  • FEED_EXPORT_ENCODING 编码格式
  • FEED_STORE_EMPTY 是否储存空数据
  • FEED_EXPORT_FIELDS 导出字段
  • FEED_EXPORT_INDENT 缩进
  • FEED_STORAGES 自定义存储引擎 {URI:storage class}
  • FEED_STORAGE_FTP_ACTIVE ftp存储引擎
  • FEED_STORAGE_S3_ACL S3存储
  • FEED_STORAGES_BASE scrapy内置的存储引擎
  • FEED_EXPORTERS 自定义数据序列化格式
  • FEED_EXPORTERS_BASE 内置支持的序列化格式

以上所有的配置项,可以整合到FEED中配置,查看详情

8. Requests和Responses

scrapy主要依靠RequestResponse两个对象爬取web网站。通常Request对象在爬虫开始时生成,然后在各个组件中传递,直到进入Downloader下载器。接着Downloader会请求Request对象指定的地址,并且返回一个Response对象给对应的爬虫类。

Request对象

通过cb_kwargscall_back方法传递额外参数
def parse(self, response):
    request = scrapy.Request('http://www.example.com/index.html',
                             callback=self.parse_page2,
                             cb_kwargs=dict(main_url=response.url))
    request.cb_kwargs['foo'] = 'bar'  # add more arguments for the callback
    yield request

def parse_page2(self, response, main_url, foo):
    yield dict(
        main_url=main_url,
        other_url=response.url,
        foo=foo,
    )
通过err_back处理异常
def parse(self, response):
    request = scrapy.Request('http://www.example.com/index.html',
                             callback=self.parse_page2,
                             errback=self.errback_page2,
                             cb_kwargs=dict(main_url=response.url))
    yield request

def parse_page2(self, response, main_url):
    pass

def errback_page2(self, failure):
	if failure.check(HttpError):
        # these exceptions come from HttpError spider middleware
        # you can get the non-200 response
        response = failure.value.response
        self.logger.error('HttpError on %s', response.url)
    yield dict(
   		# err_back中的额外参数,需要通过failure访问request对象,然后访问request对象的cb_kwargs属性
        main_url=failure.request.cb_kwargs['main_url'],
    )
Request中的一些特殊的meta值

special meta key

ps: 关于cookie信息,request对象中可以携带cookie,并且会自动合并response设置的cookie信息,再下一次请求时,携带这些cookie信息。如果想禁用这一特性,可以在request的meta中设置dont_merge_cookies为True

停止从响应中下载数据

bytes_received信号处理器中抛出一个StopDownload异常,会停止从response中响应数据

import scrapy


class StopSpider(scrapy.Spider):
    name = "stop"
    start_urls = ["https://docs.scrapy.org/en/latest/"]

    @classmethod
    def from_crawler(cls, crawler):
        spider = super().from_crawler(crawler)
        crawler.signals.connect(spider.on_bytes_received, signal=scrapy.signals.bytes_received)
        return spider

    def parse(self, response):
        # 'last_chars' show that the full response was not downloaded
        yield {"len": len(response.text), "last_chars": response.text[-40:]}

    def on_bytes_received(self, data, request, spider):
    	# 通常情况下,抛出异常之后,会调用其对应的err_back函数,这里设置fail=False以后,scrapy会认为程序并没有出错,并调用call_back函数
        raise scrapy.exceptions.StopDownload(fail=False)
常见子类
  1. FormRequest 常用来传递表单格式参数,比如模拟登陆
# 通过post方式传递参数
def parse1(self, response):
	return [FormRequest(url="http://www.example.com/post/action",
	                    formdata={'name': 'John Doe', 'age': '27'},
	                    callback=self.after_post)]
# 通过from_response模拟登陆
def parse(self, response):
    return scrapy.FormRequest.from_response(
        response,
        formdata={'username': 'john', 'password': 'secret'},
        callback=self.after_login
    )
  1. JsonRequest 传递JSON格式的参数

Response对象

通过follow和follow_all快速构建新的Request对象
# 拼接下一个uri的地址,并发起请求,和下边两行代码作用相同
yield response.follow(next_page, callback=self.parse)
next_url = response.urljoin(next_page)
yield scrapy.Request(url=next_url, callback=self.parse)

# follow_all生成一批Request对象
yield from response.follow_all(css='div.quote small.author +a', callback=self.parse_author)

常见子类
  1. TextResponse
  2. HtmlResponse
  3. XmlResponse

9. Link Extractors

链接生成器可以通过一系列规则从Response对象中筛选URL。通过rules属性指定本爬虫类需要爬取的url地址,及其对应的处理函数。scrapy.linkextractors.LinkExtractorscrapy.linkextractors.lxmlhtml.LxmlLinkExtractor是同一个对象,前者主要是为了导入方便。可以通过其extract_links(response)方法查看匹配到的URL。

rules = (Rule(LinkExtractor(allow=(r"page/\d",)), callback='parse_list', follow=True),
         Rule(LinkExtractor(allow=(r"author/.*",)), callback='parse_author', errback='err_back_test', cb_kwargs={"filter_none": True}))

10. Middleware

middleware中间件可以看做是一个桥梁,当数据从enginedownloaderspider时,需要经过这些桥梁。可以在这些桥梁中设置特定方法,进行数据处理。

Downloader Middleware

下载中间件位于enginedownloader中间,用于处理RequestResponse对象。

激活中间件

使用时,需要在settings.py文件中声明.

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

声明的中间件会与scrapy默认配置的中间件合并,并按照值从小到大进行处理。禁用默认中间件需要将值设为None即可。

内置中间件
  • CookiesMiddleware 模拟浏览器管理cookie
  • DefaultHeadersMiddleware 为请求设置settings中配置的请求头
  • DownloadTimeoutMiddleware
  • HttpAuthMiddleware 在需要认证的爬虫中设置http_userhttp_pass
  • HttpCacheMiddleware 缓存
  • HttpCompressionMiddleware 压缩
  • RedirectMiddleware 重定向
  • MetaRefreshMiddleware
  • RetryMiddleware 错误重试
  • RobotsTxtMiddleware 网站允许爬虫资源
  • DownloaderStats 下载统计 需要设置DOWNLOADER_STATS=True
  • UserAgentMiddleware user agent设置
  • AjaxCrawlMiddleware
自定义中间件

当我们想diy一个中间件时,可以实现process_requestprocess_responseprocess_exception方法。这些方法会在恰当的时间被scrapy调用,不需全部实现,满足自己的需求即可。最后记得要在settings.py中设置激活。

class TutorialDownloaderMiddleware:
	@classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_request(self, request, spider):
        # Called for each request that goes through the downloader
        # middleware.

        # Must either:
        # - return None: 继续走流程
        # - or return a Response object:流程中断,不会调用其它的process_request,process_exception以及对应的下载方法,但是会调用process_response方法
        # - or return a Request object:流程中断,并计划下载这个新的request对象
        # - or raise IgnoreRequest: 流程中断,由process_exception接管,如果中间件没有处理,则往上抛由Request.errback处理。
        # 如果代码未处理这个异常,这个异常就被忽略,并且不会被记录下来
        return None

    def process_response(self, request, response, spider):
        # Called with the response returned from the downloader.

        # Must either;
        # - return a Response object 继续走流程
        # - return a Request object 流程中断,开始准备下载新的request对象
        # - or raise IgnoreRequest 调用Request.errback,如果代码未处理这个异常,这个异常就被忽略,并且不会被记录下来
        return response

    def process_exception(self, request, exception, spider):
        # Called when a download handler or a process_request()
        # (from other downloader middleware) raises an exception.

        # Must either:
        # - return None: continue processing this exception
        # - return a Response object: stops process_exception() chain,进入到process_response的流程中
        # - return a Request object: stops process_exception() chain, 准备下载request对象
        pass

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

Spider Middleware

爬虫中间件位于enginespider中间,用于处理spider生成的request、item对象,以及engine返回的response对象。

激活中间件

使用时,需要在settings.py文件中声明.

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.CustomSpiderMiddleware': 543,
    'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': None,
}

声明的中间件会与scrapy默认配置的中间件合并,并按照值从小到大进行处理。禁用默认中间件需要将值设为None即可。

内置爬虫中间件
  • DepthMiddleware 记录爬虫的深度,在settings.py中通过DEPTH_LIMITDEPTH_STATS_VERBOSE,DEPTH_STATS_VERBOSE
  • HttpErrorMiddleware 过滤掉出错的HTTP响应,降低资源消耗 HTTPERROR_ALLOWED_CODESHTTPERROR_ALLOW_ALL
  • OffsiteMiddleware 过滤掉爬取域名以外的链接
  • UrlLengthMiddleware Url长度过滤
  • RefererMiddleware

内置服务

日志管理

scrapy自身提供的scrapy.log已经废弃了,现在则是使用logging库,每一个spider实例中都可以通过logger属性进行日志输出,默认的日志名称为爬虫的名字。

统计器

scrapy为每一个爬虫都提供了一个统计计数器stats,这个计数器会在爬虫开启时启动,在爬虫结束后关闭。爬虫类需要通过self.crawler.stats才能访问到该属性
stats对象

def parse(self, response):
     """
     stats对象是scrapy提供的统计对象,常用来计数
     """
     # count赋值
     self.crawler.stats.set_value('count', 1)
     # count++
     self.crawler.stats.inc_value('count')
     # 当5>count时,值变为5
     self.crawler.stats.max_value('count', 5)
     # 当3<count时,值变为3
     self.crawler.stats.min_value('count', 3)
     # 获取count值
     self.crawler.stats.get_value('count')
     # 获取所有统计值
     self.crawler.stats.get_stats()

使用技巧

通过Spiders Contracts进行单元测试

针对爬虫的测试比较麻烦,大部分情况下只能写单元测试。Scrapy 通过合同(contract)的方式来提供了测试 spider 的集成方法。通俗讲就是在方法内部通过@contract的方式硬编码参数。参考极客网站

  • url对应的是scrapy.contracts.default.UrlContract协议,设置了用于检查 spider 的其他 constract 状态的样例 url。
  • returns对应的是scrapy.contracts.default.CallbackKeywordArgumentsContract协议,设置 spider 返回的 items 和 requests 的上界和下界
  • scrapes对应的是class scrapy.contracts.default.ScrapesContract协议,用于检查回调函数返回的所有 item 是否有特定的 fields
def parse(self, response):
    """ This function parses a sample response. Some contracts are mingled
    with this docstring.

    @url http://www.amazon.com/s?field-keywords=selfish+gene 
    @returns items 1 16
    @returns requests 0 0
    @scrapes Title Author Year Price
    """

突破反爬虫基本技巧

爬虫和反爬虫之间的斗争一直没有停过,整理一些基础的反爬虫技巧,有待研究

  • 动态修改user agent form
    简单点多准备一些常用浏览器的User-Agent,在发起请求时,随机设置User-Agent

    复杂点,使用fake-useragent库,配合自定义的下载中间件,记得在settings.py文件中激活

class MyUserAgentDownloadMiddleware(object):

    def __init__(self, crawler):
        super(MyUserAgentDownloadMiddleware, self).__init__()
        self.ua = UserAgent()
        self.ua_type = crawler.settings.get("RANDOM_USER_AGENT", "random")

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        return cls(crawler)

    def process_request(self, request, spider):
        def get_ua():
            return getattr(self.ua, self.ua_type)
        request.headers.set_default("User-Agent", get_ua())

  • 禁用cookie,一些网站可能会通过cookie去检测爬虫
  • 设置延迟避免请求过猛,通过DOWNLOAD_DELAY设置
  • 通过访问缓存的手段,避免直接访问网站
  • 准备一个IP池。 可以直接搜索一些可代理的IP,也可以通过开源项目scrapoxy
  • 使用分布式的下载器,比如Crawlera
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页