概述

Scrapy是一个开源的爬虫框架, 参考文档 基于版本2.13.3

创建项目

scrapy startproject scrapy_tutorial [project_dir]

为什么要使用命令行来创建项目? scrapy是一个框架,它具有架构,用户只需要填充内容即可,就好像springboot一样,有种在写配置文件的感觉。 scrapy startproject是一个脚手架,会生成项目结构,不同的文件目录都有对应的作用。

Note

使用脚手架的好处是非常方便,坏处是隐藏了太多细节。

基本的项目结构如下:

scrapy.cfg
myproject/
    __init__.py
    items.py
    middlewares.py
    pipelines.py
    settings.py
    spiders/
        __init__.py
        spider1.py
        spider2.py
        ...

scrapy.cfg是配置文件,放在项目根目录下。文件中的settings表示配置文件的模块名称,即会使用哪个模块作为配置,可以配置多个,默认使用default。环境变量SCRAPY_PROJECT指定了配置的别名。 实际上一个项目中可以有多个子项目,比如myproject1,myproject2等等。

基本概念

Command Line Tools

  • shell
scrapy shell <url>

这会开启一个交互式的shell,类似直接运行python命令。 这对于调试page非常方便,避免频繁的执行代码。

  • crawl
scrapy crawl <spider_name> [- {o | O} <output_file>]

引擎的重要部分。 启动一个spider。支持多种选项。 也可以通过python script来启动。

Spider

核心就是编写Spider子类,包括name,start_urls,parse

  • name: 必须,唯一,表明spider的名称,通常和爬取的网站域名一致。
  • start_urls: 必须,列表,表明爬取的页面。
  • parse: 必须,函数,用来解析页面的内容。

爬虫的3个核心过程为: 请求 解析 存储 Spider类主要完成前面2个过程,存储可以通过配置实现。

一个简单的爬虫,通过Scrapy来实现,基本只需要定义parse即可。

除了基本得Spider外,还内置了一些常用的Spider:

  • CrawlSpider: 比较通用的Spider,核心在于通过配置Rule来实现控制。
  • XMLFeedSpider: 爬取XML格式的数据。
  • CSVFeedSpider: 爬取CSV格式的数据。
  • SitemapSpider: 爬取网站的Sitemap

Selector

有2种模式cssxpath,前者是标准的css选择器,后者是xpath选择器。 核心都是通过选择器定位dom元素,然后获取attrtext。 注意不管是dom,attr还是text都是Selector实例,需要通过get,getall方法获取值。

不管使用哪种选择器,都是对node-set中的node进行依次处理,每个node处理的结果进行flatten,得到新的node-set.

contains = response.xpath('//div.container')
contains.xpath('.//a[contains(@href, "image")]/text()').getall() # 针对上面返回的所有node,递归寻找包含字符串"image"的a标签href,获取其直接text
# :: 之前是css选择器,后面使用{ attr("<attr>") | text } 获取属性或text
# 这实际是模仿css的伪元素,W3C css并没有这2个伪元素选择器
response.css('a[href*=image] img::attr("src")').getall()
# 第一个/表示根路径(即documents),后面的表示路径分隔符,/后面是[/]{子元素 | @<attr> | text()},[/]表示递归查找,否则只查找直接子元素
response.xpath('//a[contains(@href, "image")]/img/@src').getall()

不管是css还是xpath返回的都是SelectorList,所以还可以一起使用:

response.css('a[href*=image]').xpath('img/@src').getall()

Note

SelectorList在xpath,css时是对每个元素进行map, map的结果进行flatten,得到新的node-set. get只会返回第一个node的值。

实际上Selector包含了dom的所有attr,可以直接通过<dom_selector>.attrib获取dict,但是注意如果是SelectorList只会返回第一个Selectorattrib

Selector还可以直接使用正则,但是正则返回的是List<str>,实际上就是getall的正则处理,如果只想处理第一个,使用re_first

# 捕获所有的group,没有group,整个表达式就是group
response.css('#images *::text').re(r'Name:\s*(.*)')

