scrapy 中间件

在 Sun 20 December 2015 发布于 源码分析, scrapy 分类

Scrapy框架提供的中间件共有两个:Downloader middlewaresSpider Middlewares。这两个中间件分别作用在不同的数据流向上。

Downloader middlewares

Downloader middlewares顾名思义就是下载中间件,该中间件会在Scrapy的引擎从调度器获取到下一个要爬去的Request,并转发给下载器进行下载这个中间过程进行调用。以及下载器把需要爬取的Request爬取后生成的Response对象返回给Scrapy的引擎的过程中会调用Downloader middlewares

管理Downloader middlewares的类是scrapy.middleware.DownloaderMiddlewares对象。该对象在生成Scrapy的下载器的时候会被调用,该对象通过 读取配置文件中DOWNLOADER_MIDDLEWARES来构建该对象。

class DownloaderMiddlewareManager(MiddlewareManager):

  component_name = 'downloader middleware'

  @classmethod
  def _get_mwlist_from_settings(cls, settings):
    return build_component_list(
        settings.getwithbase('DOWNLOADER_MIDDLEWARES'))

在'构造'的时候,首先是从配置文件获取Downloader middlewares的中间件字典,该字典的key为中间件对象的字符串表示,值为该中间件的权重。Scrapy在运行的时候从配置文件获取的中间件集合不仅包括settings/default_settings.py中默认的配置DOWNLOADER_MIDDLEWARES_BASE,还包括用户自定义的settings.py文件中的DOWNLOADER_MIDDLEWARES

# settings/__init__.py
def getwithbase(self, name):
    compbs = BaseSettings()
    compbs.update(self[name + '_BASE'])
    compbs.update(self[name])
    return compbs

关于中间件的权重值,可以看看scrapy.utils.conf.build_component_list的实现,简要来说,该函数会对权值不为integer或者None的进行过滤掉,然后对权值进行升序排列,并返回相应的Downloader middlewares列表,即权值越小,Downloader middleware就排在越靠前。当Downloader middleware的权值为None的时候,该中间件就不可用了,利用这个特性,可以对默认的中间件进行扩展,只要在配置的时候把默认的中间件的权值赋为None就可以了。

def build_component_list(compdict, custom=None, convert=update_classpath):

  ...
  _validate_values(compdict)
  compdict = without_none_values(_map_keys(compdict))
  return [k for k, v in sorted(six.iteritems(compdict), key=itemgetter(1))]

scrapy.middleware.DownloaderMiddlewares维护了三个回调函数队列,process_request, process_response, process_exception。在实现自定义Dowloader middleware对象的时候需要至少成员方法process_request, process_response, process_exception这三个方法中的一个,虽然可以都不实现,但是这样做没有任何意义。这三个函数会在初始化的时候添加到对应的回调队列中去。Downloader middleware对对象的成员方法会按照权值升序依次加入到process_request回调队列,Downloader middleware的成员函数process_responseprocess_exception会按照升序依次分别头插process_responseprocess_exception回调队列中去, 具体可以看看scrapy.middleware.DownloaderMiddlewares的成员方法_add_middleware

在上面说道Scrapy的引擎从调度器获取到下一个要爬去的Request,并转发给下载器进行下载这个中间过程调用Downloader middlewares, 也就是说调用各个Downloader midderware的成员函数process_request

#core/downloader/middleware.py
@defer.inlineCallbacks
def process_request(request):
    for method in self.methods['process_request']:
        response = yield method(request=request, spider=spider)
        assert response is None or isinstance(response, (Response, Request)), \
                'Middleware %s.process_request must return None, Response or Request, got %s' % \
                (six.get_method_self(method).__class__.__name__, response.__class__.__name__)
        # 如果其中一个中间件的process_request返回一个Response的实例,直接返回,就不需要走下载器了流程
        if response:
            defer.returnValue(response)
    # 当所有的中间件的process_request都调用完成后,逻辑走到这一步,就是调用下载器进行页面的下载并返回Response对象的实例
    defer.returnValue((yield download_func(request=request,spider=spider)))

从上面的代码可以看到,Scrapy会遍历调用process_request回调队列中的回调函数(对于该段代码中的Twisted API后续会单独开辟一篇讲解),该回调函数会判断每次执行回调队列中的回调函数的结果,其值只能是Request,Response或者None。还有就是,该回调函数中的回调函数会更改请求实例Request,如添加User-Agent, ReferrerHttp Header。需要注意的是当其中一个Downloader middlewareprocess_request返回的是Response的实例,Scrapy就会立即停止后续回调函数的调用,也不会执行后续的下载逻辑,并把该Response立即返回。这里通常可以实现一个PhantomJs + Selenium实现的Downloader middleware来进行动态页面的下载,通常该Downloader middleware的权值也是最大的。

当下载器完成下载的任务(也可能不需要下载器)并向Scrapy引擎返回Response对象的实例时,上面说道,这个过程依然会有Downloader middlewares的参与,此时,就会调用process_response回调队列中的回调函数。

#core/downloader/middleware.py
@defer.inlineCallbacks
 def process_response(response):
     assert response is not None, 'Received None in process_response'
     if isinstance(response, Request):
         defer.returnValue(response)

     for method in self.methods['process_response']:
         response = yield method(request=request, response=response,
                                 spider=spider)
         assert isinstance(response, (Response, Request)), \
             'Middleware %s.process_response must return Response or Request, got %s' % \
             (six.get_method_self(method).__class__.__name__, type(response))
         if isinstance(response, Request):
             defer.returnValue(response)
     defer.returnValue(response)

