scrapy架构流程图

268 阅读5分钟

scrapy架构流程图

简介

scrapy是一个可扩展的爬虫框架,整体架构采用插件式,支持深度定制化。scrapy基于twisted异步网络框架,主逻辑是典型的单线程模型,一些异步支持不完善的场景会用到线程池(比如dns解析,文件io,sdk不支持异步等)。

整体架构流程

1scrapy.png

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长度,限制请求深度等。

类依赖关系

2scrapy.png 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…