scrapy架构流程图
简介
scrapy是一个可扩展的爬虫框架,整体架构采用插件式,支持深度定制化。scrapy基于twisted异步网络框架,主逻辑是典型的单线程模型,一些异步支持不完善的场景会用到线程池(比如dns解析,文件io,sdk不支持异步等)。
整体架构流程
Engine: 从Spider获取种子request,驱动整个爬虫系统的数据流通。
Scheduler: 通过落盘支持存储大量request,LILO和FILO队列对应不同的搜索策略;计算url指纹过滤重复请求,判断Spider和Downloader的状态调整处理请求的速率。
Downloader:把不同协议的请求分派到对应的DownloaderHandler下载。控制总请求并发,同ip并发,同domain并发。
Spider:解析response,生成item或新的request。控制同时处理的response大小,防止长时间的计算阻塞其他io的处理。
ItemPipeline:处理item的管道,由用户编写,支持异步或协程。使用场景有过滤相同item,丢弃item,将item导出到文件或db等。
DownloaderMiddleWare:mws_process_request和mws_process_response,由用户编写,支持异步或协程。使用场景有处理重定向,处理robot.txt,设置UserAgent,处理鉴权,请求重试,直接生成响应等。
SpiderMiddleWare:mws_process_spider_input和mws_process_spider_output,由用户编写,不支持异步或协程。使用场景有丢弃错误响应,限制url长度,限制请求深度等。
类依赖关系
CrawlerRunner负责管理Crawler,一个程序可以有多个Crawler;一个Crawler通常对应一个Spider和一套独立的爬虫系统,属于Helper类负责启动Engine。
Scrapyer.Slot记录正在处理的总响应大小,提供backout接口供scheduler判断是否过载。
Downloader.Slot记录并发请求的数量,同样提供backout接口供scheduler判断是否过载。
Scheduler.RFPilter计算url指纹,过滤重复请求。
Downloader.DownloaderHandler对应不同的协议请求处理,内建支持http1,http2,ftp,s3等协议。
了解爬取流程
mini example
import scrapy
from twisted.internet import reactor
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging
from scrapy.utils.project import get_project_settings
class ItcastItem(scrapy.Item):
name = scrapy.Field()
title = scrapy.Field()
info = scrapy.Field()
class MySpider1(scrapy.Spider):
name = 'itcast1'
allowed_domains = ['itcast.com']
start_urls = ("http://www.itcast.cn/channel/teacher.shtml",)
def parse(self, response):
items = []
for each in response.xpath("//div[@class='li_txt']"):
item = ItcastItem()
item['name'] = each.xpath("h3/text()").extract()[0]
item['title'] = each.xpath("h4/text()").extract()[0]
item['info'] = each.xpath("p/text()").extract()[0]
items.append(item)
return items
configure_logging()
runner = CrawlerRunner(get_project_settings())
runner.crawl(MySpider1)
# runner.crawl(MySpider2)
d = runner.join()
d.addBoth(lambda _: reactor.stop())
reactor.run()
核心流程
Crawler::crawl()
@defer.inlineCallbacks
def crawl(self, *args, **kwargs):
self.spider = self._create_spider(*args, **kwargs)
self.engine = self._create_engine()
# 从spider获取种子请求,start_requests从start_urls生成
start_requests = iter(self.spider.start_requests())
# 启动爬取引擎,等待引擎处理结束
yield self.engine.open_spider(self.spider, start_requests)
yield defer.maybeDeferred(self.engine.start)
ExecutionEngine::open_spider() && ExecutionEngine::_next_request()
@inlineCallbacks
def open_spider(self, spider: Spider, start_requests: Iterable ):
# 调用process_start_requests中间件处理start_requests
start_requests = yield spidermw.process_start_requests(start_requests, spider)
self.slot = Slot(start_requests, close_if_idle, nextcall, scheduler)
# self._next_request是核心调度函数,负责获取请求交给Downloader下载,包一层CallLaterOnce是为了协调调用的时机。
nextcall = CallLaterOnce(self._next_request)
# 启动时主动调用一次_next_request,一般在下载完成时,有新request入队时都会被主动调用
self.slot.nextcall.schedule()
# 定时调用,属于兜底机制,保证队列里的请求能被处理。在Downloader并发限制或Scrapyer处理大小达到限制时,主动调用_next_request不会生效,需要等待一段时间再调用。
self.slot.heartbeat.start(5)
def _next_request(self) -> None:
# _needs_backout即根据Downloader和Scrapyer判断是否并发太多需要停止一段时间
# 优先处理scheduler队列里的请求
while not self._needs_backout() and self._next_request_from_scheduler() is not None:
pass
# 到这说明scheduler队列里没request,如果有种子request,入队列并再次调用_next_request统一处理
if self.slot.start_requests is not None and not self._needs_backout():
request = next(self.slot.start_requests)
self.slot.scheduler.enqueue_request(request)
self.slot.nextcall.schedule()
Scheduler::enqueue_request() && Scheduler::next_request()
def enqueue_request(self, request: Request) -> bool:
# 先调用RFPilter计算url指纹,过滤重复请求
if not request.dont_filter and self.df.request_seen(request):
return False
# 优先使用磁盘队列,没有再使用内存队列;从队列获取请求时相反
dqok = self._dqpush(request)
if !dqok:
self._mqpush(request)
return True
def next_request(self) -> Optional[Request]:
request = self.mqs.pop()
if !request is not None:
request = self._dqpop()
return request
ExecutionEngine::_next_request_from_scheduler() && ExecutionEngine::_handle_downloader_output()
def _next_request_from_scheduler(self) -> Optional[Deferred]:
request = self.slot.scheduler.next_request()
if request is None:
return None
# 从scheduler获取请求后调用downloader下载,下载完成调用时_handle_downloader_output
d = self.downloader.fetch(request, spider)
d.addBoth(self._handle_downloader_output, request)
# 下载完成时调度&现在马上调度下一个请求
d.addBoth(lambda _: self.slot.nextcall.schedule())
self.slot.nextcall.schedule()
return d
def _handle_downloader_output( self, result: Union[Request, Response, Failure], request: Request) -> Optional[Deferred]:
# DownloaderMiddleWare返回的request直接放到scheduler队列
if isinstance(result, Request):
self.crawl(result)
return None
# 调用scrapyer处理响应,会经过SpiderMiddleWare,Spider,ItemPipeline处理
d = self.scraper.enqueue_scrape(result, request, self.spider)
return d
Downloader::fetch()
def fetch(self, request, spider):
# active用于控制总请求并发
self.active.add(request)
# 运行DownloaderMiddleWare,通过所有MiddleWare处理后调用_enqueue_request把请求入队列,这里的队列主要作用为并发控制与请求前随机延迟,不支持落盘。
dfd = self.middleware.download(self._enqueue_request, request, spider)
return dfd.addBoth(_deactivate)
# 提供给Engine做并发控制判断的函数
def needs_backout(self):
return len(self.active) >= self.total_concurrency
Downloader::_enqueue_request() && Downloader::_process_queue()
def _enqueue_request(self, request, spider):
# slot负责更精细的并发控制,支持并发总请求限制,同ip并发控制,同domain并发控制;根据配置和request的信息分发到不同slot,相同slot的请求一般为domain相同或请求ip相同。
key, slot = self._get_slot(request, spider)
slot.active.add(request)
# 先放到内存队列,每个slot都有一个内存队列
slot.queue.append((request, deferred))
self._process_queue(spider, slot)
return deferred
def _process_queue(self, spider, slot):
# 相同slot的请求先经过随机延迟再处理
now = time()
delay = slot.download_delay()
if delay:
penalty = delay - now + slot.lastseen
if penalty > 0:
slot.latercall = reactor.callLater(penalty, self._process_queue, spider, slot)
return
# 延迟条件满足后,还要判断并发是否达到限制
# 两个条件满足后调用_download函数把请求分派到对应协议的Handler
# 下载deffered完成后最终调用Scrapyer::_scrape处理结果
while slot.queue and slot.free_transfer_slots() > 0:
slot.lastseen = now
request, deferred = slot.queue.popleft()
dfd = self._download(slot, request, spider)
Scrapyer::_scrape()
def _scrape(self, result: Union[Response, Failure], request: Request, spider: Spider) -> Deferred:
# Scrapyer自身也有内存队列,并通过Slot限制同时处理的总响应体大小,防止计算任务时间太长阻塞io操作。逻辑与Downloader的并发控制类似,跳过直接看处理队列的函数。
if isinstance(result, Response):
# 1.调用SpiderMiddleWare.process_spider_input
# 2.通过call_spider调用用户处理函数,通常为parse
# 3.调用SpiderMiddleWare.process_spider_output
self.spidermw.scrape_response(self.call_spider, result, request, spider)
else:
# 如果下载失败,直接调用call_spider用户处理函数,不经过中间件
dfd = self.call_spider(result, request, spider)
dfd.addCallback(self.handle_spider_output, request, result, spider)
return dfd
def handle_spider_output(self, output: Any, request: Request, response: Response, spider: Spider) -> Optional[Deferred]:
if isinstance(output, Request):
self.crawler.engine.crawl(request=output)
elif is_item(output):
# 当用户处理返回item时,调用ItemPipeline处理输出
dfd = self.itemproc.process_item(output, spider)
dfd.addBoth(self._itemproc_finished, output, response, spider)
return dfd
else output is None:
pass
return None
其他
scrapy官网 docs.scrapy.org/en/latest/i…
twisted教程 twisted-intro-cn.ixxoo.me/zh/
如何充分发挥 Scrapy 的异步能力 cloud.tencent.com/developer/a…