XPATH

XPATH和CSS选择器各有优劣,所以通常的做法是结合使用。

XPATH的特点:

  1. 可以使用相对路径获取元素,更直观的元素层级关系。这里的层级实际上应该是Selector意义上的层级,也就是还包括属性,文本。
  2. 属性获取更直观
  3. 复杂的class筛选语法,所以一般用css选择器处理class
  4. 支持变量,类似sql的prepared statement,比如response.xpath("//div[@id=$val]/a/text()", val="images").get()

语法说明:

  1. ”/“开头,表示根节点,即documents,否则表示相对父节点。注意根是绝对的,不受当前Selector的影响;
  2. ”/“表示分隔符,根节点后面省略了分隔符,和path表现一致,即/div,表示根节点下的直接div元素,如果是//div,则表示递归查找所有的div元素。第2个/属于后面的div表示递归查找它。如果想递归查找当前Selector下所有比如span,不能xpath("//span"),这是根路径递归,可以使用相对xpath(".//span");
  3. 元素前加上”/“表示递归查找,否则只查找直接子元素;
  4. element[1],一般用来表示查找的元素所在层级的第一个元素,结果可能有多个; (element)[1],表示查找到的第一个元素,结果只有一个,其实也可以使用;
  5. 支持扩展(部分):
    • regex,[re:test(@<attr>,r'<regex>')]
    • contains,[contains(@<attr>,'<value>')]
    • has-class,[has-class('<value>',...)]

选择器

  • following-sibling: 节点后面的同级兄弟节点,比如div/a/following-sibling::div,可以看到它是a的所有同级div

函数调用

