From 50a0d87d1e472fcc514f3dc2b028b653b7826a9c Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Thu, 3 Jan 2019 17:20:08 -0300 Subject: [PATCH 001/305] Passing keyword arguments to callbacks --- scrapy/core/scraper.py | 2 +- scrapy/http/request/__init__.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scrapy/core/scraper.py b/scrapy/core/scraper.py index ee1e95a0c..7981ce231 100644 --- a/scrapy/core/scraper.py +++ b/scrapy/core/scraper.py @@ -143,7 +143,7 @@ class Scraper(object): def call_spider(self, result, request, spider): result.request = request dfd = defer_result(result) - dfd.addCallbacks(request.callback or spider.parse, request.errback) + dfd.addCallbacks(request.callback or spider.parse, request.errback, callbackKeywords=request.kwargs) return dfd.addCallback(iterate_spider_output) def handle_spider_error(self, _failure, request, response, spider): diff --git a/scrapy/http/request/__init__.py b/scrapy/http/request/__init__.py index cd4360483..7d5cc9dae 100644 --- a/scrapy/http/request/__init__.py +++ b/scrapy/http/request/__init__.py @@ -18,7 +18,7 @@ class Request(object_ref): def __init__(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, - dont_filter=False, errback=None, flags=None): + dont_filter=False, errback=None, flags=None, kwargs=None): self._encoding = encoding # this one has to be set first self.method = str(method).upper() @@ -41,6 +41,7 @@ class Request(object_ref): self._meta = dict(meta) if meta else None self.flags = [] if flags is None else list(flags) + self.kwargs = dict(kwargs) if kwargs else None @property def meta(self): From a2b509a42266a2ab3389de64b608e616f88f77e5 Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Thu, 3 Jan 2019 17:38:06 -0300 Subject: [PATCH 002/305] Pass callback kwargs with response.follow --- scrapy/http/response/__init__.py | 5 +++-- scrapy/http/response/text.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scrapy/http/response/__init__.py b/scrapy/http/response/__init__.py index 1974259b5..99b04a26e 100644 --- a/scrapy/http/response/__init__.py +++ b/scrapy/http/response/__init__.py @@ -106,7 +106,7 @@ class Response(object_ref): def follow(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, - dont_filter=False, errback=None): + dont_filter=False, errback=None, kwargs=None): # type: (...) -> Request """ Return a :class:`~.Request` instance to follow a link ``url``. @@ -132,4 +132,5 @@ class Response(object_ref): encoding=encoding, priority=priority, dont_filter=dont_filter, - errback=errback) + errback=errback, + kwargs=kwargs) diff --git a/scrapy/http/response/text.py b/scrapy/http/response/text.py index 74a042f2c..2039621b3 100644 --- a/scrapy/http/response/text.py +++ b/scrapy/http/response/text.py @@ -123,7 +123,7 @@ class TextResponse(Response): def follow(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding=None, priority=0, - dont_filter=False, errback=None): + dont_filter=False, errback=None, kwargs=None): # type: (...) -> Request """ Return a :class:`~.Request` instance to follow a link ``url``. @@ -154,7 +154,8 @@ class TextResponse(Response): encoding=encoding, priority=priority, dont_filter=dont_filter, - errback=errback + errback=errback, + kwargs=kwargs, ) From 69a1ee79aa43bb1444e5b2a800a2e1702db6866e Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Thu, 3 Jan 2019 17:38:29 -0300 Subject: [PATCH 003/305] Copy request.kwargs --- scrapy/http/request/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/http/request/__init__.py b/scrapy/http/request/__init__.py index 7d5cc9dae..9a155f415 100644 --- a/scrapy/http/request/__init__.py +++ b/scrapy/http/request/__init__.py @@ -93,7 +93,7 @@ class Request(object_ref): given new values. """ for x in ['url', 'method', 'headers', 'body', 'cookies', 'meta', 'flags', - 'encoding', 'priority', 'dont_filter', 'callback', 'errback']: + 'encoding', 'priority', 'dont_filter', 'callback', 'errback', 'kwargs']: kwargs.setdefault(x, getattr(self, x)) cls = kwargs.pop('cls', self.__class__) return cls(*args, **kwargs) From a67f1ce512ffa111a400e2718ebbf3e7bd32d0ae Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Thu, 3 Jan 2019 17:49:41 -0300 Subject: [PATCH 004/305] Serialize Request kwargs --- scrapy/utils/reqser.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scrapy/utils/reqser.py b/scrapy/utils/reqser.py index 959dddbd5..d537057b1 100644 --- a/scrapy/utils/reqser.py +++ b/scrapy/utils/reqser.py @@ -32,7 +32,8 @@ def request_to_dict(request, spider=None): '_encoding': request._encoding, 'priority': request.priority, 'dont_filter': request.dont_filter, - 'flags': request.flags + 'flags': request.flags, + 'kwargs': request.kwargs, } if type(request) is not Request: d['_class'] = request.__module__ + '.' + request.__class__.__name__ @@ -64,7 +65,9 @@ def request_from_dict(d, spider=None): encoding=d['_encoding'], priority=d['priority'], dont_filter=d['dont_filter'], - flags=d.get('flags')) + flags=d.get('flags'), + kwargs=d.get('kwargs'), + ) def _find_method(obj, func): From 770a501fb32b7582acaa5900ac2f41ea46a321cd Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Wed, 9 Jan 2019 10:40:03 -0300 Subject: [PATCH 005/305] Test request kwargs (copy, serialization) --- scrapy/http/request/__init__.py | 8 +++++++- tests/test_http_request.py | 5 +++++ tests/test_utils_reqser.py | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/scrapy/http/request/__init__.py b/scrapy/http/request/__init__.py index 9a155f415..c016eb727 100644 --- a/scrapy/http/request/__init__.py +++ b/scrapy/http/request/__init__.py @@ -40,8 +40,14 @@ class Request(object_ref): self.dont_filter = dont_filter self._meta = dict(meta) if meta else None + self._kwargs = dict(kwargs) if kwargs else None self.flags = [] if flags is None else list(flags) - self.kwargs = dict(kwargs) if kwargs else None + + @property + def kwargs(self): + if self._kwargs is None: + self._kwargs = {} + return self._kwargs @property def meta(self): diff --git a/tests/test_http_request.py b/tests/test_http_request.py index 58326a384..610893d8a 100644 --- a/tests/test_http_request.py +++ b/tests/test_http_request.py @@ -177,6 +177,7 @@ class RequestTest(unittest.TestCase): r1 = self.request_class("http://www.example.com", flags=['f1', 'f2'], callback=somecallback, errback=somecallback) r1.meta['foo'] = 'bar' + r1.kwargs['key'] = 'value' r2 = r1.copy() # make sure copy does not propagate callbacks @@ -189,6 +190,10 @@ class RequestTest(unittest.TestCase): assert r1.flags is not r2.flags, "flags must be a shallow copy, not identical" self.assertEqual(r1.flags, r2.flags) + # make sure kwargs dict is shallow copied + assert r1.kwargs is not r2.kwargs, "kwargs must be a shallow copy, not identical" + self.assertEqual(r1.kwargs, r2.kwargs) + # make sure meta dict is shallow copied assert r1.meta is not r2.meta, "meta must be a shallow copy, not identical" self.assertEqual(r1.meta, r2.meta) diff --git a/tests/test_utils_reqser.py b/tests/test_utils_reqser.py index dcc070b8f..76de20f22 100644 --- a/tests/test_utils_reqser.py +++ b/tests/test_utils_reqser.py @@ -26,6 +26,7 @@ class RequestSerializationTest(unittest.TestCase): encoding='latin-1', priority=20, meta={'a': 'b'}, + kwargs={'k': 'v'}, flags=['testFlag']) self._assert_serializes_ok(r, spider=self.spider) @@ -52,6 +53,7 @@ class RequestSerializationTest(unittest.TestCase): self.assertEqual(r1.headers, r2.headers) self.assertEqual(r1.cookies, r2.cookies) self.assertEqual(r1.meta, r2.meta) + self.assertEqual(r1.kwargs, r2.kwargs) self.assertEqual(r1._encoding, r2._encoding) self.assertEqual(r1.priority, r2.priority) self.assertEqual(r1.dont_filter, r2.dont_filter) From 57e7c769779b9d37058d1d8839215a9c269b8c5b Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Wed, 9 Jan 2019 10:40:44 -0300 Subject: [PATCH 006/305] Test callback kwargs --- tests/spiders.py | 34 ++++++++++++++++++++++++++++++++++ tests/test_crawl.py | 8 +++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/spiders.py b/tests/spiders.py index 7816bf7c7..5a1471072 100644 --- a/tests/spiders.py +++ b/tests/spiders.py @@ -28,6 +28,40 @@ class MetaSpider(MockServerSpider): self.meta['close_reason'] = reason +class KeywordArgumentsSpider(MockServerSpider): + + name = 'kwargs' + checks = set() + + def start_requests(self): + data = {'key': 'value', 'number': 123} + yield Request(self.mockserver.url('/first'), self.parse_first, kwargs=data) + yield Request(self.mockserver.url('/general_with'), self.parse_general, kwargs=data) + yield Request(self.mockserver.url('/general_without'), self.parse_general) + yield Request(self.mockserver.url('/no_kwargs'), self.parse_no_kwargs) + + def parse_first(self, response, key, number): + self.checks.add(key == 'value') + self.checks.add(number == 123) + yield response.follow( + self.mockserver.url('/two'), + self.parse_second, + kwargs={'new_key': 'new_value'}) + + def parse_second(self, response, new_key): + self.checks.add(new_key == 'new_value') + + def parse_general(self, response, **kwargs): + if response.url.endswith('/general_with'): + self.checks.add(kwargs['key'] == 'value') + self.checks.add(kwargs['number'] == 123) + elif response.url.endswith('/general_without'): + self.checks.add(kwargs == {}) + + def parse_no_kwargs(self, response): + pass + + class FollowAllSpider(MetaSpider): name = 'follow' diff --git a/tests/test_crawl.py b/tests/test_crawl.py index 3fc13eeb7..3879a017c 100644 --- a/tests/test_crawl.py +++ b/tests/test_crawl.py @@ -8,7 +8,7 @@ from twisted.trial.unittest import TestCase from scrapy.http import Request from scrapy.crawler import CrawlerRunner from scrapy.utils.python import to_unicode -from tests.spiders import FollowAllSpider, DelaySpider, SimpleSpider, \ +from tests.spiders import FollowAllSpider, DelaySpider, SimpleSpider, KeywordArgumentsSpider, \ BrokenStartRequestsSpider, SingleRequestSpider, DuplicateStartRequestsSpider from tests.mockserver import MockServer @@ -23,6 +23,12 @@ class CrawlTestCase(TestCase): def tearDown(self): self.mockserver.__exit__(None, None, None) + @defer.inlineCallbacks + def test_callback_kwargs(self): + crawler = self.runner.create_crawler(KeywordArgumentsSpider) + yield crawler.crawl(mockserver=self.mockserver) + self.assertEqual(crawler.spider.checks, set([True])) + @defer.inlineCallbacks def test_follow_all(self): crawler = self.runner.create_crawler(FollowAllSpider) From bddfeaba4c17040b2986403f8b2ba25d4252e1b5 Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Tue, 15 Jan 2019 15:35:46 -0300 Subject: [PATCH 007/305] Add Request.kwargs docs --- docs/topics/request-response.rst | 40 +++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index e29914dbf..d12766676 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -24,7 +24,7 @@ below in :ref:`topics-request-response-ref-request-subclasses` and Request objects =============== -.. class:: Request(url[, callback, method='GET', headers, body, cookies, meta, encoding='utf-8', priority=0, dont_filter=False, errback, flags]) +.. class:: Request(url[, callback, method='GET', headers, body, cookies, meta, encoding='utf-8', priority=0, dont_filter=False, errback, flags, kwargs]) A :class:`Request` object represents an HTTP request, which is usually generated in the Spider and executed by the Downloader, and thus generating @@ -126,6 +126,9 @@ Request objects :param flags: Flags sent to the request, can be used for logging or similar purposes. :type flags: list + :param kwargs: A dict with arbitrary data that will be passed as keyword arguments to the Request's callback. + :type kwargs: dict + .. attribute:: Request.url A string containing the URL of this request. Keep in mind that this @@ -165,6 +168,17 @@ Request objects ``copy()`` or ``replace()`` methods, and can also be accessed, in your spider, from the ``response.meta`` attribute. + .. attribute:: Request.kwargs + + A dictionary that contains arbitrary metadata for this request. Its contents + will be passed to the Request's callback as keyword arguments. It is empty + for new Requests, which means by default callbacks only get a :class:`Response` + object as argument. + + This dict is `shallow copied`_ when the request is cloned using the + ``copy()`` or ``replace()`` methods, and can also be accessed, in your + spider, from the ``response.kwargs`` attribute. + .. _shallow copied: https://docs.python.org/2/library/copy.html .. method:: Request.copy() @@ -200,11 +214,9 @@ Example:: self.logger.info("Visited %s", response.url) In some cases you may be interested in passing arguments to those callback -functions so you can receive the arguments later, in the second callback. You -can use the :attr:`Request.meta` attribute for that. - -Here's an example of how to pass an item using this mechanism, to populate -different fields from different pages:: +functions so you can receive the arguments later, in the second callback. +The following two examples show how to achieve this by using the +:attr:`Request.meta` and :attr:`Request.kwargs` attributes respectively:: def parse_page1(self, response): item = MyItem() @@ -219,6 +231,22 @@ different fields from different pages:: item['other_url'] = response.url yield item +:: + + def parse_page1(self, response): + item = MyItem() + item['main_url'] = response.url + request = scrapy.Request("http://www.example.com/some_page.html", + callback=self.parse_page2) + request.kwargs['item'] = item + request.kwargs['foo'] = 'bar' + yield request + + def parse_page2(self, response, item, foo): + item['other_url'] = response.url + item['foo'] = foo + yield item + .. _topics-request-response-ref-errbacks: From 82d239f3b148d9ce69f67bd7a2cb00de7e934aa6 Mon Sep 17 00:00:00 2001 From: Anubhav Patel Date: Wed, 6 Mar 2019 12:08:09 +0530 Subject: [PATCH 008/305] docs for scrapy.logformatter --- docs/topics/logging.rst | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/topics/logging.rst b/docs/topics/logging.rst index 0986929ad..a5fecebba 100644 --- a/docs/topics/logging.rst +++ b/docs/topics/logging.rst @@ -193,6 +193,45 @@ to override some of the Scrapy settings regarding logging. Module `logging.handlers `_ Further documentation on available handlers +Custom Log Formats +------------------- + +Custom log format can be set for different actions by extending ``scrapy.logformatter.LogFormatter`` class. + +Each method of ``scrapy.logformatter.LogFormatter`` represents an action. All methods inherited from +``scrapy.logformatter.LogFormatter`` in your custom log formatting class must return a dictionary listing +the parameters ``level``, ``msg`` and ``args`` which are going to be used for constructing the log message. +Listed below is details of what each key represents : + +* ``level`` is the log level for that action, you can use those from the python logging library: + :setting:`logging.DEBUG`, :setting:`logging.INFO`, :setting:`logging.WARNING`, :setting:`logging.ERROR` + and :setting:`logging.CRITICAL`. + +* ``msg`` should be a string that can contain different formatting placeholders. This string, formatted + with the provided ``args``, is going to be the long message for that action. + +* ``args`` should be a tuple or dict with the formatting placeholders for `msg`. The final log message is + computed as ``msg % args``. + +.. note:: To use custom log formatting class, you must mention it in ``settings.py``, by adding a line + ``LOG_FORMATTER = '’`` + +.. class:: scrapy.logformatter.LogFormatter + + The default log formatting class in Scrapy. + + .. method:: crawled (request, response, spider) + + ``crawled`` is called to log message when the crawler finds a webpage. + + .. method:: scraped(item, response, spider) + + ``scraped`` is called to log message when an item scraped by a spider. + + .. method:: dropped(item, exception, response, spider) + + ``dropped`` is called to log message when an item is dropped while it is passing through the item pipeline. + Advanced customization ---------------------- From 924b67437b92f14601816d02c5d153e7281da6d4 Mon Sep 17 00:00:00 2001 From: Anubhav Patel Date: Thu, 7 Mar 2019 16:40:59 +0530 Subject: [PATCH 009/305] move api docs to source code --- docs/topics/logging.rst | 38 ++++---------------------------------- docs/topics/settings.rst | 9 +++++++++ scrapy/logformatter.py | 36 +++++++++++++++++++++++------------- 3 files changed, 36 insertions(+), 47 deletions(-) diff --git a/docs/topics/logging.rst b/docs/topics/logging.rst index a5fecebba..72f24bae6 100644 --- a/docs/topics/logging.rst +++ b/docs/topics/logging.rst @@ -196,41 +196,11 @@ to override some of the Scrapy settings regarding logging. Custom Log Formats ------------------- -Custom log format can be set for different actions by extending ``scrapy.logformatter.LogFormatter`` class. - -Each method of ``scrapy.logformatter.LogFormatter`` represents an action. All methods inherited from -``scrapy.logformatter.LogFormatter`` in your custom log formatting class must return a dictionary listing -the parameters ``level``, ``msg`` and ``args`` which are going to be used for constructing the log message. -Listed below is details of what each key represents : - -* ``level`` is the log level for that action, you can use those from the python logging library: - :setting:`logging.DEBUG`, :setting:`logging.INFO`, :setting:`logging.WARNING`, :setting:`logging.ERROR` - and :setting:`logging.CRITICAL`. - -* ``msg`` should be a string that can contain different formatting placeholders. This string, formatted - with the provided ``args``, is going to be the long message for that action. - -* ``args`` should be a tuple or dict with the formatting placeholders for `msg`. The final log message is - computed as ``msg % args``. - -.. note:: To use custom log formatting class, you must mention it in ``settings.py``, by adding a line - ``LOG_FORMATTER = '’`` +Custom log format can be set for different actions by extending :class:`~scrapy.logformatter.LogFormatter` class +and making :setting:`LOG_FORMATTER` inside ``settings.py`` point to your new class. -.. class:: scrapy.logformatter.LogFormatter - - The default log formatting class in Scrapy. - - .. method:: crawled (request, response, spider) - - ``crawled`` is called to log message when the crawler finds a webpage. - - .. method:: scraped(item, response, spider) - - ``scraped`` is called to log message when an item scraped by a spider. - - .. method:: dropped(item, exception, response, spider) - - ``dropped`` is called to log message when an item is dropped while it is passing through the item pipeline. +.. autoclass:: scrapy.logformatter.LogFormatter + :members: Advanced customization ---------------------- diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 0ac26a9bd..1dfb5b8aa 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -866,6 +866,15 @@ directives. .. _Python datetime documentation: https://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior +.. setting:: LOG_FORMATTER + +LOG_FORMATTER +------------- + +Default: ``scrapy.logformatter.LogFormatter`` + +The class to use for formatting log messages for different actions. + .. setting:: LOG_LEVEL LOG_LEVEL diff --git a/scrapy/logformatter.py b/scrapy/logformatter.py index 075a6d862..0bb8aee58 100644 --- a/scrapy/logformatter.py +++ b/scrapy/logformatter.py @@ -13,25 +13,29 @@ CRAWLEDMSG = u"Crawled (%(status)s) %(request)s%(request_flags)s (referer: %(ref class LogFormatter(object): """Class for generating log messages for different actions. - All methods must return a dictionary listing the parameters `level`, `msg` - and `args` which are going to be used for constructing the log message when - calling logging.log. + All methods must return a dictionary listing the parameters ``level``, ``msg`` + and ``args`` which are going to be used for constructing the log message when + calling ``logging.log``. Dictionary keys for the method outputs: - * `level` should be the log level for that action, you can use those - from the python logging library: logging.DEBUG, logging.INFO, - logging.WARNING, logging.ERROR and logging.CRITICAL. - * `msg` should be a string that can contain different formatting - placeholders. This string, formatted with the provided `args`, is going - to be the log message for that action. + * ``level`` is the log level for that action, you can use those from the + `python logging library `_ : + ``logging.DEBUG``, ``logging.INFO``, ``logging.WARNING``, ``logging.ERROR`` + and ``logging.CRITICAL``. + + * ``msg`` should be a string that can contain different formatting placeholders. This string, formatted + with the provided ``args``, is going to be the long message for that action. + + * ``args`` should be a tuple or dict with the formatting placeholders for ``msg``. The final log message is + computed as ``msg % args``. - * `args` should be a tuple or dict with the formatting placeholders for - `msg`. The final log message is computed as output['msg'] % - output['args']. """ def crawled(self, request, response, spider): + """ + ``crawled`` is called to log message when the crawler finds a webpage. + """ request_flags = ' %s' % str(request.flags) if request.flags else '' response_flags = ' %s' % str(response.flags) if response.flags else '' return { @@ -40,7 +44,7 @@ class LogFormatter(object): 'args': { 'status': response.status, 'request': request, - 'request_flags' : request_flags, + 'request_flags': request_flags, 'referer': referer_str(request), 'response_flags': response_flags, # backward compatibility with Scrapy logformatter below 1.4 version @@ -49,6 +53,9 @@ class LogFormatter(object): } def scraped(self, item, response, spider): + """ + ``scraped`` is called to log message when an item is scraped by a spider. + """ if isinstance(response, Failure): src = response.getErrorMessage() else: @@ -63,6 +70,9 @@ class LogFormatter(object): } def dropped(self, item, exception, response, spider): + """ + ``dropped`` is called to log message when an item is dropped while it is passing through the item pipeline. + """ return { 'level': logging.WARNING, 'msg': DROPPEDMSG, From 82049e9c41f878d84f0fe10f827c6fe2a33f7ba6 Mon Sep 17 00:00:00 2001 From: Anubhav Patel Date: Sun, 10 Mar 2019 20:14:55 +0530 Subject: [PATCH 010/305] make suggested changes. --- docs/topics/logging.rst | 10 ++++++---- docs/topics/settings.rst | 4 ++-- scrapy/logformatter.py | 26 +++++++++++++++++--------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/docs/topics/logging.rst b/docs/topics/logging.rst index 72f24bae6..006530a8c 100644 --- a/docs/topics/logging.rst +++ b/docs/topics/logging.rst @@ -193,11 +193,13 @@ to override some of the Scrapy settings regarding logging. Module `logging.handlers `_ Further documentation on available handlers -Custom Log Formats -------------------- +.. _custom-log-formats: -Custom log format can be set for different actions by extending :class:`~scrapy.logformatter.LogFormatter` class -and making :setting:`LOG_FORMATTER` inside ``settings.py`` point to your new class. +Custom Log Formats +------------------ + +A custom log format can be set for different actions by extending :class:`~scrapy.logformatter.LogFormatter` class +and making :setting:`LOG_FORMATTER` point to your new class. .. autoclass:: scrapy.logformatter.LogFormatter :members: diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 1dfb5b8aa..a36c0b34c 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -871,9 +871,9 @@ directives. LOG_FORMATTER ------------- -Default: ``scrapy.logformatter.LogFormatter`` +Default: :class:`scrapy.logformatter.LogFormatter` -The class to use for formatting log messages for different actions. +The class to use for :ref:`formatting log messages ` for different actions. .. setting:: LOG_LEVEL diff --git a/scrapy/logformatter.py b/scrapy/logformatter.py index 0bb8aee58..17c69cba8 100644 --- a/scrapy/logformatter.py +++ b/scrapy/logformatter.py @@ -30,12 +30,24 @@ class LogFormatter(object): * ``args`` should be a tuple or dict with the formatting placeholders for ``msg``. The final log message is computed as ``msg % args``. + Here is an example on how to create a custom log formatter to lower the severity level of the log message + when an item is dropped from the pipeline:: + + class PoliteLogFormatter(logformatter.LogFormatter): + def dropped(self, item, exception, response, spider): + return { + 'level': logging.INFO, # lowering the level from logging.WARNING + 'msg': u"Dropped: %(exception)s" + os.linesep + "%(item)s", + 'args': { + 'exception': exception, + 'item': item, + } + } + """ def crawled(self, request, response, spider): - """ - ``crawled`` is called to log message when the crawler finds a webpage. - """ + """Logs a message when the crawler finds a webpage.""" request_flags = ' %s' % str(request.flags) if request.flags else '' response_flags = ' %s' % str(response.flags) if response.flags else '' return { @@ -53,9 +65,7 @@ class LogFormatter(object): } def scraped(self, item, response, spider): - """ - ``scraped`` is called to log message when an item is scraped by a spider. - """ + """Logs a message when an item is scraped by a spider.""" if isinstance(response, Failure): src = response.getErrorMessage() else: @@ -70,9 +80,7 @@ class LogFormatter(object): } def dropped(self, item, exception, response, spider): - """ - ``dropped`` is called to log message when an item is dropped while it is passing through the item pipeline. - """ + """Logs a message when an item is dropped while it is passing through the item pipeline.""" return { 'level': logging.WARNING, 'msg': DROPPEDMSG, From e9cd4ee03aa41e27bea0408b10970ec5bedf35d3 Mon Sep 17 00:00:00 2001 From: Anubhav Patel Date: Sun, 10 Mar 2019 20:37:56 +0530 Subject: [PATCH 011/305] fix list alignment and line width --- scrapy/logformatter.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/scrapy/logformatter.py b/scrapy/logformatter.py index 17c69cba8..717120242 100644 --- a/scrapy/logformatter.py +++ b/scrapy/logformatter.py @@ -19,19 +19,18 @@ class LogFormatter(object): Dictionary keys for the method outputs: - * ``level`` is the log level for that action, you can use those from the - `python logging library `_ : - ``logging.DEBUG``, ``logging.INFO``, ``logging.WARNING``, ``logging.ERROR`` - and ``logging.CRITICAL``. + * ``level`` is the log level for that action, you can use those from the + `python logging library `_ : + ``logging.DEBUG``, ``logging.INFO``, ``logging.WARNING``, ``logging.ERROR`` + and ``logging.CRITICAL``. + * ``msg`` should be a string that can contain different formatting placeholders. + This string, formatted with the provided ``args``, is going to be the long message + for that action. + * ``args`` should be a tuple or dict with the formatting placeholders for ``msg``. + The final log message is computed as ``msg % args``. - * ``msg`` should be a string that can contain different formatting placeholders. This string, formatted - with the provided ``args``, is going to be the long message for that action. - - * ``args`` should be a tuple or dict with the formatting placeholders for ``msg``. The final log message is - computed as ``msg % args``. - - Here is an example on how to create a custom log formatter to lower the severity level of the log message - when an item is dropped from the pipeline:: + Here is an example on how to create a custom log formatter to lower the severity level of + the log message when an item is dropped from the pipeline:: class PoliteLogFormatter(logformatter.LogFormatter): def dropped(self, item, exception, response, spider): From a2ff647aace899982bd494d73fea5a7c35ded722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Chaves?= Date: Fri, 15 Mar 2019 06:36:15 +0100 Subject: [PATCH 012/305] Remove docs/topics/ubuntu.rst --- docs/topics/ubuntu.rst | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 docs/topics/ubuntu.rst diff --git a/docs/topics/ubuntu.rst b/docs/topics/ubuntu.rst deleted file mode 100644 index 6c993a970..000000000 --- a/docs/topics/ubuntu.rst +++ /dev/null @@ -1,41 +0,0 @@ -:orphan: Ubuntu packages are obsolete - -.. _topics-ubuntu: - -=============== -Ubuntu packages -=============== - -.. versionadded:: 0.10 - -`Scrapinghub`_ publishes apt-gettable packages which are generally fresher than -those in Ubuntu, and more stable too since they're continuously built from -`GitHub repo`_ (master & stable branches) and so they contain the latest bug -fixes. - -.. caution:: These packages are currently not updated and may not work on - Ubuntu 16.04 and above, see :issue:`2076` and :issue:`2137`. - -To use the packages: - -1. Import the GPG key used to sign Scrapy packages into APT keyring:: - - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 627220E7 - -2. Create ``/etc/apt/sources.list.d/scrapy.list`` file using the following command:: - - echo 'deb http://archive.scrapy.org/ubuntu scrapy main' | sudo tee /etc/apt/sources.list.d/scrapy.list - -3. Update package lists and install the scrapy package: - - .. parsed-literal:: - - sudo apt-get update && sudo apt-get install scrapy - -.. note:: Repeat step 3 if you are trying to upgrade Scrapy. - -.. warning:: ``python-scrapy`` is a different package provided by official debian - repositories, it's very outdated and it isn't supported by Scrapy team. - -.. _Scrapinghub: https://scrapinghub.com/ -.. _GitHub repo: https://github.com/scrapy/scrapy From 645e8d16a4c966b50bd39667aaef28dc1eeb43b8 Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Fri, 15 Mar 2019 22:20:36 +0000 Subject: [PATCH 013/305] Count keyword argument checks --- tests/spiders.py | 21 +++++++++++++-------- tests/test_crawl.py | 3 ++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/spiders.py b/tests/spiders.py index 5a1471072..7b4707f62 100644 --- a/tests/spiders.py +++ b/tests/spiders.py @@ -31,7 +31,7 @@ class MetaSpider(MockServerSpider): class KeywordArgumentsSpider(MockServerSpider): name = 'kwargs' - checks = set() + checks = list() def start_requests(self): data = {'key': 'value', 'number': 123} @@ -41,25 +41,30 @@ class KeywordArgumentsSpider(MockServerSpider): yield Request(self.mockserver.url('/no_kwargs'), self.parse_no_kwargs) def parse_first(self, response, key, number): - self.checks.add(key == 'value') - self.checks.add(number == 123) + self.checks.append(key == 'value') + self.checks.append(number == 123) + self.crawler.stats.inc_value('boolean_checks', 2) yield response.follow( self.mockserver.url('/two'), self.parse_second, kwargs={'new_key': 'new_value'}) def parse_second(self, response, new_key): - self.checks.add(new_key == 'new_value') + self.checks.append(new_key == 'new_value') + self.crawler.stats.inc_value('boolean_checks') def parse_general(self, response, **kwargs): if response.url.endswith('/general_with'): - self.checks.add(kwargs['key'] == 'value') - self.checks.add(kwargs['number'] == 123) + self.checks.append(kwargs['key'] == 'value') + self.checks.append(kwargs['number'] == 123) + self.crawler.stats.inc_value('boolean_checks', 2) elif response.url.endswith('/general_without'): - self.checks.add(kwargs == {}) + self.checks.append(kwargs == {}) + self.crawler.stats.inc_value('boolean_checks') def parse_no_kwargs(self, response): - pass + self.checks.append(response.url.endswith('/no_kwargs')) + self.crawler.stats.inc_value('boolean_checks') class FollowAllSpider(MetaSpider): diff --git a/tests/test_crawl.py b/tests/test_crawl.py index 3879a017c..9a39b8cb4 100644 --- a/tests/test_crawl.py +++ b/tests/test_crawl.py @@ -27,7 +27,8 @@ class CrawlTestCase(TestCase): def test_callback_kwargs(self): crawler = self.runner.create_crawler(KeywordArgumentsSpider) yield crawler.crawl(mockserver=self.mockserver) - self.assertEqual(crawler.spider.checks, set([True])) + self.assertTrue(all(crawler.spider.checks)) + self.assertEqual(len(crawler.spider.checks), crawler.stats.get_value('boolean_checks')) @defer.inlineCallbacks def test_follow_all(self): From 6760bca74b1f51ce83ed73318d5ddcef03c9d129 Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Fri, 15 Mar 2019 22:32:45 +0000 Subject: [PATCH 014/305] Rename Request.kwargs to Request.cb_kwargs --- docs/topics/request-response.rst | 16 ++++++++-------- scrapy/core/scraper.py | 4 +++- scrapy/http/request/__init__.py | 14 +++++++------- scrapy/http/response/__init__.py | 4 ++-- scrapy/http/response/text.py | 4 ++-- scrapy/utils/reqser.py | 4 ++-- tests/spiders.py | 6 +++--- tests/test_http_request.py | 8 ++++---- tests/test_utils_reqser.py | 4 ++-- 9 files changed, 33 insertions(+), 31 deletions(-) diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index d12766676..b3f849540 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -24,7 +24,7 @@ below in :ref:`topics-request-response-ref-request-subclasses` and Request objects =============== -.. class:: Request(url[, callback, method='GET', headers, body, cookies, meta, encoding='utf-8', priority=0, dont_filter=False, errback, flags, kwargs]) +.. class:: Request(url[, callback, method='GET', headers, body, cookies, meta, encoding='utf-8', priority=0, dont_filter=False, errback, flags, cb_kwargs]) A :class:`Request` object represents an HTTP request, which is usually generated in the Spider and executed by the Downloader, and thus generating @@ -126,8 +126,8 @@ Request objects :param flags: Flags sent to the request, can be used for logging or similar purposes. :type flags: list - :param kwargs: A dict with arbitrary data that will be passed as keyword arguments to the Request's callback. - :type kwargs: dict + :param cb_kwargs: A dict with arbitrary data that will be passed as keyword arguments to the Request's callback. + :type cb_kwargs: dict .. attribute:: Request.url @@ -168,7 +168,7 @@ Request objects ``copy()`` or ``replace()`` methods, and can also be accessed, in your spider, from the ``response.meta`` attribute. - .. attribute:: Request.kwargs + .. attribute:: Request.cb_kwargs A dictionary that contains arbitrary metadata for this request. Its contents will be passed to the Request's callback as keyword arguments. It is empty @@ -177,7 +177,7 @@ Request objects This dict is `shallow copied`_ when the request is cloned using the ``copy()`` or ``replace()`` methods, and can also be accessed, in your - spider, from the ``response.kwargs`` attribute. + spider, from the ``response.cb_kwargs`` attribute. .. _shallow copied: https://docs.python.org/2/library/copy.html @@ -216,7 +216,7 @@ Example:: In some cases you may be interested in passing arguments to those callback functions so you can receive the arguments later, in the second callback. The following two examples show how to achieve this by using the -:attr:`Request.meta` and :attr:`Request.kwargs` attributes respectively:: +:attr:`Request.meta` and :attr:`Request.cb_kwargs` attributes respectively:: def parse_page1(self, response): item = MyItem() @@ -238,8 +238,8 @@ The following two examples show how to achieve this by using the item['main_url'] = response.url request = scrapy.Request("http://www.example.com/some_page.html", callback=self.parse_page2) - request.kwargs['item'] = item - request.kwargs['foo'] = 'bar' + request.cb_kwargs['item'] = item + request.cb_kwargs['foo'] = 'bar' yield request def parse_page2(self, response, item, foo): diff --git a/scrapy/core/scraper.py b/scrapy/core/scraper.py index 7981ce231..08dd1acc5 100644 --- a/scrapy/core/scraper.py +++ b/scrapy/core/scraper.py @@ -143,7 +143,9 @@ class Scraper(object): def call_spider(self, result, request, spider): result.request = request dfd = defer_result(result) - dfd.addCallbacks(request.callback or spider.parse, request.errback, callbackKeywords=request.kwargs) + dfd.addCallbacks(callback=request.callback or spider.parse, + errback=request.errback, + callbackKeywords=request.cb_kwargs) return dfd.addCallback(iterate_spider_output) def handle_spider_error(self, _failure, request, response, spider): diff --git a/scrapy/http/request/__init__.py b/scrapy/http/request/__init__.py index c016eb727..f5935c4ef 100644 --- a/scrapy/http/request/__init__.py +++ b/scrapy/http/request/__init__.py @@ -18,7 +18,7 @@ class Request(object_ref): def __init__(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, - dont_filter=False, errback=None, flags=None, kwargs=None): + dont_filter=False, errback=None, flags=None, cb_kwargs=None): self._encoding = encoding # this one has to be set first self.method = str(method).upper() @@ -40,14 +40,14 @@ class Request(object_ref): self.dont_filter = dont_filter self._meta = dict(meta) if meta else None - self._kwargs = dict(kwargs) if kwargs else None + self._cb_kwargs = dict(cb_kwargs) if cb_kwargs else None self.flags = [] if flags is None else list(flags) @property - def kwargs(self): - if self._kwargs is None: - self._kwargs = {} - return self._kwargs + def cb_kwargs(self): + if self._cb_kwargs is None: + self._cb_kwargs = {} + return self._cb_kwargs @property def meta(self): @@ -99,7 +99,7 @@ class Request(object_ref): given new values. """ for x in ['url', 'method', 'headers', 'body', 'cookies', 'meta', 'flags', - 'encoding', 'priority', 'dont_filter', 'callback', 'errback', 'kwargs']: + 'encoding', 'priority', 'dont_filter', 'callback', 'errback', 'cb_kwargs']: kwargs.setdefault(x, getattr(self, x)) cls = kwargs.pop('cls', self.__class__) return cls(*args, **kwargs) diff --git a/scrapy/http/response/__init__.py b/scrapy/http/response/__init__.py index 99b04a26e..b0a526b72 100644 --- a/scrapy/http/response/__init__.py +++ b/scrapy/http/response/__init__.py @@ -106,7 +106,7 @@ class Response(object_ref): def follow(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, - dont_filter=False, errback=None, kwargs=None): + dont_filter=False, errback=None, cb_kwargs=None): # type: (...) -> Request """ Return a :class:`~.Request` instance to follow a link ``url``. @@ -133,4 +133,4 @@ class Response(object_ref): priority=priority, dont_filter=dont_filter, errback=errback, - kwargs=kwargs) + cb_kwargs=cb_kwargs) diff --git a/scrapy/http/response/text.py b/scrapy/http/response/text.py index 2039621b3..339913d4e 100644 --- a/scrapy/http/response/text.py +++ b/scrapy/http/response/text.py @@ -123,7 +123,7 @@ class TextResponse(Response): def follow(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding=None, priority=0, - dont_filter=False, errback=None, kwargs=None): + dont_filter=False, errback=None, cb_kwargs=None): # type: (...) -> Request """ Return a :class:`~.Request` instance to follow a link ``url``. @@ -155,7 +155,7 @@ class TextResponse(Response): priority=priority, dont_filter=dont_filter, errback=errback, - kwargs=kwargs, + cb_kwargs=cb_kwargs, ) diff --git a/scrapy/utils/reqser.py b/scrapy/utils/reqser.py index d537057b1..e7016b92a 100644 --- a/scrapy/utils/reqser.py +++ b/scrapy/utils/reqser.py @@ -33,7 +33,7 @@ def request_to_dict(request, spider=None): 'priority': request.priority, 'dont_filter': request.dont_filter, 'flags': request.flags, - 'kwargs': request.kwargs, + 'cb_kwargs': request.cb_kwargs, } if type(request) is not Request: d['_class'] = request.__module__ + '.' + request.__class__.__name__ @@ -66,7 +66,7 @@ def request_from_dict(d, spider=None): priority=d['priority'], dont_filter=d['dont_filter'], flags=d.get('flags'), - kwargs=d.get('kwargs'), + cb_kwargs=d.get('cb_kwargs'), ) diff --git a/tests/spiders.py b/tests/spiders.py index 7b4707f62..a06985837 100644 --- a/tests/spiders.py +++ b/tests/spiders.py @@ -35,8 +35,8 @@ class KeywordArgumentsSpider(MockServerSpider): def start_requests(self): data = {'key': 'value', 'number': 123} - yield Request(self.mockserver.url('/first'), self.parse_first, kwargs=data) - yield Request(self.mockserver.url('/general_with'), self.parse_general, kwargs=data) + yield Request(self.mockserver.url('/first'), self.parse_first, cb_kwargs=data) + yield Request(self.mockserver.url('/general_with'), self.parse_general, cb_kwargs=data) yield Request(self.mockserver.url('/general_without'), self.parse_general) yield Request(self.mockserver.url('/no_kwargs'), self.parse_no_kwargs) @@ -47,7 +47,7 @@ class KeywordArgumentsSpider(MockServerSpider): yield response.follow( self.mockserver.url('/two'), self.parse_second, - kwargs={'new_key': 'new_value'}) + cb_kwargs={'new_key': 'new_value'}) def parse_second(self, response, new_key): self.checks.append(new_key == 'new_value') diff --git a/tests/test_http_request.py b/tests/test_http_request.py index 610893d8a..c1949a28c 100644 --- a/tests/test_http_request.py +++ b/tests/test_http_request.py @@ -177,7 +177,7 @@ class RequestTest(unittest.TestCase): r1 = self.request_class("http://www.example.com", flags=['f1', 'f2'], callback=somecallback, errback=somecallback) r1.meta['foo'] = 'bar' - r1.kwargs['key'] = 'value' + r1.cb_kwargs['key'] = 'value' r2 = r1.copy() # make sure copy does not propagate callbacks @@ -190,9 +190,9 @@ class RequestTest(unittest.TestCase): assert r1.flags is not r2.flags, "flags must be a shallow copy, not identical" self.assertEqual(r1.flags, r2.flags) - # make sure kwargs dict is shallow copied - assert r1.kwargs is not r2.kwargs, "kwargs must be a shallow copy, not identical" - self.assertEqual(r1.kwargs, r2.kwargs) + # make sure cb_kwargs dict is shallow copied + assert r1.cb_kwargs is not r2.cb_kwargs, "cb_kwargs must be a shallow copy, not identical" + self.assertEqual(r1.cb_kwargs, r2.cb_kwargs) # make sure meta dict is shallow copied assert r1.meta is not r2.meta, "meta must be a shallow copy, not identical" diff --git a/tests/test_utils_reqser.py b/tests/test_utils_reqser.py index 76de20f22..e1601b76b 100644 --- a/tests/test_utils_reqser.py +++ b/tests/test_utils_reqser.py @@ -26,7 +26,7 @@ class RequestSerializationTest(unittest.TestCase): encoding='latin-1', priority=20, meta={'a': 'b'}, - kwargs={'k': 'v'}, + cb_kwargs={'k': 'v'}, flags=['testFlag']) self._assert_serializes_ok(r, spider=self.spider) @@ -53,7 +53,7 @@ class RequestSerializationTest(unittest.TestCase): self.assertEqual(r1.headers, r2.headers) self.assertEqual(r1.cookies, r2.cookies) self.assertEqual(r1.meta, r2.meta) - self.assertEqual(r1.kwargs, r2.kwargs) + self.assertEqual(r1.cb_kwargs, r2.cb_kwargs) self.assertEqual(r1._encoding, r2._encoding) self.assertEqual(r1.priority, r2.priority) self.assertEqual(r1.dont_filter, r2.dont_filter) From 044318920a463d2e04efe2a4c65d8f72d1b5ecb6 Mon Sep 17 00:00:00 2001 From: Anubhav Patel Date: Sun, 17 Mar 2019 16:54:28 +0530 Subject: [PATCH 015/305] doc for creating custom cache storage backend. --- docs/topics/downloader-middleware.rst | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index 0d976077b..f913b059d 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -496,6 +496,59 @@ In order to use this storage backend: .. _LevelDB: https://github.com/google/leveldb .. _leveldb python bindings: https://pypi.python.org/pypi/leveldb +.. _httpcache-storage-custom: + +Implementing custom cache storage backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can implement custom cache storage backend by creating a Python class that +defines the methods described below. + +.. module:: scrapy.extensions.httpcache + +.. class:: CacheStorage + + .. method:: open_spider(spider) + + This method gets called after a spider has been opened for crawling. + + :param spider: the spider which has been opened + :type spider: :class:`~scrapy.spiders.Spider` object + + .. method:: close_spider(spider) + + This method gets called after a spider has been closed. + + :param spider: the spider which has been closed + :type spider: :class:`~scrapy.spiders.Spider` object + + .. method:: retrieve_response(spider, request) + + Returns response if present in cache, or ``None`` otherwise. + + :param spider: the spider which generated the request + :type spider: :class:`~scrapy.spiders.Spider` object + + :param request: the request to find cached reponse for + :type request: :class:`~scrapy.http.Request` object + + .. method:: store_response(spider, request, response) + + Stores the given response in the cache. + + :param spider: the spider for which the response is intended + :type spider: :class:`~scrapy.spiders.Spider` object + + :param request: corresponding request the spider generated + :type request: :class:`~scrapy.http.Request` object + + :param response: the response to store in the cache + :type response: :class:`~scrapy.http.Response` object + +In order to use your storage backend, set: + +* :setting:`HTTPCACHE_STORAGE` to path of your custom storage class. + HTTPCache middleware settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 431f18a9a1a87ac8b789e3c260016e510ab48ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Chaves?= Date: Fri, 1 Feb 2019 13:22:38 +0100 Subject: [PATCH 016/305] Document FilesPipeline.file_path and ImagesPipeline.file_path --- docs/topics/media-pipeline.rst | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/topics/media-pipeline.rst b/docs/topics/media-pipeline.rst index c60b55391..c97b4c3c2 100644 --- a/docs/topics/media-pipeline.rst +++ b/docs/topics/media-pipeline.rst @@ -392,6 +392,36 @@ See here the methods that you can override in your custom Files Pipeline: .. class:: FilesPipeline + .. method:: file_path(request, response, info) + + This method is called once per downloaded item. It returns the + download path of the file originating from the specified + :class:`response `. + + In addition to ``response``, this method receives the original + :class:`request ` and + :class:`info `. + + You can override this method to customize the download path of each file. + + For example, if file URLs end like regular paths (e.g. + ``https://example.com/a/b/c/foo.png``), you can use the following + approach to download all files into the ``files`` folder with their + original filenames (e.g. ``files/foo.png``):: + + import os + from urllib.parse import urlparse + + from scrapy.pipelines.files import FilesPipeline + + class MyFilesPipeline(FilesPipeline): + + def file_path(self, request, response, info): + return 'files/' + os.path.basename(urlparse(request.url).path) + + By default the :meth:`file_path` method returns + ``full/.``. + .. method:: FilesPipeline.get_media_requests(item, info) As seen on the workflow, the pipeline will get the URLs of the images to @@ -475,6 +505,36 @@ See here the methods that you can override in your custom Images Pipeline: The :class:`ImagesPipeline` is an extension of the :class:`FilesPipeline`, customizing the field names and adding custom behavior for images. + .. method:: file_path(request, response, info) + + This method is called once per downloaded item. It returns the + download path of the file originating from the specified + :class:`response `. + + In addition to ``response``, this method receives the original + :class:`request ` and + :class:`info `. + + You can override this method to customize the download path of each file. + + For example, if file URLs end like regular paths (e.g. + ``https://example.com/a/b/c/foo.png``), you can use the following + approach to download all files into the ``files`` folder with their + original filenames (e.g. ``files/foo.png``):: + + import os + from urllib.parse import urlparse + + from scrapy.pipelines.images import ImagesPipeline + + class MyImagesPipeline(ImagesPipeline): + + def file_path(self, request, response, info): + return 'files/' + os.path.basename(urlparse(request.url).path) + + By default the :meth:`file_path` method returns + ``full/.``. + .. method:: ImagesPipeline.get_media_requests(item, info) Works the same way as :meth:`FilesPipeline.get_media_requests` method, From 9c9bca4e1c7984089c44f3a44e7594e06307b12f Mon Sep 17 00:00:00 2001 From: Anubhav Patel Date: Wed, 27 Mar 2019 18:29:48 +0530 Subject: [PATCH 017/305] make suggested changes. --- docs/topics/downloader-middleware.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index f913b059d..dfbcdb8fa 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -349,7 +349,7 @@ HttpCacheMiddleware * :ref:`httpcache-storage-leveldb` You can change the HTTP cache storage backend with the :setting:`HTTPCACHE_STORAGE` - setting. Or you can also implement your own storage backend. + setting. Or you can also :ref:`implement your own storage backend. ` Scrapy ships with two HTTP cache policies: @@ -498,10 +498,10 @@ In order to use this storage backend: .. _httpcache-storage-custom: -Implementing custom cache storage backend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Writing your own storage backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can implement custom cache storage backend by creating a Python class that +You can implement a cache storage backend by creating a Python class that defines the methods described below. .. module:: scrapy.extensions.httpcache @@ -510,14 +510,16 @@ defines the methods described below. .. method:: open_spider(spider) - This method gets called after a spider has been opened for crawling. + This method gets called after a spider has been opened for crawling. It handles + the :signal:`open_spider ` signal. :param spider: the spider which has been opened :type spider: :class:`~scrapy.spiders.Spider` object .. method:: close_spider(spider) - This method gets called after a spider has been closed. + This method gets called after a spider has been closed. It handles + the :signal:`close_spider ` signal. :param spider: the spider which has been closed :type spider: :class:`~scrapy.spiders.Spider` object @@ -539,7 +541,7 @@ defines the methods described below. :param spider: the spider for which the response is intended :type spider: :class:`~scrapy.spiders.Spider` object - :param request: corresponding request the spider generated + :param request: the corresponding request the spider generated :type request: :class:`~scrapy.http.Request` object :param response: the response to store in the cache @@ -547,7 +549,7 @@ defines the methods described below. In order to use your storage backend, set: -* :setting:`HTTPCACHE_STORAGE` to path of your custom storage class. +* :setting:`HTTPCACHE_STORAGE` to the Python import path of your custom storage class. HTTPCache middleware settings From 8528f5065f99046b149b5e1901d6cbe5296f048a Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Wed, 27 Mar 2019 14:42:26 -0300 Subject: [PATCH 018/305] [Doc] Update cb_kwargs example --- docs/topics/request-response.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index b3f849540..61789be0f 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -233,19 +233,19 @@ The following two examples show how to achieve this by using the :: - def parse_page1(self, response): - item = MyItem() - item['main_url'] = response.url - request = scrapy.Request("http://www.example.com/some_page.html", - callback=self.parse_page2) - request.cb_kwargs['item'] = item - request.cb_kwargs['foo'] = 'bar' + def parse(self, response): + request = scrapy.Request('http://www.example.com/index.html', + callback=self.parse_page2, + cb_kwargs=dict(main_url=response.url)) + request.cb_kwargs['foo'] = 'bar' # add more arguments for the callback yield request - def parse_page2(self, response, item, foo): - item['other_url'] = response.url - item['foo'] = foo - yield item + def parse_page2(self, response, main_url, foo): + yield dict( + main_url=main_url, + other_url=response.url, + foo=foo, + ) .. _topics-request-response-ref-errbacks: From 70a4d93aa324fb276e60d641b37bdc6eb707b1cb Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Thu, 28 Mar 2019 10:40:41 -0300 Subject: [PATCH 019/305] Callback kwargs: more tests --- tests/spiders.py | 22 ++++++++++++++++++++++ tests/test_crawl.py | 21 ++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/spiders.py b/tests/spiders.py index a06985837..8c8d50ff5 100644 --- a/tests/spiders.py +++ b/tests/spiders.py @@ -39,6 +39,9 @@ class KeywordArgumentsSpider(MockServerSpider): yield Request(self.mockserver.url('/general_with'), self.parse_general, cb_kwargs=data) yield Request(self.mockserver.url('/general_without'), self.parse_general) yield Request(self.mockserver.url('/no_kwargs'), self.parse_no_kwargs) + yield Request(self.mockserver.url('/default'), self.parse_default, cb_kwargs=data) + yield Request(self.mockserver.url('/takes_less'), self.parse_takes_less, cb_kwargs=data) + yield Request(self.mockserver.url('/takes_more'), self.parse_takes_more, cb_kwargs=data) def parse_first(self, response, key, number): self.checks.append(key == 'value') @@ -66,6 +69,25 @@ class KeywordArgumentsSpider(MockServerSpider): self.checks.append(response.url.endswith('/no_kwargs')) self.crawler.stats.inc_value('boolean_checks') + def parse_default(self, response, key, number=None, default=99): + self.checks.append(response.url.endswith('/default')) + self.checks.append(key == 'value') + self.checks.append(number == 123) + self.checks.append(default == 99) + self.crawler.stats.inc_value('boolean_checks', 4) + + def parse_takes_less(self, response, key): + """ + Should raise + TypeError: parse_takes_less() got an unexpected keyword argument 'number' + """ + + def parse_takes_more(self, response, key, number, other): + """ + Should raise + TypeError: parse_takes_more() missing 1 required positional argument: 'other' + """ + class FollowAllSpider(MetaSpider): diff --git a/tests/test_crawl.py b/tests/test_crawl.py index 9a39b8cb4..2b3e56ee9 100644 --- a/tests/test_crawl.py +++ b/tests/test_crawl.py @@ -4,6 +4,7 @@ import logging from testfixtures import LogCapture from twisted.internet import defer from twisted.trial.unittest import TestCase +import six from scrapy.http import Request from scrapy.crawler import CrawlerRunner @@ -15,6 +16,8 @@ from tests.mockserver import MockServer class CrawlTestCase(TestCase): + maxDiff = None + def setUp(self): self.mockserver = MockServer() self.mockserver.__enter__() @@ -26,9 +29,25 @@ class CrawlTestCase(TestCase): @defer.inlineCallbacks def test_callback_kwargs(self): crawler = self.runner.create_crawler(KeywordArgumentsSpider) - yield crawler.crawl(mockserver=self.mockserver) + with LogCapture() as log: + yield crawler.crawl(mockserver=self.mockserver) self.assertTrue(all(crawler.spider.checks)) self.assertEqual(len(crawler.spider.checks), crawler.stats.get_value('boolean_checks')) + # check exceptions for argument mismatch + exceptions = {} + for line in log.records: + for key in ('takes_less', 'takes_more'): + if key in line.getMessage(): + exceptions[key] = line + self.assertEqual(exceptions['takes_less'].exc_info[0], TypeError) + self.assertEqual(str(exceptions['takes_less'].exc_info[1]), "parse_takes_less() got an unexpected keyword argument 'number'") + self.assertEqual(exceptions['takes_more'].exc_info[0], TypeError) + # py2 and py3 messages are different + exc_message = str(exceptions['takes_more'].exc_info[1]) + if six.PY2: + self.assertEqual(exc_message, "parse_takes_more() takes exactly 5 arguments (4 given)") + elif six.PY3: + self.assertEqual(exc_message, "parse_takes_more() missing 1 required positional argument: 'other'") @defer.inlineCallbacks def test_follow_all(self): From 3efe3bea1cbb5ae83c024fc6dc8e1776a47a345f Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Thu, 28 Mar 2019 14:16:03 -0300 Subject: [PATCH 020/305] Update docs about cb_kwargs and meta --- docs/topics/request-response.rst | 41 ++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index dd0db8156..05ca8d6c1 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -215,24 +215,12 @@ Example:: In some cases you may be interested in passing arguments to those callback functions so you can receive the arguments later, in the second callback. -The following two examples show how to achieve this by using the -:attr:`Request.meta` and :attr:`Request.cb_kwargs` attributes respectively:: - - def parse_page1(self, response): - item = MyItem() - item['main_url'] = response.url - request = scrapy.Request("http://www.example.com/some_page.html", - callback=self.parse_page2) - request.meta['item'] = item - yield request - - def parse_page2(self, response): - item = response.meta['item'] - item['other_url'] = response.url - yield item +The following example shows how to achieve this by using the +:attr:`Request.cb_kwargs` attribute: :: + # pass information to the next callback using the Request.cb_kwargs attribute def parse(self, response): request = scrapy.Request('http://www.example.com/index.html', callback=self.parse_page2, @@ -247,6 +235,29 @@ The following two examples show how to achieve this by using the foo=foo, ) +.. caution:: :attr:`Request.cb_kwargs` was introduced in version ``1.7``. + Prior to that, :attr:`Request.meta` was the recommended option for passing + information around callbacks. However, after ``1.7`` :attr:`Request.cb_kwargs` + became the preferred way of passing user information, leaving :attr:`Request.meta` + to be used by internal components like spider or downloader middlewares. + The following example, which uses :attr:`Request.meta`, is only kept for historical + reasons. + +:: + + # pass information to the next callback using the Request.meta attribute + def parse_page1(self, response): + item = MyItem() + item['main_url'] = response.url + request = scrapy.Request("http://www.example.com/some_page.html", + callback=self.parse_page2) + request.meta['item'] = item + yield request + + def parse_page2(self, response): + item = response.meta['item'] + item['other_url'] = response.url + yield item .. _topics-request-response-ref-errbacks: From e8af6331b5ff62d71ff80eddcc52b85c25482c0e Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Thu, 28 Mar 2019 14:56:31 -0300 Subject: [PATCH 021/305] Add cb_kwargs option to the parse command --- docs/topics/commands.rst | 3 +++ scrapy/commands/parse.py | 37 ++++++++++++++++++++++++------------- tests/test_command_parse.py | 14 ++++++++++++++ 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/docs/topics/commands.rst b/docs/topics/commands.rst index 97f8311de..6644d65e4 100644 --- a/docs/topics/commands.rst +++ b/docs/topics/commands.rst @@ -461,6 +461,9 @@ Supported options: * ``--meta`` or ``-m``: additional request meta that will be passed to the callback request. This must be a valid json string. Example: --meta='{"foo" : "bar"}' +* ``--cb_kwargs``: additional keyword arguments that will be passed to the callback. + This must be a valid json string. Example: --cb_kwargs='{"foo" : "bar"}' + * ``--pipelines``: process items through pipelines * ``--rules`` or ``-r``: use :class:`~scrapy.spiders.CrawlSpider` diff --git a/scrapy/commands/parse.py b/scrapy/commands/parse.py index 69418a478..2486f3f23 100644 --- a/scrapy/commands/parse.py +++ b/scrapy/commands/parse.py @@ -51,12 +51,13 @@ class Command(ScrapyCommand): help="use this callback for parsing, instead looking for a callback") parser.add_option("-m", "--meta", dest="meta", help="inject extra meta into the Request, it must be a valid raw json string") + parser.add_option("--cb_kwargs", dest="cb_kwargs", + help="inject extra cb_kwargs into the Request, it must be a valid raw json string") parser.add_option("-d", "--depth", dest="depth", type="int", default=1, help="maximum depth for parsing requests [default: %default]") parser.add_option("-v", "--verbose", dest="verbose", action="store_true", help="print each depth level one by one") - @property def max_level(self): levels = list(self.items.keys()) + list(self.requests.keys()) @@ -111,10 +112,11 @@ class Command(ScrapyCommand): if not opts.nolinks: self.print_requests(colour=colour) - def run_callback(self, response, cb): + def run_callback(self, response, callback, cb_kwargs=None): + cb_kwargs = cb_kwargs or {} items, requests = [], [] - for x in iterate_spider_output(cb(response)): + for x in iterate_spider_output(callback(response, **cb_kwargs)): if isinstance(x, (BaseItem, dict)): items.append(x) elif isinstance(x, Request): @@ -142,8 +144,7 @@ class Command(ScrapyCommand): else: self.spidercls = spidercls_for_request(spider_loader, Request(url)) if not self.spidercls: - logger.error('Unable to find spider for: %(url)s', - {'url': url}) + logger.error('Unable to find spider for: %(url)s', {'url': url}) # Request requires callback argument as callable or None, not string request = Request(url, None) @@ -160,7 +161,7 @@ class Command(ScrapyCommand): {'url': url}) def prepare_request(self, spider, request, opts): - def callback(response): + def callback(response, **cb_kwargs): # memorize first request if not self.first_response: self.first_response = response @@ -175,7 +176,7 @@ class Command(ScrapyCommand): if not cb: logger.error('Cannot find a rule that matches %(url)r in spider: %(spider)s', - {'url': response.url, 'spider': spider.name}) + {'url': response.url, 'spider': spider.name}) return else: cb = 'parse' @@ -192,7 +193,7 @@ class Command(ScrapyCommand): # parse items and requests depth = response.meta['_depth'] - items, requests = self.run_callback(response, cb) + items, requests = self.run_callback(response, cb, cb_kwargs) if opts.pipelines: itemproc = self.pcrawler.engine.scraper.itemproc for item in items: @@ -207,10 +208,14 @@ class Command(ScrapyCommand): req.callback = callback return requests - #update request meta if any extra meta was passed through the --meta/-m opts. + # update request meta if any extra meta was passed through the --meta/-m opts. if opts.meta: request.meta.update(opts.meta) + # update cb_kwargs if any extra cb_kwargs was passed through the --cb_kwargs option. + if opts.cb_kwargs: + request.cb_kwargs.update(opts.cb_kwargs) + request.meta['_depth'] = 1 request.meta['_callback'] = request.callback request.callback = callback @@ -221,23 +226,29 @@ class Command(ScrapyCommand): self.process_spider_arguments(opts) self.process_request_meta(opts) + self.process_request_cb_kwargs(opts) def process_spider_arguments(self, opts): - try: opts.spargs = arglist_to_dict(opts.spargs) except ValueError: raise UsageError("Invalid -a value, use -a NAME=VALUE", print_help=False) def process_request_meta(self, opts): - if opts.meta: try: opts.meta = json.loads(opts.meta) except ValueError: - raise UsageError("Invalid -m/--meta value, pass a valid json string to -m or --meta. " \ - "Example: --meta='{\"foo\" : \"bar\"}'", print_help=False) + raise UsageError("Invalid -m/--meta value, pass a valid json string to -m or --meta. " + "Example: --meta='{\"foo\" : \"bar\"}'", print_help=False) + def process_request_cb_kwargs(self, opts): + if opts.cb_kwargs: + try: + opts.cb_kwargs = json.loads(opts.cb_kwargs) + except ValueError: + raise UsageError("Invalid --cb_kwargs value, pass a valid json string to --cb_kwargs. " + "Example: --cb_kwargs='{\"foo\" : \"bar\"}'", print_help=False) def run(self, args, opts): # parse arguments diff --git a/tests/test_command_parse.py b/tests/test_command_parse.py index 02037b866..1404005fb 100644 --- a/tests/test_command_parse.py +++ b/tests/test_command_parse.py @@ -43,6 +43,12 @@ class MySpider(scrapy.Spider): else: self.logger.debug('It Works!') + def parse_request_with_cb_kwargs(self, response, foo=None, key=None): + if foo == 'bar' and key == 'value': + self.logger.debug('It Works!') + else: + self.logger.debug('It Does Not Work :(') + def parse_request_without_meta(self, response): foo = response.meta.get('foo', 'bar') @@ -120,6 +126,14 @@ ITEM_PIPELINES = {'%s.pipelines.MyPipeline': 1} self.url('/html')]) self.assertIn("DEBUG: It Works!", _textmode(stderr)) + @defer.inlineCallbacks + def test_request_with_cb_kwargs(self): + raw_json_string = '{"foo" : "bar", "key": "value"}' + _, _, stderr = yield self.execute(['--spider', self.spider_name, + '--cb_kwargs', raw_json_string, + '-c', 'parse_request_with_cb_kwargs', + self.url('/html')]) + self.assertIn("DEBUG: It Works!", _textmode(stderr)) @defer.inlineCallbacks def test_request_without_meta(self): From 8fb077694fcaa50a8625c8e2e8d0068add2b056d Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Thu, 28 Mar 2019 15:18:00 -0300 Subject: [PATCH 022/305] Request.cb_kwargs: Update docs --- docs/topics/debug.rst | 13 +++++-------- docs/topics/jobs.rst | 9 +++++---- docs/topics/leaks.rst | 14 ++++++++------ docs/topics/request-response.rst | 13 ++++++------- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/docs/topics/debug.rst b/docs/topics/debug.rst index f93aa2c72..0aaad0c77 100644 --- a/docs/topics/debug.rst +++ b/docs/topics/debug.rst @@ -28,16 +28,15 @@ Consider the following scrapy spider below:: item = MyItem() # populate `item` fields # and extract item_details_url - yield scrapy.Request(item_details_url, self.parse_details, meta={'item': item}) + yield scrapy.Request(item_details_url, self.parse_details, cb_kwargs={'item': item}) - def parse_details(self, response): - item = response.meta['item'] + def parse_details(self, response, item): # populate more `item` fields return item Basically this is a simple spider which parses two pages of items (the start_urls). Items also have a details page with additional information, so we -use the ``meta`` functionality of :class:`~scrapy.http.Request` to pass a +use the ``cb_kwargs`` functionality of :class:`~scrapy.http.Request` to pass a partially populated item. @@ -100,8 +99,7 @@ Fortunately, the :command:`shell` is your bread and butter in this case (see from scrapy.shell import inspect_response - def parse_details(self, response): - item = response.meta.get('item', None) + def parse_details(self, response, item=None): if item: # populate more `item` fields return item @@ -134,8 +132,7 @@ Logging is another useful option for getting information about your spider run. Although not as convenient, it comes with the advantage that the logs will be available in all future runs should they be necessary again:: - def parse_details(self, response): - item = response.meta.get('item', None) + def parse_details(self, response, item=None): if item: # populate more `item` fields return item diff --git a/docs/topics/jobs.rst b/docs/topics/jobs.rst index 1a5d52487..9fd311c69 100644 --- a/docs/topics/jobs.rst +++ b/docs/topics/jobs.rst @@ -81,7 +81,8 @@ So, for example, this won't work:: def some_callback(self, response): somearg = 'test' - return scrapy.Request('http://www.example.com', callback=lambda r: self.other_callback(r, somearg)) + return scrapy.Request('http://www.example.com', + callback=lambda r: self.other_callback(r, somearg)) def other_callback(self, response, somearg): print("the argument passed is: %s" % somearg) @@ -90,10 +91,10 @@ But this will:: def some_callback(self, response): somearg = 'test' - return scrapy.Request('http://www.example.com', callback=self.other_callback, meta={'somearg': somearg}) + return scrapy.Request('http://www.example.com', + callback=self.other_callback, cb_kwargs={'somearg': somearg}) - def other_callback(self, response): - somearg = response.meta['somearg'] + def other_callback(self, response, somearg): print("the argument passed is: %s" % somearg) If you wish to log the requests that couldn't be serialized, you can set the diff --git a/docs/topics/leaks.rst b/docs/topics/leaks.rst index af14d14e8..8278e9849 100644 --- a/docs/topics/leaks.rst +++ b/docs/topics/leaks.rst @@ -27,10 +27,11 @@ Common causes of memory leaks It happens quite often (sometimes by accident, sometimes on purpose) that the Scrapy developer passes objects referenced in Requests (for example, using the -:attr:`~scrapy.http.Request.meta` attribute or the request callback function) -and that effectively bounds the lifetime of those referenced objects to the -lifetime of the Request. This is, by far, the most common cause of memory leaks -in Scrapy projects, and a quite difficult one to debug for newcomers. +:attr:`~scrapy.http.Request.cb_kwargs` or :attr:`~scrapy.http.Request.meta` +attributes or the request callback function) and that effectively bounds the +lifetime of those referenced objects to the lifetime of the Request. This is, +by far, the most common cause of memory leaks in Scrapy projects, and a quite +difficult one to debug for newcomers. In big projects, the spiders are typically written by different people and some of those spiders could be "leaking" and thus affecting the rest of the other @@ -48,7 +49,8 @@ Too Many Requests? By default Scrapy keeps the request queue in memory; it includes :class:`~scrapy.http.Request` objects and all objects -referenced in Request attributes (e.g. in :attr:`~scrapy.http.Request.meta`). +referenced in Request attributes (e.g. in :attr:`~scrapy.http.Request.cb_kwargs` +and :attr:`~scrapy.http.Request.meta`). While not necessarily a leak, this can take a lot of memory. Enabling :ref:`persistent job queue ` could help keeping memory usage in control. @@ -101,7 +103,7 @@ Let's see a concrete example of a hypothetical case of memory leaks. Suppose we have some spider with a line similar to this one:: return Request("http://www.somenastyspider.com/product.php?pid=%d" % product_id, - callback=self.parse, meta={referer: response}) + callback=self.parse, cb_kwargs={'referer': response}) That line is passing a response reference inside a request which effectively ties the response lifetime to the requests' one, and that would definitely diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index 05ca8d6c1..f299c2cff 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -186,12 +186,12 @@ Request objects Return a new Request which is a copy of this Request. See also: :ref:`topics-request-response-ref-request-callback-arguments`. - .. method:: Request.replace([url, method, headers, body, cookies, meta, encoding, dont_filter, callback, errback]) + .. method:: Request.replace([url, method, headers, body, cookies, meta, flags, encoding, priority, dont_filter, callback, errback, cb_kwargs]) Return a Request object with the same members, except for those members given new values by whichever keyword arguments are specified. The - attribute :attr:`Request.meta` is copied by default (unless a new value - is given in the ``meta`` argument). See also + :attr:`Request.cb_kwargs` and :attr:`Request.meta` attributes are copied by default + (unless new values are given as arguments). See also :ref:`topics-request-response-ref-request-callback-arguments`. .. _topics-request-response-ref-request-callback-arguments: @@ -237,11 +237,10 @@ The following example shows how to achieve this by using the .. caution:: :attr:`Request.cb_kwargs` was introduced in version ``1.7``. Prior to that, :attr:`Request.meta` was the recommended option for passing - information around callbacks. However, after ``1.7`` :attr:`Request.cb_kwargs` + information around callbacks. However, after ``1.7``, using :attr:`Request.cb_kwargs` became the preferred way of passing user information, leaving :attr:`Request.meta` - to be used by internal components like spider or downloader middlewares. - The following example, which uses :attr:`Request.meta`, is only kept for historical - reasons. + to be populated by internal components like spider or downloader middlewares. + The following :attr:`Request.meta` example is only kept for historical reasons. :: From f5e0b6b89ace437af850e0225651329101a59862 Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Fri, 29 Mar 2019 14:03:26 -0300 Subject: [PATCH 023/305] parse command: rename cb_kwargs option to cbkwargs --- scrapy/commands/parse.py | 18 +++++++++--------- tests/test_command_parse.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scrapy/commands/parse.py b/scrapy/commands/parse.py index 2486f3f23..e948d6406 100644 --- a/scrapy/commands/parse.py +++ b/scrapy/commands/parse.py @@ -51,8 +51,8 @@ class Command(ScrapyCommand): help="use this callback for parsing, instead looking for a callback") parser.add_option("-m", "--meta", dest="meta", help="inject extra meta into the Request, it must be a valid raw json string") - parser.add_option("--cb_kwargs", dest="cb_kwargs", - help="inject extra cb_kwargs into the Request, it must be a valid raw json string") + parser.add_option("--cbkwargs", dest="cbkwargs", + help="inject extra cbkwargs into the Request, it must be a valid raw json string") parser.add_option("-d", "--depth", dest="depth", type="int", default=1, help="maximum depth for parsing requests [default: %default]") parser.add_option("-v", "--verbose", dest="verbose", action="store_true", @@ -212,9 +212,9 @@ class Command(ScrapyCommand): if opts.meta: request.meta.update(opts.meta) - # update cb_kwargs if any extra cb_kwargs was passed through the --cb_kwargs option. - if opts.cb_kwargs: - request.cb_kwargs.update(opts.cb_kwargs) + # update cb_kwargs if any extra values were was passed through the --cbkwargs option. + if opts.cbkwargs: + request.cb_kwargs.update(opts.cbkwargs) request.meta['_depth'] = 1 request.meta['_callback'] = request.callback @@ -243,12 +243,12 @@ class Command(ScrapyCommand): "Example: --meta='{\"foo\" : \"bar\"}'", print_help=False) def process_request_cb_kwargs(self, opts): - if opts.cb_kwargs: + if opts.cbkwargs: try: - opts.cb_kwargs = json.loads(opts.cb_kwargs) + opts.cbkwargs = json.loads(opts.cbkwargs) except ValueError: - raise UsageError("Invalid --cb_kwargs value, pass a valid json string to --cb_kwargs. " - "Example: --cb_kwargs='{\"foo\" : \"bar\"}'", print_help=False) + raise UsageError("Invalid --cbkwargs value, pass a valid json string to --cbkwargs. " + "Example: --cbkwargs='{\"foo\" : \"bar\"}'", print_help=False) def run(self, args, opts): # parse arguments diff --git a/tests/test_command_parse.py b/tests/test_command_parse.py index 1404005fb..c18a6ce9f 100644 --- a/tests/test_command_parse.py +++ b/tests/test_command_parse.py @@ -130,7 +130,7 @@ ITEM_PIPELINES = {'%s.pipelines.MyPipeline': 1} def test_request_with_cb_kwargs(self): raw_json_string = '{"foo" : "bar", "key": "value"}' _, _, stderr = yield self.execute(['--spider', self.spider_name, - '--cb_kwargs', raw_json_string, + '--cbkwargs', raw_json_string, '-c', 'parse_request_with_cb_kwargs', self.url('/html')]) self.assertIn("DEBUG: It Works!", _textmode(stderr)) From ccb56a317ee249978496e918ee5b74d83c2d7199 Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Fri, 29 Mar 2019 14:12:26 -0300 Subject: [PATCH 024/305] Update docs about cb_kwargs and meta --- docs/topics/request-response.rst | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index f299c2cff..4e81ce878 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -190,8 +190,8 @@ Request objects Return a Request object with the same members, except for those members given new values by whichever keyword arguments are specified. The - :attr:`Request.cb_kwargs` and :attr:`Request.meta` attributes are copied by default - (unless new values are given as arguments). See also + :attr:`Request.cb_kwargs` and :attr:`Request.meta` attributes are shallow + copied by default (unless new values are given as arguments). See also :ref:`topics-request-response-ref-request-callback-arguments`. .. _topics-request-response-ref-request-callback-arguments: @@ -220,7 +220,6 @@ The following example shows how to achieve this by using the :: - # pass information to the next callback using the Request.cb_kwargs attribute def parse(self, response): request = scrapy.Request('http://www.example.com/index.html', callback=self.parse_page2, @@ -236,27 +235,10 @@ The following example shows how to achieve this by using the ) .. caution:: :attr:`Request.cb_kwargs` was introduced in version ``1.7``. - Prior to that, :attr:`Request.meta` was the recommended option for passing - information around callbacks. However, after ``1.7``, using :attr:`Request.cb_kwargs` - became the preferred way of passing user information, leaving :attr:`Request.meta` - to be populated by internal components like spider or downloader middlewares. - The following :attr:`Request.meta` example is only kept for historical reasons. - -:: - - # pass information to the next callback using the Request.meta attribute - def parse_page1(self, response): - item = MyItem() - item['main_url'] = response.url - request = scrapy.Request("http://www.example.com/some_page.html", - callback=self.parse_page2) - request.meta['item'] = item - yield request - - def parse_page2(self, response): - item = response.meta['item'] - item['other_url'] = response.url - yield item + Prior to that, using :attr:`Request.meta` was recommended for passing + information around callbacks. After ``1.7``, :attr:`Request.cb_kwargs` + became the preferred way for handling user information, leaving :attr:`Request.meta` + for communication with components like middlewares and extensions. .. _topics-request-response-ref-errbacks: From 294ef51bb24782a0527892ea93bb4876daa7ca50 Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Fri, 29 Mar 2019 16:12:55 -0300 Subject: [PATCH 025/305] parse command: update docs about passing callback keyword arguments --- docs/topics/commands.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/commands.rst b/docs/topics/commands.rst index 6644d65e4..a93bee06b 100644 --- a/docs/topics/commands.rst +++ b/docs/topics/commands.rst @@ -461,8 +461,8 @@ Supported options: * ``--meta`` or ``-m``: additional request meta that will be passed to the callback request. This must be a valid json string. Example: --meta='{"foo" : "bar"}' -* ``--cb_kwargs``: additional keyword arguments that will be passed to the callback. - This must be a valid json string. Example: --cb_kwargs='{"foo" : "bar"}' +* ``--cbkwargs``: additional keyword arguments that will be passed to the callback. + This must be a valid json string. Example: --cbkwargs='{"foo" : "bar"}' * ``--pipelines``: process items through pipelines From 0522fe35c334141e90741644fec368cdbd12044e Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Fri, 29 Mar 2019 16:15:34 -0300 Subject: [PATCH 026/305] parse command: improve option description --- scrapy/commands/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/commands/parse.py b/scrapy/commands/parse.py index e948d6406..d4f2234b0 100644 --- a/scrapy/commands/parse.py +++ b/scrapy/commands/parse.py @@ -52,7 +52,7 @@ class Command(ScrapyCommand): parser.add_option("-m", "--meta", dest="meta", help="inject extra meta into the Request, it must be a valid raw json string") parser.add_option("--cbkwargs", dest="cbkwargs", - help="inject extra cbkwargs into the Request, it must be a valid raw json string") + help="inject extra callback kwargs into the Request, it must be a valid raw json string") parser.add_option("-d", "--depth", dest="depth", type="int", default=1, help="maximum depth for parsing requests [default: %default]") parser.add_option("-v", "--verbose", dest="verbose", action="store_true", From 07ff9248a5fd2eac4f53da92766dcb5a7ca48569 Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Mon, 1 Apr 2019 12:31:26 -0300 Subject: [PATCH 027/305] [Docs] CrawlSpider: add note about link text --- docs/topics/spiders.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/topics/spiders.rst b/docs/topics/spiders.rst index 8c4049f85..5417ef129 100644 --- a/docs/topics/spiders.rst +++ b/docs/topics/spiders.rst @@ -377,7 +377,10 @@ Crawling rules .. class:: Rule(link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=None) ``link_extractor`` is a :ref:`Link Extractor ` object which - defines how links will be extracted from each crawled page. + defines how links will be extracted from each crawled page. Each produced link will + be used to generate a :class:`~scrapy.http.Request` object, which will contain the + link's text in its ``meta`` attribute. + The link text can be accessed from the callback method though ``response.meta['link_text']`` ``callback`` is a callable or a string (in which case a method from the spider object with that name will be used) to be called for each link extracted with @@ -438,6 +441,7 @@ Let's now take a look at an example CrawlSpider with rules:: item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)') item['name'] = response.xpath('//td[@id="item_name"]/text()').get() item['description'] = response.xpath('//td[@id="item_description"]/text()').get() + item['link_text'] = response.meta['link_text'] return item From 8ebbc731b2bdf8e2a2b5a2f0673da838369f31b5 Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Mon, 1 Apr 2019 16:15:03 -0300 Subject: [PATCH 028/305] [Docs] Rephrase Rule docs --- docs/topics/spiders.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/topics/spiders.rst b/docs/topics/spiders.rst index 5417ef129..7290bb844 100644 --- a/docs/topics/spiders.rst +++ b/docs/topics/spiders.rst @@ -380,13 +380,13 @@ Crawling rules defines how links will be extracted from each crawled page. Each produced link will be used to generate a :class:`~scrapy.http.Request` object, which will contain the link's text in its ``meta`` attribute. - The link text can be accessed from the callback method though ``response.meta['link_text']`` ``callback`` is a callable or a string (in which case a method from the spider object with that name will be used) to be called for each link extracted with - the specified link_extractor. This callback receives a response as its first - argument and must return a list containing :class:`~scrapy.item.Item` and/or - :class:`~scrapy.http.Request` objects (or any subclass of them). + the specified link extractor. This callback receives a :class:`~scrapy.http.Response` + as its first argument and must return either a single instance or an iterable of + :class:`~scrapy.item.Item`, ``dict`` and/or :class:`~scrapy.http.Request` objects + (or any subclass of them). .. warning:: When writing crawl spider rules, avoid using ``parse`` as callback, since the :class:`CrawlSpider` uses the ``parse`` method From 7a38623cecc6c60d7ffe14c75d1fe679bb04b774 Mon Sep 17 00:00:00 2001 From: Eugenio Lacuesta Date: Mon, 1 Apr 2019 17:09:49 -0300 Subject: [PATCH 029/305] [Docs] Clarify comment about meta dictionary --- docs/topics/spiders.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/spiders.rst b/docs/topics/spiders.rst index 7290bb844..3cd051cdf 100644 --- a/docs/topics/spiders.rst +++ b/docs/topics/spiders.rst @@ -379,7 +379,7 @@ Crawling rules ``link_extractor`` is a :ref:`Link Extractor ` object which defines how links will be extracted from each crawled page. Each produced link will be used to generate a :class:`~scrapy.http.Request` object, which will contain the - link's text in its ``meta`` attribute. + link's text in its ``meta`` dictionary (under the ``link_text`` key). ``callback`` is a callable or a string (in which case a method from the spider object with that name will be used) to be called for each link extracted with From 611249bb7f3a7bb5a92a67d90d6c97f17494768f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Chaves?= Date: Wed, 8 May 2019 12:52:29 +0200 Subject: [PATCH 030/305] Implement the METAREFRESH_IGNORE_TAGS setting --- docs/topics/downloader-middleware.rst | 10 ++++++++++ scrapy/downloadermiddlewares/redirect.py | 4 +++- scrapy/settings/default_settings.py | 1 + scrapy/utils/response.py | 4 ++-- tests/test_downloadermiddleware_redirect.py | 19 +++++++++++++++++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index f2f3ef466..fa65f66ed 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -805,6 +805,7 @@ The :class:`MetaRefreshMiddleware` can be configured through the following settings (see the settings documentation for more info): * :setting:`METAREFRESH_ENABLED` +* :setting:`METAREFRESH_IGNORE_TAGS` * :setting:`METAREFRESH_MAXDELAY` This middleware obey :setting:`REDIRECT_MAX_TIMES` setting, :reqmeta:`dont_redirect`, @@ -826,6 +827,15 @@ Default: ``True`` Whether the Meta Refresh middleware will be enabled. +.. setting:: METAREFRESH_IGNORE_TAGS + +METAREFRESH_IGNORE_TAGS +^^^^^^^^^^^^^^^^^^^^^^^ + +Default: ``['script', 'noscript']`` + +Meta tags within these tags are ignored. + .. setting:: METAREFRESH_MAXDELAY METAREFRESH_MAXDELAY diff --git a/scrapy/downloadermiddlewares/redirect.py b/scrapy/downloadermiddlewares/redirect.py index cb59d3fd2..49468a2e4 100644 --- a/scrapy/downloadermiddlewares/redirect.py +++ b/scrapy/downloadermiddlewares/redirect.py @@ -88,6 +88,7 @@ class MetaRefreshMiddleware(BaseRedirectMiddleware): def __init__(self, settings): super(MetaRefreshMiddleware, self).__init__(settings) + self._ignore_tags = settings.getlist('METAREFRESH_IGNORE_TAGS') self._maxdelay = settings.getint('REDIRECT_MAX_METAREFRESH_DELAY', settings.getint('METAREFRESH_MAXDELAY')) @@ -96,7 +97,8 @@ class MetaRefreshMiddleware(BaseRedirectMiddleware): not isinstance(response, HtmlResponse): return response - interval, url = get_meta_refresh(response) + interval, url = get_meta_refresh(response, + ignore_tags=self._ignore_tags) if url and interval < self._maxdelay: redirected = self._redirect_request_using_get(request, url) return self._redirect(redirected, request, spider, 'meta refresh') diff --git a/scrapy/settings/default_settings.py b/scrapy/settings/default_settings.py index 9986827d8..1ce1516e5 100644 --- a/scrapy/settings/default_settings.py +++ b/scrapy/settings/default_settings.py @@ -221,6 +221,7 @@ MEMUSAGE_NOTIFY_MAIL = [] MEMUSAGE_WARNING_MB = 0 METAREFRESH_ENABLED = True +METAREFRESH_IGNORE_TAGS = ['script', 'noscript'] METAREFRESH_MAXDELAY = 100 NEWSPIDER_MODULE = '' diff --git a/scrapy/utils/response.py b/scrapy/utils/response.py index bf276b5ca..122af28b0 100644 --- a/scrapy/utils/response.py +++ b/scrapy/utils/response.py @@ -31,12 +31,12 @@ def get_base_url(response): _metaref_cache = weakref.WeakKeyDictionary() -def get_meta_refresh(response): +def get_meta_refresh(response, ignore_tags=('script', 'noscript')): """Parse the http-equiv refrsh parameter from the given response""" if response not in _metaref_cache: text = response.text[0:4096] _metaref_cache[response] = html.get_meta_refresh(text, response.url, - response.encoding, ignore_tags=('script', 'noscript')) + response.encoding, ignore_tags=ignore_tags) return _metaref_cache[response] diff --git a/tests/test_downloadermiddleware_redirect.py b/tests/test_downloadermiddleware_redirect.py index 6c81c94ca..0e841489d 100644 --- a/tests/test_downloadermiddleware_redirect.py +++ b/tests/test_downloadermiddleware_redirect.py @@ -279,5 +279,24 @@ class MetaRefreshMiddlewareTest(unittest.TestCase): self.assertEqual(req2.meta['redirect_reasons'], ['meta refresh']) self.assertEqual(req3.meta['redirect_reasons'], ['meta refresh', 'meta refresh']) + def test_ignore_tags_default(self): + req = Request(url='http://example.org') + body = ('''''') + rsp = HtmlResponse(req.url, body=body.encode()) + response = self.mw.process_response(req, rsp, self.spider) + assert isinstance(response, Response) + + def test_ignore_tags_empty_list(self): + crawler = get_crawler(Spider, {'METAREFRESH_IGNORE_TAGS': []}) + mw = MetaRefreshMiddleware.from_crawler(crawler) + req = Request(url='http://example.org') + body = ('''''') + rsp = HtmlResponse(req.url, body=body.encode()) + req2 = mw.process_response(req, rsp, self.spider) + assert isinstance(req2, Request) + self.assertEqual(req2.url, 'http://example.org/newpage') + if __name__ == "__main__": unittest.main() From 461682fc3dca72d9a34ddc22ad1896787c9dc518 Mon Sep 17 00:00:00 2001 From: Claudio Salazar Date: Sat, 25 May 2019 11:01:19 +0200 Subject: [PATCH 031/305] Whitelist form methods in FormRequest.from_response method --- scrapy/http/request/form.py | 7 ++++++- tests/test_http_request.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/scrapy/http/request/form.py b/scrapy/http/request/form.py index c2413b431..2182b9b53 100644 --- a/scrapy/http/request/form.py +++ b/scrapy/http/request/form.py @@ -18,6 +18,7 @@ from scrapy.utils.response import get_base_url class FormRequest(Request): + valid_form_methods = ['GET', 'POST', 'DIALOG'] def __init__(self, *args, **kwargs): formdata = kwargs.pop('formdata', None) @@ -48,7 +49,11 @@ class FormRequest(Request): form = _get_form(response, formname, formid, formnumber, formxpath) formdata = _get_inputs(form, formdata, dont_click, clickdata, response) url = _get_form_url(form, kwargs.pop('url', None)) - method = kwargs.pop('method', form.method) + + method = kwargs.pop('method', form.method).upper() + if method not in cls.valid_form_methods: + raise ValueError('Invalid form method in chosen form') + return cls(url=url, method=method, formdata=formdata, **kwargs) diff --git a/tests/test_http_request.py b/tests/test_http_request.py index 49f148016..8fdafb286 100644 --- a/tests/test_http_request.py +++ b/tests/test_http_request.py @@ -1100,6 +1100,19 @@ class FormRequestTest(RequestTest): self.assertRaises(ValueError, self.request_class.from_response, response, formcss="input[name='abc']") + def test_from_response_valid_form_methods(self): + body = """
+ +
""" + + for method in self.request_class.valid_form_methods: + response = _buildresponse(body % method) + r1 = self.request_class.from_response(response) + self.assertEqual(r1.method, method) + + response = _buildresponse(body % 'UNKNOWN') + self.assertRaises(ValueError, self.request_class.from_response, response) + def _buildresponse(body, **kwargs): kwargs.setdefault('body', body) From da82ede8a0751cf3e8496f35252eb0dcef4f197e Mon Sep 17 00:00:00 2001 From: Anubhav Patel Date: Sat, 25 May 2019 17:19:10 +0530 Subject: [PATCH 032/305] describe method as a command --- docs/topics/downloader-middleware.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index dfbcdb8fa..236150059 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -526,7 +526,7 @@ defines the methods described below. .. method:: retrieve_response(spider, request) - Returns response if present in cache, or ``None`` otherwise. + Return response if present in cache, or ``None`` otherwise. :param spider: the spider which generated the request :type spider: :class:`~scrapy.spiders.Spider` object @@ -536,7 +536,7 @@ defines the methods described below. .. method:: store_response(spider, request, response) - Stores the given response in the cache. + Store the given response in the cache. :param spider: the spider for which the response is intended :type spider: :class:`~scrapy.spiders.Spider` object From 0c50879568dee2363df5cbe25e9bdd7adaed5da4 Mon Sep 17 00:00:00 2001 From: Claudio Salazar Date: Thu, 6 Jun 2019 22:10:59 +0200 Subject: [PATCH 033/305] Change behavior to use method GET when there are unknown methods in the form --- scrapy/http/request/form.py | 2 +- tests/test_http_request.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scrapy/http/request/form.py b/scrapy/http/request/form.py index 2182b9b53..8b29aae4b 100644 --- a/scrapy/http/request/form.py +++ b/scrapy/http/request/form.py @@ -52,7 +52,7 @@ class FormRequest(Request): method = kwargs.pop('method', form.method).upper() if method not in cls.valid_form_methods: - raise ValueError('Invalid form method in chosen form') + method = 'GET' return cls(url=url, method=method, formdata=formdata, **kwargs) diff --git a/tests/test_http_request.py b/tests/test_http_request.py index 8fdafb286..258b48dce 100644 --- a/tests/test_http_request.py +++ b/tests/test_http_request.py @@ -1107,11 +1107,12 @@ class FormRequestTest(RequestTest): for method in self.request_class.valid_form_methods: response = _buildresponse(body % method) - r1 = self.request_class.from_response(response) - self.assertEqual(r1.method, method) + r = self.request_class.from_response(response) + self.assertEqual(r.method, method) response = _buildresponse(body % 'UNKNOWN') - self.assertRaises(ValueError, self.request_class.from_response, response) + r = self.request_class.from_response(response) + self.assertEqual(r.method, 'GET') def _buildresponse(body, **kwargs): From 8d1e0e09bb6fdeb4f1348b408a268c92dc9e7a8f Mon Sep 17 00:00:00 2001 From: Mabel Villalba Date: Thu, 20 Jun 2019 10:06:06 +0200 Subject: [PATCH 034/305] [itemloader-errors] added error message in get_value --- scrapy/loader/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scrapy/loader/__init__.py b/scrapy/loader/__init__.py index 20f0f90c3..5055de015 100644 --- a/scrapy/loader/__init__.py +++ b/scrapy/loader/__init__.py @@ -106,11 +106,17 @@ class ItemLoader(object): value = arg_to_iter(value) value = flatten(extract_regex(regex, x) for x in value) - for proc in processors: + for _proc in processors: if value is None: break - proc = wrap_loader_context(proc, self.context) - value = proc(value) + proc = wrap_loader_context(_proc, self.context) + try: + value = proc(value) + except Exception as e: + raise ValueError( + "Error with processor %s value=%r error='%s: %s'" % + (_proc.__class__.__name__, value, type(e).__name__, + str(e))) return value def load_item(self): From 663352b2a5250c377bbbe2d53c5d5b7da3a1836a Mon Sep 17 00:00:00 2001 From: Mabel Villalba Date: Thu, 20 Jun 2019 10:10:16 +0200 Subject: [PATCH 035/305] [itemloader-errors] added error message to _process_input_value --- scrapy/loader/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scrapy/loader/__init__.py b/scrapy/loader/__init__.py index 5055de015..fc8b10bee 100644 --- a/scrapy/loader/__init__.py +++ b/scrapy/loader/__init__.py @@ -155,9 +155,15 @@ class ItemLoader(object): return proc def _process_input_value(self, field_name, value): - proc = self.get_input_processor(field_name) - proc = wrap_loader_context(proc, self.context) - return proc(value) + _proc = self.get_input_processor(field_name) + proc = wrap_loader_context(_proc, self.context) + try: + return proc(value) + except Exception as e: + raise ValueError( + "Error with inputput processor %s: field=%r value=%r " + "error='%s: %s'" % (_proc.__class__.__name__, field_name, + value, type(e).__name__, str(e))) def _get_item_field_attr(self, field_name, key, default=None): if isinstance(self.item, Item): From 8a3b15eb91169ab262e4dca60105f56467ecd1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Chaves?= Date: Wed, 27 Mar 2019 08:50:33 +0100 Subject: [PATCH 036/305] Document how to select dynamically-loaded content --- docs/index.rst | 4 + docs/topics/dynamic-content.rst | 246 ++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 docs/topics/dynamic-content.rst diff --git a/docs/index.rst b/docs/index.rst index cedde8f38..6d5f9e77d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -158,6 +158,7 @@ Solving specific problems topics/practices topics/broad-crawls topics/developer-tools + topics/dynamic-content topics/leaks topics/media-pipeline topics/deploy @@ -183,6 +184,9 @@ Solving specific problems :doc:`topics/developer-tools` Learn how to scrape with your browser's developer tools. +:doc:`topics/dynamic-content` + Read webpage data that is loaded dynamically. + :doc:`topics/leaks` Learn how to find and get rid of memory leaks in your crawler. diff --git a/docs/topics/dynamic-content.rst b/docs/topics/dynamic-content.rst new file mode 100644 index 000000000..8b5dacf56 --- /dev/null +++ b/docs/topics/dynamic-content.rst @@ -0,0 +1,246 @@ +.. _topics-dynamic-content: + +==================================== +Selecting dynamically-loaded content +==================================== + +Some webpages show the desired data when you load them in a web browser. +However, when you download them using Scrapy, you cannot reach the desired data +using :ref:`selectors `. + +When this happens, the recommended approach is to +:ref:`find the data source ` and extract the data +from it. + +If you fail to do that, and you can nonetheless access the desired data through +the :ref:`DOM ` from your web browser, see +:ref:`topics-javascript-rendering`. + +.. _topics-finding-data-source: + +Finding the data source +======================= + +To extract the desired data, you must first find its source location. + +If the data is in a non-text-based format, such as an image or a PDF document, +use the :ref:`network tool ` of your web browser to find +the corresponding request, and :ref:`reproduce it +`. + +If your web browser lets you select the desired data as text, the data may be +defined in embedded JavaScript code, or loaded from an external resource in a +text-based format. + +In that case, you can use a tool like wgrep_ to find the URL of that resource. + +If the data turns out to come from the original URL itself, you must +:ref:`inspect the source code of the webpage ` to +determine where the data is located. + +If the data comes from a different URL, you will need to :ref:`reproduce the +corresponding request `. + +.. _topics-inspecting-source: + +Inspecting the source code of a webpage +======================================= + +Sometimes you need to inspect the source code of a webpage (not the +:ref:`DOM `) to determine where some desired data is located. + +Use Scrapy’s :command:`fetch` command to download the webpage contents as seen +by Scrapy:: + + scrapy fetch --nolog https://example.com > response.html + +If the desired data is in embedded JavaScript code within a ``