scrapy框架_scrapy 框架

框架图(高清官方) https://doc.scrapy.org/en/latest/_images/scrapy_architecture_02.png

b4901b3b40dd3c57f9b6fa16ca886cb8.png

图上没有的 class Crawler

很多类创建的时候依赖 Crawler。重要的有ExecutionEngine、downloader、所以上面启动流程很难绕过 Crawler 创建。

class ExecutionEngine

上图中间的engine。Crawler 像管理者,ExecutionEngine像执行经理,联系着downloader、spiders、scheduler、pipelines、

其他先看 downloader

爬虫就是不停的爬,每个请求封装成Request(对应地也封装Response)。好处是每个请求变成了一个个对象,方便处理。

所以downloader如果需要简单实现,是可以的。scrapy的downloader核心代码在core.downloader.handles,其中其实只关心http的handle即可

downloader和其他模块的耦合不大,甚至可以自己实现,或者提取这部分模块用作其他项目。所以不需要了解细节。要留意的是downloader的工作是异步的,利用了 twisted defer reactor,这部分比较有意思。

因为耦合小,我们实现downloader来实践一下。利用一般人熟悉的 requests (使用gevent异步)

from twisted.internet import reactor, defer
import requests
import gevent
from gevent import monkey
from scrapy.http import TextResponse

monkey.patch_socket()

def request_worker(request, callback=None): 
    callback(requests.get(request.url))

def download_request(request, _=None):
    df = defer.Deferred()
    def callback(res): 
        response = TextResponse(url=res.url, status=res.status_code, headers=res.headers, body=res.text, encoding='utf8')
        df.callback(response)
    gevent.spawn(request_worker, request, callback).join()
    return df

class Downloader(object):
    def __init__(self, crawler):
        self.settings = crawler.settings
        self.active = set()

    def fetch(self, request, spider): 
        return download_request(request, spider)

    def needs_backout(self): 
        return False

    def close(self): pass

没看错,最简单的实现就是这样,scrapy之所以感觉复杂是因为做得完备,大项目的副作用。

替换测试下,发现确实可以处理一般的请求了。

中间件处理现在没了,但其实想支持也很简单,代码稍微改下。

from twisted.internet import reactor, defer
import requests
import gevent
from gevent import monkey
from scrapy.http import TextResponse
from scrapy.core.downloader.middleware import DownloaderMiddlewareManager

monkey.patch_socket()

def request_worker(request, callback=None): callback(requests.get(request.url))

def download_request(request, spider=None):
    df = defer.Deferred()
    def callback(res): 
        if 'Content-Encoding' in res.headers: del res.headers['Content-Encoding']
        df.callback(TextResponse(url=res.url, status=res.status_code, headers=res.headers, body=res.text, encoding='utf8'))
    gevent.spawn(request_worker, request, callback).join()
    return df

class Downloader(object):
    def __init__(self, crawler):
        self.settings = crawler.settings
        self.active = set()
        self.middleware = DownloaderMiddlewareManager.from_crawler(crawler)
    def fetch(self, request, spider): 
        return self.middleware.download(download_request, request, spider)
        # return download_request(request, spider)
    def needs_backout(self): return False
    def close(self): pass

所以downloader启动流程其实就是初始化好 middleware

然后回到 scheduler,把他精简下,看看核心代码逻辑

import os
import json
import logging

from scrapy.utils.misc import load_object, create_instance
from scrapy.utils.job import job_dir

logger = logging.getLogger(__name__)

class Scheduler(object):

    def __init__(self, mqclass=None, pqclass=None):
        self.pqclass = pqclass
        self.mqclass = mqclass

    @classmethod
    def from_crawler(cls, crawler):
        settings = crawler.settings
        pqclass = load_object(settings['SCHEDULER_PRIORITY_QUEUE'])
        mqclass = load_object(settings['SCHEDULER_MEMORY_QUEUE'])
        return cls(pqclass=pqclass, mqclass=mqclass)

    def has_pending_requests(self):
        return len(self) > 0

    def open(self, spider):
        self.spider = spider
        self.mqs = self.pqclass(self._newmq)

    def close(self, reason):
        pass

    def enqueue_request(self, request):
        self._mqpush(request)
        return True

    def next_request(self):
        request = self.mqs.pop()
        print('next_request', request)
        return request

    def __len__(self):
        return len(self.mqs)

    def _mqpush(self, request):
        self.mqs.push(request, -request.priority)

    def _newmq(self, priority):
        return self.mqclass()

其实删掉的代码是持久化队列之类,一般用不上。

Spider 模块的基类

import logging
import warnings

from scrapy.http import Request


class Spider(object):
    name = None
    def __init__(self, name=None, **kwargs):
        print('hi~~~~~~~~~~~~~~~~')
        self.name = name or 'xxx'

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = cls(*args, **kwargs)
        spider._set_crawler(crawler)
        return spider

    def _set_crawler(self, crawler):
        self.crawler = crawler
        self.settings = crawler.settings

    def start_requests(self):
        for url in self.start_urls:
            yield Request(url, dont_filter=True)

    def parse(self, response):
        raise NotImplementedError('{}.parse callback is not defined'.format(self.__class__.__name__))

    @classmethod
    def update_settings(cls, settings):
        pass

    @staticmethod
    def close(spider, reason):
        pass

其实就是不断的 yield Request (执行交给 engine模块),由于具体流程需要子类实现,spider的难点是 yield 的使用。

下面代码

def test_yield():
    def func():
        yield '电影详情页'
        for x in range(0,2):
            yield '电影每一集播放页'
    for e in func():
        print(e)
输出如下
电影详情页
电影每一集播放页
电影每一集播放页

yield在scrapy的理解需要结合整个流程,先暂停起来。

