mirror of https://github.com/scrapy/scrapy.git
Merge pull request #1473 from ArturGaspar/master
[MRG+1] Support for returning deferreds in middlewares
This commit is contained in:
commit
dd473145f2
|
|
@ -58,6 +58,8 @@ more of the following methods:
|
|||
|
||||
.. class:: DownloaderMiddleware
|
||||
|
||||
.. note:: Any of the downloader middleware methods may also return a deferred.
|
||||
|
||||
.. method:: process_request(request, spider)
|
||||
|
||||
This method is called for each request that goes through the download
|
||||
|
|
@ -877,12 +879,6 @@ RobotsTxtMiddleware
|
|||
To make sure Scrapy respects robots.txt make sure the middleware is enabled
|
||||
and the :setting:`ROBOTSTXT_OBEY` setting is enabled.
|
||||
|
||||
.. warning:: Keep in mind that, if you crawl using multiple concurrent
|
||||
requests per domain, Scrapy could still download some forbidden pages
|
||||
if they were requested before the robots.txt file was downloaded. This
|
||||
is a known limitation of the current robots.txt middleware and will
|
||||
be fixed in the future.
|
||||
|
||||
.. reqmeta:: dont_obey_robotstxt
|
||||
|
||||
If :attr:`Request.meta <scrapy.http.Request.meta>` has
|
||||
|
|
|
|||
|
|
@ -4,11 +4,15 @@ Downloader Middleware manager
|
|||
See documentation in docs/topics/downloader-middleware.rst
|
||||
"""
|
||||
import six
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from scrapy.http import Request, Response
|
||||
from scrapy.middleware import MiddlewareManager
|
||||
from scrapy.utils.defer import mustbe_deferred
|
||||
from scrapy.utils.conf import build_component_list
|
||||
|
||||
|
||||
class DownloaderMiddlewareManager(MiddlewareManager):
|
||||
|
||||
component_name = 'downloader middleware'
|
||||
|
|
@ -27,40 +31,45 @@ class DownloaderMiddlewareManager(MiddlewareManager):
|
|||
self.methods['process_exception'].insert(0, mw.process_exception)
|
||||
|
||||
def download(self, download_func, request, spider):
|
||||
@defer.inlineCallbacks
|
||||
def process_request(request):
|
||||
for method in self.methods['process_request']:
|
||||
response = method(request=request, spider=spider)
|
||||
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__)
|
||||
if response:
|
||||
return response
|
||||
return download_func(request=request, spider=spider)
|
||||
defer.returnValue(response)
|
||||
defer.returnValue((yield download_func(request=request,spider=spider)))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def process_response(response):
|
||||
assert response is not None, 'Received None in process_response'
|
||||
if isinstance(response, Request):
|
||||
return response
|
||||
defer.returnValue(response)
|
||||
|
||||
for method in self.methods['process_response']:
|
||||
response = method(request=request, response=response, spider=spider)
|
||||
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):
|
||||
return response
|
||||
return response
|
||||
defer.returnValue(response)
|
||||
defer.returnValue(response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def process_exception(_failure):
|
||||
exception = _failure.value
|
||||
for method in self.methods['process_exception']:
|
||||
response = method(request=request, exception=exception, spider=spider)
|
||||
response = yield method(request=request, exception=exception,
|
||||
spider=spider)
|
||||
assert response is None or isinstance(response, (Response, Request)), \
|
||||
'Middleware %s.process_exception must return None, Response or Request, got %s' % \
|
||||
(six.get_method_self(method).__class__.__name__, type(response))
|
||||
if response:
|
||||
return response
|
||||
return _failure
|
||||
defer.returnValue(response)
|
||||
defer.returnValue(_failure)
|
||||
|
||||
deferred = mustbe_deferred(process_request, request)
|
||||
deferred.addErrback(process_exception)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import logging
|
|||
|
||||
from six.moves.urllib import robotparser
|
||||
|
||||
from twisted.internet.defer import Deferred, maybeDeferred
|
||||
from scrapy.exceptions import NotConfigured, IgnoreRequest
|
||||
from scrapy.http import Request
|
||||
from scrapy.utils.httpobj import urlparse_cached
|
||||
|
|
@ -34,17 +35,22 @@ class RobotsTxtMiddleware(object):
|
|||
def process_request(self, request, spider):
|
||||
if request.meta.get('dont_obey_robotstxt'):
|
||||
return
|
||||
rp = self.robot_parser(request, spider)
|
||||
if rp and not rp.can_fetch(self._useragent, request.url):
|
||||
d = maybeDeferred(self.robot_parser, request, spider)
|
||||
d.addCallback(self.process_request_2, request, spider)
|
||||
return d
|
||||
|
||||
def process_request_2(self, rp, request, spider):
|
||||
if rp is not None and not rp.can_fetch(self._useragent, request.url):
|
||||
logger.debug("Forbidden by robots.txt: %(request)s",
|
||||
{'request': request}, extra={'spider': spider})
|
||||
raise IgnoreRequest
|
||||
raise IgnoreRequest()
|
||||
|
||||
def robot_parser(self, request, spider):
|
||||
url = urlparse_cached(request)
|
||||
netloc = url.netloc
|
||||
|
||||
if netloc not in self._parsers:
|
||||
self._parsers[netloc] = None
|
||||
self._parsers[netloc] = Deferred()
|
||||
robotsurl = "%s://%s/robots.txt" % (url.scheme, url.netloc)
|
||||
robotsreq = Request(
|
||||
robotsurl,
|
||||
|
|
@ -52,9 +58,19 @@ class RobotsTxtMiddleware(object):
|
|||
meta={'dont_obey_robotstxt': True}
|
||||
)
|
||||
dfd = self.crawler.engine.download(robotsreq, spider)
|
||||
dfd.addCallback(self._parse_robots)
|
||||
dfd.addCallback(self._parse_robots, netloc)
|
||||
dfd.addErrback(self._logerror, robotsreq, spider)
|
||||
return self._parsers[netloc]
|
||||
dfd.addErrback(self._robots_error, netloc)
|
||||
|
||||
if isinstance(self._parsers[netloc], Deferred):
|
||||
d = Deferred()
|
||||
def cb(result):
|
||||
d.callback(result)
|
||||
return result
|
||||
self._parsers[netloc].addCallback(cb)
|
||||
return d
|
||||
else:
|
||||
return self._parsers[netloc]
|
||||
|
||||
def _logerror(self, failure, request, spider):
|
||||
if failure.type is not IgnoreRequest:
|
||||
|
|
@ -62,8 +78,9 @@ class RobotsTxtMiddleware(object):
|
|||
{'request': request, 'f_exception': failure.value},
|
||||
exc_info=failure_to_exc_info(failure),
|
||||
extra={'spider': spider})
|
||||
return failure
|
||||
|
||||
def _parse_robots(self, response):
|
||||
def _parse_robots(self, response, netloc):
|
||||
rp = robotparser.RobotFileParser(response.url)
|
||||
body = ''
|
||||
if hasattr(response, 'body_as_unicode'):
|
||||
|
|
@ -78,4 +95,10 @@ class RobotsTxtMiddleware(object):
|
|||
# 'disallow all' to 'allow any'.
|
||||
pass
|
||||
rp.parse(body.splitlines())
|
||||
self._parsers[urlparse_cached(response).netloc] = rp
|
||||
|
||||
rp_dfd = self._parsers[netloc]
|
||||
self._parsers[netloc] = rp
|
||||
rp_dfd.callback(rp)
|
||||
|
||||
def _robots_error(self, failure, netloc):
|
||||
self._parsers.pop(netloc).callback(None)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from scrapy.http import Request, Response
|
|||
from scrapy.spiders import Spider
|
||||
from scrapy.core.downloader.middleware import DownloaderMiddlewareManager
|
||||
from scrapy.utils.test import get_crawler
|
||||
from tests import mock
|
||||
|
||||
|
||||
class ManagerTestCase(TestCase):
|
||||
|
|
@ -90,3 +91,26 @@ class DefaultsTest(ManagerTestCase):
|
|||
'Location': 'http://example.com/login',
|
||||
})
|
||||
self.assertRaises(IOError, self._download, request=req, response=resp)
|
||||
|
||||
|
||||
class ResponseFromProcessRequestTest(ManagerTestCase):
|
||||
"""Tests middleware returning a response from process_request."""
|
||||
|
||||
def test_download_func_not_called(self):
|
||||
resp = Response('http://example.com/index.html')
|
||||
|
||||
class ResponseMiddleware(object):
|
||||
def process_request(self, request, spider):
|
||||
return resp
|
||||
|
||||
self.mwman._add_middleware(ResponseMiddleware())
|
||||
|
||||
req = Request('http://example.com/index.html')
|
||||
download_func = mock.MagicMock()
|
||||
dfd = self.mwman.download(download_func, req, self.spider)
|
||||
results = []
|
||||
dfd.addBoth(results.append)
|
||||
self._wait(dfd)
|
||||
|
||||
self.assertIs(results[0], resp)
|
||||
self.assertFalse(download_func.called)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
from __future__ import absolute_import
|
||||
import re
|
||||
from twisted.internet import reactor, error
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.defer import Deferred, DeferredList, maybeDeferred
|
||||
from twisted.python import failure
|
||||
from twisted.trial import unittest
|
||||
from scrapy.downloadermiddlewares.robotstxt import RobotsTxtMiddleware
|
||||
from scrapy.downloadermiddlewares.robotstxt import (RobotsTxtMiddleware,
|
||||
logger as mw_module_logger)
|
||||
from scrapy.exceptions import IgnoreRequest, NotConfigured
|
||||
from scrapy.http import Request, Response, TextResponse
|
||||
from scrapy.settings import Settings
|
||||
|
|
@ -44,32 +45,26 @@ class RobotsTxtMiddlewareTest(unittest.TestCase):
|
|||
|
||||
def test_robotstxt(self):
|
||||
middleware = RobotsTxtMiddleware(self._get_successful_crawler())
|
||||
# There is a bit of neglect in robotstxt.py: robots.txt is fetched asynchronously,
|
||||
# and it is actually fetched only *after* first process_request completes.
|
||||
# So, first process_request will always succeed.
|
||||
# We defer test() because otherwise robots.txt download mock will be called after assertRaises failure.
|
||||
self.assertNotIgnored(Request('http://site.local'), middleware)
|
||||
def test(r):
|
||||
self.assertNotIgnored(Request('http://site.local/allowed'), middleware)
|
||||
self.assertIgnored(Request('http://site.local/admin/main'), middleware)
|
||||
return DeferredList([
|
||||
self.assertNotIgnored(Request('http://site.local/allowed'), middleware),
|
||||
self.assertIgnored(Request('http://site.local/admin/main'), middleware),
|
||||
self.assertIgnored(Request('http://site.local/static/'), middleware)
|
||||
deferred = Deferred()
|
||||
deferred.addCallback(test)
|
||||
reactor.callFromThread(deferred.callback, None)
|
||||
return deferred
|
||||
], fireOnOneErrback=True)
|
||||
|
||||
def test_robotstxt_ready_parser(self):
|
||||
middleware = RobotsTxtMiddleware(self._get_successful_crawler())
|
||||
d = self.assertNotIgnored(Request('http://site.local/allowed'), middleware)
|
||||
d.addCallback(lambda _: self.assertNotIgnored(Request('http://site.local/allowed'), middleware))
|
||||
return d
|
||||
|
||||
def test_robotstxt_meta(self):
|
||||
middleware = RobotsTxtMiddleware(self._get_successful_crawler())
|
||||
meta = {'dont_obey_robotstxt': True}
|
||||
self.assertNotIgnored(Request('http://site.local', meta=meta), middleware)
|
||||
def test(r):
|
||||
self.assertNotIgnored(Request('http://site.local/allowed', meta=meta), middleware)
|
||||
self.assertNotIgnored(Request('http://site.local/admin/main', meta=meta), middleware)
|
||||
return DeferredList([
|
||||
self.assertNotIgnored(Request('http://site.local/allowed', meta=meta), middleware),
|
||||
self.assertNotIgnored(Request('http://site.local/admin/main', meta=meta), middleware),
|
||||
self.assertNotIgnored(Request('http://site.local/static/', meta=meta), middleware)
|
||||
deferred = Deferred()
|
||||
deferred.addCallback(test)
|
||||
reactor.callFromThread(deferred.callback, None)
|
||||
return deferred
|
||||
], fireOnOneErrback=True)
|
||||
|
||||
def _get_garbage_crawler(self):
|
||||
crawler = self.crawler
|
||||
|
|
@ -85,17 +80,12 @@ class RobotsTxtMiddlewareTest(unittest.TestCase):
|
|||
def test_robotstxt_garbage(self):
|
||||
# garbage response should be discarded, equal 'allow all'
|
||||
middleware = RobotsTxtMiddleware(self._get_garbage_crawler())
|
||||
middleware._logerror = mock.MagicMock()
|
||||
middleware.process_request(Request('http://site.local'), None)
|
||||
self.assertNotIgnored(Request('http://site.local'), middleware)
|
||||
def test(r):
|
||||
self.assertNotIgnored(Request('http://site.local/allowed'), middleware)
|
||||
self.assertNotIgnored(Request('http://site.local/admin/main'), middleware)
|
||||
deferred = DeferredList([
|
||||
self.assertNotIgnored(Request('http://site.local'), middleware),
|
||||
self.assertNotIgnored(Request('http://site.local/allowed'), middleware),
|
||||
self.assertNotIgnored(Request('http://site.local/admin/main'), middleware),
|
||||
self.assertNotIgnored(Request('http://site.local/static/'), middleware)
|
||||
deferred = Deferred()
|
||||
deferred.addCallback(test)
|
||||
deferred.addErrback(lambda _: self.assertIsNone(middleware._logerror.assert_any_call()))
|
||||
reactor.callFromThread(deferred.callback, None)
|
||||
], fireOnOneErrback=True)
|
||||
return deferred
|
||||
|
||||
def _get_emptybody_crawler(self):
|
||||
|
|
@ -112,15 +102,11 @@ class RobotsTxtMiddlewareTest(unittest.TestCase):
|
|||
def test_robotstxt_empty_response(self):
|
||||
# empty response should equal 'allow all'
|
||||
middleware = RobotsTxtMiddleware(self._get_emptybody_crawler())
|
||||
self.assertNotIgnored(Request('http://site.local'), middleware)
|
||||
def test(r):
|
||||
self.assertNotIgnored(Request('http://site.local/allowed'), middleware)
|
||||
self.assertNotIgnored(Request('http://site.local/admin/main'), middleware)
|
||||
return DeferredList([
|
||||
self.assertNotIgnored(Request('http://site.local/allowed'), middleware),
|
||||
self.assertNotIgnored(Request('http://site.local/admin/main'), middleware),
|
||||
self.assertNotIgnored(Request('http://site.local/static/'), middleware)
|
||||
deferred = Deferred()
|
||||
deferred.addCallback(test)
|
||||
reactor.callFromThread(deferred.callback, None)
|
||||
return deferred
|
||||
], fireOnOneErrback=True)
|
||||
|
||||
def test_robotstxt_error(self):
|
||||
self.crawler.settings.set('ROBOTSTXT_OBEY', True)
|
||||
|
|
@ -132,17 +118,33 @@ class RobotsTxtMiddlewareTest(unittest.TestCase):
|
|||
self.crawler.engine.download.side_effect = return_failure
|
||||
|
||||
middleware = RobotsTxtMiddleware(self.crawler)
|
||||
middleware._logerror = mock.MagicMock()
|
||||
middleware.process_request(Request('http://site.local'), None)
|
||||
deferred = Deferred()
|
||||
deferred.addErrback(lambda _: self.assertIsNone(middleware._logerror.assert_any_call()))
|
||||
reactor.callFromThread(deferred.callback, None)
|
||||
middleware._logerror = mock.MagicMock(side_effect=middleware._logerror)
|
||||
deferred = middleware.process_request(Request('http://site.local'), None)
|
||||
deferred.addCallback(lambda _: self.assertTrue(middleware._logerror.called))
|
||||
return deferred
|
||||
|
||||
def test_ignore_robotstxt_request(self):
|
||||
self.crawler.settings.set('ROBOTSTXT_OBEY', True)
|
||||
def ignore_request(request, spider):
|
||||
deferred = Deferred()
|
||||
reactor.callFromThread(deferred.errback, failure.Failure(IgnoreRequest()))
|
||||
return deferred
|
||||
self.crawler.engine.download.side_effect = ignore_request
|
||||
|
||||
middleware = RobotsTxtMiddleware(self.crawler)
|
||||
mw_module_logger.error = mock.MagicMock()
|
||||
|
||||
d = self.assertNotIgnored(Request('http://site.local/allowed'), middleware)
|
||||
d.addCallback(lambda _: self.assertFalse(mw_module_logger.error.called))
|
||||
return d
|
||||
|
||||
def assertNotIgnored(self, request, middleware):
|
||||
spider = None # not actually used
|
||||
self.assertIsNone(middleware.process_request(request, spider))
|
||||
dfd = maybeDeferred(middleware.process_request, request, spider)
|
||||
dfd.addCallback(self.assertIsNone)
|
||||
return dfd
|
||||
|
||||
def assertIgnored(self, request, middleware):
|
||||
spider = None # not actually used
|
||||
self.assertRaises(IgnoreRequest, middleware.process_request, request, spider)
|
||||
return self.assertFailure(maybeDeferred(middleware.process_request, request, spider),
|
||||
IgnoreRequest)
|
||||
|
|
|
|||
Loading…
Reference in New Issue