概述
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种模式css和xpath,前者是标准的css选择器,后者是xpath选择器。
核心都是通过选择器定位dom元素,然后获取attr或text。
注意不管是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只会返回第一个Selector的attrib。
Selector还可以直接使用正则,但是正则返回的是List<str>,实际上就是getall的正则处理,如果只想处理第一个,使用re_first
# 捕获所有的group,没有group,整个表达式就是group
response.css('#images *::text').re(r'Name:\s*(.*)')XPATH
XPATH和CSS选择器各有优劣,所以通常的做法是结合使用。
XPATH的特点:
- 可以使用相对路径获取元素,更直观的元素层级关系。这里的层级实际上应该是
Selector意义上的层级,也就是还包括属性,文本。 - 属性获取更直观
- 复杂的
class筛选语法,所以一般用css选择器处理class - 支持变量,类似sql的prepared statement,比如
response.xpath("//div[@id=$val]/a/text()", val="images").get()
语法说明:
- ”/“开头,表示根节点,即
documents,否则表示相对父节点。注意根是绝对的,不受当前Selector的影响; - ”/“表示分隔符,根节点后面省略了分隔符,和path表现一致,即
/div,表示根节点下的直接div元素,如果是//div,则表示递归查找所有的div元素。第2个/属于后面的div表示递归查找它。如果想递归查找当前Selector下所有比如span,不能xpath("//span"),这是根路径递归,可以使用相对xpath(".//span"); - 元素前加上”/“表示递归查找,否则只查找直接子元素;
element[1],一般用来表示查找的元素所在层级的第一个元素,结果可能有多个;(element)[1],表示查找到的第一个元素,结果只有一个,其实也可以使用;- 支持扩展(部分):
- regex,
[re:test(@<attr>,r'<regex>')] - contains,
[contains(@<attr>,'<value>')] - has-class,
[has-class('<value>',...)]
- regex,
选择器
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,使用[]作为条件过滤nodesubstring-before(string, substring): 返回字符串中出现的第一个substring之前的部分,substring-before("1999/04/01","/")返回1999substring-after(string, substring): 返回字符串中出现的第一个substring之后的部分,substring-after("1999/04/01","/")返回04/01substring(string, start, [end]): 返回字符串中的start位置(index从1开始)开始的字符串,substring("1999/04/01",6)返回04/01position(): 返回符合位置的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可以是:
dictscrapy.Item,可以看作dict的再封装@dataclass
item的构造,建议通过ItemLoader构建,符合scrapy工作流:
构造一个空item对象给ItemLoader,然后由ItemLoader填充它。
当然也可以继承ItemLoader并设置自己的default_item_class(ItemLoader是dict),本质也是调用这个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构造器,主要功能为:
- 根据
Selectorextract数据 - 使用input processor清洗这些数据,并暂存在loader中
- 在
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_processor,ItemLoader在add和load时就可以使用它们。
Note
class也是对象,使用元类来构建。
ItemAdapter
所有对item的操作都要用ItemAdapter包装过后,通过这个adapter处理。
为什么呢?
因为item有多种类型,为了适配,ItemAdapter会通过:
ScrapyItemAdapterDictAdapterDataclassAdapterAttrsAdapterPydanticAdapter进行再次包装,就可以通过统一的API来访问item,比如getter,setter。
常见的:
adapter = ItemAdapter(item)
id = adapter["id"]
adapter["name"] = "name"processor
有3各地方可以定义,优先级从高到低:
ItemLoader类属性,field_in,field_outField元信息,input_processor,output_processorItemLoader.default_input_processor,ItemLoader.default_output_processor,可以继承修改。
context
ItemLoader在构建的时候可以传入**kwargs,这会被当作context,如果processor有loader_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用来分析过滤数据。
支持导出的格式:
JSONJSON linesCSVXMLPickle: python独有的,可以以二进制形式将任何python对象序列化与反序列化,所以反序列化的对象可以执行任何代码,这个是不安全的Marshal: 类似Pickle,但是能序列化的范围更窄,所以更适合python内部对象的使用,对于用户对象Pickle更合适
存储backend :
Local filesystemFTPS3Google cloud storageStandard 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
-
FEEDSdict结构,key为feedURI,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
优先级由高到低
- Command-line settings (highest precedence)
- Spider settings
有很多种方式:
- 通过
custom_settings - 重写
update_settings()方法,通过调用settings.set修改配置 - 重写
from_crawler,最后依然调用settings.set
- 通过
- Project settings
即
settings.py文件 也可以通过scrapy.utils.project.get_project_settings接口配置。 - Add-on settings
- Command-specific default settings
- Global default settings (lowest precedence)
动态内容
尤其是现在流行的sap应用,2种处理模式:
-
分析api
不同的返回内容对应的处理模式:
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种方式:
Scrapyd,开源,提供了http apiZyte 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接口:
process_request(request, spider)返回值:
None: 最常见的,一般就是修改request,继续执行下一个。Response: 不会继续执行后面的middleware.process_request,也不会到downloader中发起请求,但是process_response还是会依次执行。Request: 停止执行后面的process_request,将这个request加入到调度中准备执行。- raise
IgnoreRequest: 会去调用installed middlewares’sprocess_exception方法,如果没有被catch,则会被Request.errback调用,如果这里没有处理,将会被忽略。
process_response(request, response, spider)返回值:
Response: 常规,传递给下一个middlewareRequest: 和process_request返回Request行为相同- raise
IgnoreRequest: 调用Request.errback,如果没有处理,将被忽略
process_exception(request, exception, spider)downloader handler和process_request抛出异常时调用。 返回值:
None: 继续去其他middleware中执行process_exceptionResponse: 开始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是否为TrueCOOKIES_DEBUG: 开启后可以看到更多的关于cookie的输出 -
DefaultHeadersMiddlewaresettings中的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 = statsNote
缓存的配置较复杂,涉及缓存策略,存储模式等
-
OffsiteMiddleware用于实现spider中的
allow_domainsrequest中如果使用
dont_filter和allow_offsite来绕过OffsiteMiddleware的过滤。 -
RedirectMiddleware用于处理重定向 使用
REDIRECT_ENABLED和REDIRECT_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中禁止的请求。 通过enableROBOTSTEXT_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的处理了,且没有被dropeditem_droped(item, response, exception, spider): 被dropeditem_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 exceptionfeed_slot_closed(slot)feed_exporter_closed()
request signals:
request_scheduled(request, spider): engine asked, before reaches the schedulerrequest_droped(request, spider): reject by the schedulerrequest_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收到bytesresponse_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
对象的序列化一般都要借助库来完成,否则难度太大,除非需求很简单,甚至没有反序列化需求,但是可以在给到库之前对某些字段进行一定程度的自定义转换。
自定义序列化:
-
可以在
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) -
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_crawler或from_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种设置实践:
- 在settings中配置选项,一般以component名字作为前缀,大写,
<PREFIX>_<SOME_OPTION>。 - 在settings中只配置boolean,选择是否开启某些特性,
<PREFIX>_<SOME_FEATURE>_ENABLED。
小技巧: 可以在你的组件中声明依赖,比如版本需要等,不满足时抛出异常,记录日志等。