参考

  • string(object?): 将对象转换为字符串,可以用来去掉tag,达到类似xpath("//text()")的效果,但是注意如果使用string("//text()"),或者只要参数是node-set,则只会对第一个node进行string转化,这种情况应该用string(.),所有处理text的函数都遵循这个逻辑。

    Tip

    This is because the expression .//text() yields a collection of text elements – a node-set. And when a node-set is converted to a string, which happens when it is passed as argument to a string function like contains() or starts-with(), it results in the text for the first element only. A node-set is converted to a string by returning the string-value of the node in the node-set that is first in document order. If the node-set is empty, an empty string is returned.

  • concat(string1, string2, ...): 将多个字符串连接起来,xpath('concat("__",//text(), "__")')
  • starts-with(string, prefix): 判断字符串是否以prefix开头,使用[]作为条件过滤node,比如xpath("*[starts-with(string(.),'Hello')]")
  • contains(string, substring): 判断字符串是否包含substring,使用[]作为条件过滤node
  • substring-before(string, substring): 返回字符串中出现的第一个substring之前的部分, substring-before("1999/04/01","/")返回1999
  • substring-after(string, substring): 返回字符串中出现的第一个substring之后的部分,substring-after("1999/04/01","/")返回04/01
  • substring(string, start, [end]): 返回字符串中的start位置(index从1开始)开始的字符串,substring("1999/04/01",6)返回04/01
  • position(): 返回符合位置的node,也就是在list中的位置,比如table.xpath('tr[position() > 1])

注意

[]条件中的node展开,比如//text(),并不会在结果中体现,它只是用来过滤

正则

支持re扩展

sel.xpath('//li[re:test(@class, "item-\d$")]//@href').getall()

另: Selector也可以通过re,re-first方法,直接过滤:

response.xpath('//a[contains(@href, "image")]/text()').re(r"Name:\s*(.*)")

Items

extracted datas即为item item可以是:

  • dict
  • scrapy.Item,可以看作dict的再封装
  • @dataclass

item的构造,建议通过ItemLoader构建,符合scrapy工作流:

构造一个空item对象给ItemLoader,然后由ItemLoader填充它。 当然也可以继承ItemLoader并设置自己的default_item_class(ItemLoaderdict),本质也是调用这个class的无参构造函数。

@dataclass由于必须传入所有的field值(即没有无参构造函数),所以需要特殊处理:

from dataclasses import dataclass, field
from typing import Optional
 
@dataclass
class InventoryItem:
    name: Optional[str] = field(default=None)
    price: Optional[float] = field(default=None)
    stock: Optional[int] = field(default=None)

ItemLoader

Tip

建议使用更加强大且通用的pydantic来定义item,尤其是涉及到数据验证的情况。 但是此时就无法使用ItemLoader了,ItemLoader只能处理Item。

ItemLoader本质上就是一个item构造器,主要功能为:

  1. 根据Selector extract数据
  2. 使用input processor清洗这些数据,并暂存在loader中
  3. load_item时使用output processor处理数据,然后assign to item

Note

In other words, items provide the container of scraped data, while Item Loaders provide the mechanism for populating that container.

基本用法:

# 构建loader,传入一个空Item和Selector
l = ItemLoader(Product(), some_selector)
# add_xpath,add_css,add_value,清洗数据并添加进loader._values
l.add_xpath("name", xpath1)  # (1)
l.add_xpath("name", xpath2)  # (2)
l.add_css("name", css)  # (3)
l.add_value("name", "test")  # (4)
# 处理数据,填充Item并返回
return l.load_item()  # (5)

ItemLoader的input/output processor,还会读取item中对应的元信息,以scrapy.Item为例子:

from itemloaders.processors import TakeFirst
 
class Product(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field(output_processor = TakeFirst())
    stock = scrapy.Field()
    tags = scrapy.Field()
    last_updated = scrapy.Field(serializer=str)
 

这个class在创建时会使用指定的metaclass来构建,会读取这些类属性,然后生成fields类属性,包含所有的属性元信息。 这个fields可以被ItemLoader读取来分析处理数据,比如可以在Field中定义input_processor,output_processorItemLoader在add和load时就可以使用它们。

Note

class也是对象,使用元类来构建。

ItemAdapter

所有对item的操作都要用ItemAdapter包装过后,通过这个adapter处理。

为什么呢? 因为item有多种类型,为了适配,ItemAdapter会通过:

  • ScrapyItemAdapter
  • DictAdapter
  • DataclassAdapter
  • AttrsAdapter
  • PydanticAdapter 进行再次包装,就可以通过统一的API来访问item,比如getter,setter

常见的:

adapter = ItemAdapter(item)
id = adapter["id"]
adapter["name"]  = "name"

processor

有3各地方可以定义,优先级从高到低:

  1. ItemLoader类属性,field_in,field_out
  2. Field元信息,input_processor,output_processor
  3. ItemLoader.default_input_processor,ItemLoader.default_output_processor,可以继承修改。

context

ItemLoader在构建的时候可以传入**kwargs,这会被当作context,如果processorloader_context参数,会将context传入。

context也可以在loader中手动修改。

像官方提供的,比如MapCompose等用来构建processor的工具类,也支持context,会和loader的context合并成新的context传给processor

nest loader

loader可以有子loader,就像dom元素一样。 子loader中的Selector都将相对于创建子loader的Selector

loader = ItemLoader(item=Item())
# load stuff not in the footer
footer_loader = loader.nested_xpath("//footer")
# relative to //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()

子loader和父loader共享item,即填充的是同一个对象。

Item Pipeline

常规的中间件,用于处理item的管道,即链式工具。

常规的作用:

  • 清理数据,更高层级的processor
  • 校验数据
  • 检查重复

Note

虽然很多地方都可以做上面的操作,但是实践上应该功能分块,区分责任,以方便扩展。

Pipeline没有接口类,约定上需要实现一个process_item方法:

process_item(self, item, spider)
    return item
    # raise DropItem # or
 
# 可选实现
# spider open时调用
open_spider(self, spider)
# spider close时调用
close_spider(self, spider)

需要将Pipeline的模块限定名放入settings中的ITEM_PIPELINES中,才会生效,并且还要设置顺序(0 - 1000,从小到大的顺序调用)。

Feed exports

获取 分析 存储

Spider抓取数据 Selector,item用来分析过滤数据。

支持导出的格式:

  • JSON
  • JSON lines
  • CSV
  • XML
  • Pickle: python独有的,可以以二进制形式将任何python对象序列化与反序列化,所以反序列化的对象可以执行任何代码,这个是不安全的
  • Marshal: 类似Pickle,但是能序列化的范围更窄,所以更适合python内部对象的使用,对于用户对象Pickle更合适

存储backend

  • Local filesystem
  • FTP
  • S3
  • Google cloud storage
  • Standard output

不管是导出的格式,还是存储的后端,有些是需要相关依赖的,当然也可以自行扩展。

导出 + 存储是通过FEEDS完成设置:

settings = {
    "FEEDS": {
        "format": "json"
        "uri": "file///tmp/xxx.json"
    }
}

写入uri对应的backend,并不是立即抓取到item时就写入,会先写入临时文件,写完再上传到backend,可以通过设置FEED_EXPORT_BATCH_ITEM_COUNT来控制一个file中最多可以有几个item,一旦写入COUNT个就会上传到backend,这个参数用来split items。

Item filter

可以针对性的进行exports,定义一个实现accepts(item: any) -> bool方法的class,并注册在feed_options中即可。 默认的from scrapy.extensions.feedexport.ItemFilter无参构造对象,会直接返回True

post-processing

这实际上是一个pre-storages钩子机制,在写入storage之前调用,可以使用内置或自定义插件实现,插件必须实现相关方法,然后在feed_options中注册即可。

feed config

  • FEEDS dict结构,key为feed URI,value为对应的参数配置,即feed_options,用于构建一些hooks关联对象。 URI还可以包括参数,比如时间,序号等方便命名,参数也可以自定义实现。

    {
      # file://items.json
      'items.json': {
          'format': 'json',
          'encoding': 'utf8',
          'store_empty': False,
          # 用于默认的ItemFilter
          'item_classes': [MyItemClass1, 'myproject.items.MyItemClass2'],
          'fields': None,
          'indent': 4,
          'item_export_kwargs': {
             'export_empty_fields': True,
          },
      },
     }
     

Settings

优先级由高到低

  1. Command-line settings (highest precedence)
  2. Spider settings 有很多种方式:
    • 通过custom_settings
    • 重写update_settings()方法,通过调用settings.set修改配置
    • 重写from_crawler,最后依然调用settings.set
  3. Project settings 即settings.py文件 也可以通过scrapy.utils.project.get_project_settings接口配置。
  4. Add-on settings
  5. Command-specific default settings
  6. Global default settings (lowest precedence)

动态内容

尤其是现在流行的sap应用,2种处理模式:

  • 分析api

    不同的返回内容对应的处理模式:

    1. html,xml,json

    使用Selector,json还可以直接response.json()反序列化 对于内嵌的html/xml,可以获取对应的字符串,然后构建Selector("html_str")。2. css

    response.text正则处理3. 二进制文件(图片,文件等)

    bytes模式读取response.body,然后使用其他工具处理4. Javascript

    有些数据硬编码在js中。

    如果是文件,可以直接读取response.text获取 如果在<script/>标签中,使用Selector 对于读取到的text,一般使用正则来处理(treesitter?)

    使用库,比如chompjs,js2xml

  • headless浏览器(模拟真实请求)

    对于有些实在无法模拟api请求的时候,可以直接模拟浏览器环境,完成整个网页的请求。 可以使用playwright-python,也可以使用scrapy版,scrapy-playwright

deploy

官方建议的有2种方式:

  1. Scrapyd,开源,提供了http api
  2. Zyte Scrapy Cloud,直接部署到云服务

AutoThrottle

这算是个君子协定,爬虫不要对被爬服务器造成过多压力,这个扩展可以弹性节流。

settings中启用AUTOTHROTTLE_ENABLED即可。

架构

架构图

可以清晰的看到各组件的功能,作用顺序,以及middleware在哪里生效。

关于Scheduler组件的作用:用于调度request,实际上只有初始请求是从spider中获取给到crawl,后续的request都是从调度器中获取。

Note

Downloader Middleware

Note

像各种拦截器实现,比如spring security。

用于request/response过程中的hook,比如典型的UserAgentMiddleware,用于增加user_agent header:

def process_request(
        self, request: Request, spider: Spider
    ) -> Request | Response | None:
        if self.user_agent:
            request.headers.setdefault(b"User-Agent", self.user_agent)
        return None

DOWNLOADER_MIDDLEWARES中注册自定义或者要修改的middlewares,注册的middleware会和DOWNLOADER_MIDDLEWARES_BASE中的进行合并,并按order进行排序。 request(engine 到 downloader)按从小到大的顺序,response(downloader 到 engine)则相反。

engine [middleware,…] downloader

注意顺序,有些middleware会依赖前面的处理。 对于内置的,可以通过将order设置为None来disable。

middleware接口:

  1. process_request(request, spider) 返回值:
  • None: 最常见的,一般就是修改request,继续执行下一个。
  • Response: 不会继续执行后面的middleware.process_request,也不会到downloader中发起请求,但是process_response还是会依次执行。
  • Request: 停止执行后面的process_request,将这个request加入到调度中准备执行。
  • raise IgnoreRequest: 会去调用installed middlewares’s process_exception方法,如果没有被catch,则会被Request.errback调用,如果这里没有处理,将会被忽略。
  1. process_response(request, response, spider) 返回值:
  • Response: 常规,传递给下一个middleware
  • Request: 和process_request返回Request行为相同
  • raise IgnoreRequest: 调用Request.errback,如果没有处理,将被忽略
  1. process_exception(request, exception, spider) downloader handler和process_request抛出异常时调用。 返回值:
  • None: 继续去其他middleware中执行process_exception
  • Response: 开始process_response,后续的process_exception不会再执行
  • Request: 会resheduled这个请求,后续的process_exception不会再执行

内置middlewares

  • CookiesMiddleware

    用于cookie管理,服务端响应的Set-Cookie,会被管理起来,供后续的请求使用。 管理cookie的工具叫cookiejar,基本模拟了浏览器对cookie的管理,包括新增,删除,匹配等 默认情况下一个spider会使用一个cookiejar,也就是一个会话(相同的会覆盖之前的),如果要建立多个会话,或者说模拟多用户的情况,就需要指定对应的cookiejar 比如:

    for i, url in enumerate(urls):
      # 设置meta标记,标记一个cookiejar,表示这个请求使用这个jar,包括set cookies和get cookies
      yield scrapy.Request(url, meta={"cookiejar": i}, callback=self.parse_page)

    对于所有的情况,包括子请求都需要这么做,scrapy并不会自动对应。

    COOKIES_ENABLED: 如果为False,将不会发送cookies到服务端。 meta['dont_merge_cookies']如果设置为True时,也不会发送cookies,同时不会合并response中的Set-Cookie,不管此时COOKIES_ENABLED是否为True

    COOKIES_DEBUG: 开启后可以看到更多的关于cookie的输出

  • DefaultHeadersMiddleware settings中的DEFAULT_REQUEST_HEADERS,会被设置进请求的headers中。

  • DownloadTimeoutMiddleware
    根据settings中的DOWNLOAD_TIMEOUT或者spider的download_timeout

    也可以单独设置Request.meta['download_timeout'],这和middleware无关。

  • HttpAuthMiddleware 可以根据配置自动设置http basic auth header 当spider中设置了http_user,http_pass,http_auth_domain时,会自动使用这个middleware.

    class MySpider(CrawlSpider):
        http_user = "user"
        http_pass = "pass"
        # 设置为None会给所有的request添加basic auth
        http_auth_domain = "spider.domain.com"
  • HttpCacheMiddleware

    用于缓存处理

    相关配置:

    def __init__(self, settings: Settings, stats: StatsCollector) -> None:
          if not settings.getbool("HTTPCACHE_ENABLED"):
              raise NotConfigured
          self.policy = load_object(settings["HTTPCACHE_POLICY"])(settings)
          self.storage = load_object(settings["HTTPCACHE_STORAGE"])(settings)
          self.ignore_missing = settings.getbool("HTTPCACHE_IGNORE_MISSING")
          self.stats = stats

    Note

    缓存的配置较复杂,涉及缓存策略,存储模式等

  • OffsiteMiddleware

    用于实现spider中的allow_domains

    request中如果使用dont_filterallow_offsite来绕过OffsiteMiddleware的过滤。

  • RedirectMiddleware

    用于处理重定向 使用REDIRECT_ENABLEDREDIRECT_MAX_TIMES进行控制。

    同样可以在Request.meta中设置dont_redirect,这个中间件会忽略这个请求,即最原始的响应会给到spider

    在spider中可以设置handle_httpstatus_list,比如[301,302],这样返回这些状态码的响应会给到spider而不是被这个middleware拦截处理了,一般用于更精细化的重定向控制。

    # 4种情况都会直接放行
    if (
              request.meta.get("dont_redirect", False)
              or response.status in getattr(spider, "handle_httpstatus_list", [])
              or response.status in request.meta.get("handle_httpstatus_list", [])
              or request.meta.get("handle_httpstatus_all", False)
          ):
              return response
     
  • MetaRefreshMiddleware 用来处理<meta http-equiv="refresh" content="5;url=http://www.example.com/new-page">这种在html标签中设置的重定向。

    METAREFRESH_IGNORE_TAGS, 这里指定的标签里的<meta />会被忽略,即不会被这个中间件发现。

  • RetryMiddleware 对于出现问题的响应,进行重试。 并不是出现问题了立即重试,而是先被collected,在spider抓取完所有正常页面后,重新计划这些失败的请求。 可以根据response code 和 exception来配置什么情况下需要retry

  • RobotsTxtMiddleware 这个中间件会排除Robots.txt中禁止的请求。 通过enable ROBOTSTEXT_OBEY开启,同样对于单个请求也有对应的Request.meta.dont_obey_robotstxt配置绕过这个middleware. 这个中间件需要配置一个Robots.txt的解析器,默认是用Protego

  • DownloaderStats 用于相关数据的统计,包括request,response,exception

  • UserAgentMiddleware 默认为Scrapy,可以自行设置。

Spider Middleware

类似上面的downloader middleware engine [middleware,…] spider

根据架构图可知,engine和spider之间需要传递的是response和item/requst

和downloader middleware同样的顺序控制,同样的可以取消在SPIDER_MIDDLEWARES_BASE中定义的默认middlewares 同样的经过特定的方法,built-in middlewares

Extensions

根据架构图可知,扩展组件并没有特定的role,所以是很灵活的,可以用在其他组件之外的任何地方。

同样的可以在EXTENSIONS定义,会自动合并EXTENSIONS_BASE,大部分情况下扩展之间都没有依赖关系,所以EXTENSIONS_BASE中的扩展order都是0。

一般extensions的驱动是通过signals触发的,就是常规的事件驱动模型。

crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened) # signal -> ext.spider_opened(spider)
crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)
crawler.signals.connect(ext.item_scraped, signal=signals.item_scraped)

