手写Scrapy系列(五)

计划表:

  • 基本的Request/Response (完成)
  • 实现异步下载 (完成)
  • 加入队列Queue,为实现调度器做准备 (完成)
  • 加入引擎管理 (完成)
  • 加入调度器管理 (当前进度)
  • 加入下载器管理
  • 加入下载器中间件管理
  • 加入爬虫进程管理
  • 加入信号机制管理

上节加入了引擎,其实就是把,数据初始化到队列从队列获取数据并下载处理下载后的结果三个步骤,以异步的逻辑整合到一个模块中去了,当然和原生Scrapy框架肯定比不了啦

这节我们再添加一个模块,调度器模块
我们先来看一下整体结构:

class Scheduler:...

class CallLaterOnce:...

class Slot:...

class Engine:...

class Request:...

class Response:...

class MySpider:...

其中有几个我们眼熟的模块,我们来介绍下:


  • MySpider: 用于存放爬虫逻辑的类
class MySpider:
    start_urls = ['http://www.czasg.xyz', 'http://www.czasg.xyz', 'http://www.czasg.xyz', 'http://www.czasg.xyz',
                  'http://www.czasg.xyz', 'http://www.czasg.xyz', 'http://www.czasg.xyz', 'http://www.czasg.xyz']

    def start_requests(self):
        yield from (Request(url, self.parse) for url in self.start_urls)

    def parse(self, response):
        print(response.url)
  • Request / Response:存放请求和结果的对象
  • Engine:引擎,相比于上一节,我们应该不难猜出,这部分代码有一定的调整,我们需要把对Queue的处理,拆分并整合到调度器模块中,接下来看看如何重新设计此模块
class Engine:
    def __init__(self):...

    @defer.inlineCallbacks
    def start(self):...

    @defer.inlineCallbacks
    def open_spider(self, spider, start_requests):...

    def _next_request(self, spider):...

    def _next_request_from_scheduler(self, spider):...

    def _handle_downloader_output(self, byte_content, request, spider):...

我们可以看到,多出了一个_next_request_from_scheduler函数,从名字中我们可以猜到两分,就是从调度器获取Request的作用。我们直接上代码,其他模块也都有相应的调整,但是基本功能都是类似的。

class Engine:
    def __init__(self):
        self.max_pool_size = 4
        self.crawling = []
        self.slot = None
        self.running = True

    @defer.inlineCallbacks
    def start(self):
        self._closewait = defer.Deferred()
        yield self._closewait

    @defer.inlineCallbacks
    def open_spider(self, spider, start_requests):
        nextcall = CallLaterOnce(self._next_request, spider)
        scheduler = Scheduler.from_crawler()
        self.slot = Slot(start_requests, nextcall, scheduler)
        yield scheduler.open()
        self.slot.nextcall.schedule()

    def _next_request(self, spider):
        slot = self.slot
        if not slot:
            return

        while not slot.scheduler.isEmpty():
            if not self._next_request_from_scheduler(spider):
                break

        if slot.start_requests:
            try:
                request = next(slot.start_requests)
                slot.inprogress.append(request)
            except StopIteration:
                slot.start_requests = None
            else:
                slot.scheduler.enqueue_request(request)
                slot.nextcall.schedule()

        if slot.start_requests is None and slot.scheduler.isEmpty() and not slot.inprogress:
            self._closewait.callback(None)

    def _next_request_from_scheduler(self, spider):
        request = self.slot.scheduler.next_request()
        if not request:
            return
        dfd = getPage(request.url.encode())
        dfd.addBoth(self._handle_downloader_output, request, spider)
        dfd.addBoth(lambda _: self.slot.inprogress.remove(request))
        dfd.addBoth(lambda _: self.slot.nextcall.schedule())
        return dfd

    def _handle_downloader_output(self, byte_content, request, spider):
        response = Response(byte_content, request)
        request.callback(response)
  • Slot: 引擎内部管理模块,包含入口start_requests对象,调度器scherduler,魔术方法nextcall,并维护一个正在执行对象的列表
class Slot:
    def __init__(self, start_requests, nextcall, scheduler):
        self.start_requests = iter(start_requests)
        self.nextcall = nextcall
        self.scheduler = scheduler
        self.inprogress = []
  • CallLaterOnce:魔术方法,Scrapy源码搬过来的,大致意思就是,该类是一个可调用类,每次调用都会执行__call__方法,故在
    def schedule(self):绑定自身类,每次调用此方法都会执行一次自身类,进而调用__call__方法
class CallLaterOnce:
    def __init__(self, func, *args, **kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs
        self._call = None

    def schedule(self):
        if self._call is None:
            self._call = reactor.callLater(0, self)

    def cancel(self):
        if self._call:
            self._call.cancel()

    def __call__(self, *args, **kwargs):
        self._call = None
        return self.func(*self.args, **self.kwargs)
  • Scheduler: 调度器,该模块就是维护一个队列,并实现一些对此队列的操作方法
class Scheduler:
    def __init__(self):
        self.mq = Queue()

    @classmethod
    def from_crawler(cls):
        return cls()

    def open(self):
        return

    def next_request(self):
        return self.mq.get(block=False)

    def enqueue_request(self, request):
        self.mq.put(request)

    def isEmpty(self):
        return self.mq.qsize() == 0

接下来就是如何启动了:

if __name__ == '__main__':
    @defer.inlineCallbacks
    def crawl():
        spider = MySpider()
        engine = Engine()
        yield engine.open_spider(spider, spider.start_requests())
        yield engine.start()
    d = crawl()
    d.addBoth(lambda _: reactor.stop())
    reactor.run()

嘿嘿,其实启动方法和上节一样。

  1. yield engine.open_spider(spider, spider.start_requests()):初始化引擎,包括:CallLaterOnce魔术方法的绑定Scheduler调度器的初始化Slot管理类的初始化
  2. yield engine.start(): 获取一个Deffered句柄,绑定reactor.stop()以便停止爬虫。

然后介绍下重要知识点:

    @defer.inlineCallbacks
    def open_spider(self, spider, start_requests):
        nextcall = CallLaterOnce(self._next_request, spider)
        scheduler = Scheduler.from_crawler()
        self.slot = Slot(start_requests, nextcall, scheduler)
        yield scheduler.open()
        self.slot.nextcall.schedule()

上述代码中,CallLaterOnce(self._next_request, spider)非常有意思,绑定函数self._next_request,并在最后一行执行一次self.slot.nextcall.schedule(),从代码层面意思看,就是执行一次self._next_request函数。

    def _next_request(self, spider):
        slot = self.slot
        if not slot:
            return

        while not slot.scheduler.isEmpty():
            if not self._next_request_from_scheduler(spider):
                break

        if slot.start_requests:
            try:
                request = next(slot.start_requests)
                slot.inprogress.append(request)
            except StopIteration:
                slot.start_requests = None
            else:
                slot.scheduler.enqueue_request(request)
                slot.nextcall.schedule()

        if slot.start_requests is None and slot.scheduler.isEmpty() and not slot.inprogress:
            self._closewait.callback(None)

而该函数的功能,当调度器队列不为空时,执行self._next_request_from_scheduler(spider),把队列榨干,然后在后面又获取一个Request对象,request = next(slot.start_requests),再把此Request推到调度器维护的队列中slot.scheduler.enqueue_request(request)

emmm...这其中滋味,确实得好好体味一番

github地址:https://github.com/CzaOrz/ioco/blob/t426/open_source_project/scrapy_simulate_tutorial/5.py

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容