再回到 Crawler 类

很多类都有个 from_crawler 风格的创建函数 crawler 作为必要参数。代码可以精简到(而且有不少是log和统计的非核心代码)

class Crawler(object):

    def __init__(self, spidercls, settings=None):
        self.spidercls = spidercls
        self.settings = settings.copy()
        # self.spidercls.update_settings(self.settings)

        self.signals = SignalManager(self)
        self.stats = load_object(self.settings['STATS_CLASS'])(self)

        handler = LogCounterHandler(self, level=self.settings.get('LOG_LEVEL'))
        logging.root.addHandler(handler)

        lf_cls = load_object(self.settings['LOG_FORMATTER'])
        self.logformatter = lf_cls.from_crawler(self)

        self.settings.freeze()
        self.spider = None
        self.engine = None


    @defer.inlineCallbacks
    def crawl(self, *args, **kwargs):
        try:
            self.spider = self._create_spider(*args, **kwargs)
            self.engine = self._create_engine()
            start_requests = iter(self.spider.start_requests())
            yield self.engine.open_spider(self.spider, start_requests)
            yield defer.maybeDeferred(self.engine.start)
        except Exception:
            if self.engine is not None:
                yield self.engine.close()
            raise

    def _create_spider(self, *args, **kwargs):
        return self.spidercls.from_crawler(self, *args, **kwargs)

    def _create_engine(self):
        return ExecutionEngine(self, lambda _: self.stop())

    @defer.inlineCallbacks
    def stop(self):
            yield defer.maybeDeferred(self.engine.stop)

可以看到,实际工作的是

5a05c556e214a37445f1ed852abb2ed4.png

再回到 ExecutionEngine 类

首先,有个 slot 记录正在请求的Request,slot 还引用 nextcall scheduler。nextcall 就是 CallLaterOnce(self._next_request, spider),顾名思义。调用 slot.nextcall.schedule() 开始定时。实际上,直接调用 self._next_request(spider) 开始也行,不过需要改写下 _needs_backout。

由于不熟悉 yield 和 reactor,_next_request 是难点。

spider 都有 start_requests,这个没问题

e7e8f389a8e8766a22617bf82cb88379.png

这个一般会找不到下一个 请求

897e4abd5cf9813417b423e89e1716eb.png

但 _next_request 会不停被调用,问题来了,_next_request怎么不停进入的?是定时检查?用户自己触发?都不是。_next_request写得比较绕,理解上有坑。

第一次是

f4e6792ad09d7806788213ddb336cba2.png

然后 self.crawl(request, spider)里面关键的是又调用了nextcall.schedule(),说白了,只要调用一次nextcall.schedule() 就会触发一次_next_request。而且看CallLaterOnce的实现会发现,可以发起多次,但只会到点触发一次。

那如果不跑self.crawl,_next_request_from_scheduler符合条件,开始了第一个请求,下一次怎么触发 _next_request?

720f08390ff1428a790388b11d2747bd.png

67ad381d90849ef9804a6aa65643d5f5.png

但还有个终极的保证,

967c6dc4acae59bbcd057937d9982a6a.png

自己启动定时器任务检查!

20dbda3c828288aa5142edd0afe1cf91.png

彻底搞清楚循环后,就可以理解 parse 回调中的 yield 了

然后是 engine._handle_downloader_output,engine的每个请求的会掉都会跑这里

c4172acd4bf26e98143dac433d707074.png

72d16056623172cc58ec411f6570894c.png

跟踪代码一直到

546b7003d36b0f5bdd61b67dd4b4f054.png

所以为啥spider第一个入口叫 parse,为啥request定义要加callback

5a9495e78a5d1c1308d7d35c986f246b.png

现在,scrapy已经入门了。

然后用 scrapy的几乎都会在这里产生疑问。正常人的思维应该如下图。

3a98ae332e39ab016c21763af53ed8e3.png

但实际例子却是用 yield

找到源码

a4314dab10fbae8f5f6dbb21ddd6f106.png

(这里要熟悉 refer 这个库,简单说就是一个回调链,建议看其他专门的文章)。

ad87fa5dcb1fe0572b89c696a91835af.png

最终,就算你不用 yield,直接return,系统也会帮你变成iter(迭代器)

ca7736ef277a7698412726383caec3c3.png

用 return 写其实没问题的,但推荐用yield,比如如下逻辑

3766dda141142d9401c90986cbc1b8eb.png

用 return 就不优雅了。

理解启动流程中多次出现的 yield的意义。

ab3024a419aeca9e00a6c813fce94980.png

d46c70ffbf9e5eeba15313127704bb73.png

我发现这些 yield 有些去掉就跑不起来。首先 yield 嵌套是深度优先的

6ef527a8b126ccad9f231e7b4f48bf6a.png

所以 crawler.crawl 中 engine.open_spider 是先全部迭代执行完后才到 engine.start 内容。但 engine open_spider 中又有 yield。启动的关键代码看不出有触发这些迭代器的样子。

2460d7e1ea4d8bc43ead7aecab3364be.png

其实关键是这个

dcdca1c8f7dc4f4fff432f3687a5a3cd.png

具体需要另外说

linx:scrapy 启动时的 inlineCallbacks​zhuanlan.zhihu.com

最后的 item pipelines

在 Scraper 类中,

c29872c099f47ee9b2353edc82f88aee.png

触发在

0d07497b2a24d5a2998258112b3becf8.png

e5a7da4ef100588d08679a06f297131a.png

对于数据的保存 完全可以自己写一套服务和scrapy通信就行,所以item pipelines并不是scrapy框架中必要的角色。

到此,我们自己也可以写一个类似scrapy的简陋的demo框架了,

可以基于twisted,也可以到github找独立的 defer 库

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值