Signals

Scrapy当特定事件发生后会发送相应的信号(和参数),可以通过catch信号来完成特定的任务。

有些信号支持handler返回awaitable对象,也就是可以异步操作,不阻塞Scrapy

基本实现

基本上是一个发布-订阅模式,扮演中间件的是SignalManager

built-in signals:

engine signals:

  • engin_started()
  • engin_stoped()
  • scheduler_empty(): 当engine从scheduler中获取request返回None时

item signals:

  • item_scraped(item, response, exception, spider): item被完整的获取,即经过了item pipelines的处理了,且没有被droped
  • item_droped(item, response, exception, spider): 被droped
  • item_error(item, response, spider, failure)

spider signals:

  • spider_closed(spider, reason)
  • spider_opened(spider)
  • spinder_idle(spider): 空闲状态
  • spider_error(failure, response, spider): spider callback raise exception
  • feed_slot_closed(slot)
  • feed_exporter_closed()

request signals:

  • request_scheduled(request, spider): engine asked, before reaches the scheduler
  • request_droped(request, spider): reject by the scheduler
  • request_reached_downloader(request, spider)
  • request_left_downloader(request, spider)
  • bytes_received(data, request, spider): 每次收到bytes都会触发,也就是一次request可能触发多次
  • headers_received(headers, body_length, request, spider): 收到headers时触发,此时body还没过来,对比bytes_received更晚点