从上面的代码逻辑可以看出,在执行process_response队列之前,会判断传入的参数response是不是Request的实例,如果是的话,立即返回,不会执行该回调队列。如果在执行该回调队列的过程中,如果有Downloader middleware的process_response返回了Request的实例(在分析该Response的时候,发现需要进一步请求的可能),会立即停止后续的回调函数的调用,立即返回。如同上面说道的,回调队列中的回调函数也会修改Response实例,并在执行完所有的回调函数后,返回该Response实例。

scrapy.middleware.DownloaderMiddlewares的成员函数download中最后的几行代码,说明了Scrapy引擎,Downloader middlewares以及下载是怎样互动的:

#Scrapy引擎到下载器的流程
deferred = mustbe_deferred(process_request, request)
deferred.addErrback(process_exception)
#下载器到Scrapy引擎的流程
deffered.addCallback(process_response)
return deferred

Spider Middlewares

Spider Middlewares,是Scrapy引擎和用户自定义的spider处理Response的中间数据流向管道。Spider Middlewares会在下面的两个过程被调用: <1>Spider Middlewares会在Scrapy引擎从下载器中接收到Response并发送给用户自定义spider处理;<2>用户自定义的spider处理Response并返回爬取到的Item及新的RequestScrapy引擎的过程。其和Download Middlewares有很多相似的地方,通过其源码文件core/spidermw.py可以看出Spider Middlewares的创建和初始化过程和Downloader Middlewares极为相似,这里就不多复述。和Downloader Middlewares的管理类一样,Spider Middlewares的管理类core.spidermw.SpiderMiddlewareManager也有四个回调队列:process_spider_input回调队列会在上面所说的过程<1>中被调用;process_spider_ouput会在上面所说的过程<2>别调用;process_spider_exception这个会在process_spider_input调用队列中回调函数错误时会被调用;process_start_requests

用户在实现自定义的 Spider Middleware 时实现的 process_spider_input,process_spider_output,process_spider_exception以及process_start_requests这几个成员函数正好对应着上述四个回调队列。 需要注意的是,除了process_spider_input队列之外,Spider Middlerware在其余的回调队列中的排序方式都是降序的。

在上述的过程<1>中会调用process_spider_input回调队列,该回调队列会顺序执行各个Spider Middleware的成员方法process_spider_input。在调用的过程中只能接受该方法返回None, 该过程是可能会修改Response对象实例,当队列所有的回调函数执行成功后,该Response对象实例,就交由用户自定义的Spider来处理。

#core/spidermw.py
def scrape_response(self, scrape_func, response, request, spider):
    fname = lambda f:'%s.%s' % (
            six.get_method_self(f).__class__.__name__,
            six.get_method_function(f).__name__)

    def process_spider_input(response):
        for method in self.methods['process_spider_input']:
            try:
                result = method(response=response, spider=spider)
                assert result is None, \
                        'Middleware %s must returns None or ' \
                        'raise an exception, got %s ' \
                        % (fname(method), type(result))
            except:
                return scrape_func(Failure(), request, spider)
        return scrape_func(response, request, spider)

在上面的代码中,都是通过scrape_func来完成process_spider_input回调队列的调用的。该函数是通过参数传递进来的,具体就是call_spider

#core/scraper.py
def _scrape(self, response, request, spider):
     """Handle the downloaded response or failure through the spider
     callback/errback"""
     assert isinstance(response, (Response, Failure))

     dfd = self._scrape2(response, request, spider) # returns spiders processed output
     dfd.addErrback(self.handle_spider_error, request, response, spider)
     #这里就是调用Spider Middlewares来处理从Spider返回的Item或者Request
     dfd.addCallback(self.handle_spider_output, request, response, spider)
     return dfd

 def _scrape2(self, request_result, request, spider):
     """Handle the different cases of request's result been a Response or a
     Failure"""
     if not isinstance(request_result, Failure):
         return self.spidermw.scrape_response(
             self.call_spider, request_result, request, spider)
     else:
         # FIXME: don't ignore errors in spider middleware
         dfd = self.call_spider(request_result, request, spider)
         return dfd.addErrback(
             self._log_download_errors, request_result, request, spider)

def call_spider(self, result, request, spider):
    result.request = request
    dfd = defer_result(result)
    #这里用到了用户自定义Spider是,构造的Request对象实例中的callback,以及errback
    dfd.addCallbacks(request.callback or spider.parse, request.errback)
    #处理用户自定义的Spider中处理Response逻辑的输出
    return dfd.addCallback(iterate_spider_output)

从上面的代码可以看到,Request对象实例通过下载器后生成的Response对象的实例会作为结果传递给该Request对象实例的callback方法,也就是我们们在自定义Spider时所写的逻辑,这个callback方法的逻辑要么就是生成最终页面解析的Item要么就是需要进行进一步的请求而生成的Request对象实例。process_spider_output的处理就很简单了,其按照顺序调用Spider Middleware的成员方法process_spider_output,并返回最后一个回调函数的结果给Spider引擎, 需要注意的是,确保回调队列中最后一个回调函数返回的是一个可遍历的对象实例。

#core/spidermw.py
def process_spider_output(result):
    for method in self.methods['process_spider_output']:
        result = method(response=response, result=result, spider=spider)
        assert _isiterable(result), \
            'Middleware %s must returns an iterable object, got %s ' % \
            (fname(method), type(result))
    return result

最后这个就是process_start_requests回调队列,这个很好立即,其同process_spider_output回调队列一样,也是在过程<2>的过程被调用,其处理的只有Request对象实例,该Request对象实例就是我们在写Spider时候需要实现的start_requests方法,也就是对爬取源的逻辑处理。