response signals:

  • response_received(response, request, spider): engine收到response,注意不是downloader收到bytes
  • response_downloaded(response, request, spider): downloader接收到完整的response后触发

Scheduler

核心作用是接收从engine传过来的requests,并存储,然后在engine请求request时返回。

可自定义自己的Scheduler。

Item Exporters

处理最终获得的items,比如可以导出为CSV,JSON,XML等。 不同于feed exports的高度封装,item exporters更底层,适合高度自定义的场景。

item exporters的抽象接口见class BaseItemExporter,内置的exporters都实现了它。核心包括3个方法:

class BaseItemExporter:
    # 具体动作
    def export_item(self, item: Any) -> None:
        raise NotImplementedError
 
    #  前置准备
    def start_exporting(self) -> None:
        pass
 
    # 后置准备
    def finish_exporting(self) -> None:
        pass
 
    ...
 

Note

文档中有个典型的例子,借助了pipeline,如果只用Feed exports基本无法实现。

serialization

导出绕不开的问题就是序列化。

Note

对象的序列化一般都要借助库来完成,否则难度太大,除非需求很简单,甚至没有反序列化需求,但是可以在给到库之前对某些字段进行一定程度的自定义转换。

自定义序列化:

  1. 可以在scrapy.Item的元信息中定义serializer方法,进行初步转换

    def some_date_formater(value: Any) -> str:
        pass
     
    class Product(scrapy.Item):
    name = scrapy.Field()
    last_updated = scrapy.Field(serializer=some_date_formater)
     
  2. override serialize_field方法 这个方法默认实现是从field中获取元信息serializer进行序列化,也就是上面serializer生效的地方

    class BaseItemExporter:
     
        def serialize_field(
            self, field: Mapping[str, Any] | Field, name: str, value: Any
        ) -> Any:
            serializer: Callable[[Any], Any] = field.get("serializer", lambda x: x)
            return serializer(value)

    field就是元信息,如果是item是dict,field肯定是空的,就会使用lambda x: x,即什么都不做。

内置exporters

框架提供了常规的exporters,可以直接使用。

总结

简单场景,直接Feed exports即可,需要自定义的场景可以通过pipeline结合内置的exporters完成,内置无法完成的情况自行实现BaseItemExporter

组件

不管是各种middlewares, extensions,还是exporters,都是基本组件,当然可以扩展第三方组件。 组件的基本规范为,一般可以通过from_crawlerfrom_settings build单例。engine在启动的时候会根据配置获取对应的组件,然后调用这个方法构建单例。

Note

很多框架都有这种配置思路,比如spring,在初始化时会根据配置构建一系列对象。

@classmethod
def from_crawler(cls: type[T], crawler: Crawler, *args, **kargs) -> Self:
    ...
 
@classmethod
def from_settings(cls: type[T], settings: Settings) -> Self:
    ...
 

实际上scrapy提供了工具scrapy.utils.misc.build_from_crawler,它可以自行选择调用from_crawler,from_settings,或者直接调用构造函数来build instance。

这2个方法都可以直接或间接的访问settings,所以不一定要传入args,kwargs作为选项,即可完成对组件的配置。

文档中给出了2种设置实践:

  1. 在settings中配置选项,一般以component名字作为前缀,大写,<PREFIX>_<SOME_OPTION>
  2. 在settings中只配置boolean,选择是否开启某些特性,<PREFIX>_<SOME_FEATURE>_ENABLED

小技巧: 可以在你的组件中声明依赖,比如版本需要等,不满足时抛出异常,记录日志等。