From a2dd5e5ad9d647c5971c416bc9a610a8d8639c42 Mon Sep 17 00:00:00 2001 From: Alexander Chekunkov Date: Fri, 11 Jul 2014 16:21:57 +0300 Subject: [PATCH 0001/2974] Added referer to "Spider error processing" log message + fixed some pep8 issues --- scrapy/core/scraper.py | 47 +++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/scrapy/core/scraper.py b/scrapy/core/scraper.py index e5120ec0d..3409a0e7c 100644 --- a/scrapy/core/scraper.py +++ b/scrapy/core/scraper.py @@ -57,6 +57,7 @@ class Slot(object): def needs_backout(self): return self.active_size > self.max_active_size + class Scraper(object): def __init__(self, crawler): @@ -100,8 +101,8 @@ class Scraper(object): self._scrape_next(spider, slot) return _ dfd.addBoth(finish_scraping) - dfd.addErrback(log.err, 'Scraper bug processing %s' % request, \ - spider=spider) + dfd.addErrback( + log.err, 'Scraper bug processing %s' % request, spider=spider) self._scrape_next(spider, slot) return dfd @@ -124,13 +125,13 @@ class Scraper(object): """Handle the different cases of request's result been a Response or a Failure""" if not isinstance(request_result, Failure): - return self.spidermw.scrape_response(self.call_spider, \ - request_result, request, spider) + return self.spidermw.scrape_response( + self.call_spider, request_result, request, spider) else: # FIXME: don't ignore errors in spider middleware dfd = self.call_spider(request_result, request, spider) - return dfd.addErrback(self._log_download_errors, \ - request_result, request, spider) + return dfd.addErrback( + self._log_download_errors, request_result, request, spider) def call_spider(self, result, request, spider): result.request = request @@ -143,11 +144,21 @@ class Scraper(object): if isinstance(exc, CloseSpider): self.crawler.engine.close_spider(spider, exc.reason or 'cancelled') return - log.err(_failure, "Spider error processing %s" % request, spider=spider) - self.signals.send_catch_log(signal=signals.spider_error, failure=_failure, response=response, \ - spider=spider) - self.crawler.stats.inc_value("spider_exceptions/%s" % _failure.value.__class__.__name__, \ - spider=spider) + referer = request.headers.get('Referer') + log.err( + _failure, + "Spider error processing %s (referer: %s)" % (request, referer), + spider=spider + ) + self.signals.send_catch_log( + signal=signals.spider_error, + failure=_failure, response=response, + spider=spider + ) + self.crawler.stats.inc_value( + "spider_exceptions/%s" % _failure.value.__class__.__name__, + spider=spider + ) def handle_spider_output(self, result, request, response, spider): if not result: @@ -180,8 +191,8 @@ class Scraper(object): """Log and silence errors that come from the engine (typically download errors that got propagated thru here) """ - if isinstance(download_failure, Failure) \ - and not download_failure.check(IgnoreRequest): + if (isinstance(download_failure, Failure) and + not download_failure.check(IgnoreRequest)): if download_failure.frames: log.err(download_failure, 'Error downloading %s' % request, spider=spider) @@ -204,13 +215,15 @@ class Scraper(object): if isinstance(ex, DropItem): logkws = self.logformatter.dropped(item, ex, response, spider) log.msg(spider=spider, **logkws) - return self.signals.send_catch_log_deferred(signal=signals.item_dropped, \ - item=item, response=response, spider=spider, exception=output.value) + return self.signals.send_catch_log_deferred( + signal=signals.item_dropped, item=item, response=response, + spider=spider, exception=output.value) else: log.err(output, 'Error processing %s' % item, spider=spider) else: logkws = self.logformatter.scraped(output, response, spider) log.msg(spider=spider, **logkws) - return self.signals.send_catch_log_deferred(signal=signals.item_scraped, \ - item=output, response=response, spider=spider) + return self.signals.send_catch_log_deferred( + signal=signals.item_scraped, item=output, response=response, + spider=spider) From 84fa004793cb1be07c7a3d0ac6fd80a83b4e8487 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Mon, 30 Jun 2014 01:35:58 -0300 Subject: [PATCH 0002/2974] Add from_crawler class method to base Spider --- docs/topics/spiders.rst | 38 ++++++++++++++++++++++++++++++++++++++ scrapy/spider.py | 29 +++++++++++++++++++---------- tests/test_spider.py | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/docs/topics/spiders.rst b/docs/topics/spiders.rst index 73c34e75f..de8f988c0 100644 --- a/docs/topics/spiders.rst +++ b/docs/topics/spiders.rst @@ -133,6 +133,44 @@ Spider listed here. The subsequent URLs will be generated successively from data contained in the start URLs. + .. attribute:: crawler + + This attribute is set by the :meth:`from_crawler` class method after + initializating the class, and links to the + :class:`~scrapy.crawler.Crawler` object to which this spider instance is + bound. + + Crawlers encapsulate a lot of components in the project for their single + entry access (such as extensions, middlewares, signals managers, etc). + See :ref:`topics-api-crawler` to know more about them. + + .. attribute:: settings + + Configuration on which this spider is been ran. This is a + :class:`~scrapy.settings.Settings` instance, see the + :ref:`topics-settings` topic for a detailed introduction on this subject. + + .. method:: from_crawler(crawler, \*args, \**kwargs) + + This is the class method used by Scrapy to create your spiders. + + You probably won't need to override this directly, since the default + implementation acts as a proxy to the :meth:`__init__` method, calling + it with the given arguments `args` and named arguments `kwargs`. + + Nonetheless, this method sets the :attr:`crawler` and :attr:`settings` + attributes in the new instance, so they can be accessed later inside the + spider's code. + + :param crawler: crawler to which the spider will be bound + :type crawler: :class:`~scrapy.crawler.Crawler` instance + + :param args: arguments passed to the :meth:`__init__` method + :type args: list + + :param kwargs: keyword arguments passed to the :meth:`__init__` method + :type kwargs: dict + .. method:: start_requests() This method must return an iterable with the first Requests to crawl for diff --git a/scrapy/spider.py b/scrapy/spider.py index 8ecfae2a0..89f78d6ba 100644 --- a/scrapy/spider.py +++ b/scrapy/spider.py @@ -3,11 +3,14 @@ Base class for Scrapy spiders See documentation in docs/topics/spiders.rst """ +import warnings + from scrapy import log from scrapy.http import Request from scrapy.utils.trackref import object_ref from scrapy.utils.url import url_is_from_spider from scrapy.utils.deprecate import create_deprecated_class +from scrapy.exceptions import ScrapyDeprecationWarning class Spider(object_ref): @@ -32,18 +35,24 @@ class Spider(object_ref): """ log.msg(message, spider=self, level=level, **kw) + @classmethod + def from_crawler(cls, crawler, *args, **kwargs): + spider = cls(*args, **kwargs) + spider._set_crawler(crawler) + return spider + def set_crawler(self, crawler): - assert not hasattr(self, '_crawler'), "Spider already bounded to %s" % crawler - self._crawler = crawler + warnings.warn("set_crawler is deprecated, instantiate and bound the " + "spider to this crawler with from_crawler method " + "instead.", + category=ScrapyDeprecationWarning, stacklevel=2) + assert not hasattr(self, 'crawler'), "Spider already bounded to a " \ + "crawler" + self._set_crawler(crawler) - @property - def crawler(self): - assert hasattr(self, '_crawler'), "Spider not bounded to any crawler" - return self._crawler - - @property - def settings(self): - return self.crawler.settings + def _set_crawler(self, crawler): + self.crawler = crawler + self.settings = crawler.settings def start_requests(self): for url in self.start_urls: diff --git a/tests/test_spider.py b/tests/test_spider.py index 903eff7b1..53daf39fb 100644 --- a/tests/test_spider.py +++ b/tests/test_spider.py @@ -1,10 +1,12 @@ import gzip import inspect import warnings -from scrapy.utils.trackref import object_ref from io import BytesIO - from twisted.trial import unittest +try: + from unittest import mock +except ImportError: + import mock from scrapy.spider import Spider, BaseSpider from scrapy.http import Request, Response, TextResponse, XmlResponse, HtmlResponse @@ -13,6 +15,8 @@ from scrapy.contrib.spiders import CrawlSpider, Rule, XMLFeedSpider, \ CSVFeedSpider, SitemapSpider from scrapy.contrib.linkextractors import LinkExtractor from scrapy.exceptions import ScrapyDeprecationWarning +from scrapy.utils.trackref import object_ref +from scrapy.utils.test import get_crawler class SpiderTest(unittest.TestCase): @@ -46,6 +50,32 @@ class SpiderTest(unittest.TestCase): self.assertRaises(ValueError, self.spider_class) self.assertRaises(ValueError, self.spider_class, somearg='foo') + def test_deprecated_set_crawler_method(self): + spider = self.spider_class('example.com') + crawler = get_crawler() + with warnings.catch_warnings(record=True) as w: + spider.set_crawler(crawler) + self.assertIn("set_crawler", str(w[0].message)) + self.assertTrue(hasattr(spider, 'crawler')) + self.assertIs(spider.crawler, crawler) + self.assertTrue(hasattr(spider, 'settings')) + self.assertIs(spider.settings, crawler.settings) + + def test_from_crawler_crawler_and_settings_population(self): + crawler = get_crawler() + spider = self.spider_class.from_crawler(crawler, 'example.com') + self.assertTrue(hasattr(spider, 'crawler')) + self.assertIs(spider.crawler, crawler) + self.assertTrue(hasattr(spider, 'settings')) + self.assertIs(spider.settings, crawler.settings) + + def test_from_crawler_init_call(self): + with mock.patch.object(self.spider_class, '__init__', + return_value=None) as mock_init: + self.spider_class.from_crawler(get_crawler(), 'example.com', + foo='bar') + mock_init.assert_called_once_with('example.com', foo='bar') + class InitSpiderTest(SpiderTest): From eb0253e5301ea54d4f37da8974d0dac295ebe871 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Mon, 30 Jun 2014 03:20:05 -0300 Subject: [PATCH 0003/2974] Update from_crawler method as well as set_crawler on CrawlSpider --- scrapy/contrib/spiders/crawl.py | 7 +++++++ tests/test_spider.py | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/scrapy/contrib/spiders/crawl.py b/scrapy/contrib/spiders/crawl.py index d7e42f6f8..7dc3dacd6 100644 --- a/scrapy/contrib/spiders/crawl.py +++ b/scrapy/contrib/spiders/crawl.py @@ -86,6 +86,13 @@ class CrawlSpider(Spider): rule.process_links = get_method(rule.process_links) rule.process_request = get_method(rule.process_request) + @classmethod + def from_crawler(cls, crawler, *args, **kwargs): + spider = super(CrawlSpider, cls).from_crawler(crawler, *args, **kwargs) + spider._follow_links = crawler.settings.getbool( + 'CRAWLSPIDER_FOLLOW_LINKS', True) + return spider + def set_crawler(self, crawler): super(CrawlSpider, self).set_crawler(crawler) self._follow_links = crawler.settings.getbool('CRAWLSPIDER_FOLLOW_LINKS', True) diff --git a/tests/test_spider.py b/tests/test_spider.py index 53daf39fb..188bef6e4 100644 --- a/tests/test_spider.py +++ b/tests/test_spider.py @@ -220,6 +220,30 @@ class CrawlSpiderTest(SpiderTest): 'http://example.org/about.html', 'http://example.org/nofollow.html']) + def test_follow_links_attribute_population(self): + crawler = get_crawler() + spider = self.spider_class.from_crawler(crawler, 'example.com') + self.assertTrue(hasattr(spider, '_follow_links')) + self.assertTrue(spider._follow_links) + + crawler.settings.set('CRAWLSPIDER_FOLLOW_LINKS', False) + spider = self.spider_class.from_crawler(crawler, 'example.com') + self.assertTrue(hasattr(spider, '_follow_links')) + self.assertFalse(spider._follow_links) + + def test_follow_links_attribute_deprecated_population(self): + spider = self.spider_class('example.com') + self.assertFalse(hasattr(spider, '_follow_links')) + + spider.set_crawler(get_crawler()) + self.assertTrue(hasattr(spider, '_follow_links')) + self.assertTrue(spider._follow_links) + + spider = self.spider_class('example.com') + spider.set_crawler(get_crawler({'CRAWLSPIDER_FOLLOW_LINKS': False})) + self.assertTrue(hasattr(spider, '_follow_links')) + self.assertFalse(spider._follow_links) + class SitemapSpiderTest(SpiderTest): From a995727117d10133a20553a648e85970fc6a6543 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 17 Jul 2014 10:49:15 -0300 Subject: [PATCH 0004/2974] Connect spider_closed signal after a crawler is bound to a Spider --- scrapy/spider.py | 8 ++++++++ tests/test_spider.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/scrapy/spider.py b/scrapy/spider.py index 89f78d6ba..df367b700 100644 --- a/scrapy/spider.py +++ b/scrapy/spider.py @@ -6,6 +6,7 @@ See documentation in docs/topics/spiders.rst import warnings from scrapy import log +from scrapy import signals from scrapy.http import Request from scrapy.utils.trackref import object_ref from scrapy.utils.url import url_is_from_spider @@ -53,6 +54,7 @@ class Spider(object_ref): def _set_crawler(self, crawler): self.crawler = crawler self.settings = crawler.settings + crawler.signals.connect(self.close, signals.spider_closed) def start_requests(self): for url in self.start_urls: @@ -68,6 +70,12 @@ class Spider(object_ref): def handles_request(cls, request): return url_is_from_spider(request.url, cls) + @staticmethod + def close(spider, reason): + closed = getattr(spider, 'closed', None) + if callable(closed): + return closed(reason) + def __str__(self): return "<%s %r at 0x%0x>" % (type(self).__name__, self.name, id(self)) diff --git a/tests/test_spider.py b/tests/test_spider.py index 188bef6e4..903ea684a 100644 --- a/tests/test_spider.py +++ b/tests/test_spider.py @@ -8,6 +8,7 @@ try: except ImportError: import mock +from scrapy import signals from scrapy.spider import Spider, BaseSpider from scrapy.http import Request, Response, TextResponse, XmlResponse, HtmlResponse from scrapy.contrib.spiders.init import InitSpider @@ -76,6 +77,21 @@ class SpiderTest(unittest.TestCase): foo='bar') mock_init.assert_called_once_with('example.com', foo='bar') + def test_closed_signal_call(self): + class TestSpider(self.spider_class): + closed_called = False + + def closed(self, reason): + self.closed_called = True + + crawler = get_crawler() + spider = TestSpider.from_crawler(crawler, 'example.com') + crawler.signals.send_catch_log(signal=signals.spider_opened, + spider=spider) + crawler.signals.send_catch_log(signal=signals.spider_closed, + spider=spider, reason=None) + self.assertTrue(spider.closed_called) + class InitSpiderTest(SpiderTest): From 3ae971468ff3a6712aa47ad3a5d2b9f0c9663b60 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Tue, 29 Jul 2014 18:47:49 -0300 Subject: [PATCH 0005/2974] Add Settings.copy, freeze and frozencopy method --- docs/topics/api.rst | 24 ++++++++++++++++++++++ scrapy/settings/__init__.py | 16 +++++++++++++++ tests/test_settings/__init__.py | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 341340c2a..16bfe5f8f 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -264,6 +264,30 @@ Settings API :param default: the value to return if no setting is found :type default: any + .. method:: copy() + + Make a deep copy of current settings. + + This method returns a new instance of the :class:`Settings` class, + populated with the same values and their priorities. + + Modifications to the new object won't be reflected on the original + settings. + + .. method:: freeze() + + Disable further changes to the current settings. + + After calling this method, the present state of the settings will become + immutable. Trying to change values through the :meth:`~set` method and + its variants won't be possible and will be alerted. + + .. method:: frozencopy() + + Return an immutable copy of the current settings. + + Alias for a :meth:`~freeze` call in the object returned by :meth:`copy` + .. _topics-api-signals: Signals API diff --git a/scrapy/settings/__init__.py b/scrapy/settings/__init__.py index 2dd6a2920..978174694 100644 --- a/scrapy/settings/__init__.py +++ b/scrapy/settings/__init__.py @@ -1,5 +1,6 @@ import six import json +import copy import warnings from collections import MutableMapping from importlib import import_module @@ -46,6 +47,7 @@ class SettingsAttribute(object): class Settings(object): def __init__(self, values=None, priority='project'): + self.frozen = False self.attributes = {} self.setmodule(default_settings, priority='default') if values is not None: @@ -93,6 +95,7 @@ class Settings(object): raise ValueError("Cannot convert value for setting '%s' to dict: '%s'" % (name, value)) def set(self, name, value, priority='project'): + assert not self.frozen, "Trying to modify an immutable Settings object" if isinstance(priority, six.string_types): priority = SETTINGS_PRIORITIES[priority] if name not in self.attributes: @@ -101,16 +104,29 @@ class Settings(object): self.attributes[name].set(value, priority) def setdict(self, values, priority='project'): + assert not self.frozen, "Trying to modify an immutable Settings object" for name, value in six.iteritems(values): self.set(name, value, priority) def setmodule(self, module, priority='project'): + assert not self.frozen, "Trying to modify an immutable Settings object" if isinstance(module, six.string_types): module = import_module(module) for key in dir(module): if key.isupper(): self.set(key, getattr(module, key), priority) + def copy(self): + return copy.deepcopy(self) + + def freeze(self): + self.frozen = True + + def frozencopy(self): + copy = self.copy() + copy.freeze() + return copy + @property def overrides(self): warnings.warn("`Settings.overrides` attribute is deprecated and won't " diff --git a/tests/test_settings/__init__.py b/tests/test_settings/__init__.py index 39e47dec6..c7e0914d6 100644 --- a/tests/test_settings/__init__.py +++ b/tests/test_settings/__init__.py @@ -190,6 +190,42 @@ class SettingsTest(unittest.TestCase): self.assertEqual(settings.getdict('TEST_DICT3', {'key1': 5}), {'key1': 5}) self.assertRaises(ValueError, settings.getdict, 'TEST_LIST1') + def test_copy(self): + values = { + 'TEST_BOOL': True, + 'TEST_LIST': ['one', 'two'], + 'TEST_LIST_OF_LISTS': [['first_one', 'first_two'], + ['second_one', 'second_two']] + } + self.settings.setdict(values) + copy = self.settings.copy() + self.settings.set('TEST_BOOL', False) + self.assertTrue(copy.get('TEST_BOOL')) + + test_list = self.settings.get('TEST_LIST') + test_list.append('three') + self.assertListEqual(copy.get('TEST_LIST'), ['one', 'two']) + + test_list_of_lists = self.settings.get('TEST_LIST_OF_LISTS') + test_list_of_lists[0].append('first_three') + self.assertListEqual(copy.get('TEST_LIST_OF_LISTS')[0], + ['first_one', 'first_two']) + + def test_freeze(self): + self.settings.freeze() + with self.assertRaises(AssertionError) as cm: + self.settings.set('TEST_BOOL', False) + self.assertEqual(str(cm.exception), + "Trying to modify an immutable Settings object") + + def test_frozencopy(self): + with mock.patch.object(self.settings, 'copy') as mock_copy: + with mock.patch.object(mock_copy, 'freeze') as mock_freeze: + mock_object = self.settings.frozencopy() + mock_copy.assert_call_once() + mock_freeze.assert_call_once() + self.assertEqual(mock_object, mock_copy.return_value) + def test_deprecated_attribute_overrides(self): self.settings.set('BAR', 'fuz', priority='cmdline') with warnings.catch_warnings(record=True) as w: From 39c6a80f9db6ec04cac59f116ee9620c3d540be0 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Fri, 1 Aug 2014 00:42:25 -0300 Subject: [PATCH 0006/2974] Both getdict and getlist return copies of the requested values --- docs/topics/api.rst | 16 ++++++++++++++-- scrapy/settings/__init__.py | 19 ++++++------------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 16bfe5f8f..6e636e826 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -252,8 +252,8 @@ Settings API .. method:: getlist(name, default=None) - Get a setting value as a list. If the setting original type is a list it - will be returned verbatim. If it's a string it will be split by ",". + Get a setting value as a list. If the setting original type is a list, a + copy of it will be returned. If it's a string it will be split by ",". For example, settings populated through environment variables set to ``'one,two'`` will return a list ['one', 'two'] when using this method. @@ -264,6 +264,18 @@ Settings API :param default: the value to return if no setting is found :type default: any + .. method:: getdict(name, default=None) + + Get a setting value as a dictionary. If the setting original type is a + dictionary, a copy of it will be returned. If it's a string it will + evaluated as a json dictionary. + + :param name: the setting name + :type name: string + + :param default: the value to return if no setting is found + :type default: any + .. method:: copy() Make a deep copy of current settings. diff --git a/scrapy/settings/__init__.py b/scrapy/settings/__init__.py index 978174694..bbe8ef481 100644 --- a/scrapy/settings/__init__.py +++ b/scrapy/settings/__init__.py @@ -76,23 +76,16 @@ class Settings(object): return float(self.get(name, default)) def getlist(self, name, default=None): - value = self.get(name) - if value is None: - return default or [] - elif hasattr(value, '__iter__'): - return value - else: - return str(value).split(',') + value = self.get(name, default or []) + if isinstance(value, six.string_types): + value = value.split(',') + return list(value) def getdict(self, name, default=None): - value = self.get(name) - if value is None: - return default or {} + value = self.get(name, default or {}) if isinstance(value, six.string_types): value = json.loads(value) - if isinstance(value, dict): - return value - raise ValueError("Cannot convert value for setting '%s' to dict: '%s'" % (name, value)) + return dict(value) def set(self, name, value, priority='project'): assert not self.frozen, "Trying to modify an immutable Settings object" From d7038b2a136a2b79df9fb16d7b3327cb29f9c46f Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 17 Jul 2014 10:25:07 -0300 Subject: [PATCH 0007/2974] SpiderManager interface cleanup --- docs/topics/api.rst | 51 ++++++++++++++++++- docs/topics/settings.rst | 10 ++++ scrapy/interfaces.py | 10 ++-- scrapy/spidermanager.py | 31 +++-------- tests/test_spidermanager/__init__.py | 27 +++++----- .../test_spiders/spider4.py | 10 ---- 6 files changed, 85 insertions(+), 54 deletions(-) delete mode 100644 tests/test_spidermanager/test_spiders/spider4.py diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 6e636e826..348305fb7 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -77,8 +77,7 @@ how you :ref:`configure the downloader middlewares .. attribute:: spiders - The spider manager which takes care of loading and instantiating - spiders. + The spider manager which takes care of loading spiders. Most extensions won't need to access this attribute. @@ -300,6 +299,54 @@ Settings API Alias for a :meth:`~freeze` call in the object returned by :meth:`copy` +.. _topics-api-spidermanager: + +SpiderManager API +================= + +.. module:: scrapy.spidermanager + :synopsis: The spider manager + +.. class:: SpiderManager + + This class is in charge of retrieving and handling the spider classes + defined across the project. + + Custom spider managers can be employed by specifying their path in the + :setting:`SPIDER_MANAGER_CLASS` project setting. They must fully implement + the :class:`scrapy.interfaces.ISpiderManager` interface to guarantee an + errorless execution. + + .. method:: from_settings(settings) + + This class method is used by Scrapy to create an instance of the class. + It's called with the current project settings, and it loads the spiders + found in the modules of the :setting:`SPIDER_MODULES` setting. + + :param settings: project settings + :type settings: :class:`~scrapy.settings.Settings` instance + + .. method:: load(spider_name) + + Get the Spider class with the given name. It'll look into the previously + loaded spiders for a spider class with name `spider_name` and will raise + a KeyError if not found. + + :param spider_name: spider class name + :type spider_name: str + + .. method:: list() + + Get the names of the available spiders in the project. + + .. method:: find_by_request(request) + + List the spiders' names that can handle the given request. Will try to + match the request's url against the domains of the spiders. + + :param request: queried request + :type request: :class:`~scrapy.http.Request` instance + .. _topics-api-signals: Signals API diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 629fac2dd..8eb72eaea 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -768,6 +768,16 @@ A dict containing the scrapy contracts enabled by default in Scrapy. You should never modify this setting in your project, modify :setting:`SPIDER_CONTRACTS` instead. For more info see :ref:`topics-contracts`. +.. setting:: SPIDER_MANAGER_CLASS + +SPIDER_MANAGER_CLASS +-------------------- + +Default: ``'scrapy.spidermanager.SpiderManager'`` + +The class that will be used for handling spiders, which must implement the +:ref:`topics-api-spidermanager`. + .. setting:: SPIDER_MIDDLEWARES SPIDER_MIDDLEWARES diff --git a/scrapy/interfaces.py b/scrapy/interfaces.py index 5d8d85aca..d4596407e 100644 --- a/scrapy/interfaces.py +++ b/scrapy/interfaces.py @@ -2,10 +2,12 @@ from zope.interface import Interface class ISpiderManager(Interface): - def create(spider_name, **spider_args): - """Returns a new Spider instance for the given spider name, and using - the given spider arguments. If the spider name is not found, it must - raise a KeyError.""" + def from_settings(settings): + """Returns an instance of the class for the given settings""" + + def load(spider_name): + """Returns the Spider class for the given spider name. If the spider + name is not found, it must raise a KeyError.""" def list(): """Return a list with the names of all spiders available in the diff --git a/scrapy/spidermanager.py b/scrapy/spidermanager.py index 5a0951cb4..5715b7793 100644 --- a/scrapy/spidermanager.py +++ b/scrapy/spidermanager.py @@ -6,7 +6,6 @@ spiders from zope.interface import implementer import six -from scrapy import signals from scrapy.interfaces import ISpiderManager from scrapy.utils.misc import walk_modules from scrapy.utils.spider import iter_spider_classes @@ -15,8 +14,8 @@ from scrapy.utils.spider import iter_spider_classes @implementer(ISpiderManager) class SpiderManager(object): - def __init__(self, spider_modules): - self.spider_modules = spider_modules + def __init__(self, settings): + self.spider_modules = settings['SPIDER_MODULES'] self._spiders = {} for name in self.spider_modules: for module in walk_modules(name): @@ -28,33 +27,17 @@ class SpiderManager(object): @classmethod def from_settings(cls, settings): - return cls(settings.getlist('SPIDER_MODULES')) + return cls(settings) - @classmethod - def from_crawler(cls, crawler): - sm = cls.from_settings(crawler.settings) - sm.crawler = crawler - crawler.signals.connect(sm.close_spider, signals.spider_closed) - return sm - - def create(self, spider_name, **spider_kwargs): + def load(self, spider_name): try: - spcls = self._spiders[spider_name] + return self._spiders[spider_name] except KeyError: - raise KeyError("Spider not found: %s" % spider_name) - if hasattr(self, 'crawler') and hasattr(spcls, 'from_crawler'): - return spcls.from_crawler(self.crawler, **spider_kwargs) - else: - return spcls(**spider_kwargs) + raise KeyError("Spider not found: {}".format(spider_name)) def find_by_request(self, request): return [name for name, cls in six.iteritems(self._spiders) if cls.handles_request(request)] def list(self): - return self._spiders.keys() - - def close_spider(self, spider, reason): - closed = getattr(spider, 'closed', None) - if callable(closed): - return closed(reason) + return list(self._spiders.keys()) diff --git a/tests/test_spidermanager/__init__.py b/tests/test_spidermanager/__init__.py index b0dd9a851..69ab3b82a 100644 --- a/tests/test_spidermanager/__init__.py +++ b/tests/test_spidermanager/__init__.py @@ -10,6 +10,7 @@ from twisted.trial import unittest # alone from scrapy.interfaces import ISpiderManager from scrapy.spidermanager import SpiderManager +from scrapy.settings import Settings from scrapy.http import Request module_dir = os.path.dirname(os.path.abspath(__file__)) @@ -23,7 +24,8 @@ class SpiderManagerTest(unittest.TestCase): self.spiders_dir = os.path.join(self.tmpdir, 'test_spiders_xxx') shutil.copytree(orig_spiders_dir, self.spiders_dir) sys.path.append(self.tmpdir) - self.spiderman = SpiderManager(['test_spiders_xxx']) + settings = Settings({'SPIDER_MODULES': ['test_spiders_xxx']}) + self.spiderman = SpiderManager.from_settings(settings) def tearDown(self): del self.spiderman @@ -35,14 +37,11 @@ class SpiderManagerTest(unittest.TestCase): def test_list(self): self.assertEqual(set(self.spiderman.list()), - set(['spider1', 'spider2', 'spider3', 'spider4'])) + set(['spider1', 'spider2', 'spider3'])) - def test_create(self): - spider1 = self.spiderman.create("spider1") - self.assertEqual(spider1.__class__.__name__, 'Spider1') - spider2 = self.spiderman.create("spider2", foo="bar") - self.assertEqual(spider2.__class__.__name__, 'Spider2') - self.assertEqual(spider2.foo, 'bar') + def test_load(self): + spider1 = self.spiderman.load("spider1") + self.assertEqual(spider1.__name__, 'Spider1') def test_find_by_request(self): self.assertEqual(self.spiderman.find_by_request(Request('http://scrapy1.org/test')), @@ -59,13 +58,13 @@ class SpiderManagerTest(unittest.TestCase): ['spider3']) def test_load_spider_module(self): - self.spiderman = SpiderManager(['tests.test_spidermanager.test_spiders.spider1']) + module = 'tests.test_spidermanager.test_spiders.spider1' + settings = Settings({'SPIDER_MODULES': [module]}) + self.spiderman = SpiderManager.from_settings(settings) assert len(self.spiderman._spiders) == 1 def test_load_base_spider(self): - self.spiderman = SpiderManager(['tests.test_spidermanager.test_spiders.spider0']) + module = 'tests.test_spidermanager.test_spiders.spider0' + settings = Settings({'SPIDER_MODULES': [module]}) + self.spiderman = SpiderManager.from_settings(settings) assert len(self.spiderman._spiders) == 0 - - def test_load_from_crawler(self): - spider = self.spiderman.create('spider4', a='OK') - self.assertEqual(spider.a, 'OK') diff --git a/tests/test_spidermanager/test_spiders/spider4.py b/tests/test_spidermanager/test_spiders/spider4.py deleted file mode 100644 index e883e4d93..000000000 --- a/tests/test_spidermanager/test_spiders/spider4.py +++ /dev/null @@ -1,10 +0,0 @@ -from scrapy.spider import Spider - -class Spider4(Spider): - name = "spider4" - - @classmethod - def from_crawler(cls, crawler, **kwargs): - o = cls(**kwargs) - o.crawler = crawler - return o From 980e30a18758f21f416036c2076bf98630c70193 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Tue, 29 Jul 2014 17:46:42 -0300 Subject: [PATCH 0008/2974] Crawler interface cleanup --- docs/topics/api.rst | 17 ++++++----- scrapy/crawler.py | 70 ++++++++++++++++++++++----------------------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 348305fb7..229943c55 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -28,9 +28,10 @@ contains a dictionary of all available extensions and their order similar to how you :ref:`configure the downloader middlewares `. -.. class:: Crawler(settings) +.. class:: Crawler(spidercls, settings) The Crawler object must be instantiated with a + :class:`scrapy.spider.Spider` subclass and a :class:`scrapy.settings.Settings` object. .. attribute:: settings @@ -90,16 +91,18 @@ how you :ref:`configure the downloader middlewares or modify the downloader and scheduler behaviour, although this is an advanced use and this API is not yet stable. - .. method:: configure() + .. attribute:: spider - Configure the crawler. + Spider currently being crawled. This is an instance of the spider class + provided while constructing the crawler, and it is created after the + arguments given in the :meth:`crawl` method. - This loads extensions, middlewares and spiders, leaving the crawler - ready to be started. It also configures the execution engine. + .. method:: crawl(\*args, \**kwargs) - .. method:: start() + Starts the crawler by instantiating its spider class with the given + `args` and `kwargs` arguments, while setting the execution engine in + motion. - Start the crawler. This calls :meth:`configure` if it hasn't been called yet. Returns a deferred that is fired when the crawl is finished. .. _topics-api-settings: diff --git a/scrapy/crawler.py b/scrapy/crawler.py index cfd6c8003..db1a083dd 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -13,16 +13,22 @@ from scrapy import log, signals class Crawler(object): - def __init__(self, settings): - self.configured = False + def __init__(self, spidercls, settings): + self.spidercls = spidercls self.settings = settings self.signals = SignalManager(self) - self.stats = load_object(settings['STATS_CLASS'])(self) - self._start_requests = lambda: () - self._spider = None - # TODO: move SpiderManager to CrawlerProcess + self.stats = load_object(self.settings['STATS_CLASS'])(self) + lf_cls = load_object(self.settings['LOG_FORMATTER']) + self.logformatter = lf_cls.from_crawler(self) + self.extensions = ExtensionManager.from_crawler(self) + + # Attribute kept for backward compatibility (Use CrawlerRunner.spiders) spman_cls = load_object(self.settings['SPIDER_MANAGER_CLASS']) - self.spiders = spman_cls.from_crawler(self) + self.spiders = spman_cls.from_settings(self.settings) + + self.crawling = False + self.spider = None + self.engine = None def install(self): # TODO: remove together with scrapy.project.crawler usage @@ -36,39 +42,31 @@ class Crawler(object): assert hasattr(scrapy.project, 'crawler'), "crawler not installed" del scrapy.project.crawler - def configure(self): - if self.configured: - return - - self.configured = True - lf_cls = load_object(self.settings['LOG_FORMATTER']) - self.logformatter = lf_cls.from_crawler(self) - self.extensions = ExtensionManager.from_crawler(self) - self.engine = ExecutionEngine(self, self._spider_closed) - - def crawl(self, spider, requests=None): - assert self._spider is None, 'Spider already attached' - self._spider = spider - spider.set_crawler(self) - if requests is None: - self._start_requests = spider.start_requests - else: - self._start_requests = lambda: requests - - def _spider_closed(self, spider=None): - if not self.engine.open_spiders: - self.stop() - @defer.inlineCallbacks - def start(self): - yield defer.maybeDeferred(self.configure) - if self._spider: - yield self.engine.open_spider(self._spider, self._start_requests()) - yield defer.maybeDeferred(self.engine.start) + def crawl(self, *args, **kwargs): + assert not self.crawling, "Crawling already taking place" + self.crawling = True + + try: + self.spider = self._create_spider(*args, **kwargs) + self.engine = self._create_engine() + start_requests = iter(self.spider.start_requests()) + yield self.engine.open_spider(self.spider, start_requests) + yield defer.maybeDeferred(self.engine.start) + except Exception: + self.crawling = False + raise + + def _create_spider(self, *args, **kwargs): + return self.spidercls.from_crawler(self, *args, **kwargs) + + def _create_engine(self): + return ExecutionEngine(self, lambda _: self.stop()) @defer.inlineCallbacks def stop(self): - if self.configured and self.engine.running: + if self.crawling: + self.crawling = False yield defer.maybeDeferred(self.engine.stop) From d40273561dad76a409f847b5f8ce1daafdb1dc7c Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Wed, 30 Jul 2014 05:35:18 -0300 Subject: [PATCH 0009/2974] CrawlerProcess cleanup changes --- docs/topics/api.rst | 48 +++++++++++++++++++ docs/topics/practices.rst | 76 ++++++++++++++++++------------ scrapy/crawler.py | 98 ++++++++++++++++++++------------------- 3 files changed, 144 insertions(+), 78 deletions(-) diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 229943c55..0329e2a8f 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -105,6 +105,53 @@ how you :ref:`configure the downloader middlewares Returns a deferred that is fired when the crawl is finished. +.. class:: CrawlerRunner(settings) + + This is a convenient helper class that creates, configures and runs + crawlers inside an already setup Twisted `reactor`_. + + The CrawlerRunner object must be instantiated with a + :class:`~scrapy.settings.Settings` object. + + This class shouldn't be needed (since Scrapy is responsible of using it + accordingly) unless writing scripts that manually handle the crawling + process. See :ref:`run-from-script` for an example. + + .. attribute:: crawlers + + Set of :class:`crawlers ` created by the + :meth:`crawl` method. + + .. attribute:: crawl_deferreds + + Set of the `deferreds`_ return by the :meth:`crawl` method. This + collection it's useful for keeping track of current crawling state. + + .. method:: crawl(spidercls, \*args, \**kwargs) + + This method sets up the crawling of the given `spidercls` with the + provided arguments. + + It takes care of loading the spider class while configuring and starting + a crawler for it. + + Returns a deferred that is fired when the crawl is finished. + + :param spidercls: spider class or spider's name inside the project + :type spidercls: :class:`~scrapy.spider.Spider` subclass or str + + :param args: arguments to initializate the spider + :type args: list + + :param kwargs: keyword arguments to initializate the spider + :type kwargs: dict + + .. method:: stop() + + Stops simultaneously all the crawling jobs taking place. + + Returns a deferred that is fired when they all have ended. + .. _topics-api-settings: Settings API @@ -470,3 +517,4 @@ class (which they all inherit from). .. _deferreds: http://twistedmatrix.com/documents/current/core/howto/defer.html .. _deferred: http://twistedmatrix.com/documents/current/core/howto/defer.html +.. _reactor: http://twistedmatrix.com/documents/current/core/howto/reactor-basics.html diff --git a/docs/topics/practices.rst b/docs/topics/practices.rst index 64b3a2da7..e84478d3c 100644 --- a/docs/topics/practices.rst +++ b/docs/topics/practices.rst @@ -19,8 +19,9 @@ Remember that Scrapy is built on top of the Twisted asynchronous networking library, so you need to run it inside the Twisted reactor. Note that you will also have to shutdown the Twisted reactor yourself after the -spider is finished. This can be achieved by connecting a handler to the -``signals.spider_closed`` signal. +spider is finished. This can be achieved by adding callbacks to the deferred +returned by the :meth:`CrawlerRunner.crawl +` method. What follows is a working example of how to do that, using the `testspiders`_ project as example. @@ -28,20 +29,21 @@ project as example. :: from twisted.internet import reactor - from scrapy.crawler import Crawler - from scrapy import log, signals - from testspiders.spiders.followall import FollowAllSpider + from scrapy.crawler import CrawlerRunner from scrapy.utils.project import get_project_settings - spider = FollowAllSpider(domain='scrapinghub.com') - settings = get_project_settings() - crawler = Crawler(settings) - crawler.signals.connect(reactor.stop, signal=signals.spider_closed) - crawler.configure() - crawler.crawl(spider) - crawler.start() - log.start() - reactor.run() # the script will block here until the spider_closed signal was sent + # If you aren't inside a Scrapy project, you could use an instance of the + # Settings class in scrapy.settings instead of the configuration returned + # by get_project_settings + runner = CrawlerRunner(get_project_settings()) + + # 'followall' is the name of one of the spiders of the project. If you + # aren't working in a Scrapy project, use the spider class as first + # argument instead of its name (or set the SPIDER_MODULES setting so Scrapy + # knows where to look at) + d = runner.crawl('followall', domain='scrapinghub.com') + d.addBoth(lambda _: reactor.stop()) + reactor.run() # the script will block here until the crawling is finished .. seealso:: `Twisted Reactor Overview`_. @@ -52,28 +54,42 @@ By default, Scrapy runs a single spider per process when you run ``scrapy crawl``. However, Scrapy supports running multiple spiders per process using the :ref:`internal API `. -Here is an example, using the `testspiders`_ project: +Here is an example that runs multiple spiders simultaneously, using the +`testspiders`_ project: :: - from twisted.internet import reactor - from scrapy.crawler import Crawler - from scrapy import log - from testspiders.spiders.followall import FollowAllSpider + from twisted.internet import reactor, defer + from scrapy.crawler import CrawlerRunner from scrapy.utils.project import get_project_settings - def setup_crawler(domain): - spider = FollowAllSpider(domain=domain) - settings = get_project_settings() - crawler = Crawler(settings) - crawler.configure() - crawler.crawl(spider) - crawler.start() - + runner = CrawlerRunner(get_project_settings()) + dfs = set() for domain in ['scrapinghub.com', 'insophia.com']: - setup_crawler(domain) - log.start() - reactor.run() + d = runner.crawl('followall', domain=domain) + dfs.add(d) + + defer.DeferredList(dfs).addBoth(lambda _: reactor.stop()) + reactor.run() # the script will block here until all crawling jobs are finished + +Same example but running the spiders sequentially by chaining the deferreds: + +:: + + from twisted.internet import reactor, defer + from scrapy.crawler import CrawlerRunner + from scrapy.utils.project import get_project_settings + + runner = CrawlerRunner(get_project_settings()) + + @defer.inlineCallbacks + def crawl(): + for domain in ['scrapinghub.com', 'insophia.com']: + yield runner.crawl('followall', domain=domain) + reactor.stop() + + crawl() + reactor.run() # the script will block here until the last crawl call is finished .. seealso:: :ref:`run-from-script`. diff --git a/scrapy/crawler.py b/scrapy/crawler.py index db1a083dd..56823166b 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -1,3 +1,4 @@ +import six import signal from twisted.internet import reactor, defer @@ -70,31 +71,50 @@ class Crawler(object): yield defer.maybeDeferred(self.engine.stop) -class CrawlerProcess(object): - """ A class to run multiple scrapy crawlers in a process sequentially""" +class CrawlerRunner(object): def __init__(self, settings): - install_shutdown_handlers(self._signal_shutdown) self.settings = settings - self.crawlers = {} - self.stopping = False - self._started = None + smcls = load_object(settings['SPIDER_MANAGER_CLASS']) + self.spiders = smcls.from_settings(settings.frozencopy()) + self.crawlers = set() + self.crawl_deferreds = set() - def create_crawler(self, name=None): - if name not in self.crawlers: - self.crawlers[name] = Crawler(self.settings) + def crawl(self, spidercls, *args, **kwargs): + crawler = self._create_logged_crawler(spidercls) + self.crawlers.add(crawler) - return self.crawlers[name] + crawler.install() + crawler.signals.connect(crawler.uninstall, signals.engine_stopped) - def start(self): - if self.start_crawling(): - self.start_reactor() + d = crawler.crawl(*args, **kwargs) + self.crawl_deferreds.add(d) + return d + + def _create_logged_crawler(self, spidercls): + crawler = self._create_crawler(spidercls) + log_observer = log.start_from_crawler(crawler) + if log_observer: + crawler.signals.connect(log_observer.stop, signals.engine_stopped) + return crawler + + def _create_crawler(self, spidercls): + if isinstance(spidercls, six.string_types): + spidercls = self.spiders.load(spidercls) + crawler = Crawler(spidercls, self.settings.frozencopy()) + return crawler - @defer.inlineCallbacks def stop(self): - self.stopping = True - if self._active_crawler: - yield self._active_crawler.stop() + return defer.DeferredList(c.stop() for c in self.crawlers) + + +class CrawlerProcess(CrawlerRunner): + """A class to run multiple scrapy crawlers in a process simultaneously""" + + def __init__(self, settings): + super(CrawlerProcess, self).__init__(settings) + install_shutdown_handlers(self._signal_shutdown) + self.stopping = False def _signal_shutdown(self, signum, _): install_shutdown_handlers(self._signal_kill) @@ -110,43 +130,25 @@ class CrawlerProcess(object): level=log.INFO, signame=signame) reactor.callFromThread(self._stop_reactor) - # ------------------------------------------------------------------------# - # The following public methods can't be considered stable and may change at - # any moment. - # - # start_crawling and start_reactor are called from scrapy.commands.shell - # They are splitted because reactor is started on a different thread than IPython shell. - # - def start_crawling(self): - log.scrapy_info(self.settings) - return self._start_crawler() is not None + def start(self, stop_after_crawl=True): + self._start_logging() + self._start_reactor(stop_after_crawl) - def start_reactor(self): + def _start_logging(self): + log.scrapy_info(self.settings) + + def _start_reactor(self, stop_after_crawl=True): + if stop_after_crawl: + d = defer.DeferredList(self.crawl_deferreds) + if d.called: + # Don't start the reactor if the deferreds are already fired + return + d.addBoth(lambda _: self._stop_reactor()) if self.settings.getbool('DNSCACHE_ENABLED'): reactor.installResolver(CachingThreadedResolver(reactor)) reactor.addSystemEventTrigger('before', 'shutdown', self.stop) reactor.run(installSignalHandlers=False) # blocking call - def _start_crawler(self): - if not self.crawlers or self.stopping: - return - - name, crawler = self.crawlers.popitem() - self._active_crawler = crawler - log_observer = log.start_from_crawler(crawler) - crawler.configure() - crawler.install() - crawler.signals.connect(crawler.uninstall, signals.engine_stopped) - if log_observer: - crawler.signals.connect(log_observer.stop, signals.engine_stopped) - crawler.signals.connect(self._check_done, signals.engine_stopped) - crawler.start() - return name, crawler - - def _check_done(self, **kwargs): - if not self._start_crawler(): - self._stop_reactor() - def _stop_reactor(self, _=None): try: reactor.stop() From 870438e5f4665de836f7ff423055895b305a4e7f Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 31 Jul 2014 04:12:12 -0300 Subject: [PATCH 0010/2974] Update tests utils, fixing get_crawler and removing docrawl --- scrapy/utils/test.py | 15 +- tests/py3-ignores.txt | 5 + tests/test_closespider.py | 38 +++--- tests/test_crawl.py | 128 +++++++++--------- tests/test_downloader_handlers.py | 9 +- tests/test_downloadermiddleware.py | 5 +- ...test_downloadermiddleware_ajaxcrawlable.py | 4 +- ...est_downloadermiddleware_defaultheaders.py | 5 +- ...st_downloadermiddleware_downloadtimeout.py | 5 +- tests/test_downloadermiddleware_httpcache.py | 4 +- tests/test_downloadermiddleware_redirect.py | 8 +- tests/test_downloadermiddleware_retry.py | 4 +- tests/test_downloadermiddleware_stats.py | 4 +- tests/test_downloadermiddleware_useragent.py | 5 +- tests/test_engine.py | 8 +- tests/test_proxy_connect.py | 28 ++-- tests/test_spider.py | 6 +- tests/test_spidermiddleware_depth.py | 5 +- tests/test_spidermiddleware_httperror.py | 22 +-- tests/test_spidermiddleware_offsite.py | 14 +- tests/test_stats.py | 4 +- 21 files changed, 160 insertions(+), 166 deletions(-) diff --git a/scrapy/utils/test.py b/scrapy/utils/test.py index e6376d519..a4b769970 100644 --- a/scrapy/utils/test.py +++ b/scrapy/utils/test.py @@ -20,15 +20,17 @@ def assert_aws_environ(): if 'AWS_ACCESS_KEY_ID' not in os.environ: raise SkipTest("AWS keys not found") -def get_crawler(settings_dict=None): +def get_crawler(spidercls=None, settings_dict=None): """Return an unconfigured Crawler object. If settings_dict is given, it will be used to populate the crawler settings with a project level priority. """ - from scrapy.crawler import Crawler + from scrapy.crawler import CrawlerRunner from scrapy.settings import Settings + from scrapy.spider import Spider - return Crawler(Settings(settings_dict)) + runner = CrawlerRunner(Settings(settings_dict)) + return runner._create_crawler(spidercls or Spider) def get_pythonpath(): """Return a PYTHONPATH suitable to use in processes so that they find this @@ -62,10 +64,3 @@ def assert_samelines(testcase, text1, text2, msg=None): line endings between platforms """ testcase.assertEqual(text1.splitlines(), text2.splitlines(), msg) - -def docrawl(spider, settings=None): - """Configure and start Crawler; return the result of crawler.start()""" - crawler = get_crawler(settings) - crawler.configure() - crawler.crawl(spider) - return crawler.start() diff --git a/tests/py3-ignores.txt b/tests/py3-ignores.txt index c1619b3ae..ef88eab7e 100644 --- a/tests/py3-ignores.txt +++ b/tests/py3-ignores.txt @@ -14,6 +14,7 @@ tests/test_downloadermiddleware_ajaxcrawlable.py tests/test_downloadermiddleware_cookies.py tests/test_downloadermiddleware_decompression.py tests/test_downloadermiddleware_defaultheaders.py +tests/test_downloadermiddleware_downloadtimeout.py tests/test_downloadermiddleware_httpauth.py tests/test_downloadermiddleware_httpcache.py tests/test_downloadermiddleware_httpcompression.py @@ -22,6 +23,7 @@ tests/test_downloadermiddleware.py tests/test_downloadermiddleware_redirect.py tests/test_downloadermiddleware_retry.py tests/test_downloadermiddleware_robotstxt.py +tests/test_downloadermiddleware_stats.py tests/test_downloadermiddleware_useragent.py tests/test_dupefilter.py tests/test_engine.py @@ -48,9 +50,12 @@ tests/test_spidermanager/test_spiders/spider1.py tests/test_spidermanager/test_spiders/spider2.py tests/test_spidermanager/test_spiders/spider3.py tests/test_spidermanager/test_spiders/spider4.py +tests/test_spidermiddleware_depth.py tests/test_spidermiddleware_httperror.py +tests/test_spidermiddleware_offsite.py tests/test_spidermiddleware_referer.py tests/test_spider.py +tests/test_stats.py tests/test_utils_defer.py tests/test_utils_iterators.py tests/test_utils_jsonrpc.py diff --git a/tests/test_closespider.py b/tests/test_closespider.py index 8d30a4643..1700a861e 100644 --- a/tests/test_closespider.py +++ b/tests/test_closespider.py @@ -1,6 +1,6 @@ from twisted.internet import defer from twisted.trial.unittest import TestCase -from scrapy.utils.test import docrawl +from scrapy.utils.test import get_crawler from tests.spiders import FollowAllSpider, ItemSpider, ErrorSpider from tests.mockserver import MockServer @@ -16,45 +16,45 @@ class TestCloseSpider(TestCase): @defer.inlineCallbacks def test_closespider_itemcount(self): - spider = ItemSpider() close_on = 5 - yield docrawl(spider, {'CLOSESPIDER_ITEMCOUNT': close_on}) - reason = spider.meta['close_reason'] + crawler = get_crawler(ItemSpider, {'CLOSESPIDER_ITEMCOUNT': close_on}) + yield crawler.crawl() + reason = crawler.spider.meta['close_reason'] self.assertEqual(reason, 'closespider_itemcount') - itemcount = spider.crawler.stats.get_value('item_scraped_count') + itemcount = crawler.stats.get_value('item_scraped_count') self.assertTrue(itemcount >= close_on) @defer.inlineCallbacks def test_closespider_pagecount(self): - spider = FollowAllSpider() close_on = 5 - yield docrawl(spider, {'CLOSESPIDER_PAGECOUNT': close_on}) - reason = spider.meta['close_reason'] + crawler = get_crawler(FollowAllSpider, {'CLOSESPIDER_PAGECOUNT': close_on}) + yield crawler.crawl() + reason = crawler.spider.meta['close_reason'] self.assertEqual(reason, 'closespider_pagecount') - pagecount = spider.crawler.stats.get_value('response_received_count') + pagecount = crawler.stats.get_value('response_received_count') self.assertTrue(pagecount >= close_on) @defer.inlineCallbacks def test_closespider_errorcount(self): - spider = ErrorSpider(total=1000000) close_on = 5 - yield docrawl(spider, {'CLOSESPIDER_ERRORCOUNT': close_on}) - self.flushLoggedErrors(spider.exception_cls) - reason = spider.meta['close_reason'] + crawler = get_crawler(ErrorSpider, {'CLOSESPIDER_ERRORCOUNT': close_on}) + yield crawler.crawl(total=1000000) + self.flushLoggedErrors(crawler.spider.exception_cls) + reason = crawler.spider.meta['close_reason'] self.assertEqual(reason, 'closespider_errorcount') key = 'spider_exceptions/{name}'\ - .format(name=spider.exception_cls.__name__) - errorcount = spider.crawler.stats.get_value(key) + .format(name=crawler.spider.exception_cls.__name__) + errorcount = crawler.stats.get_value(key) self.assertTrue(errorcount >= close_on) @defer.inlineCallbacks def test_closespider_timeout(self): - spider = FollowAllSpider(total=1000000) close_on = 0.1 - yield docrawl(spider, {'CLOSESPIDER_TIMEOUT': close_on}) - reason = spider.meta['close_reason'] + crawler = get_crawler(FollowAllSpider, {'CLOSESPIDER_TIMEOUT': close_on}) + yield crawler.crawl(total=1000000) + reason = crawler.spider.meta['close_reason'] self.assertEqual(reason, 'closespider_timeout') - stats = spider.crawler.stats + stats = crawler.stats start = stats.get_value('start_time') stop = stats.get_value('finish_time') diff = stop - start diff --git a/tests/test_crawl.py b/tests/test_crawl.py index 9401bd0c9..48931d6ff 100644 --- a/tests/test_crawl.py +++ b/tests/test_crawl.py @@ -3,7 +3,7 @@ import socket import mock from twisted.internet import defer from twisted.trial.unittest import TestCase -from scrapy.utils.test import docrawl, get_testlog +from scrapy.utils.test import get_crawler, get_testlog from tests.spiders import FollowAllSpider, DelaySpider, SimpleSpider, \ BrokenStartRequestsSpider, SingleRequestSpider, DuplicateStartRequestsSpider from tests.mockserver import MockServer @@ -21,9 +21,9 @@ class CrawlTestCase(TestCase): @defer.inlineCallbacks def test_follow_all(self): - spider = FollowAllSpider() - yield docrawl(spider) - self.assertEqual(len(spider.urls_visited), 11) # 10 + start_url + crawler = get_crawler(FollowAllSpider) + yield crawler.crawl() + self.assertEqual(len(crawler.spider.urls_visited), 11) # 10 + start_url @defer.inlineCallbacks def test_delay(self): @@ -37,9 +37,9 @@ class CrawlTestCase(TestCase): @defer.inlineCallbacks def _test_delay(self, delay, randomize): settings = {"DOWNLOAD_DELAY": delay, 'RANDOMIZE_DOWNLOAD_DELAY': randomize} - spider = FollowAllSpider(maxlatency=delay * 2) - yield docrawl(spider, settings) - t = spider.times + crawler = get_crawler(FollowAllSpider, settings) + yield crawler.crawl(maxlatency=delay * 2) + t = crawler.spider.times totaltime = t[-1] - t[0] avgd = totaltime / (len(t) - 1) tolerance = 0.6 if randomize else 0.2 @@ -48,85 +48,79 @@ class CrawlTestCase(TestCase): @defer.inlineCallbacks def test_timeout_success(self): - spider = DelaySpider(n=0.5) - yield docrawl(spider) - self.assertTrue(spider.t1 > 0) - self.assertTrue(spider.t2 > 0) - self.assertTrue(spider.t2 > spider.t1) + crawler = get_crawler(DelaySpider) + yield crawler.crawl(n=0.5) + self.assertTrue(crawler.spider.t1 > 0) + self.assertTrue(crawler.spider.t2 > 0) + self.assertTrue(crawler.spider.t2 > crawler.spider.t1) @defer.inlineCallbacks def test_timeout_failure(self): - spider = DelaySpider(n=0.5) - yield docrawl(spider, {"DOWNLOAD_TIMEOUT": 0.35}) - self.assertTrue(spider.t1 > 0) - self.assertTrue(spider.t2 == 0) - self.assertTrue(spider.t2_err > 0) - self.assertTrue(spider.t2_err > spider.t1) + crawler = get_crawler(DelaySpider, {"DOWNLOAD_TIMEOUT": 0.35}) + yield crawler.crawl(n=0.5) + self.assertTrue(crawler.spider.t1 > 0) + self.assertTrue(crawler.spider.t2 == 0) + self.assertTrue(crawler.spider.t2_err > 0) + self.assertTrue(crawler.spider.t2_err > crawler.spider.t1) # server hangs after receiving response headers - spider = DelaySpider(n=0.5, b=1) - yield docrawl(spider, {"DOWNLOAD_TIMEOUT": 0.35}) - self.assertTrue(spider.t1 > 0) - self.assertTrue(spider.t2 == 0) - self.assertTrue(spider.t2_err > 0) - self.assertTrue(spider.t2_err > spider.t1) + yield crawler.crawl(n=0.5, b=1) + self.assertTrue(crawler.spider.t1 > 0) + self.assertTrue(crawler.spider.t2 == 0) + self.assertTrue(crawler.spider.t2_err > 0) + self.assertTrue(crawler.spider.t2_err > crawler.spider.t1) @defer.inlineCallbacks def test_retry_503(self): - spider = SimpleSpider("http://localhost:8998/status?n=503") - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("http://localhost:8998/status?n=503") self._assert_retried() @defer.inlineCallbacks def test_retry_conn_failed(self): - spider = SimpleSpider("http://localhost:65432/status?n=503") - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("http://localhost:65432/status?n=503") self._assert_retried() @defer.inlineCallbacks def test_retry_dns_error(self): with mock.patch('socket.gethostbyname', side_effect=socket.gaierror(-5, 'No address associated with hostname')): - spider = SimpleSpider("http://example.com/") - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("http://example.com/") self._assert_retried() @defer.inlineCallbacks def test_start_requests_bug_before_yield(self): - spider = BrokenStartRequestsSpider(fail_before_yield=1) - yield docrawl(spider) + crawler = get_crawler(BrokenStartRequestsSpider) + yield crawler.crawl(fail_before_yield=1) errors = self.flushLoggedErrors(ZeroDivisionError) self.assertEqual(len(errors), 1) @defer.inlineCallbacks def test_start_requests_bug_yielding(self): - spider = BrokenStartRequestsSpider(fail_yielding=1) - yield docrawl(spider) + crawler = get_crawler(BrokenStartRequestsSpider) + yield crawler.crawl(fail_yielding=1) errors = self.flushLoggedErrors(ZeroDivisionError) self.assertEqual(len(errors), 1) @defer.inlineCallbacks def test_start_requests_lazyness(self): settings = {"CONCURRENT_REQUESTS": 1} - spider = BrokenStartRequestsSpider() - yield docrawl(spider, settings) - #self.assertTrue(False, spider.seedsseen) - #self.assertTrue(spider.seedsseen.index(None) < spider.seedsseen.index(99), - # spider.seedsseen) + crawler = get_crawler(BrokenStartRequestsSpider, settings) + yield crawler.crawl() + #self.assertTrue(False, crawler.spider.seedsseen) + #self.assertTrue(crawler.spider.seedsseen.index(None) < crawler.spider.seedsseen.index(99), + # crawler.spider.seedsseen) @defer.inlineCallbacks def test_start_requests_dupes(self): settings = {"CONCURRENT_REQUESTS": 1} - spider = DuplicateStartRequestsSpider(dont_filter=True, - distinct_urls=2, - dupe_factor=3) - yield docrawl(spider, settings) - self.assertEqual(spider.visited, 6) + crawler = get_crawler(DuplicateStartRequestsSpider, settings) + yield crawler.crawl(dont_filter=True, distinct_urls=2, dupe_factor=3) + self.assertEqual(crawler.spider.visited, 6) - spider = DuplicateStartRequestsSpider(dont_filter=False, - distinct_urls=3, - dupe_factor=4) - yield docrawl(spider, settings) - self.assertEqual(spider.visited, 3) + yield crawler.crawl(dont_filter=False, distinct_urls=3, dupe_factor=4) + self.assertEqual(crawler.spider.visited, 3) @defer.inlineCallbacks def test_unbounded_response(self): @@ -150,23 +144,23 @@ Connection: close foo body with multiples lines '''}) - spider = SimpleSpider("http://localhost:8998/raw?{0}".format(query)) - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("http://localhost:8998/raw?{0}".format(query)) log = get_testlog() self.assertEqual(log.count("Got response 200"), 1) @defer.inlineCallbacks def test_retry_conn_lost(self): # connection lost after receiving data - spider = SimpleSpider("http://localhost:8998/drop?abort=0") - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("http://localhost:8998/drop?abort=0") self._assert_retried() @defer.inlineCallbacks def test_retry_conn_aborted(self): # connection lost before receiving data - spider = SimpleSpider("http://localhost:8998/drop?abort=1") - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("http://localhost:8998/drop?abort=1") self._assert_retried() def _assert_retried(self): @@ -184,22 +178,22 @@ with multiples lines req0.meta['next'] = req1 req1.meta['next'] = req2 req2.meta['next'] = req3 - spider = SingleRequestSpider(seed=req0) - yield docrawl(spider) + crawler = get_crawler(SingleRequestSpider) + yield crawler.crawl(seed=req0) # basic asserts in case of weird communication errors - self.assertIn('responses', spider.meta) - self.assertNotIn('failures', spider.meta) + self.assertIn('responses', crawler.spider.meta) + self.assertNotIn('failures', crawler.spider.meta) # start requests doesn't set Referer header - echo0 = json.loads(spider.meta['responses'][2].body) + echo0 = json.loads(crawler.spider.meta['responses'][2].body) self.assertNotIn('Referer', echo0['headers']) # following request sets Referer to start request url - echo1 = json.loads(spider.meta['responses'][1].body) + echo1 = json.loads(crawler.spider.meta['responses'][1].body) self.assertEqual(echo1['headers'].get('Referer'), [req0.url]) # next request avoids Referer header - echo2 = json.loads(spider.meta['responses'][2].body) + echo2 = json.loads(crawler.spider.meta['responses'][2].body) self.assertNotIn('Referer', echo2['headers']) # last request explicitly sets a Referer header - echo3 = json.loads(spider.meta['responses'][3].body) + echo3 = json.loads(crawler.spider.meta['responses'][3].body) self.assertEqual(echo3['headers'].get('Referer'), ['http://example.com']) @defer.inlineCallbacks @@ -208,11 +202,11 @@ with multiples lines est = [] def cb(response): - est.append(get_engine_status(spider.crawler.engine)) + est.append(get_engine_status(crawler.engine)) - spider = SingleRequestSpider(seed='http://localhost:8998/', callback_func=cb) - yield docrawl(spider) + crawler = get_crawler(SingleRequestSpider) + yield crawler.crawl(seed='http://localhost:8998/', callback_func=cb) self.assertEqual(len(est), 1, est) s = dict(est[0]) - self.assertEqual(s['engine.spider.name'], spider.name) + self.assertEqual(s['engine.spider.name'], crawler.spider.name) self.assertEqual(s['len(engine.scraper.slot.active)'], 1) diff --git a/tests/test_downloader_handlers.py b/tests/test_downloader_handlers.py index 6a3115004..c444d35fa 100644 --- a/tests/test_downloader_handlers.py +++ b/tests/test_downloader_handlers.py @@ -47,19 +47,22 @@ class LoadTestCase(unittest.TestCase): def test_enabled_handler(self): handlers = {'scheme': 'tests.test_downloader_handlers.DummyDH'} - dh = DownloadHandlers(get_crawler({'DOWNLOAD_HANDLERS': handlers})) + crawler = get_crawler(settings_dict={'DOWNLOAD_HANDLERS': handlers}) + dh = DownloadHandlers(crawler) self.assertIn('scheme', dh._handlers) self.assertNotIn('scheme', dh._notconfigured) def test_not_configured_handler(self): handlers = {'scheme': 'tests.test_downloader_handlers.OffDH'} - dh = DownloadHandlers(get_crawler({'DOWNLOAD_HANDLERS': handlers})) + crawler = get_crawler(settings_dict={'DOWNLOAD_HANDLERS': handlers}) + dh = DownloadHandlers(crawler) self.assertNotIn('scheme', dh._handlers) self.assertIn('scheme', dh._notconfigured) def test_disabled_handler(self): handlers = {'scheme': None} - dh = DownloadHandlers(get_crawler({'DOWNLOAD_HANDLERS': handlers})) + crawler = get_crawler(settings_dict={'DOWNLOAD_HANDLERS': handlers}) + dh = DownloadHandlers(crawler) self.assertNotIn('scheme', dh._handlers) self.assertNotIn('scheme', dh._notconfigured) diff --git a/tests/test_downloadermiddleware.py b/tests/test_downloadermiddleware.py index b7d3594cd..282035f5c 100644 --- a/tests/test_downloadermiddleware.py +++ b/tests/test_downloadermiddleware.py @@ -12,9 +12,8 @@ class ManagerTestCase(TestCase): settings_dict = None def setUp(self): - self.crawler = get_crawler(self.settings_dict) - self.spider = Spider('foo') - self.spider.set_crawler(self.crawler) + self.crawler = get_crawler(Spider, self.settings_dict) + self.spider = self.crawler._create_spider('foo') self.mwman = DownloaderMiddlewareManager.from_crawler(self.crawler) # some mw depends on stats collector self.crawler.stats.open_spider(self.spider) diff --git a/tests/test_downloadermiddleware_ajaxcrawlable.py b/tests/test_downloadermiddleware_ajaxcrawlable.py index 3e5ce6052..e73e62538 100644 --- a/tests/test_downloadermiddleware_ajaxcrawlable.py +++ b/tests/test_downloadermiddleware_ajaxcrawlable.py @@ -9,8 +9,8 @@ __doctests__ = ['scrapy.contrib.downloadermiddleware.ajaxcrawl'] class AjaxCrawlMiddlewareTest(unittest.TestCase): def setUp(self): - self.spider = Spider('foo') - crawler = get_crawler({'AJAXCRAWL_ENABLED': True}) + crawler = get_crawler(Spider, {'AJAXCRAWL_ENABLED': True}) + self.spider = crawler._create_spider('foo') self.mw = AjaxCrawlMiddleware.from_crawler(crawler) def _ajaxcrawlable_body(self): diff --git a/tests/test_downloadermiddleware_defaultheaders.py b/tests/test_downloadermiddleware_defaultheaders.py index b37a02336..09973b367 100644 --- a/tests/test_downloadermiddleware_defaultheaders.py +++ b/tests/test_downloadermiddleware_defaultheaders.py @@ -10,9 +10,8 @@ from scrapy.utils.test import get_crawler class TestDefaultHeadersMiddleware(TestCase): def get_defaults_spider_mw(self): - crawler = get_crawler() - spider = Spider('foo') - spider.set_crawler(crawler) + crawler = get_crawler(Spider) + spider = crawler._create_spider('foo') defaults = dict([(k, [v]) for k, v in \ six.iteritems(crawler.settings.get('DEFAULT_REQUEST_HEADERS'))]) return defaults, spider, DefaultHeadersMiddleware.from_crawler(crawler) diff --git a/tests/test_downloadermiddleware_downloadtimeout.py b/tests/test_downloadermiddleware_downloadtimeout.py index 52a0cc09d..3e3ff2401 100644 --- a/tests/test_downloadermiddleware_downloadtimeout.py +++ b/tests/test_downloadermiddleware_downloadtimeout.py @@ -9,9 +9,8 @@ from scrapy.utils.test import get_crawler class DownloadTimeoutMiddlewareTest(unittest.TestCase): def get_request_spider_mw(self): - crawler = get_crawler() - spider = Spider('foo') - spider.set_crawler(crawler) + crawler = get_crawler(Spider) + spider = crawler._create_spider('foo') request = Request('http://scrapytest.org/') return request, spider, DownloadTimeoutMiddleware.from_crawler(crawler) diff --git a/tests/test_downloadermiddleware_httpcache.py b/tests/test_downloadermiddleware_httpcache.py index 0eb5e7144..1e22ae661 100644 --- a/tests/test_downloadermiddleware_httpcache.py +++ b/tests/test_downloadermiddleware_httpcache.py @@ -24,8 +24,8 @@ class _BaseTest(unittest.TestCase): self.yesterday = email.utils.formatdate(time.time() - 86400) self.today = email.utils.formatdate() self.tomorrow = email.utils.formatdate(time.time() + 86400) - self.crawler = get_crawler() - self.spider = Spider('example.com') + self.crawler = get_crawler(Spider) + self.spider = self.crawler._create_spider('example.com') self.tmpdir = tempfile.mkdtemp() self.request = Request('http://www.example.com', headers={'User-Agent': 'test'}) diff --git a/tests/test_downloadermiddleware_redirect.py b/tests/test_downloadermiddleware_redirect.py index 8b871c7bc..beadfd362 100644 --- a/tests/test_downloadermiddleware_redirect.py +++ b/tests/test_downloadermiddleware_redirect.py @@ -10,8 +10,8 @@ from scrapy.utils.test import get_crawler class RedirectMiddlewareTest(unittest.TestCase): def setUp(self): - crawler = get_crawler() - self.spider = Spider('foo') + crawler = get_crawler(Spider) + self.spider = crawler._create_spider('foo') self.mw = RedirectMiddleware.from_crawler(crawler) def test_priority_adjust(self): @@ -123,8 +123,8 @@ class RedirectMiddlewareTest(unittest.TestCase): class MetaRefreshMiddlewareTest(unittest.TestCase): def setUp(self): - crawler = get_crawler() - self.spider = Spider('foo') + crawler = get_crawler(Spider) + self.spider = crawler._create_spider('foo') self.mw = MetaRefreshMiddleware.from_crawler(crawler) def _body(self, interval=5, url='http://example.org/newpage'): diff --git a/tests/test_downloadermiddleware_retry.py b/tests/test_downloadermiddleware_retry.py index 4c771f18e..e3e7c87d6 100644 --- a/tests/test_downloadermiddleware_retry.py +++ b/tests/test_downloadermiddleware_retry.py @@ -14,8 +14,8 @@ from scrapy.utils.test import get_crawler class RetryTest(unittest.TestCase): def setUp(self): - crawler = get_crawler() - self.spider = Spider('foo') + crawler = get_crawler(Spider) + self.spider = crawler._create_spider('foo') self.mw = RetryMiddleware.from_crawler(crawler) self.mw.max_retry_times = 2 diff --git a/tests/test_downloadermiddleware_stats.py b/tests/test_downloadermiddleware_stats.py index edc26e543..b790ff09a 100644 --- a/tests/test_downloadermiddleware_stats.py +++ b/tests/test_downloadermiddleware_stats.py @@ -9,8 +9,8 @@ from scrapy.utils.test import get_crawler class TestDownloaderStats(TestCase): def setUp(self): - self.crawler = get_crawler() - self.spider = Spider('scrapytest.org') + self.crawler = get_crawler(Spider) + self.spider = self.crawler._create_spider('scrapytest.org') self.mw = DownloaderStats(self.crawler.stats) self.crawler.stats.open_spider(self.spider) diff --git a/tests/test_downloadermiddleware_useragent.py b/tests/test_downloadermiddleware_useragent.py index 5fd5c24be..909d03ba5 100644 --- a/tests/test_downloadermiddleware_useragent.py +++ b/tests/test_downloadermiddleware_useragent.py @@ -9,9 +9,8 @@ from scrapy.utils.test import get_crawler class UserAgentMiddlewareTest(TestCase): def get_spider_and_mw(self, default_useragent): - crawler = get_crawler({'USER_AGENT': default_useragent}) - spider = Spider('foo') - spider.set_crawler(crawler) + crawler = get_crawler(Spider, {'USER_AGENT': default_useragent}) + spider = crawler._create_spider('foo') return spider, UserAgentMiddleware.from_crawler(crawler) def test_default_agent(self): diff --git a/tests/test_engine.py b/tests/test_engine.py index 6a0314a02..244d339ef 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -87,20 +87,18 @@ class CrawlerRun(object): self.portno = self.port.getHost().port start_urls = [self.geturl("/"), self.geturl("/redirect")] - self.spider = TestSpider(start_urls=start_urls) for name, signal in vars(signals).items(): if not name.startswith('_'): dispatcher.connect(self.record_signal, signal) - self.crawler = get_crawler() + self.crawler = get_crawler(TestSpider) self.crawler.install() - self.crawler.configure() self.crawler.signals.connect(self.item_scraped, signals.item_scraped) self.crawler.signals.connect(self.request_scheduled, signals.request_scheduled) self.crawler.signals.connect(self.response_downloaded, signals.response_downloaded) - self.crawler.crawl(self.spider) - self.crawler.start() + self.crawler.crawl(start_urls=start_urls) + self.spider = self.crawler.spider self.deferred = defer.Deferred() dispatcher.connect(self.stop, signals.engine_stopped) diff --git a/tests/test_proxy_connect.py b/tests/test_proxy_connect.py index 8999e102e..5ce48ebf8 100644 --- a/tests/test_proxy_connect.py +++ b/tests/test_proxy_connect.py @@ -8,7 +8,7 @@ from netlib import http_auth from twisted.internet import defer from twisted.trial.unittest import TestCase -from scrapy.utils.test import get_testlog, docrawl +from scrapy.utils.test import get_testlog, get_crawler from scrapy.http import Request from tests.spiders import SimpleSpider, SingleRequestSpider from tests.mockserver import MockServer @@ -49,29 +49,29 @@ class ProxyConnectTestCase(TestCase): @defer.inlineCallbacks def test_https_connect_tunnel(self): - spider = SimpleSpider("https://localhost:8999/status?n=200") - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("https://localhost:8999/status?n=200") self._assert_got_response_code(200) @defer.inlineCallbacks def test_https_noconnect(self): os.environ['https_proxy'] = 'http://scrapy:scrapy@localhost:8888?noconnect' - spider = SimpleSpider("https://localhost:8999/status?n=200") - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("https://localhost:8999/status?n=200") self._assert_got_response_code(200) os.environ['https_proxy'] = 'http://scrapy:scrapy@localhost:8888' @defer.inlineCallbacks def test_https_connect_tunnel_error(self): - spider = SimpleSpider("https://localhost:99999/status?n=200") - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("https://localhost:99999/status?n=200") self._assert_got_tunnel_error() @defer.inlineCallbacks def test_https_tunnel_auth_error(self): os.environ['https_proxy'] = 'http://wrong:wronger@localhost:8888' - spider = SimpleSpider("https://localhost:8999/status?n=200") - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("https://localhost:8999/status?n=200") # The proxy returns a 407 error code but it does not reach the client; # he just sees a TunnelError. self._assert_got_tunnel_error() @@ -80,17 +80,17 @@ class ProxyConnectTestCase(TestCase): @defer.inlineCallbacks def test_https_tunnel_without_leak_proxy_authorization_header(self): request = Request("https://localhost:8999/echo") - spider = SingleRequestSpider(seed=request) - yield docrawl(spider) + crawler = get_crawler(SingleRequestSpider) + yield crawler.crawl(seed=request) self._assert_got_response_code(200) - echo = json.loads(spider.meta['responses'][0].body) + echo = json.loads(crawler.spider.meta['responses'][0].body) self.assertTrue('Proxy-Authorization' not in echo['headers']) @defer.inlineCallbacks def test_https_noconnect_auth_error(self): os.environ['https_proxy'] = 'http://wrong:wronger@localhost:8888?noconnect' - spider = SimpleSpider("https://localhost:8999/status?n=200") - yield docrawl(spider) + crawler = get_crawler(SimpleSpider) + yield crawler.crawl("https://localhost:8999/status?n=200") self._assert_got_response_code(407) def _assert_got_response_code(self, code): diff --git a/tests/test_spider.py b/tests/test_spider.py index 903ea684a..148a872dd 100644 --- a/tests/test_spider.py +++ b/tests/test_spider.py @@ -242,7 +242,8 @@ class CrawlSpiderTest(SpiderTest): self.assertTrue(hasattr(spider, '_follow_links')) self.assertTrue(spider._follow_links) - crawler.settings.set('CRAWLSPIDER_FOLLOW_LINKS', False) + settings_dict = {'CRAWLSPIDER_FOLLOW_LINKS': False} + crawler = get_crawler(settings_dict=settings_dict) spider = self.spider_class.from_crawler(crawler, 'example.com') self.assertTrue(hasattr(spider, '_follow_links')) self.assertFalse(spider._follow_links) @@ -256,7 +257,8 @@ class CrawlSpiderTest(SpiderTest): self.assertTrue(spider._follow_links) spider = self.spider_class('example.com') - spider.set_crawler(get_crawler({'CRAWLSPIDER_FOLLOW_LINKS': False})) + settings_dict = {'CRAWLSPIDER_FOLLOW_LINKS': False} + spider.set_crawler(get_crawler(settings_dict=settings_dict)) self.assertTrue(hasattr(spider, '_follow_links')) self.assertFalse(spider._follow_links) diff --git a/tests/test_spidermiddleware_depth.py b/tests/test_spidermiddleware_depth.py index 94404ff41..e7ae75ed2 100644 --- a/tests/test_spidermiddleware_depth.py +++ b/tests/test_spidermiddleware_depth.py @@ -10,9 +10,10 @@ from scrapy.utils.test import get_crawler class TestDepthMiddleware(TestCase): def setUp(self): - self.spider = Spider('scrapytest.org') + crawler = get_crawler(Spider) + self.spider = crawler._create_spider('scrapytest.org') - self.stats = StatsCollector(get_crawler()) + self.stats = StatsCollector(crawler) self.stats.open_spider(self.spider) self.mw = DepthMiddleware(1, self.stats, True) diff --git a/tests/test_spidermiddleware_httperror.py b/tests/test_spidermiddleware_httperror.py index 788a0986b..5394f0eee 100644 --- a/tests/test_spidermiddleware_httperror.py +++ b/tests/test_spidermiddleware_httperror.py @@ -3,7 +3,7 @@ from unittest import TestCase from twisted.trial.unittest import TestCase as TrialTestCase from twisted.internet import defer -from scrapy.utils.test import docrawl, get_testlog +from scrapy.utils.test import get_crawler, get_testlog from tests.mockserver import MockServer from scrapy.http import Response, Request from scrapy.spider import Spider @@ -165,20 +165,20 @@ class TestHttpErrorMiddlewareIntegrational(TrialTestCase): @defer.inlineCallbacks def test_middleware_works(self): - spider = _HttpErrorSpider() - yield docrawl(spider) - assert not spider.skipped, spider.skipped - self.assertEqual(spider.parsed, {'200'}) - self.assertEqual(spider.failed, {'404', '402', '500'}) + crawler = get_crawler(_HttpErrorSpider) + yield crawler.crawl() + assert not crawler.spider.skipped, crawler.spider.skipped + self.assertEqual(crawler.spider.parsed, {'200'}) + self.assertEqual(crawler.spider.failed, {'404', '402', '500'}) @defer.inlineCallbacks def test_logging(self): - spider = _HttpErrorSpider(bypass_status_codes={402}) - yield docrawl(spider) + crawler = get_crawler(_HttpErrorSpider) + yield crawler.crawl(bypass_status_codes={402}) # print(get_testlog()) - self.assertEqual(spider.parsed, {'200', '402'}) - self.assertEqual(spider.skipped, {'402'}) - self.assertEqual(spider.failed, {'404', '500'}) + self.assertEqual(crawler.spider.parsed, {'200', '402'}) + self.assertEqual(crawler.spider.skipped, {'402'}) + self.assertEqual(crawler.spider.failed, {'404', '500'}) log = get_testlog() self.assertIn('Ignoring response <404', log) diff --git a/tests/test_spidermiddleware_offsite.py b/tests/test_spidermiddleware_offsite.py index 298cba6e4..e5e99002a 100644 --- a/tests/test_spidermiddleware_offsite.py +++ b/tests/test_spidermiddleware_offsite.py @@ -10,13 +10,13 @@ from scrapy.utils.test import get_crawler class TestOffsiteMiddleware(TestCase): def setUp(self): - self.spider = self._get_spider() - crawler = get_crawler() + crawler = get_crawler(Spider) + self.spider = crawler._create_spider(**self._get_spiderargs()) self.mw = OffsiteMiddleware.from_crawler(crawler) self.mw.spider_opened(self.spider) - def _get_spider(self): - return Spider('foo', allowed_domains=['scrapytest.org', 'scrapy.org']) + def _get_spiderargs(self): + return dict(name='foo', allowed_domains=['scrapytest.org', 'scrapy.org']) def test_process_spider_output(self): res = Response('http://scrapytest.org') @@ -39,8 +39,8 @@ class TestOffsiteMiddleware(TestCase): class TestOffsiteMiddleware2(TestOffsiteMiddleware): - def _get_spider(self): - return Spider('foo', allowed_domains=None) + def _get_spiderargs(self): + return dict(name='foo', allowed_domains=None) def test_process_spider_output(self): res = Response('http://scrapytest.org') @@ -58,7 +58,7 @@ class TestOffsiteMiddleware4(TestOffsiteMiddleware3): def _get_spider(self): bad_hostname = urlparse('http:////scrapytest.org').hostname - return Spider('foo', allowed_domains=['scrapytest.org', None, bad_hostname]) + return dict(name='foo', allowed_domains=['scrapytest.org', None, bad_hostname]) def test_process_spider_output(self): res = Response('http://scrapytest.org') diff --git a/tests/test_stats.py b/tests/test_stats.py index 795e8e3bd..db1f50712 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -7,8 +7,8 @@ from scrapy.utils.test import get_crawler class StatsCollectorTest(unittest.TestCase): def setUp(self): - self.crawler = get_crawler() - self.spider = Spider('foo') + self.crawler = get_crawler(Spider) + self.spider = self.crawler._create_spider('foo') def test_collector(self): stats = StatsCollector(self.crawler) From d0edad4b0bd93ed34a680ddd6563387be7797128 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 31 Jul 2014 10:10:25 -0300 Subject: [PATCH 0011/2974] Drop support for ScrapyCommand.crawler property --- scrapy/command.py | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/scrapy/command.py b/scrapy/command.py index b2eb9cf8f..9ac013098 100644 --- a/scrapy/command.py +++ b/scrapy/command.py @@ -2,12 +2,11 @@ Base class for Scrapy commands """ import os -import warnings from optparse import OptionGroup from twisted.python import failure from scrapy.utils.conf import arglist_to_dict -from scrapy.exceptions import UsageError, ScrapyDeprecationWarning +from scrapy.exceptions import UsageError class ScrapyCommand(object): @@ -27,31 +26,6 @@ class ScrapyCommand(object): assert not hasattr(self, '_crawler'), "crawler already set" self._crawler = crawler - @property - def crawler(self): - warnings.warn("Command's default `crawler` is deprecated and will be removed. " - "Use `create_crawler` method to instatiate crawlers.", - ScrapyDeprecationWarning) - - if not hasattr(self, '_crawler'): - crawler = self.crawler_process.create_crawler() - - old_start = crawler.start - self.crawler_process.started = False - - def wrapped_start(): - if self.crawler_process.started: - old_start() - else: - self.crawler_process.started = True - self.crawler_process.start() - - crawler.start = wrapped_start - - self.set_crawler(crawler) - - return self._crawler - def syntax(self): """ Command syntax (preferably one-line). Do not include command name. From 89df18bd2bc6fdc7f2084454a2c69f4db03008ad Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 31 Jul 2014 10:16:25 -0300 Subject: [PATCH 0012/2974] Fix usage of crawler_process in ScrapyCommands --- scrapy/commands/bench.py | 4 +--- scrapy/commands/check.py | 32 +++++++++----------------------- scrapy/commands/crawl.py | 4 +--- scrapy/commands/edit.py | 7 +++---- scrapy/commands/genspider.py | 5 ++--- scrapy/commands/list.py | 3 +-- scrapy/commands/runspider.py | 5 ++--- scrapy/contracts/__init__.py | 17 +++++++++++++++++ 8 files changed, 36 insertions(+), 41 deletions(-) diff --git a/scrapy/commands/bench.py b/scrapy/commands/bench.py index 18934f976..395597546 100644 --- a/scrapy/commands/bench.py +++ b/scrapy/commands/bench.py @@ -22,9 +22,7 @@ class Command(ScrapyCommand): def run(self, args, opts): with _BenchServer(): - spider = _BenchSpider(total=100000) - crawler = self.crawler_process.create_crawler() - crawler.crawl(spider) + self.crawler_process.crawl(_BenchSpider, total=100000) self.crawler_process.start() diff --git a/scrapy/commands/check.py b/scrapy/commands/check.py index 40ff9014b..014b00eeb 100644 --- a/scrapy/commands/check.py +++ b/scrapy/commands/check.py @@ -69,20 +69,18 @@ class Command(ScrapyCommand): # contract requests contract_reqs = defaultdict(list) - spman_cls = load_object(self.settings['SPIDER_MANAGER_CLASS']) - spiders = spman_cls.from_settings(self.settings) + spiders = self.crawler_process.spiders - for spider in args or spiders.list(): - spider = spiders.create(spider) - requests = self.get_requests(spider, conman, result) - contract_reqs[spider.name] = [] + for spidername in args or spiders.list(): + spidercls = spiders.load(spidername) + spidercls.start_requests = lambda s: conman.from_spider(s, result) + tested_methods = conman.tested_methods_from_spidercls(spidercls) if opts.list: - for req in requests: - contract_reqs[spider.name].append(req.callback.__name__) - elif requests: - crawler = self.crawler_process.create_crawler(spider.name) - crawler.crawl(spider, requests) + for method in tested_methods: + contract_reqs[spidercls.name].append(method) + elif tested_methods: + self.crawler_process.crawl(spidercls) # start checks if opts.list: @@ -101,15 +99,3 @@ class Command(ScrapyCommand): result.printSummary(start, stop) self.exitcode = int(not result.wasSuccessful()) - def get_requests(self, spider, conman, result): - requests = [] - - for key, value in vars(type(spider)).items(): - if callable(value) and value.__doc__: - bound_method = value.__get__(spider, type(spider)) - request = conman.from_method(bound_method, result) - - if request: - requests.append(request) - - return requests diff --git a/scrapy/commands/crawl.py b/scrapy/commands/crawl.py index 0976de51b..b7fea7b80 100644 --- a/scrapy/commands/crawl.py +++ b/scrapy/commands/crawl.py @@ -54,7 +54,5 @@ class Command(ScrapyCommand): raise UsageError("running 'scrapy crawl' with more than one spider is no longer supported") spname = args[0] - crawler = self.crawler_process.create_crawler() - spider = crawler.spiders.create(spname, **opts.spargs) - crawler.crawl(spider) + self.crawler_process.crawl(spname, **opts.spargs) self.crawler_process.start() diff --git a/scrapy/commands/edit.py b/scrapy/commands/edit.py index b542217e1..e20e7c2e5 100644 --- a/scrapy/commands/edit.py +++ b/scrapy/commands/edit.py @@ -25,13 +25,12 @@ class Command(ScrapyCommand): if len(args) != 1: raise UsageError() - crawler = self.crawler_process.create_crawler() - editor = crawler.settings['EDITOR'] + editor = self.settings['EDITOR'] try: - spider = crawler.spiders.create(args[0]) + spidercls = self.crawler_process.spiders.load(args[0]) except KeyError: return self._err("Spider not found: %s" % args[0]) - sfile = sys.modules[spider.__module__].__file__ + sfile = sys.modules[spidercls.__module__].__file__ sfile = sfile.replace('.pyc', '.py') self.exitcode = os.system('%s "%s"' % (editor, sfile)) diff --git a/scrapy/commands/genspider.py b/scrapy/commands/genspider.py index 3e2e24b21..52c5d9f94 100644 --- a/scrapy/commands/genspider.py +++ b/scrapy/commands/genspider.py @@ -65,15 +65,14 @@ class Command(ScrapyCommand): return try: - crawler = self.crawler_process.create_crawler() - spider = crawler.spiders.create(name) + spidercls = self.crawler_process.spiders.load(name) except KeyError: pass else: # if spider already exists and not --force then halt if not opts.force: print("Spider %r already exists in module:" % name) - print(" %s" % spider.__module__) + print(" %s" % spidercls.__module__) return template_file = self._find_template(opts.template) if template_file: diff --git a/scrapy/commands/list.py b/scrapy/commands/list.py index 0ea9c2313..2d55d59bd 100644 --- a/scrapy/commands/list.py +++ b/scrapy/commands/list.py @@ -10,6 +10,5 @@ class Command(ScrapyCommand): return "List available spiders" def run(self, args, opts): - crawler = self.crawler_process.create_crawler() - for s in sorted(crawler.spiders.list()): + for s in sorted(self.crawler_process.spiders.list()): print(s) diff --git a/scrapy/commands/runspider.py b/scrapy/commands/runspider.py index b1d501842..b67838619 100644 --- a/scrapy/commands/runspider.py +++ b/scrapy/commands/runspider.py @@ -83,8 +83,7 @@ class Command(ScrapyCommand): spclasses = list(iter_spider_classes(module)) if not spclasses: raise UsageError("No spider found in file: %s\n" % filename) - spider = spclasses.pop()(**opts.spargs) + spidercls = spclasses.pop() - crawler = self.crawler_process.create_crawler() - crawler.crawl(spider) + self.crawler_process.crawl(spidercls, **opts.spargs) self.crawler_process.start() diff --git a/scrapy/contracts/__init__.py b/scrapy/contracts/__init__.py index 03e6e4e0c..5eaee3d11 100644 --- a/scrapy/contracts/__init__.py +++ b/scrapy/contracts/__init__.py @@ -15,6 +15,15 @@ class ContractsManager(object): for contract in contracts: self.contracts[contract.name] = contract + def tested_methods_from_spidercls(self, spidercls): + methods = [] + for key, value in vars(spidercls).items(): + if (callable(value) and value.__doc__ and + re.search(r'^\s*@', value.__doc__, re.MULTILINE)): + methods.append(key) + + return methods + def extract_contracts(self, method): contracts = [] for line in method.__doc__.split('\n'): @@ -28,6 +37,14 @@ class ContractsManager(object): return contracts + def from_spider(self, spider, results): + requests = [] + for method in self.tested_methods_from_spidercls(type(spider)): + bound_method = spider.__getattribute__(method) + requests.append(self.from_method(bound_method, results)) + + return requests + def from_method(self, method, results): contracts = self.extract_contracts(method) if contracts: From 900a487682b11696ccab5d18c9f13e0addd25f12 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Tue, 5 Aug 2014 21:01:57 -0300 Subject: [PATCH 0013/2974] Support multiple simultaneous LogObservers listening different crawlers --- docs/topics/logging.rst | 13 +++++++++--- scrapy/crawler.py | 5 +++++ scrapy/log.py | 30 +++++++++++++++------------ tests/test_log.py | 46 ++++++++++++++++++++++++++++++++++------- 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/docs/topics/logging.rst b/docs/topics/logging.rst index 1a9e975d8..819884ac2 100644 --- a/docs/topics/logging.rst +++ b/docs/topics/logging.rst @@ -10,7 +10,11 @@ logging`_ but this may change in the future. .. _Twisted logging: http://twistedmatrix.com/projects/core/documentation/howto/logging.html -The logging service must be explicitly started through the :func:`scrapy.log.start` function. +The logging service must be explicitly started through the +:func:`scrapy.log.start` function to catch the top level Scrapy's log messages. +On top of that, each crawler has its own independent log observer +(automatically attached when it's created) that intercepts its spider's log +messages. .. _topics-logging-levels: @@ -55,8 +59,11 @@ scrapy.log module .. function:: start(logfile=None, loglevel=None, logstdout=None) - Start the logging facility. This must be called before actually logging any - messages. Otherwise, messages logged before this call will get lost. + Start the top level Scrapy logger. This must be called before actually + logging any top level messages (those logged using this module's + :func:`~scrapy.log.msg` function instead of the :meth:`Spider.log + ` method). Otherwise, messages logged before this + call will get lost. :param logfile: the file path to use for logging output. If omitted, the :setting:`LOG_FILE` setting will be used. If both are ``None``, the log diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 56823166b..597bb2e9d 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -128,6 +128,7 @@ class CrawlerProcess(CrawlerRunner): signame = signal_names[signum] log.msg(format='Received %(signame)s twice, forcing unclean shutdown', level=log.INFO, signame=signame) + self._stop_logging() reactor.callFromThread(self._stop_reactor) def start(self, stop_after_crawl=True): @@ -135,6 +136,7 @@ class CrawlerProcess(CrawlerRunner): self._start_reactor(stop_after_crawl) def _start_logging(self): + self.log_observer = log.start_from_settings(self.settings) log.scrapy_info(self.settings) def _start_reactor(self, stop_after_crawl=True): @@ -149,6 +151,9 @@ class CrawlerProcess(CrawlerRunner): reactor.addSystemEventTrigger('before', 'shutdown', self.stop) reactor.run(installSignalHandlers=False) # blocking call + def _stop_logging(self): + self.log_observer.stop() + def _stop_reactor(self, _=None): try: reactor.stop() diff --git a/scrapy/log.py b/scrapy/log.py index 1f32003e7..aa53e3574 100644 --- a/scrapy/log.py +++ b/scrapy/log.py @@ -35,15 +35,16 @@ class ScrapyFileLogObserver(log.FileLogObserver): def __init__(self, f, level=INFO, encoding='utf-8', crawler=None): self.level = level self.encoding = encoding + self.crawler = crawler if crawler: - self.crawler = crawler self.emit = self._emit_with_crawler else: self.emit = self._emit log.FileLogObserver.__init__(self, f) def _emit(self, eventDict): - ev = _adapt_eventdict(eventDict, self.level, self.encoding) + ev = _adapt_eventdict(eventDict, self.level, self.encoding, + self.crawler) if ev is not None: log.FileLogObserver.emit(self, ev) return ev @@ -55,7 +56,8 @@ class ScrapyFileLogObserver(log.FileLogObserver): sname = 'log_count/%s' % level_names.get(level, level) self.crawler.stats.inc_value(sname) -def _adapt_eventdict(eventDict, log_level=INFO, encoding='utf-8', prepend_level=True): +def _adapt_eventdict(eventDict, log_level=INFO, encoding='utf-8', + crawler=None, prepend_level=True): """Adapt Twisted log eventDict making it suitable for logging with a Scrapy log observer. It may return None to indicate that the event should be ignored by a Scrapy log observer. @@ -78,6 +80,12 @@ def _adapt_eventdict(eventDict, log_level=INFO, encoding='utf-8', prepend_level= spider = ev.get('spider') if spider: ev['system'] = unicode_to_str(spider.name, encoding) + if crawler and (not spider or spider.crawler is not crawler): + # ignore events not triggered by own spiders in crawlers' observers + return + if not crawler and spider: + # ignore spiders' events in observers without crawler + return lvlname = level_names.get(level, 'NOLEVEL') message = ev.get('message') @@ -140,18 +148,14 @@ def start_from_settings(settings, crawler=None): settings['LOG_ENCODING'], crawler) def scrapy_info(settings): - log_observer = start_from_settings(settings) - if log_observer: - msg("Scrapy %s started (bot: %s)" % (scrapy.__version__, - settings['BOT_NAME'])) + msg("Scrapy %s started (bot: %s)" % (scrapy.__version__, + settings['BOT_NAME'])) - msg("Optional features available: %s" % ", ".join(scrapy.optional_features), - level=INFO) + msg("Optional features available: %s" % ", ".join(scrapy.optional_features), + level=INFO) - d = dict(overridden_settings(settings)) - msg(format="Overridden settings: %(settings)r", settings=d, level=INFO) - - log_observer.stop() + d = dict(overridden_settings(settings)) + msg(format="Overridden settings: %(settings)r", settings=d, level=INFO) def start_from_crawler(crawler): return start_from_settings(crawler.settings, crawler) diff --git a/tests/test_log.py b/tests/test_log.py index 3263cb42e..113d10004 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -6,6 +6,7 @@ from twisted.trial import unittest from scrapy import log from scrapy.spider import Spider from scrapy.settings import default_settings +from scrapy.utils.test import get_crawler class LogTest(unittest.TestCase): @@ -40,10 +41,10 @@ class ScrapyFileLogObserverTest(unittest.TestCase): log.msg("Hello") self.assertEqual(self.logged(), "[scrapy] INFO: Hello") - def test_msg_spider(self): + def test_msg_ignore_spider(self): spider = Spider("myspider") log.msg("Hello", spider=spider) - self.assertEqual(self.logged(), "[myspider] INFO: Hello") + self.failIf(self.logged()) def test_msg_level1(self): log.msg("Hello", level=log.WARNING) @@ -57,11 +58,6 @@ class ScrapyFileLogObserverTest(unittest.TestCase): log.msg("Hello", level=9999) self.assertEqual(self.logged(), "[scrapy] NOLEVEL: Hello") - def test_msg_level_spider(self): - spider = Spider("myspider") - log.msg("Hello", spider=spider, level=log.WARNING) - self.assertEqual(self.logged(), "[myspider] WARNING: Hello") - def test_msg_encoding(self): log.msg(u"Price: \xa3100") self.assertEqual(self.logged(), "[scrapy] INFO: Price: \xc2\xa3100") @@ -133,5 +129,41 @@ class Latin1ScrapyFileLogObserverTest(ScrapyFileLogObserverTest): # self.assertEqual(self.first_log_line(), "[scrapy] ERROR: \xa3") +class CrawlerScrapyFileLogObserverTest(unittest.TestCase): + + def setUp(self): + self.f = BytesIO() + self.crawler = get_crawler(Spider) + self.spider = self.crawler.spider = self.crawler._create_spider('test') + self.log_observer = log.ScrapyFileLogObserver(self.f, log.INFO, + 'utf-8', self.crawler) + self.log_observer.start() + + def tearDown(self): + self.flushLoggedErrors() + self.log_observer.stop() + + def logged(self): + return self.f.getvalue().strip()[25:] + + def test_msg_basic(self): + log.msg("Hello", spider=self.spider) + self.assertEqual(self.logged(), "[test] INFO: Hello") + + def test_msg_ignore_scrapy_channel(self): + log.msg("Hello") + self.failIf(self.logged()) + + def test_msg_ignore_another_crawler(self): + crawler = get_crawler(Spider) + log.msg("Hello", spider=crawler._create_spider('test')) + self.failIf(self.logged()) + + def test_msg_stats_log(self): + assert self.crawler.stats.get_value('log_count/INFO', 0) == 0 + log.msg("Hello", spider=self.spider) + self.assertEqual(self.crawler.stats.get_value('log_count/INFO'), 1) + + if __name__ == "__main__": unittest.main() From 9cbbfd8b04835c40568b687ef8b13d901db988cb Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Wed, 6 Aug 2014 08:51:12 -0300 Subject: [PATCH 0014/2974] Adjust spiders' utils to new SpiderManager API --- scrapy/commands/fetch.py | 14 +++++------- scrapy/commands/parse.py | 49 +++++++++++++++++++++------------------- scrapy/commands/shell.py | 24 +++++++++++++++----- scrapy/shell.py | 9 +++----- scrapy/utils/spider.py | 18 +++++++++------ 5 files changed, 64 insertions(+), 50 deletions(-) diff --git a/scrapy/commands/fetch.py b/scrapy/commands/fetch.py index 373d323c7..ca9fd57f5 100644 --- a/scrapy/commands/fetch.py +++ b/scrapy/commands/fetch.py @@ -3,9 +3,8 @@ from w3lib.url import is_url from scrapy.command import ScrapyCommand from scrapy.http import Request -from scrapy.spider import Spider from scrapy.exceptions import UsageError -from scrapy.utils.spider import create_spider_for_request +from scrapy.utils.spider import spidercls_for_request, DefaultSpider class Command(ScrapyCommand): @@ -48,12 +47,11 @@ class Command(ScrapyCommand): request = Request(args[0], callback=cb, dont_filter=True) request.meta['handle_httpstatus_all'] = True - crawler = self.crawler_process.create_crawler() - spider = None + spidercls = DefaultSpider + spiders = self.crawler_process.spiders if opts.spider: - spider = crawler.spiders.create(opts.spider) + spidercls = spiders.load(opts.spider) else: - spider = create_spider_for_request(crawler.spiders, request, \ - default_spider=Spider('default')) - crawler.crawl(spider, [request]) + spidercls = spidercls_for_request(spiders, request, spidercls) + self.crawler_process.crawl(spidercls, start_requests=lambda: [request]) self.crawler_process.start() diff --git a/scrapy/commands/parse.py b/scrapy/commands/parse.py index 0867a21a0..01c7fff0a 100644 --- a/scrapy/commands/parse.py +++ b/scrapy/commands/parse.py @@ -5,7 +5,7 @@ from scrapy.http import Request from scrapy.item import BaseItem from scrapy.utils import display from scrapy.utils.conf import arglist_to_dict -from scrapy.utils.spider import iterate_spider_output, create_spider_for_request +from scrapy.utils.spider import iterate_spider_output, spidercls_for_request from scrapy.exceptions import UsageError from scrapy import log @@ -113,41 +113,45 @@ class Command(ScrapyCommand): requests.append(x) return items, requests - def get_callback_from_rules(self, response): - if getattr(self.spider, 'rules', None): - for rule in self.spider.rules: + def get_callback_from_rules(self, spider, response): + if getattr(spider, 'rules', None): + for rule in spider.rules: if rule.link_extractor.matches(response.url) and rule.callback: return rule.callback else: log.msg(format='No CrawlSpider rules found in spider %(spider)r, ' 'please specify a callback to use for parsing', - level=log.ERROR, spider=self.spider.name) + level=log.ERROR, spider=spider.name) - def set_spider(self, url, opts): + def set_spidercls(self, url, opts): + spiders = self.crawler_process.spiders if opts.spider: try: - self.spider = self.pcrawler.spiders.create(opts.spider, **opts.spargs) + self.spidercls = spiders.load(opts.spider) except KeyError: log.msg(format='Unable to find spider: %(spider)s', level=log.ERROR, spider=opts.spider) else: - self.spider = create_spider_for_request(self.pcrawler.spiders, Request(url), **opts.spargs) - if not self.spider: + self.spidercls = spidercls_for_request(spiders, Request(url)) + if not self.spidercls: log.msg(format='Unable to find spider for: %(url)s', level=log.ERROR, url=url) - def start_parsing(self, url, opts): request = Request(url, opts.callback) - request = self.prepare_request(request, opts) + _start_requests = lambda s: [self.prepare_request(s, request, opts)] + self.spidercls.start_requests = _start_requests - self.pcrawler.crawl(self.spider, [request]) + + def start_parsing(self, url, opts): + self.crawler_process.crawl(self.spidercls, **opts.spargs) + self.pcrawler = list(self.crawler_process.crawlers)[0] self.crawler_process.start() if not self.first_response: - log.msg(format='No response downloaded for: %(request)s', - level=log.ERROR, request=request) + log.msg(format='No response downloaded for: %(url)s', + level=log.ERROR, url=url) - def prepare_request(self, request, opts): + def prepare_request(self, spider, request, opts): def callback(response): # memorize first request if not self.first_response: @@ -157,17 +161,17 @@ class Command(ScrapyCommand): cb = response.meta['_callback'] if not cb: if opts.rules and self.first_response == response: - cb = self.get_callback_from_rules(response) + cb = self.get_callback_from_rules(spider, response) else: cb = 'parse' if not callable(cb): - cb_method = getattr(self.spider, cb, None) + cb_method = getattr(spider, cb, None) if callable(cb_method): cb = cb_method else: log.msg(format='Cannot find callback %(callback)r in spider: %(spider)s', - callback=callback, spider=self.spider.name, level=log.ERROR) + callback=callback, spider=spider.name, level=log.ERROR) return # parse items and requests @@ -177,7 +181,7 @@ class Command(ScrapyCommand): if opts.pipelines: itemproc = self.pcrawler.engine.scraper.itemproc for item in items: - itemproc.process_item(item, self.spider) + itemproc.process_item(item, spider) self.add_items(depth, items) self.add_requests(depth, requests) @@ -207,10 +211,9 @@ class Command(ScrapyCommand): else: url = args[0] - # prepare spider - self.pcrawler = self.crawler_process.create_crawler() - self.set_spider(url, opts) + # prepare spidercls + self.set_spidercls(url, opts) - if self.spider and opts.depth > 0: + if self.spidercls and opts.depth > 0: self.start_parsing(url, opts) self.print_results(opts) diff --git a/scrapy/commands/shell.py b/scrapy/commands/shell.py index ab170e665..e4d32c314 100644 --- a/scrapy/commands/shell.py +++ b/scrapy/commands/shell.py @@ -8,6 +8,9 @@ from threading import Thread from scrapy.command import ScrapyCommand from scrapy.shell import Shell +from scrapy.http import Request +from scrapy import log +from scrapy.utils.spider import spidercls_for_request, DefaultSpider class Command(ScrapyCommand): @@ -38,18 +41,27 @@ class Command(ScrapyCommand): pass def run(self, args, opts): - crawler = self.crawler_process.create_crawler() - url = args[0] if args else None - spider = crawler.spiders.create(opts.spider) if opts.spider else None + spiders = self.crawler_process.spiders - self.crawler_process.start_crawling() + spidercls = DefaultSpider + if opts.spider: + spidercls = spiders.load(opts.spider) + elif url: + spidercls = spidercls_for_request(spiders, Request(url), + spidercls, log_multiple=True) + crawler = self.crawler_process._create_logged_crawler(spidercls) + crawler.engine = crawler._create_engine() + crawler.engine.start() + + self.crawler_process._start_logging() self._start_crawler_thread() shell = Shell(crawler, update_vars=self.update_vars, code=opts.code) - shell.start(url=url, spider=spider) + shell.start(url=url) def _start_crawler_thread(self): - t = Thread(target=self.crawler_process.start_reactor) + t = Thread(target=self.crawler_process._start_reactor, + kwargs={'stop_after_crawl': False}) t.daemon = True t.start() diff --git a/scrapy/shell.py b/scrapy/shell.py index 74eaef40f..6c48ef186 100644 --- a/scrapy/shell.py +++ b/scrapy/shell.py @@ -21,7 +21,6 @@ from scrapy.spider import Spider from scrapy.utils.console import start_python_console from scrapy.utils.misc import load_object from scrapy.utils.response import open_in_browser -from scrapy.utils.spider import create_spider_for_request class Shell(object): @@ -67,11 +66,9 @@ class Shell(object): return self.spider if spider is None: - spider = create_spider_for_request(self.crawler.spiders, - request, - Spider('default'), - log_multiple=True) - spider.set_crawler(self.crawler) + spider = self.crawler.spider or self.crawler._create_spider() + + self.crawler.spider = spider self.crawler.engine.open_spider(spider, close_if_idle=False) self.spider = spider return spider diff --git a/scrapy/utils/spider.py b/scrapy/utils/spider.py index 4e43bc13f..b81cf2b9b 100644 --- a/scrapy/utils/spider.py +++ b/scrapy/utils/spider.py @@ -4,6 +4,7 @@ import six from scrapy import log from scrapy.item import BaseItem +from scrapy.spider import Spider from scrapy.utils.misc import arg_to_iter @@ -25,21 +26,21 @@ def iter_spider_classes(module): getattr(obj, 'name', None): yield obj -def create_spider_for_request(spidermanager, request, default_spider=None, \ - log_none=False, log_multiple=False, **spider_kwargs): - """Create a spider to handle the given Request. +def spidercls_for_request(spidermanager, request, default_spidercls=None, + log_none=False, log_multiple=False): + """Return a spider class that handles the given Request. This will look for the spiders that can handle the given request (using - the spider manager) and return a (new) Spider if (and only if) there is + the spider manager) and return a Spider class if (and only if) there is only one Spider able to handle the Request. If multiple spiders (or no spider) are found, it will return the - default_spider passed. It can optionally log if multiple or no spiders + default_spidercls passed. It can optionally log if multiple or no spiders are found. """ snames = spidermanager.find_by_request(request) if len(snames) == 1: - return spidermanager.create(snames[0], **spider_kwargs) + return spidermanager.load(snames[0]) if len(snames) > 1 and log_multiple: log.msg(format='More than one spider can handle: %(request)s - %(snames)s', @@ -49,5 +50,8 @@ def create_spider_for_request(spidermanager, request, default_spider=None, \ log.msg(format='Unable to find spider that handles: %(request)s', level=log.ERROR, request=request) - return default_spider + return default_spidercls + +class DefaultSpider(Spider): + name = 'default' From c90977ca98dd51b93d91739115d843f44e6a8a94 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Fri, 8 Aug 2014 06:15:20 -0300 Subject: [PATCH 0015/2974] Drop support for scrapy.project.crawler (And scrapy.stats consequently) --- conftest.py | 2 +- docs/faq.rst | 31 ------------------------------- docs/topics/shell.rst | 2 +- scrapy/crawler.py | 15 --------------- scrapy/project.py | 18 +++++++++++------- scrapy/shell.py | 5 ++--- scrapy/spider.py | 2 +- scrapy/stats.py | 13 +++++++------ tests/test_engine.py | 2 -- 9 files changed, 23 insertions(+), 67 deletions(-) diff --git a/conftest.py b/conftest.py index 9f9a5bca7..aa27ddd2b 100644 --- a/conftest.py +++ b/conftest.py @@ -4,7 +4,7 @@ from twisted.python import log from scrapy import optional_features -collect_ignore = ["scrapy/stats.py"] +collect_ignore = ["scrapy/stats.py", "scrapy/project.py"] if 'django' not in optional_features: collect_ignore.append("tests/test_djangoitem/models.py") diff --git a/docs/faq.rst b/docs/faq.rst index 47bfede71..1d6c56d97 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -280,37 +280,6 @@ I'm scraping a XML document and my XPath selector doesn't return any items You may need to remove namespaces. See :ref:`removing-namespaces`. - -I'm getting an error: "cannot import name crawler" --------------------------------------------------- - -This is caused by Scrapy changes due to the singletons removal. The error is -most likely raised by a module (extension, middleware, pipeline or spider) in -your Scrapy project that imports ``crawler`` from ``scrapy.project``. For -example:: - - from scrapy.project import crawler - - class SomeExtension(object): - def __init__(self): - self.crawler = crawler - # ... - -This way to access the crawler object is deprecated, the code should be ported -to use ``from_crawler`` class method, for example:: - - class SomeExtension(object): - - @classmethod - def from_crawler(cls, crawler): - o = cls() - o.crawler = crawler - return o - -Scrapy command line tool has some backwards compatibility in place to support -the old import mechanism (with a deprecation warning), but this mechanism may -not work if you use Scrapy differently (for example, as a library). - .. _user agents: http://en.wikipedia.org/wiki/User_agent .. _LIFO: http://en.wikipedia.org/wiki/LIFO .. _DFO order: http://en.wikipedia.org/wiki/Depth-first_search diff --git a/docs/topics/shell.rst b/docs/topics/shell.rst index 37268c3ca..5c1cfbd47 100644 --- a/docs/topics/shell.rst +++ b/docs/topics/shell.rst @@ -186,7 +186,7 @@ Here's an example of how you would call it from your spider:: # We want to inspect one specific response. if ".org" in response.url: from scrapy.shell import inspect_response - inspect_response(response) + inspect_response(response, self) # Rest of parsing code. diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 597bb2e9d..352cff6e5 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -31,18 +31,6 @@ class Crawler(object): self.spider = None self.engine = None - def install(self): - # TODO: remove together with scrapy.project.crawler usage - import scrapy.project - assert not hasattr(scrapy.project, 'crawler'), "crawler already installed" - scrapy.project.crawler = self - - def uninstall(self): - # TODO: remove together with scrapy.project.crawler usage - import scrapy.project - assert hasattr(scrapy.project, 'crawler'), "crawler not installed" - del scrapy.project.crawler - @defer.inlineCallbacks def crawl(self, *args, **kwargs): assert not self.crawling, "Crawling already taking place" @@ -84,9 +72,6 @@ class CrawlerRunner(object): crawler = self._create_logged_crawler(spidercls) self.crawlers.add(crawler) - crawler.install() - crawler.signals.connect(crawler.uninstall, signals.engine_stopped) - d = crawler.crawl(*args, **kwargs) self.crawl_deferreds.add(d) return d diff --git a/scrapy/project.py b/scrapy/project.py index bbe947761..d8973a6c7 100644 --- a/scrapy/project.py +++ b/scrapy/project.py @@ -1,13 +1,17 @@ -""" ---------- WARNING: THIS MODULE IS DEPRECATED ----------- -This module is deprecated. If you want to get the Scrapy crawler from your -extension, middleware or pipeline implement the `from_crawler` class method. +""" +Obsolete module, kept for giving a meaningful error message when trying to +import. +""" + +raise ImportError("""scrapy.project usage has become obsolete. + +If you want to get the Scrapy crawler from your extension, middleware or +pipeline implement the `from_crawler` class method (or look up for extending +components that have already done it, such as spiders). For example: @classmethod def from_crawler(cls, crawler): - return cls(crawler) - -""" + return cls(crawler)""") diff --git a/scrapy/shell.py b/scrapy/shell.py index 6c48ef186..8f87fcb41 100644 --- a/scrapy/shell.py +++ b/scrapy/shell.py @@ -123,10 +123,9 @@ class Shell(object): return isinstance(value, self.relevant_classes) -def inspect_response(response, spider=None): +def inspect_response(response, spider): """Open a shell to inspect the given response""" - from scrapy.project import crawler - Shell(crawler).start(response=response, spider=spider) + Shell(spider.crawler).start(response=response) def _request_deferred(request): diff --git a/scrapy/spider.py b/scrapy/spider.py index df367b700..943925042 100644 --- a/scrapy/spider.py +++ b/scrapy/spider.py @@ -93,6 +93,6 @@ class ObsoleteClass(object): raise AttributeError(self.message) spiders = ObsoleteClass(""" -"from scrapy.spider import spiders" no longer works - use "from scrapy.project import crawler" and then access crawler.spiders attribute" +"from scrapy.spider import spiders" no longer works - use "from scrapy.spidermanager import SpiderManager" and instantiate it with your project settings" """) diff --git a/scrapy/stats.py b/scrapy/stats.py index b8128dfc2..710601430 100644 --- a/scrapy/stats.py +++ b/scrapy/stats.py @@ -1,7 +1,8 @@ -from scrapy.project import crawler -stats = crawler.stats -import warnings -from scrapy.exceptions import ScrapyDeprecationWarning -warnings.warn("Module `scrapy.stats` is deprecated, use `crawler.stats` attribute instead", - ScrapyDeprecationWarning, stacklevel=2) +""" +Obsolete module, kept for giving a meaningful error message when trying to +import. +""" + +raise ImportError("scrapy.stats usage has become obsolete, use " + "`crawler.stats` attribute instead") diff --git a/tests/test_engine.py b/tests/test_engine.py index 244d339ef..67fb8ae79 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -93,7 +93,6 @@ class CrawlerRun(object): dispatcher.connect(self.record_signal, signal) self.crawler = get_crawler(TestSpider) - self.crawler.install() self.crawler.signals.connect(self.item_scraped, signals.item_scraped) self.crawler.signals.connect(self.request_scheduled, signals.request_scheduled) self.crawler.signals.connect(self.response_downloaded, signals.response_downloaded) @@ -109,7 +108,6 @@ class CrawlerRun(object): for name, signal in vars(signals).items(): if not name.startswith('_'): disconnect_all(signal) - self.crawler.uninstall() self.deferred.callback(None) def geturl(self, path): From 419026615f040d6277e311b9307a3950f8532468 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 14 Aug 2014 09:19:41 -0300 Subject: [PATCH 0016/2974] Deprecate Crawler.spiders attribute --- docs/topics/api.rst | 6 ------ scrapy/crawler.py | 18 ++++++++++++++---- tests/py3-ignores.txt | 1 + tests/test_crawler.py | 24 ++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 tests/test_crawler.py diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 0329e2a8f..03a0b4124 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -76,12 +76,6 @@ how you :ref:`configure the downloader middlewares For an introduction on extensions and a list of available extensions on Scrapy see :ref:`topics-extensions`. - .. attribute:: spiders - - The spider manager which takes care of loading spiders. - - Most extensions won't need to access this attribute. - .. attribute:: engine The execution engine, which coordinates the core crawling logic diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 352cff6e5..52e57fe83 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -1,5 +1,6 @@ import six import signal +import warnings from twisted.internet import reactor, defer @@ -7,6 +8,7 @@ from scrapy.core.engine import ExecutionEngine from scrapy.resolver import CachingThreadedResolver from scrapy.extension import ExtensionManager from scrapy.signalmanager import SignalManager +from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.utils.ossignal import install_shutdown_handlers, signal_names from scrapy.utils.misc import load_object from scrapy import log, signals @@ -23,14 +25,22 @@ class Crawler(object): self.logformatter = lf_cls.from_crawler(self) self.extensions = ExtensionManager.from_crawler(self) - # Attribute kept for backward compatibility (Use CrawlerRunner.spiders) - spman_cls = load_object(self.settings['SPIDER_MANAGER_CLASS']) - self.spiders = spman_cls.from_settings(self.settings) - self.crawling = False self.spider = None self.engine = None + @property + def spiders(self): + if not hasattr(self, '_spiders'): + warnings.warn("Crawler.spiders is deprecated, use " + "CrawlerRunner.spiders or instantiate " + "scrapy.spidermanager.SpiderManager with your " + "settings.", + category=ScrapyDeprecationWarning, stacklevel=2) + spman_cls = load_object(self.settings['SPIDER_MANAGER_CLASS']) + self._spiders = spman_cls.from_settings(self.settings) + return self._spiders + @defer.inlineCallbacks def crawl(self, *args, **kwargs): assert not self.crawling, "Crawling already taking place" diff --git a/tests/py3-ignores.txt b/tests/py3-ignores.txt index ef88eab7e..f3c667cd0 100644 --- a/tests/py3-ignores.txt +++ b/tests/py3-ignores.txt @@ -8,6 +8,7 @@ tests/test_contrib_exporter.py tests/test_contrib_linkextractors.py tests/test_contrib_loader.py tests/test_crawl.py +tests/test_crawler.py tests/test_djangoitem/__init__.py tests/test_downloader_handlers.py tests/test_downloadermiddleware_ajaxcrawlable.py diff --git a/tests/test_crawler.py b/tests/test_crawler.py new file mode 100644 index 000000000..55381c030 --- /dev/null +++ b/tests/test_crawler.py @@ -0,0 +1,24 @@ +import warnings +import unittest + +from scrapy.crawler import Crawler +from scrapy.settings import Settings +from scrapy.utils.spider import DefaultSpider +from scrapy.utils.misc import load_object + + +class CrawlerTestCase(unittest.TestCase): + + def setUp(self): + self.crawler = Crawler(DefaultSpider, Settings()) + + def test_deprecated_attribute_spiders(self): + with warnings.catch_warnings(record=True) as w: + spiders = self.crawler.spiders + self.assertEqual(len(w), 1) + self.assertIn("Crawler.spiders", str(w[0].message)) + sm_cls = load_object(self.crawler.settings['SPIDER_MANAGER_CLASS']) + self.assertIsInstance(spiders, sm_cls) + + self.crawler.spiders + self.assertEqual(len(w), 1, "Warn deprecated access only once") From 3547ca6e618e19dda86ad1505323b24e82d317bd Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 14 Aug 2014 11:50:33 -0300 Subject: [PATCH 0017/2974] Add example on running spiders outside projects --- docs/topics/practices.rst | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/topics/practices.rst b/docs/topics/practices.rst index e84478d3c..b188ee562 100644 --- a/docs/topics/practices.rst +++ b/docs/topics/practices.rst @@ -32,19 +32,41 @@ project as example. from scrapy.crawler import CrawlerRunner from scrapy.utils.project import get_project_settings - # If you aren't inside a Scrapy project, you could use an instance of the - # Settings class in scrapy.settings instead of the configuration returned - # by get_project_settings runner = CrawlerRunner(get_project_settings()) - # 'followall' is the name of one of the spiders of the project. If you - # aren't working in a Scrapy project, use the spider class as first - # argument instead of its name (or set the SPIDER_MODULES setting so Scrapy - # knows where to look at) + # 'followall' is the name of one of the spiders of the project. d = runner.crawl('followall', domain='scrapinghub.com') d.addBoth(lambda _: reactor.stop()) reactor.run() # the script will block here until the crawling is finished +Running spiders outside projects it's not much different. You have to create a +generic :class:`~scrapy.settings.Settings` object and populate it as needed +(See :ref:`topics-settings-ref` for the available settings), instead of using +the configuration returned by `get_project_settings`. + +Spiders can still be referenced by their name if :setting:`SPIDER_MODULES` is +set with the modules where Scrapy should look for spiders. Otherwise, passing +the spider class as first argument in the :meth:`CrawlerRunner.crawl +` method is enough. + +:: + + from twisted.internet import reactor + from scrapy.spider import Spider + from scrapy.crawler import CrawlerRunner + from scrapy.settings import Settings + + class MySpider(Spider): + # Your spider definition + ... + + settings = Settings({'USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)'}) + runner = CrawlerRunner(settings) + + d = runner.crawl(MySpider) + d.addBoth(lambda _: reactor.stop()) + reactor.run() # the script will block here until the crawling is finished + .. seealso:: `Twisted Reactor Overview`_. Running multiple spiders in the same process From 70f2010db17b3796c3edce9c6ca3e00c092d6064 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 14 Aug 2014 11:59:25 -0300 Subject: [PATCH 0018/2974] Change error type when updating frozen settings --- scrapy/settings/__init__.py | 10 +++++++--- tests/test_settings/__init__.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/scrapy/settings/__init__.py b/scrapy/settings/__init__.py index bbe8ef481..938b93564 100644 --- a/scrapy/settings/__init__.py +++ b/scrapy/settings/__init__.py @@ -88,7 +88,7 @@ class Settings(object): return dict(value) def set(self, name, value, priority='project'): - assert not self.frozen, "Trying to modify an immutable Settings object" + self._assert_mutability() if isinstance(priority, six.string_types): priority = SETTINGS_PRIORITIES[priority] if name not in self.attributes: @@ -97,18 +97,22 @@ class Settings(object): self.attributes[name].set(value, priority) def setdict(self, values, priority='project'): - assert not self.frozen, "Trying to modify an immutable Settings object" + self._assert_mutability() for name, value in six.iteritems(values): self.set(name, value, priority) def setmodule(self, module, priority='project'): - assert not self.frozen, "Trying to modify an immutable Settings object" + self._assert_mutability() if isinstance(module, six.string_types): module = import_module(module) for key in dir(module): if key.isupper(): self.set(key, getattr(module, key), priority) + def _assert_mutability(self): + if self.frozen: + raise TypeError("Trying to modify an immutable Settings object") + def copy(self): return copy.deepcopy(self) diff --git a/tests/test_settings/__init__.py b/tests/test_settings/__init__.py index c7e0914d6..38797ad45 100644 --- a/tests/test_settings/__init__.py +++ b/tests/test_settings/__init__.py @@ -213,7 +213,7 @@ class SettingsTest(unittest.TestCase): def test_freeze(self): self.settings.freeze() - with self.assertRaises(AssertionError) as cm: + with self.assertRaises(TypeError) as cm: self.settings.set('TEST_BOOL', False) self.assertEqual(str(cm.exception), "Trying to modify an immutable Settings object") From 6339864f95d4ecaed5e428b342277dce9457d954 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 14 Aug 2014 12:32:37 -0300 Subject: [PATCH 0019/2974] Minor refactor in the docs and functions used in the shell command --- scrapy/commands/shell.py | 6 +++++- scrapy/crawler.py | 8 +++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/scrapy/commands/shell.py b/scrapy/commands/shell.py index e4d32c314..e2ef1545e 100644 --- a/scrapy/commands/shell.py +++ b/scrapy/commands/shell.py @@ -50,11 +50,15 @@ class Command(ScrapyCommand): elif url: spidercls = spidercls_for_request(spiders, Request(url), spidercls, log_multiple=True) + + # The crawler is created this way since the Shell manually handles the + # crawling engine, so the set up in the crawl method won't work crawler = self.crawler_process._create_logged_crawler(spidercls) + # The Shell class needs a persistent engine in the crawler crawler.engine = crawler._create_engine() crawler.engine.start() - self.crawler_process._start_logging() + self.crawler_process.start(start_reactor=False) self._start_crawler_thread() shell = Shell(crawler, update_vars=self.update_vars, code=opts.code) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 52e57fe83..e0524021a 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -126,13 +126,11 @@ class CrawlerProcess(CrawlerRunner): self._stop_logging() reactor.callFromThread(self._stop_reactor) - def start(self, stop_after_crawl=True): - self._start_logging() - self._start_reactor(stop_after_crawl) - - def _start_logging(self): + def start(self, stop_after_crawl=True, start_reactor=True): self.log_observer = log.start_from_settings(self.settings) log.scrapy_info(self.settings) + if start_reactor: + self._start_reactor(stop_after_crawl) def _start_reactor(self, stop_after_crawl=True): if stop_after_crawl: From 6f50cf55a4ded1de1238a71b4dac7ebd85f07a75 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Wed, 13 Aug 2014 21:39:31 -0300 Subject: [PATCH 0020/2974] fix IPython shell scope issue and load IPython user config --- scrapy/utils/console.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scrapy/utils/console.py b/scrapy/utils/console.py index 5dd4539ed..924501002 100644 --- a/scrapy/utils/console.py +++ b/scrapy/utils/console.py @@ -13,16 +13,16 @@ def start_python_console(namespace=None, noipython=False, banner=''): raise ImportError() try: - try: - from IPython.terminal import embed - except ImportError: - from IPython.frontend.terminal import embed - sh = embed.InteractiveShellEmbed(banner1=banner) + from IPython.terminal.embed import InteractiveShellEmbed + from IPython.terminal.ipapp import load_default_config except ImportError: - from IPython.Shell import IPShellEmbed - sh = IPShellEmbed(banner=banner) + from IPython.frontend.terminal.embed import InteractiveShellEmbed + from IPython.frontend.terminal.ipapp import load_default_config - sh(global_ns={}, local_ns=namespace) + config = load_default_config() + shell = InteractiveShellEmbed( + banner1=banner, user_ns=namespace, config=config) + shell() except ImportError: import code try: # readline module is only available on unix systems From d49766a6ac2b2685f66d69a93e4726b6a392ab2f Mon Sep 17 00:00:00 2001 From: Uyounghz Date: Thu, 28 Aug 2014 19:58:58 +0800 Subject: [PATCH 0021/2974] Duplicate comma in request-response.rst --- docs/topics/request-response.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index 192f4caee..18a0ae23f 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -51,7 +51,7 @@ Request objects :param body: the request body. If a ``unicode`` is passed, then it's encoded to ``str`` using the `encoding` passed (which defaults to ``utf-8``). If - ``body`` is not given,, an empty string is stored. Regardless of the + ``body`` is not given, an empty string is stored. Regardless of the type of this argument, the final value stored will be a ``str`` (never ``unicode`` or ``None``). :type body: str or unicode From e4689556f01fd74502584c85cba7c72c2b57640d Mon Sep 17 00:00:00 2001 From: yakxxx Date: Thu, 28 Aug 2014 18:47:49 +0200 Subject: [PATCH 0022/2974] SgmlLinkExtractor - fix for parsing tag with Unicode present --- scrapy/contrib/linkextractors/sgml.py | 1 + tests/test_contrib_linkextractors.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/scrapy/contrib/linkextractors/sgml.py b/scrapy/contrib/linkextractors/sgml.py index 9a55c1581..3eb5fd91f 100644 --- a/scrapy/contrib/linkextractors/sgml.py +++ b/scrapy/contrib/linkextractors/sgml.py @@ -67,6 +67,7 @@ class BaseSgmlLinkExtractor(SGMLParser): SGMLParser.reset(self) self.links = [] self.base_url = None + self.current_link = None def unknown_starttag(self, tag, attrs): if tag == 'base': diff --git a/tests/test_contrib_linkextractors.py b/tests/test_contrib_linkextractors.py index 3617cb810..3902d4c50 100644 --- a/tests/test_contrib_linkextractors.py +++ b/tests/test_contrib_linkextractors.py @@ -284,6 +284,17 @@ class SgmlLinkExtractorTestCase(unittest.TestCase): [Link(url='http://example.org/foo', text=u'>\u4eac<\u4e1c', fragment='', nofollow=False)]) + def test_area_tag_with_unicode_present(self): + body = """\xbe\xa9""" + response = HtmlResponse("http://example.org", body=body, encoding='utf-8') + lx = self.extractor_cls() + lx.extract_links(response) + lx.extract_links(response) + lx.extract_links(response) + self.assertEqual(lx.extract_links(response), + [Link(url='http://example.org/foo', text=u'', + fragment='', nofollow=False)]) + def test_encoded_url(self): body = """
BinB""" response = HtmlResponse("http://known.fm/AC%2FDC/", body=body, encoding='utf8') From d92914d2978c0fc3acc27c832184eb98b5f6c3cc Mon Sep 17 00:00:00 2001 From: Adam Donahue Date: Thu, 28 Aug 2014 20:30:50 -0400 Subject: [PATCH 0023/2974] Fix typo --- scrapy/utils/trackref.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/utils/trackref.py b/scrapy/utils/trackref.py index 252c40632..f250aeefa 100644 --- a/scrapy/utils/trackref.py +++ b/scrapy/utils/trackref.py @@ -2,7 +2,7 @@ references to live object instances. If you want live objects for a particular class to be tracked, you only have to -subclass form object_ref (instead of object). +subclass from object_ref (instead of object). About performance: This library has a minimal performance impact when enabled, and no performance penalty at all when disabled (as object_ref becomes just an From 4932ec43a76a209c6e5d32440e3bc5b41cfbfe0d Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Wed, 13 Aug 2014 01:41:16 -0300 Subject: [PATCH 0024/2974] Per-spider settings implementation --- scrapy/crawler.py | 7 ++++++- scrapy/settings/__init__.py | 1 + scrapy/spider.py | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index e0524021a..eb174a7d8 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -96,7 +96,12 @@ class CrawlerRunner(object): def _create_crawler(self, spidercls): if isinstance(spidercls, six.string_types): spidercls = self.spiders.load(spidercls) - crawler = Crawler(spidercls, self.settings.frozencopy()) + + crawler_settings = self.settings.copy() + spidercls.update_settings(crawler_settings) + crawler_settings.freeze() + + crawler = Crawler(spidercls, crawler_settings) return crawler def stop(self): diff --git a/scrapy/settings/__init__.py b/scrapy/settings/__init__.py index 938b93564..af0d0dff1 100644 --- a/scrapy/settings/__init__.py +++ b/scrapy/settings/__init__.py @@ -15,6 +15,7 @@ SETTINGS_PRIORITIES = { 'default': 0, 'command': 10, 'project': 20, + 'spider': 30, 'cmdline': 40, } diff --git a/scrapy/spider.py b/scrapy/spider.py index 943925042..0b13d2ee6 100644 --- a/scrapy/spider.py +++ b/scrapy/spider.py @@ -20,6 +20,7 @@ class Spider(object_ref): """ name = None + custom_settings = {} def __init__(self, name=None, **kwargs): if name is not None: @@ -66,6 +67,10 @@ class Spider(object_ref): def parse(self, response): raise NotImplementedError + @classmethod + def update_settings(cls, settings): + settings.setdict(cls.custom_settings, priority='spider') + @classmethod def handles_request(cls, request): return url_is_from_spider(request.url, cls) From 9ef3972cfbe5badaff52b75883c1cb889dcd501c Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Wed, 13 Aug 2014 01:41:50 -0300 Subject: [PATCH 0025/2974] Per-spider settings tests --- tests/test_spider.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_spider.py b/tests/test_spider.py index 148a872dd..585b4b53b 100644 --- a/tests/test_spider.py +++ b/tests/test_spider.py @@ -10,6 +10,7 @@ except ImportError: from scrapy import signals from scrapy.spider import Spider, BaseSpider +from scrapy.settings import Settings from scrapy.http import Request, Response, TextResponse, XmlResponse, HtmlResponse from scrapy.contrib.spiders.init import InitSpider from scrapy.contrib.spiders import CrawlSpider, Rule, XMLFeedSpider, \ @@ -92,6 +93,16 @@ class SpiderTest(unittest.TestCase): spider=spider, reason=None) self.assertTrue(spider.closed_called) + def test_update_settings(self): + spider_settings = {'TEST1': 'spider', 'TEST2': 'spider'} + project_settings = {'TEST1': 'project', 'TEST3': 'project'} + self.spider_class.custom_settings = spider_settings + settings = Settings(project_settings, priority='project') + + self.spider_class.update_settings(settings) + self.assertEqual(settings.get('TEST1'), 'spider') + self.assertEqual(settings.get('TEST2'), 'spider') + self.assertEqual(settings.get('TEST3'), 'project') class InitSpiderTest(SpiderTest): From 16e62e9c9bffc418af0b9534d5f05879281dcf9c Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Wed, 13 Aug 2014 01:42:34 -0300 Subject: [PATCH 0026/2974] Per-spider settings documentation --- docs/topics/api.rst | 1 + docs/topics/settings.rst | 20 ++++++++++++++------ docs/topics/spiders.rst | 9 +++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 03a0b4124..57b8ee0cf 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -172,6 +172,7 @@ Settings API 'default': 0, 'command': 10, 'project': 20, + 'spider': 30, 'cmdline': 40, } diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index e0c432cb3..89ee7605a 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -36,9 +36,10 @@ different precedence. Here is the list of them in decreasing order of precedence: 1. Command line options (most precedence) - 2. Project settings module - 3. Default settings per-command - 4. Default global settings (less precedence) + 2. Settings per-spider + 3. Project settings module + 4. Default settings per-command + 5. Default global settings (less precedence) The population of these settings sources is taken care of internally, but a manual handling is possible using API calls. See the @@ -59,14 +60,21 @@ Example:: scrapy crawl myspider -s LOG_FILE=scrapy.log -2. Project settings module +2. Settings per-spider +---------------------- + +Spiders (See the :ref:`topics-spiders` chapter for reference) can define their +own settings that will take precedence and override the project ones. They can +do so by setting their :attr:`scrapy.spider.Spider.custom_settings` attribute. + +3. Project settings module -------------------------- The project settings module is the standard configuration file for your Scrapy project. It's where most of your custom settings will be populated. For example:: ``myproject.settings``. -3. Default settings per-command +4. Default settings per-command ------------------------------- Each :doc:`Scrapy tool ` command can have its own default @@ -74,7 +82,7 @@ settings, which override the global default settings. Those custom command settings are specified in the ``default_settings`` attribute of the command class. -4. Default global settings +5. Default global settings -------------------------- The global defaults are located in the ``scrapy.settings.default_settings`` diff --git a/docs/topics/spiders.rst b/docs/topics/spiders.rst index de8f988c0..188b51836 100644 --- a/docs/topics/spiders.rst +++ b/docs/topics/spiders.rst @@ -133,6 +133,15 @@ Spider listed here. The subsequent URLs will be generated successively from data contained in the start URLs. + .. attribute:: custom_settings + + A dictionary of settings that will be overridden from the project wide + configuration when running this spider. It must be defined as a class + attribute since the settings are updated before instantiation. + + For a list of available built-in settings see: + :ref:`topics-settings-ref`. + .. attribute:: crawler This attribute is set by the :meth:`from_crawler` class method after From 77bd26a66de7dd80152c89d0c09abf3e7b830612 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 14 Aug 2014 14:01:12 -0300 Subject: [PATCH 0027/2974] Non mutable default in Spider.custom_settings --- scrapy/spider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapy/spider.py b/scrapy/spider.py index 0b13d2ee6..dada6236b 100644 --- a/scrapy/spider.py +++ b/scrapy/spider.py @@ -20,7 +20,7 @@ class Spider(object_ref): """ name = None - custom_settings = {} + custom_settings = None def __init__(self, name=None, **kwargs): if name is not None: @@ -69,7 +69,7 @@ class Spider(object_ref): @classmethod def update_settings(cls, settings): - settings.setdict(cls.custom_settings, priority='spider') + settings.setdict(cls.custom_settings or {}, priority='spider') @classmethod def handles_request(cls, request): From c2592b39fde98ac8ba46c165deb4a1245954f3a1 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 14 Aug 2014 14:17:18 -0300 Subject: [PATCH 0028/2974] Test verifying that CrawlerRunner populates spider class settings --- tests/test_crawler.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/test_crawler.py b/tests/test_crawler.py index 55381c030..0031c8215 100644 --- a/tests/test_crawler.py +++ b/tests/test_crawler.py @@ -1,7 +1,9 @@ import warnings import unittest -from scrapy.crawler import Crawler +from twisted.internet import defer + +from scrapy.crawler import Crawler, CrawlerRunner from scrapy.settings import Settings from scrapy.utils.spider import DefaultSpider from scrapy.utils.misc import load_object @@ -22,3 +24,26 @@ class CrawlerTestCase(unittest.TestCase): self.crawler.spiders self.assertEqual(len(w), 1, "Warn deprecated access only once") + + +class CrawlerRunnerTest(unittest.TestCase): + + def setUp(self): + self.crawler_runner = CrawlerRunner(Settings()) + + @defer.inlineCallbacks + def test_populate_spidercls_settings(self): + spider_settings = {'TEST1': 'spider', 'TEST2': 'spider'} + project_settings = {'TEST1': 'project', 'TEST3': 'project'} + + class CustomSettingsSpider(DefaultSpider): + custom_settings = spider_settings + + self.crawler_runner.settings.setdict(project_settings, + priority='project') + + yield self.crawler_runner.crawl(CustomSettingsSpider) + crawler = self.crawler_runner.crawlers.pop() + self.assertEqual(crawler.settings.get('TEST1'), 'spider') + self.assertEqual(crawler.settings.get('TEST2'), 'spider') + self.assertEqual(crawler.settings.get('TEST3'), 'project') From 1dff1fbf75d8277c52cfdd003966fd7e48b972f1 Mon Sep 17 00:00:00 2001 From: eltermann Date: Tue, 2 Sep 2014 08:33:36 -0300 Subject: [PATCH 0029/2974] Removed unused 'load=False' parameter from walk_modules() --- scrapy/utils/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/utils/misc.py b/scrapy/utils/misc.py index be394eb1d..4cfd35342 100644 --- a/scrapy/utils/misc.py +++ b/scrapy/utils/misc.py @@ -54,7 +54,7 @@ def load_object(path): return obj -def walk_modules(path, load=False): +def walk_modules(path): """Loads a module and all its submodules from a the given module path and returns them. If *any* module throws an exception while importing, that exception is thrown back. From dfca7b3c8001d8489f8bc7c609b6da5fd072b87a Mon Sep 17 00:00:00 2001 From: andrewshir Date: Sat, 6 Sep 2014 18:23:27 +0600 Subject: [PATCH 0030/2974] Fix bug for ".local" host name It's necessary to put new list member in squared brackets (i.e. create new list) to merge lists properly, otherwise we will get result list with character elements instead of string element. --- scrapy/http/cookies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/http/cookies.py b/scrapy/http/cookies.py index cc96cf8ac..3e3dff741 100644 --- a/scrapy/http/cookies.py +++ b/scrapy/http/cookies.py @@ -29,7 +29,7 @@ class CookieJar(object): if not IPV4_RE.search(req_host): hosts = potential_domain_matches(req_host) if req_host.find(".") == -1: - hosts += req_host + ".local" + hosts += [req_host + ".local"] else: hosts = [req_host] From d513b5a542f5581a60a10200e07b58ec294caea5 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Sun, 7 Sep 2014 13:02:39 -0300 Subject: [PATCH 0031/2974] Run root logger in CrawlerProcess creation instead of in its start method --- scrapy/crawler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index eb174a7d8..062f2f9fe 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -115,6 +115,8 @@ class CrawlerProcess(CrawlerRunner): super(CrawlerProcess, self).__init__(settings) install_shutdown_handlers(self._signal_shutdown) self.stopping = False + self.log_observer = log.start_from_settings(self.settings) + log.scrapy_info(settings) def _signal_shutdown(self, signum, _): install_shutdown_handlers(self._signal_kill) @@ -132,8 +134,6 @@ class CrawlerProcess(CrawlerRunner): reactor.callFromThread(self._stop_reactor) def start(self, stop_after_crawl=True, start_reactor=True): - self.log_observer = log.start_from_settings(self.settings) - log.scrapy_info(self.settings) if start_reactor: self._start_reactor(stop_after_crawl) From 51532af69a551292663fa87279eea0317b4ea3a5 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Sun, 7 Sep 2014 13:03:34 -0300 Subject: [PATCH 0032/2974] Erase unneeded flag in CrawlerProcess.start --- scrapy/commands/shell.py | 3 +-- scrapy/crawler.py | 6 +----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/scrapy/commands/shell.py b/scrapy/commands/shell.py index e2ef1545e..ff8c0d156 100644 --- a/scrapy/commands/shell.py +++ b/scrapy/commands/shell.py @@ -58,14 +58,13 @@ class Command(ScrapyCommand): crawler.engine = crawler._create_engine() crawler.engine.start() - self.crawler_process.start(start_reactor=False) self._start_crawler_thread() shell = Shell(crawler, update_vars=self.update_vars, code=opts.code) shell.start(url=url) def _start_crawler_thread(self): - t = Thread(target=self.crawler_process._start_reactor, + t = Thread(target=self.crawler_process.start, kwargs={'stop_after_crawl': False}) t.daemon = True t.start() diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 062f2f9fe..00de7d0c0 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -133,11 +133,7 @@ class CrawlerProcess(CrawlerRunner): self._stop_logging() reactor.callFromThread(self._stop_reactor) - def start(self, stop_after_crawl=True, start_reactor=True): - if start_reactor: - self._start_reactor(stop_after_crawl) - - def _start_reactor(self, stop_after_crawl=True): + def start(self, stop_after_crawl=True): if stop_after_crawl: d = defer.DeferredList(self.crawl_deferreds) if d.called: From 8ddf0811a886bf6a604ae5cf946a7180632b51c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Tue, 2 Sep 2014 18:07:32 -0300 Subject: [PATCH 0033/2974] Correctly detect when all managed crawlers are done in CrawlerRunner --- scrapy/commands/shell.py | 3 ++- scrapy/crawler.py | 44 ++++++++++++++++++++++++---------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/scrapy/commands/shell.py b/scrapy/commands/shell.py index ff8c0d156..7c0706482 100644 --- a/scrapy/commands/shell.py +++ b/scrapy/commands/shell.py @@ -53,7 +53,8 @@ class Command(ScrapyCommand): # The crawler is created this way since the Shell manually handles the # crawling engine, so the set up in the crawl method won't work - crawler = self.crawler_process._create_logged_crawler(spidercls) + crawler = self.crawler_process._create_crawler(spidercls) + self.crawler_process._setup_crawler_logging(crawler) # The Shell class needs a persistent engine in the crawler crawler.engine = crawler._create_engine() crawler.engine.start() diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 00de7d0c0..f18760390 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -76,22 +76,21 @@ class CrawlerRunner(object): smcls = load_object(settings['SPIDER_MANAGER_CLASS']) self.spiders = smcls.from_settings(settings.frozencopy()) self.crawlers = set() - self.crawl_deferreds = set() + self._active = set() def crawl(self, spidercls, *args, **kwargs): - crawler = self._create_logged_crawler(spidercls) - self.crawlers.add(crawler) - - d = crawler.crawl(*args, **kwargs) - self.crawl_deferreds.add(d) - return d - - def _create_logged_crawler(self, spidercls): crawler = self._create_crawler(spidercls) - log_observer = log.start_from_crawler(crawler) - if log_observer: - crawler.signals.connect(log_observer.stop, signals.engine_stopped) - return crawler + self._setup_crawler_logging(crawler) + self.crawlers.add(crawler) + d = crawler.crawl(*args, **kwargs) + self._active.add(d) + + def _done(result): + self.crawlers.discard(crawler) + self._active.discard(d) + return result + + return d.addBoth(_done) def _create_crawler(self, spidercls): if isinstance(spidercls, six.string_types): @@ -100,13 +99,22 @@ class CrawlerRunner(object): crawler_settings = self.settings.copy() spidercls.update_settings(crawler_settings) crawler_settings.freeze() + return Crawler(spidercls, crawler_settings) - crawler = Crawler(spidercls, crawler_settings) - return crawler + def _setup_crawler_logging(self, crawler): + log_observer = log.start_from_crawler(crawler) + if log_observer: + crawler.signals.connect(log_observer.stop, signals.engine_stopped) def stop(self): return defer.DeferredList(c.stop() for c in self.crawlers) + @defer.inlineCallbacks + def join(self): + """Wait for all managed crawlers to complete""" + while self._active: + yield defer.DeferredList(self._active) + class CrawlerProcess(CrawlerRunner): """A class to run multiple scrapy crawlers in a process simultaneously""" @@ -135,13 +143,15 @@ class CrawlerProcess(CrawlerRunner): def start(self, stop_after_crawl=True): if stop_after_crawl: - d = defer.DeferredList(self.crawl_deferreds) + d = self.join() + # Don't start the reactor if the deferreds are already fired if d.called: - # Don't start the reactor if the deferreds are already fired return d.addBoth(lambda _: self._stop_reactor()) + if self.settings.getbool('DNSCACHE_ENABLED'): reactor.installResolver(CachingThreadedResolver(reactor)) + reactor.addSystemEventTrigger('before', 'shutdown', self.stop) reactor.run(installSignalHandlers=False) # blocking call From 99971dc8a883fd94909b7e1087ea3b2b450bd319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Tue, 9 Sep 2014 20:59:07 +0000 Subject: [PATCH 0034/2974] Do not pop the crawler from the managed list --- tests/test_crawler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_crawler.py b/tests/test_crawler.py index 0031c8215..ca3af783e 100644 --- a/tests/test_crawler.py +++ b/tests/test_crawler.py @@ -42,8 +42,9 @@ class CrawlerRunnerTest(unittest.TestCase): self.crawler_runner.settings.setdict(project_settings, priority='project') - yield self.crawler_runner.crawl(CustomSettingsSpider) - crawler = self.crawler_runner.crawlers.pop() + d = self.crawler_runner.crawl(CustomSettingsSpider) + crawler = list(self.crawler_runner.crawlers)[0] + yield d self.assertEqual(crawler.settings.get('TEST1'), 'spider') self.assertEqual(crawler.settings.get('TEST2'), 'spider') self.assertEqual(crawler.settings.get('TEST3'), 'project') From ce180227fa8a056a4128fd1e381c9ae5b83b1be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Wed, 10 Sep 2014 12:04:14 -0300 Subject: [PATCH 0035/2974] Twisted 11.1.0 (precise) can not deal with generators in DeferredList Also create a list of the crawlers before iterating them because crawlers are removed from the set once stopped --- scrapy/crawler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index f18760390..ded09c1c7 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -107,7 +107,7 @@ class CrawlerRunner(object): crawler.signals.connect(log_observer.stop, signals.engine_stopped) def stop(self): - return defer.DeferredList(c.stop() for c in self.crawlers) + return defer.DeferredList([c.stop() for c in list(self.crawlers)]) @defer.inlineCallbacks def join(self): From ec93c0fdccb09c91f1eb560a538e00565cc0f2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Wed, 10 Sep 2014 12:05:18 -0300 Subject: [PATCH 0036/2974] Add the tests changes for previous commit --- tests/test_crawler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_crawler.py b/tests/test_crawler.py index ca3af783e..5128ea6e9 100644 --- a/tests/test_crawler.py +++ b/tests/test_crawler.py @@ -31,6 +31,9 @@ class CrawlerRunnerTest(unittest.TestCase): def setUp(self): self.crawler_runner = CrawlerRunner(Settings()) + def tearDown(self): + return self.crawler_runner.stop() + @defer.inlineCallbacks def test_populate_spidercls_settings(self): spider_settings = {'TEST1': 'spider', 'TEST2': 'spider'} From a823207f18a339ffbe66bee6d8a6ef87a447f1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Wed, 10 Sep 2014 12:09:07 -0300 Subject: [PATCH 0037/2974] Stop logobserver only when set --- scrapy/crawler.py | 3 ++- tox.ini | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index ded09c1c7..6866be809 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -156,7 +156,8 @@ class CrawlerProcess(CrawlerRunner): reactor.run(installSignalHandlers=False) # blocking call def _stop_logging(self): - self.log_observer.stop() + if self.log_observer: + self.log_observer.stop() def _stop_reactor(self, _=None): try: diff --git a/tox.ini b/tox.ini index 624f550e1..8aeb1492e 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,6 @@ deps = lxml==2.3.2 Twisted==11.1.0 boto==2.2.2 - Pillow<2.0 django==1.3.1 cssselect==0.9.1 zope.interface==3.6.1 From c05e99a4f4bbe7a3f124a99f696d88e5a30fd066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Wed, 10 Sep 2014 12:21:08 -0300 Subject: [PATCH 0038/2974] oops, restore Pillow from precise test requirements --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 8aeb1492e..624f550e1 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ deps = lxml==2.3.2 Twisted==11.1.0 boto==2.2.2 + Pillow<2.0 django==1.3.1 cssselect==0.9.1 zope.interface==3.6.1 From 5bcabfe9c93b649c35fc4b9d1b0441db146bd406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Wed, 10 Sep 2014 23:25:57 -0300 Subject: [PATCH 0039/2974] SPIDER_MODULES can be set as a csv string --- scrapy/spidermanager.py | 2 +- tests/test_spidermanager/__init__.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/scrapy/spidermanager.py b/scrapy/spidermanager.py index 5715b7793..93e91bcb4 100644 --- a/scrapy/spidermanager.py +++ b/scrapy/spidermanager.py @@ -15,7 +15,7 @@ from scrapy.utils.spider import iter_spider_classes class SpiderManager(object): def __init__(self, settings): - self.spider_modules = settings['SPIDER_MODULES'] + self.spider_modules = settings.getlist('SPIDER_MODULES') self._spiders = {} for name in self.spider_modules: for module in walk_modules(name): diff --git a/tests/test_spidermanager/__init__.py b/tests/test_spidermanager/__init__.py index 69ab3b82a..94b8a1dc8 100644 --- a/tests/test_spidermanager/__init__.py +++ b/tests/test_spidermanager/__init__.py @@ -63,6 +63,13 @@ class SpiderManagerTest(unittest.TestCase): self.spiderman = SpiderManager.from_settings(settings) assert len(self.spiderman._spiders) == 1 + def test_load_spider_module(self): + prefix = 'tests.test_spidermanager.test_spiders.' + module = ','.join(prefix + s for s in ('spider1', 'spider2')) + settings = Settings({'SPIDER_MODULES': module}) + self.spiderman = SpiderManager.from_settings(settings) + assert len(self.spiderman._spiders) == 2 + def test_load_base_spider(self): module = 'tests.test_spidermanager.test_spiders.spider0' settings = Settings({'SPIDER_MODULES': [module]}) From 47b6dff9f1f47fe97de94e2419d8cff38c7bacb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20=C3=85hl=C3=A9n?= Date: Sat, 13 Sep 2014 02:14:57 +0200 Subject: [PATCH 0040/2974] Allow to specify the quotechar in CSVFeedSpider --- docs/topics/spiders.rst | 6 ++++++ scrapy/contrib/spiders/feed.py | 5 +++-- scrapy/utils/iterators.py | 16 ++++++++++------ tests/sample_data/feeds/feed-sample6.csv | 6 ++++++ tests/test_utils_iterators.py | 22 ++++++++++++++++++++++ 5 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 tests/sample_data/feeds/feed-sample6.csv diff --git a/docs/topics/spiders.rst b/docs/topics/spiders.rst index 188b51836..cb3f6caeb 100644 --- a/docs/topics/spiders.rst +++ b/docs/topics/spiders.rst @@ -523,6 +523,11 @@ CSVFeedSpider A string with the separator character for each field in the CSV file Defaults to ``','`` (comma). + .. attribute:: quotechar + + A string with the enclosure character for each field in the CSV file + Defaults to ``'"'`` (quotation mark). + .. attribute:: headers A list of the rows contained in the file CSV feed which will be used to @@ -550,6 +555,7 @@ Let's see an example similar to the previous one, but using a allowed_domains = ['example.com'] start_urls = ['http://www.example.com/feed.csv'] delimiter = ';' + quotechar = "'" headers = ['id', 'name', 'description'] def parse_row(self, response, row): diff --git a/scrapy/contrib/spiders/feed.py b/scrapy/contrib/spiders/feed.py index fa538f473..1a95c5c35 100644 --- a/scrapy/contrib/spiders/feed.py +++ b/scrapy/contrib/spiders/feed.py @@ -97,11 +97,12 @@ class CSVFeedSpider(Spider): It receives a CSV file in a response; iterates through each of its rows, and calls parse_row with a dict containing each field's data. - You can set some options regarding the CSV file, such as the delimiter + You can set some options regarding the CSV file, such as the delimiter, quotechar and the file's headers. """ delimiter = None # When this is None, python's csv module's default delimiter is used + quotechar = None # When this is None, python's csv module's default quotechar is used headers = None def process_results(self, response, results): @@ -123,7 +124,7 @@ class CSVFeedSpider(Spider): process_results methods for pre and post-processing purposes. """ - for row in csviter(response, self.delimiter, self.headers): + for row in csviter(response, self.delimiter, self.headers, self.quotechar): ret = self.parse_row(response, row) if isinstance(ret, (BaseItem, Request)): ret = [ret] diff --git a/scrapy/utils/iterators.py b/scrapy/utils/iterators.py index 150b077ae..78ea7114e 100644 --- a/scrapy/utils/iterators.py +++ b/scrapy/utils/iterators.py @@ -35,7 +35,7 @@ def xmliter(obj, nodename): yield Selector(text=nodetext, type='xml').xpath('//' + nodename)[0] -def csviter(obj, delimiter=None, headers=None, encoding=None): +def csviter(obj, delimiter=None, headers=None, encoding=None, quotechar=None): """ Returns an iterator of dictionaries from the given csv object obj can be: @@ -43,20 +43,24 @@ def csviter(obj, delimiter=None, headers=None, encoding=None): - a unicode string - a string encoded as utf-8 - delimiter is the character used to separate field on the given obj. + delimiter is the character used to separate fields on the given obj. headers is an iterable that when provided offers the keys for the returned dictionaries, if not the first row is used. + + quotechar is the character used to enclosure fields on the given obj. """ + encoding = obj.encoding if isinstance(obj, TextResponse) else encoding or 'utf-8' def _getrow(csv_r): return [str_to_unicode(field, encoding) for field in next(csv_r)] lines = BytesIO(_body_or_str(obj, unicode=False)) - if delimiter: - csv_r = csv.reader(lines, delimiter=delimiter) - else: - csv_r = csv.reader(lines) + + kwargs = {} + if delimiter: kwargs["delimiter"] = delimiter + if quotechar: kwargs["quotechar"] = quotechar + csv_r = csv.reader(lines, **kwargs) if not headers: headers = _getrow(csv_r) diff --git a/tests/sample_data/feeds/feed-sample6.csv b/tests/sample_data/feeds/feed-sample6.csv new file mode 100644 index 000000000..a2604653e --- /dev/null +++ b/tests/sample_data/feeds/feed-sample6.csv @@ -0,0 +1,6 @@ +'id','name','value' +1,'alpha','foobar' +2,'unicode','únícódé‽' +'3','multi','foo +bar' +4,'empty', diff --git a/tests/test_utils_iterators.py b/tests/test_utils_iterators.py index fe53f831f..544941de1 100644 --- a/tests/test_utils_iterators.py +++ b/tests/test_utils_iterators.py @@ -159,6 +159,28 @@ class UtilsCsvTestCase(unittest.TestCase): {u'id': u'3', u'name': u'multi', u'value': FOOBAR_NL}, {u'id': u'4', u'name': u'empty', u'value': u''}]) + def test_csviter_quotechar(self): + body1 = get_testdata('feeds', 'feed-sample6.csv') + body2 = get_testdata('feeds', 'feed-sample6.csv').replace(",", '|') + + response1 = TextResponse(url="http://example.com/", body=body1) + csv1 = csviter(response1, quotechar="'") + + self.assertEqual([row for row in csv1], + [{u'id': u'1', u'name': u'alpha', u'value': u'foobar'}, + {u'id': u'2', u'name': u'unicode', u'value': u'\xfan\xedc\xf3d\xe9\u203d'}, + {u'id': u'3', u'name': u'multi', u'value': FOOBAR_NL}, + {u'id': u'4', u'name': u'empty', u'value': u''}]) + + response2 = TextResponse(url="http://example.com/", body=body2) + csv2 = csviter(response2, delimiter="|", quotechar="'") + + self.assertEqual([row for row in csv2], + [{u'id': u'1', u'name': u'alpha', u'value': u'foobar'}, + {u'id': u'2', u'name': u'unicode', u'value': u'\xfan\xedc\xf3d\xe9\u203d'}, + {u'id': u'3', u'name': u'multi', u'value': FOOBAR_NL}, + {u'id': u'4', u'name': u'empty', u'value': u''}]) + def test_csviter_delimiter_binary_response_assume_utf8_encoding(self): body = get_testdata('feeds', 'feed-sample3.csv').replace(',', '\t') response = Response(url="http://example.com/", body=body) From 22da1783bdc7eac04345261b28a942419391eaab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20=C3=85hl=C3=A9n?= Date: Sat, 13 Sep 2014 03:47:40 +0200 Subject: [PATCH 0041/2974] added a test-case for wrong quotechar --- tests/test_utils_iterators.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_utils_iterators.py b/tests/test_utils_iterators.py index 544941de1..840f4c596 100644 --- a/tests/test_utils_iterators.py +++ b/tests/test_utils_iterators.py @@ -181,6 +181,17 @@ class UtilsCsvTestCase(unittest.TestCase): {u'id': u'3', u'name': u'multi', u'value': FOOBAR_NL}, {u'id': u'4', u'name': u'empty', u'value': u''}]) + def test_csviter_wrong_quotechar(self): + body = get_testdata('feeds', 'feed-sample6.csv') + response = TextResponse(url="http://example.com/", body=body) + csv = csviter(response) + + self.assertEqual([row for row in csv], + [{u"'id'": u"1", u"'name'": u"'alpha'", u"'value'": u"'foobar'"}, + {u"'id'": u"2", u"'name'": u"'unicode'", u"'value'": u"'\xfan\xedc\xf3d\xe9\u203d'"}, + {u"'id'": u"'3'", u"'name'": u"'multi'", u"'value'": u"'foo"}, + {u"'id'": u"4", u"'name'": u"'empty'", u"'value'": u""}]) + def test_csviter_delimiter_binary_response_assume_utf8_encoding(self): body = get_testdata('feeds', 'feed-sample3.csv').replace(',', '\t') response = Response(url="http://example.com/", body=body) From e583c030db440c3ac0e877804f7195aa53aa460f Mon Sep 17 00:00:00 2001 From: andrewshir Date: Sun, 14 Sep 2014 14:24:16 +0600 Subject: [PATCH 0042/2974] Test for local domains (without dots) added --- tests/test_downloadermiddleware_cookies.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_downloadermiddleware_cookies.py b/tests/test_downloadermiddleware_cookies.py index ffa3a550c..1ca52ef84 100644 --- a/tests/test_downloadermiddleware_cookies.py +++ b/tests/test_downloadermiddleware_cookies.py @@ -8,6 +8,12 @@ from scrapy.contrib.downloadermiddleware.cookies import CookiesMiddleware class CookiesMiddlewareTest(TestCase): + def test_local_domain(self): + request = Request("http://example-host/", cookies={'currencyCookie': 'USD'}) + assert self.mw.process_request(request, self.spider) is None + assert 'Cookie' in request.headers + assert 'currencyCookie' in request.headers['Cookie'] + def assertCookieValEqual(self, first, second, msg=None): cookievaleq = lambda cv: re.split(';\s*', cv) return self.assertEqual( From a312ebfb435be109dc12d65c622e3a6ad85b3f86 Mon Sep 17 00:00:00 2001 From: John-Scott Atlakson Date: Sun, 14 Sep 2014 10:54:43 -0400 Subject: [PATCH 0043/2974] Update request-response.rst Fixed minor typo --- docs/topics/request-response.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index 54cff9100..6438f2f2b 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -545,7 +545,7 @@ TextResponse objects unicode(response.body) - Since, in the latter case, you would be using you system default encoding + Since, in the latter case, you would be using the system default encoding (typically `ascii`) to convert the body to unicode, instead of the response encoding. From e435b3e3a36e6742d8d756b231c868f4be7b528b Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Sun, 21 Sep 2014 00:19:24 +0600 Subject: [PATCH 0044/2974] DOC simplify extension docs --- docs/topics/downloader-middleware.rst | 4 ++-- docs/topics/extensions.rst | 20 ++++++++------------ docs/topics/item-pipeline.rst | 3 +-- docs/topics/spider-middleware.rst | 4 ++-- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index 614e4fff6..bff0d3e1c 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -51,8 +51,8 @@ particular setting. See each middleware documentation for more info. Writing your own downloader middleware ====================================== -Writing your own downloader middleware is easy. Each middleware component is a -single Python class that defines one or more of the following methods: +Each middleware component is a Python class that defines one or +more of the following methods: .. module:: scrapy.contrib.downloadermiddleware diff --git a/docs/topics/extensions.rst b/docs/topics/extensions.rst index 593a08ddc..c23e783bf 100644 --- a/docs/topics/extensions.rst +++ b/docs/topics/extensions.rst @@ -5,7 +5,7 @@ Extensions ========== The extensions framework provides a mechanism for inserting your own -custom functionality into Scrapy. +custom functionality into Scrapy. Extensions are just regular classes that are instantiated at Scrapy startup, when extensions are initialized. @@ -75,14 +75,10 @@ included in the :setting:`EXTENSIONS_BASE` setting) you must set its order to Writing your own extension ========================== -Writing your own extension is easy. Each extension is a single Python class -which doesn't need to implement any particular method. - -The main entry point for a Scrapy extension (this also includes middlewares and -pipelines) is the ``from_crawler`` class method which receives a -``Crawler`` instance which is the main object controlling the Scrapy crawler. -Through that object you can access settings, signals, stats, and also control -the crawler behaviour, if your extension needs to such thing. +Each extension is a Python class. The main entry point for a Scrapy extension +(this also includes middlewares and pipelines) is the ``from_crawler`` +class method which receives a ``Crawler`` instance. Through the Crawler object +you can access settings, signals, stats, and also control the crawling behaviour. Typically, extensions connect to :ref:`signals ` and perform tasks triggered by them. @@ -133,7 +129,7 @@ Here is the code of such extension:: crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed) crawler.signals.connect(ext.item_scraped, signal=signals.item_scraped) - # return the extension object + # return the extension object return ext def spider_opened(self, spider): @@ -183,12 +179,12 @@ Telnet console extension ~~~~~~~~~~~~~~~~~~~~~~~~ .. module:: scrapy.telnet - :synopsis: Telnet console + :synopsis: Telnet console .. class:: scrapy.telnet.TelnetConsole Provides a telnet console for getting into a Python interpreter inside the -currently running Scrapy process, which can be very useful for debugging. +currently running Scrapy process, which can be very useful for debugging. The telnet console must be enabled by the :setting:`TELNETCONSOLE_ENABLED` setting, and the server will listen in the port specified in diff --git a/docs/topics/item-pipeline.rst b/docs/topics/item-pipeline.rst index 146f6cbce..9cd198999 100644 --- a/docs/topics/item-pipeline.rst +++ b/docs/topics/item-pipeline.rst @@ -23,8 +23,7 @@ Typical use for item pipelines are: Writing your own item pipeline ============================== -Writing your own item pipeline is easy. Each item pipeline component is a -single Python class that must implement the following method: +Each item pipeline component is a Python class that must implement the following method: .. method:: process_item(item, spider) diff --git a/docs/topics/spider-middleware.rst b/docs/topics/spider-middleware.rst index 3df59998b..92dc6ac47 100644 --- a/docs/topics/spider-middleware.rst +++ b/docs/topics/spider-middleware.rst @@ -52,8 +52,8 @@ particular setting. See each middleware documentation for more info. Writing your own spider middleware ================================== -Writing your own spider middleware is easy. Each middleware component is a -single Python class that defines one or more of the following methods: +Each middleware component is a Python class that defines one or more of the +following methods: .. module:: scrapy.contrib.spidermiddleware From 49645d4bf950cbd18f4a2c3839b93b3bf979e2f5 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Sun, 21 Sep 2014 05:31:34 +0600 Subject: [PATCH 0045/2974] TST small cleanup of a cookie test --- tests/test_downloadermiddleware_cookies.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_downloadermiddleware_cookies.py b/tests/test_downloadermiddleware_cookies.py index 1ca52ef84..3b1853c82 100644 --- a/tests/test_downloadermiddleware_cookies.py +++ b/tests/test_downloadermiddleware_cookies.py @@ -8,12 +8,6 @@ from scrapy.contrib.downloadermiddleware.cookies import CookiesMiddleware class CookiesMiddlewareTest(TestCase): - def test_local_domain(self): - request = Request("http://example-host/", cookies={'currencyCookie': 'USD'}) - assert self.mw.process_request(request, self.spider) is None - assert 'Cookie' in request.headers - assert 'currencyCookie' in request.headers['Cookie'] - def assertCookieValEqual(self, first, second, msg=None): cookievaleq = lambda cv: re.split(';\s*', cv) return self.assertEqual( @@ -153,3 +147,10 @@ class CookiesMiddlewareTest(TestCase): req6 = Request('file:///scrapy/sometempfile') assert self.mw.process_request(req6, self.spider) is None self.assertEquals(req6.headers.get('Cookie'), None) + + def test_local_domain(self): + request = Request("http://example-host/", cookies={'currencyCookie': 'USD'}) + assert self.mw.process_request(request, self.spider) is None + self.assertIn('Cookie', request.headers) + self.assertIn('currencyCookie', request.headers['Cookie']) + From 7be3479c204c2b110bc79cd8bdc9c7bdd1519163 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Sun, 21 Sep 2014 06:37:32 +0600 Subject: [PATCH 0046/2974] CookieJar cleanup --- scrapy/http/cookies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/http/cookies.py b/scrapy/http/cookies.py index 3e3dff741..b1eb767cc 100644 --- a/scrapy/http/cookies.py +++ b/scrapy/http/cookies.py @@ -28,7 +28,7 @@ class CookieJar(object): if not IPV4_RE.search(req_host): hosts = potential_domain_matches(req_host) - if req_host.find(".") == -1: + if '.' not in req_host: hosts += [req_host + ".local"] else: hosts = [req_host] From a122fdbfeae42147f862e0e5a7dfe3848b8a7eac Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Wed, 15 Jan 2014 17:39:54 +0600 Subject: [PATCH 0047/2974] Update leaks.rst: there is now only a single spider in a process. --- docs/topics/leaks.rst | 82 +++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 54 deletions(-) diff --git a/docs/topics/leaks.rst b/docs/topics/leaks.rst index 27c50a225..c838b3c30 100644 --- a/docs/topics/leaks.rst +++ b/docs/topics/leaks.rst @@ -32,20 +32,13 @@ 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 -(well-written) spiders when they get to run concurrently, which, in turn, -affects the whole crawling process. - -At the same time, it's hard to avoid the reasons that cause these leaks -without restricting the power of the framework, so we have decided not to -restrict the functionally but provide useful tools for debugging these leaks, -which quite often consist in an answer to the question: *which spider is leaking?*. - The leak could also come from a custom middleware, pipeline or extension that you have written, if you are not releasing the (previously allocated) resources -properly. For example, if you're allocating resources on -:signal:`spider_opened` but not releasing them on :signal:`spider_closed`. +properly. + +It's hard to avoid the reasons that cause these leaks +without restricting the power of the framework, so we have decided not to +restrict the functionally but provide useful tools for debugging these leaks. .. _topics-leaks-trackrefs: @@ -54,7 +47,7 @@ Debugging memory leaks with ``trackref`` ``trackref`` is a module provided by Scrapy to debug the most common cases of memory leaks. It basically tracks the references to all live Requests, -Responses, Item and Selector objects. +Responses, Item and Selector objects. You can enter the telnet console and inspect how many objects (of the classes mentioned above) are currently alive using the ``prefs()`` function which is an @@ -71,12 +64,7 @@ alias to the :func:`~scrapy.utils.trackref.print_live_refs` function:: FormRequest 878 oldest: 7s ago As you can see, that report also shows the "age" of the oldest object in each -class. - -If you do have leaks, chances are you can figure out which spider is leaking by -looking at the oldest request or response. You can get the oldest object of -each class using the :func:`~scrapy.utils.trackref.get_oldest` function like -this (from the telnet console). +class. Which objects are tracked? -------------------------- @@ -84,17 +72,16 @@ Which objects are tracked? The objects tracked by ``trackrefs`` are all from these classes (and all its subclasses): -* ``scrapy.http.Request`` -* ``scrapy.http.Response`` -* ``scrapy.item.Item`` -* ``scrapy.selector.Selector`` -* ``scrapy.spider.Spider`` +* :class:`scrapy.http.Request` +* :class:`scrapy.http.Response` +* :class:`scrapy.item.Item` +* :class:`scrapy.selector.Selector` +* :class:`scrapy.spider.Spider` A real example -------------- Let's see a concrete example of an 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, @@ -104,7 +91,7 @@ That line is passing a response reference inside a request which effectively ties the response lifetime to the requests' one, and that would definitely cause memory leaks. -Let's see how we can discover which one is the nasty spider (without knowing it +Let's see how we can discover the cause (without knowing it a-priori, of course) by using the ``trackref`` tool. After the crawler is running for a few minutes and we notice its memory usage @@ -121,20 +108,21 @@ references:: The fact that there are so many live responses (and that they're so old) is definitely suspicious, as responses should have a relatively short lifetime -compared to Requests. So let's check the oldest response:: +compared to Requests. The number of responses is similar to the number +of requests, so it looks like they are tied in a some way. We can now go +and check the code of the spider to discover the nasty line that is +generating the leaks (passing response references inside requests). + +Sometimes extra information about live objects can be helpful. +Let's check the oldest response:: >>> from scrapy.utils.trackref import get_oldest >>> r = get_oldest('HtmlResponse') >>> r.url 'http://www.somenastyspider.com/product.php?pid=123' -There it is. By looking at the URL of the oldest response we can see it belongs -to the ``somenastyspider.com`` spider. We can now go and check the code of that -spider to discover the nasty line that is generating the leaks (passing -response references inside requests). - If you want to iterate over all objects, instead of getting the oldest one, you -can use the :func:`iter_all` function:: +can use the :func:`scrapy.utils.trackref.iter_all` function:: >>> from scrapy.utils.trackref import iter_all >>> [r.url for r in iter_all('HtmlResponse')] @@ -142,19 +130,6 @@ can use the :func:`iter_all` function:: 'http://www.somenastyspider.com/product.php?pid=584', ... -Too many spiders? ------------------ - -If your project has too many spiders, the output of ``prefs()`` can be -difficult to read. For this reason, that function has a ``ignore`` argument -which can be used to ignore a particular class (and all its subclases). For -example, using:: - - >>> from scrapy.spider import Spider - >>> prefs(ignore=Spider) - -Won't show any live references to spiders. - .. module:: scrapy.utils.trackref :synopsis: Track references of live objects @@ -198,15 +173,13 @@ leaks, but it only keeps track of the objects that are more likely to cause memory leaks (Requests, Responses, Items, and Selectors). However, there are other cases where the memory leaks could come from other (more or less obscure) objects. If this is your case, and you can't find your leaks using ``trackref``, -you still have another resource: the `Guppy library`_. +you still have another resource: the `Guppy library`_. .. _Guppy library: http://pypi.python.org/pypi/guppy -If you use ``setuptools``, you can install Guppy with the following command:: +If you use ``pip``, you can install Guppy with the following command:: - easy_install guppy - -.. _setuptools: http://pypi.python.org/pypi/setuptools + pip install guppy The telnet console also comes with a built-in shortcut (``hpy``) for accessing Guppy heap objects. Here's an example to view all Python objects available in @@ -279,7 +252,8 @@ completely. To quote the paper: to move to a compacting garbage collector, which is able to move objects in memory. This would require significant changes to the Python interpreter.* -This problem will be fixed in future Scrapy releases, where we plan to adopt a -new process model and run spiders in a pool of recyclable sub-processes. - .. _this paper: http://evanjones.ca/memoryallocator/ + +To keep memory consumption reasonable you can split the job into several +smaller jobs or enable :ref:`persistent job queue ` +and stop/start spider from time to time. From bc0f481a7355713978ee206d36a9356ab4be9d61 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Sun, 21 Sep 2014 07:12:01 +0600 Subject: [PATCH 0048/2974] DOC bring back notes about multiple spiders per process because it is now documented how to do that --- docs/topics/leaks.rst | 30 ++++++++++++++++++++++++------ docs/topics/practices.rst | 2 ++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/topics/leaks.rst b/docs/topics/leaks.rst index c838b3c30..95bb882e9 100644 --- a/docs/topics/leaks.rst +++ b/docs/topics/leaks.rst @@ -32,13 +32,16 @@ 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 +(well-written) spiders when they get to run concurrently, which, in turn, +affects the whole crawling process. + The leak could also come from a custom middleware, pipeline or extension that you have written, if you are not releasing the (previously allocated) resources -properly. - -It's hard to avoid the reasons that cause these leaks -without restricting the power of the framework, so we have decided not to -restrict the functionally but provide useful tools for debugging these leaks. +properly. For example, allocating resources on :signal:`spider_opened` +but not releasing them on :signal:`spider_closed` may cause problems if +you're running :ref:`multiple spiders per process `. .. _topics-leaks-trackrefs: @@ -64,7 +67,10 @@ alias to the :func:`~scrapy.utils.trackref.print_live_refs` function:: FormRequest 878 oldest: 7s ago As you can see, that report also shows the "age" of the oldest object in each -class. +class. If you're running multiple spiders per process chances are you can +figure out which spider is leaking by looking at the oldest request or response. +You can get the oldest object of each class using the +:func:`~scrapy.utils.trackref.get_oldest` function (from the telnet console). Which objects are tracked? -------------------------- @@ -130,6 +136,18 @@ can use the :func:`scrapy.utils.trackref.iter_all` function:: 'http://www.somenastyspider.com/product.php?pid=584', ... +Too many spiders? +----------------- + +If your project has too many spiders executed in parallel, +the output of :func:`prefs()` can be difficult to read. +For this reason, that function has a ``ignore`` argument which can be used to +ignore a particular class (and all its subclases). For +example, this won't show any live references to spiders:: + + >>> from scrapy.spider import Spider + >>> prefs(ignore=Spider) + .. module:: scrapy.utils.trackref :synopsis: Track references of live objects diff --git a/docs/topics/practices.rst b/docs/topics/practices.rst index b188ee562..e9c7a94bf 100644 --- a/docs/topics/practices.rst +++ b/docs/topics/practices.rst @@ -69,6 +69,8 @@ the spider class as first argument in the :meth:`CrawlerRunner.crawl .. seealso:: `Twisted Reactor Overview`_. +.. _run-multiple-spiders: + Running multiple spiders in the same process ============================================ From bdbca1e2d7b4bf13332eec24968d8a7b9aea2de4 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Sun, 21 Sep 2014 07:30:44 +0600 Subject: [PATCH 0049/2974] DOC request queue memory usage --- docs/topics/leaks.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/topics/leaks.rst b/docs/topics/leaks.rst index 95bb882e9..d6530859b 100644 --- a/docs/topics/leaks.rst +++ b/docs/topics/leaks.rst @@ -43,12 +43,22 @@ properly. For example, allocating resources on :signal:`spider_opened` but not releasing them on :signal:`spider_closed` may cause problems if you're running :ref:`multiple spiders per process `. +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`). +While not necesserily a leak, this can take a lot of memory. Enabling +:ref:`persistent job queue ` could help keeping memory usage +in control. + .. _topics-leaks-trackrefs: Debugging memory leaks with ``trackref`` ======================================== -``trackref`` is a module provided by Scrapy to debug the most common cases of +:mod:`trackref` is a module provided by Scrapy to debug the most common cases of memory leaks. It basically tracks the references to all live Requests, Responses, Item and Selector objects. From d11c8595e67680baad002bedf08dfd91f347cfb2 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Mon, 22 Sep 2014 04:29:22 +0600 Subject: [PATCH 0050/2974] drop support for CONCURRENT_REQUESTS_PER_SPIDER --- scrapy/core/downloader/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/scrapy/core/downloader/__init__.py b/scrapy/core/downloader/__init__.py index a5e62a75d..ecbf33039 100644 --- a/scrapy/core/downloader/__init__.py +++ b/scrapy/core/downloader/__init__.py @@ -49,14 +49,6 @@ def _get_concurrency_delay(concurrency, spider, settings): if hasattr(spider, 'download_delay'): delay = spider.download_delay - # TODO: remove for Scrapy 0.15 - c = settings.getint('CONCURRENT_REQUESTS_PER_SPIDER') - if c: - warnings.warn("CONCURRENT_REQUESTS_PER_SPIDER setting is deprecated, " - "use CONCURRENT_REQUESTS_PER_DOMAIN instead", ScrapyDeprecationWarning) - concurrency = c - # ---------------------------- - if hasattr(spider, 'max_concurrent_requests'): concurrency = spider.max_concurrent_requests From fe6f3efe95eb9f0a33154d239f9b1a6b1c02028d Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Mon, 22 Sep 2014 22:56:54 +0600 Subject: [PATCH 0051/2974] RobotsTxtMiddleware: remove unused attribute --- scrapy/contrib/downloadermiddleware/robotstxt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scrapy/contrib/downloadermiddleware/robotstxt.py b/scrapy/contrib/downloadermiddleware/robotstxt.py index f1e8012e7..f9d1396fb 100644 --- a/scrapy/contrib/downloadermiddleware/robotstxt.py +++ b/scrapy/contrib/downloadermiddleware/robotstxt.py @@ -22,7 +22,6 @@ class RobotsTxtMiddleware(object): self.crawler = crawler self._useragent = crawler.settings.get('USER_AGENT') self._parsers = {} - self._spider_netlocs = set() @classmethod def from_crawler(cls, crawler): @@ -45,7 +44,6 @@ class RobotsTxtMiddleware(object): robotsreq = Request(robotsurl, priority=self.DOWNLOAD_PRIORITY) dfd = self.crawler.engine.download(robotsreq, spider) dfd.addCallback(self._parse_robots) - self._spider_netlocs.add(netloc) return self._parsers[netloc] def _parse_robots(self, response): From 36eec8f4136a314d20c04cd8318a32e75c31a649 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Tue, 23 Sep 2014 00:10:43 +0600 Subject: [PATCH 0052/2974] dont_obey_robotstxt meta key; don't process requests to /robots.txt --- docs/topics/downloader-middleware.rst | 10 +++- docs/topics/request-response.rst | 1 + .../contrib/downloadermiddleware/robotstxt.py | 11 +++- tests/test_downloadermiddleware_robotstxt.py | 60 ++++++++++++++----- 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index 614e4fff6..e4e490f61 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -785,11 +785,19 @@ RobotsTxtMiddleware and the :setting:`ROBOTSTXT_OBEY` setting is enabled. .. warning:: Keep in mind that, if you crawl using multiple concurrent - requests per domain, Scrapy could still download some forbidden pages + requests per domain, Scrapy could still download some forbidden pages if they were requested before the robots.txt file was downloaded. This is a known limitation of the current robots.txt middleware and will be fixed in the future. +.. reqmeta:: dont_obey_robotstxt + +If :attr:`Request.meta ` has +``dont_obey_robotstxt`` key set to True +the request will be ignored by this middleware even if +:setting:`ROBOTSTXT_OBEY` is enabled. + + DownloaderStats --------------- diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index 6438f2f2b..b3e9d9a1d 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -228,6 +228,7 @@ Those are: * :reqmeta:`cookiejar` * :reqmeta:`redirect_urls` * :reqmeta:`bindaddress` +* :reqmeta:`dont_obey_robotstxt` .. reqmeta:: bindaddress diff --git a/scrapy/contrib/downloadermiddleware/robotstxt.py b/scrapy/contrib/downloadermiddleware/robotstxt.py index f9d1396fb..0ea402719 100644 --- a/scrapy/contrib/downloadermiddleware/robotstxt.py +++ b/scrapy/contrib/downloadermiddleware/robotstxt.py @@ -28,9 +28,10 @@ class RobotsTxtMiddleware(object): return cls(crawler) def process_request(self, request, spider): - useragent = self._useragent + if request.meta.get('dont_obey_robotstxt'): + return rp = self.robot_parser(request, spider) - if rp and not rp.can_fetch(useragent, request.url): + if rp and not rp.can_fetch(self._useragent, request.url): log.msg(format="Forbidden by robots.txt: %(request)s", level=log.DEBUG, request=request) raise IgnoreRequest @@ -41,7 +42,11 @@ class RobotsTxtMiddleware(object): if netloc not in self._parsers: self._parsers[netloc] = None robotsurl = "%s://%s/robots.txt" % (url.scheme, url.netloc) - robotsreq = Request(robotsurl, priority=self.DOWNLOAD_PRIORITY) + robotsreq = Request( + robotsurl, + priority=self.DOWNLOAD_PRIORITY, + meta={'dont_obey_robotstxt': True} + ) dfd = self.crawler.engine.download(robotsreq, spider) dfd.addCallback(self._parse_robots) return self._parsers[netloc] diff --git a/tests/test_downloadermiddleware_robotstxt.py b/tests/test_downloadermiddleware_robotstxt.py index 31d7f0087..eba337cbd 100644 --- a/tests/test_downloadermiddleware_robotstxt.py +++ b/tests/test_downloadermiddleware_robotstxt.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import re import mock from twisted.internet import reactor @@ -11,7 +12,44 @@ from scrapy.settings import Settings class RobotsTxtMiddlewareTest(unittest.TestCase): - def test(self): + def test_robotstxt(self): + middleware = self._get_middleware() + # There is a bit of neglect in robotstxt.py: robots.txt is fetched asynchronously, + # and it is actually fetched only *after* first process_request completes. + # So, first process_request will always succeed. + # We defer test() because otherwise robots.txt download mock will be called after assertRaises failure. + self.assertNotIgnored(Request('http://site.local'), middleware) + def test(r): + self.assertNotIgnored(Request('http://site.local/allowed'), middleware) + self.assertIgnored(Request('http://site.local/admin/main'), middleware) + self.assertIgnored(Request('http://site.local/static/'), middleware) + deferred = Deferred() + deferred.addCallback(test) + reactor.callFromThread(deferred.callback, None) + return deferred + + def test_robotstxt_meta(self): + meta = {'dont_obey_robotstxt': True} + middleware = self._get_middleware() + self.assertNotIgnored(Request('http://site.local', meta=meta), middleware) + def test(r): + self.assertNotIgnored(Request('http://site.local/allowed', meta=meta), middleware) + self.assertNotIgnored(Request('http://site.local/admin/main', meta=meta), middleware) + self.assertNotIgnored(Request('http://site.local/static/', meta=meta), middleware) + deferred = Deferred() + deferred.addCallback(test) + reactor.callFromThread(deferred.callback, None) + return deferred + + def assertNotIgnored(self, request, middleware): + spider = None # not actually used + self.assertIsNone(middleware.process_request(request, spider)) + + def assertIgnored(self, request, middleware): + spider = None # not actually used + self.assertRaises(IgnoreRequest, middleware.process_request, request, spider) + + def _get_crawler(self): crawler = mock.MagicMock() crawler.settings = Settings() crawler.settings.set('USER_AGENT', 'CustomAgent') @@ -29,18 +67,8 @@ class RobotsTxtMiddlewareTest(unittest.TestCase): reactor.callFromThread(deferred.callback, response) return deferred crawler.engine.download.side_effect = return_response - middleware = RobotsTxtMiddleware(crawler) - spider = None # not actually used - # There is a bit of neglect in robotstxt.py: robots.txt is fetched asynchronously, - # and it is actually fetched only *after* first process_request completes. - # So, first process_request will always succeed. - # We defer test() because otherwise robots.txt download mock will be called after assertRaises failure. - self.assertIsNone(middleware.process_request(Request('http://site.local'), spider)) # not affected by robots.txt - def test(r): - self.assertIsNone(middleware.process_request(Request('http://site.local/allowed'), spider)) - self.assertRaises(IgnoreRequest, middleware.process_request, Request('http://site.local/admin/main'), spider) - self.assertRaises(IgnoreRequest, middleware.process_request, Request('http://site.local/static/'), spider) - deferred = Deferred() - deferred.addCallback(test) - reactor.callFromThread(deferred.callback, None) - return deferred + return crawler + + def _get_middleware(self): + crawler = self._get_crawler() + return RobotsTxtMiddleware(crawler) From 50862629138d8d6a10d3f8ceed2db01d1b58abdb Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Wed, 24 Sep 2014 13:27:14 +0600 Subject: [PATCH 0053/2974] don't hide original exception in scrapy.utils.misc.load_object --- scrapy/utils/misc.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scrapy/utils/misc.py b/scrapy/utils/misc.py index 4cfd35342..3152db6c7 100644 --- a/scrapy/utils/misc.py +++ b/scrapy/utils/misc.py @@ -41,10 +41,7 @@ def load_object(path): raise ValueError("Error loading object '%s': not a full path" % path) module, name = path[:dot], path[dot+1:] - try: - mod = import_module(module) - except ImportError as e: - raise ImportError("Error loading object '%s': %s" % (path, e)) + mod = import_module(module) try: obj = getattr(mod, name) From 6fcf9dce50bb631ddd659217c3fa74b0e06ac809 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Thu, 25 Sep 2014 03:13:51 +0600 Subject: [PATCH 0054/2974] DOC document from_crawler method for item pipelines; add an example. --- docs/topics/item-pipeline.rst | 71 ++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/docs/topics/item-pipeline.rst b/docs/topics/item-pipeline.rst index 146f6cbce..cdd52f79e 100644 --- a/docs/topics/item-pipeline.rst +++ b/docs/topics/item-pipeline.rst @@ -26,7 +26,7 @@ Writing your own item pipeline Writing your own item pipeline is easy. Each item pipeline component is a single Python class that must implement the following method: -.. method:: process_item(item, spider) +.. method:: process_item(self, item, spider) This method is called for every item pipeline component and must either return a :class:`~scrapy.item.Item` (or any descendant class) object or raise a @@ -41,20 +41,31 @@ single Python class that must implement the following method: Additionally, they may also implement the following methods: -.. method:: open_spider(spider) +.. method:: open_spider(self, spider) This method is called when the spider is opened. :param spider: the spider which was opened :type spider: :class:`~scrapy.spider.Spider` object -.. method:: close_spider(spider) +.. method:: close_spider(self, spider) This method is called when the spider is closed. :param spider: the spider which was closed :type spider: :class:`~scrapy.spider.Spider` object +.. method:: from_crawler(cls, crawler) + + If present, this classmethod is called to create a pipeline instance + from a :class:`~scrapy.crawler.Crawler`. It must return a new instance + of the pipeline. Crawler object provides access to all Scrapy core + components like settings and signals; it is a way for pipeline to + access them and hook its functionality into Scrapy. + + :param crawler: crawler that uses this pipeline + :type crawler: :class:`~scrapy.crawler.Crawler` object + Item pipeline example ===================== @@ -62,9 +73,10 @@ Item pipeline example Price validation and dropping items with no prices -------------------------------------------------- -Let's take a look at the following hypothetical pipeline that adjusts the ``price`` -attribute for those items that do not include VAT (``price_excludes_vat`` -attribute), and drops those items which don't contain a price:: +Let's take a look at the following hypothetical pipeline that adjusts the +``price`` attribute for those items that do not include VAT +(``price_excludes_vat`` attribute), and drops those items which don't +contain a price:: from scrapy.exceptions import DropItem @@ -104,6 +116,53 @@ format:: item pipelines. If you really want to store all scraped items into a JSON file you should use the :ref:`Feed exports `. +Write items to MongoDB +---------------------- + +In this example we'll write items to MongoDB_ using pymongo_. +MongoDB address and database name are specified in Scrapy settings; +MongoDB collection is named after item class. + +The main point of this example is to show how to use :meth:`from_crawler` +method and how to clean up the resources properly. + +.. note:: + + Previous example (JsonWriterPipeline) doesn't clean up resources properly. + Fixing it is left as an exercise for the reader. + +:: + + import pymongo + + class MongoPipeline(object): + + def __init__(self, mongo_uri, mongo_db): + self.mongo_uri = mongo_uri + self.mongo_db = mongo_db + + @classmethod + def from_crawler(cls, crawler): + return cls( + mongo_uri=crawler.settings.get('MONGO_URI'), + mongo_db=crawler.settings.get('MONGO_DATABASE', 'items') + ) + + def open_spider(self, spider): + self.client = pymongo.MongoClient(self.mongo_uri) + self.db = self.client[self.mongo_db] + + def close_spider(self, spider): + self.client.close() + + def process_item(self, item, spider): + collection_name = item.__class__.__name__ + self.db[collection_name].insert(dict(item)) + return item + +.. _MongoDB: http://www.mongodb.org/ +.. _pymongo: http://api.mongodb.org/python/current/ + Duplicates filter ----------------- From 993b543e1b895772ec8ac1aeb8db5a87d2b5f784 Mon Sep 17 00:00:00 2001 From: Pablo Hoffman Date: Thu, 2 Oct 2014 01:17:26 -0300 Subject: [PATCH 0055/2974] mark SEP-019 as Final --- sep/sep-019.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sep/sep-019.rst b/sep/sep-019.rst index 6c5e8bdd9..9fbf6a223 100644 --- a/sep/sep-019.rst +++ b/sep/sep-019.rst @@ -3,7 +3,7 @@ SEP 19 Title Per-spider settings Author Pablo Hoffman, Nicolás Ramirez, Julia Medina Created 2013-03-07 -Status Draft +Status Final (implemented with minor variations) ======= =================== ====================================================== From ea3b372b4fd34dd133ec433d86a7ae6aaf0105bf Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Thu, 2 Oct 2014 15:20:13 +0600 Subject: [PATCH 0056/2974] DOC typo fix in leaks.rst --- docs/topics/leaks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/leaks.rst b/docs/topics/leaks.rst index d6530859b..372691c8e 100644 --- a/docs/topics/leaks.rst +++ b/docs/topics/leaks.rst @@ -49,7 +49,7 @@ 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`). -While not necesserily a leak, this can take a lot of memory. Enabling +While not necessarily a leak, this can take a lot of memory. Enabling :ref:`persistent job queue ` could help keeping memory usage in control. From 33a7c1d4380944d0a6e2efc017b85b1341fc72a3 Mon Sep 17 00:00:00 2001 From: VKen Date: Fri, 3 Oct 2014 04:16:21 +0800 Subject: [PATCH 0057/2974] updated deprecated cgi.parse_qsl to use six's parse_qsl --- scrapy/utils/url.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scrapy/utils/url.py b/scrapy/utils/url.py index ab4d75f87..36490a39d 100644 --- a/scrapy/utils/url.py +++ b/scrapy/utils/url.py @@ -6,9 +6,9 @@ Some of the functions that used to be imported from this module have been moved to the w3lib.url module. Always import those from there instead. """ import posixpath -from six.moves.urllib.parse import ParseResult, urlunparse, urldefrag, urlparse +from six.moves.urllib.parse import (ParseResult, urlunparse, urldefrag, + urlparse, parse_qsl) import urllib -import cgi # scrapy.utils.url was moved to w3lib.url and import * ensures this move doesn't break old code from w3lib.url import * @@ -54,7 +54,7 @@ def canonicalize_url(url, keep_blank_values=True, keep_fragments=False, """ scheme, netloc, path, params, query, fragment = parse_url(url) - keyvals = cgi.parse_qsl(query, keep_blank_values) + keyvals = parse_qsl(query, keep_blank_values) keyvals.sort() query = urllib.urlencode(keyvals) path = safe_url_string(_unquotepath(path)) or '/' From 7db6bbce27e694243a96307e149c7092195da9b9 Mon Sep 17 00:00:00 2001 From: nyov Date: Fri, 3 Oct 2014 18:41:29 +0000 Subject: [PATCH 0058/2974] Drop old engine code * remove Downloader import unused since 1fba64 * remove CONCURRENT_SPIDERS deprecation warning from a1dbc6 (2011) --- scrapy/core/engine.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/scrapy/core/engine.py b/scrapy/core/engine.py index 717b34764..bd1a9f04b 100644 --- a/scrapy/core/engine.py +++ b/scrapy/core/engine.py @@ -4,16 +4,14 @@ This is the Scrapy engine which controls the Scheduler, Downloader and Spiders. For more information see docs/topics/architecture.rst """ -import warnings from time import time from twisted.internet import defer from twisted.python.failure import Failure from scrapy import log, signals -from scrapy.core.downloader import Downloader from scrapy.core.scraper import Scraper -from scrapy.exceptions import DontCloseSpider, ScrapyDeprecationWarning +from scrapy.exceptions import DontCloseSpider from scrapy.http import Response, Request from scrapy.utils.misc import load_object from scrapy.utils.reactor import CallLaterOnce @@ -63,10 +61,6 @@ class ExecutionEngine(object): downloader_cls = load_object(self.settings['DOWNLOADER']) self.downloader = downloader_cls(crawler) self.scraper = Scraper(crawler) - self._concurrent_spiders = self.settings.getint('CONCURRENT_SPIDERS', 1) - if self._concurrent_spiders != 1: - warnings.warn("CONCURRENT_SPIDERS settings is deprecated, use " \ - "Scrapyd max_proc config instead", ScrapyDeprecationWarning) self._spider_closed_callback = spider_closed_callback @defer.inlineCallbacks From 7d68b084a4a0a63d5769c51c5cde5e9c1108ddb9 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Tue, 23 Sep 2014 02:59:35 +0600 Subject: [PATCH 0059/2974] DOC document download_timeout Request.meta key and download_timeout spider attribute. --- docs/topics/downloader-middleware.rst | 9 ++++++++- docs/topics/request-response.rst | 10 ++++++++++ docs/topics/settings.rst | 6 ++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index e4e490f61..e96ec8852 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -263,7 +263,14 @@ DownloadTimeoutMiddleware .. class:: DownloadTimeoutMiddleware This middleware sets the download timeout for requests specified in the - :setting:`DOWNLOAD_TIMEOUT` setting. + :setting:`DOWNLOAD_TIMEOUT` setting or :attr:`download_timeout` + spider attribute. + +.. note:: + + You can also set download timeout per-request using + :reqmeta:`download_timeout` Request.meta key; this is supported + even when DownloadTimeoutMiddleware is disabled. HttpAuthMiddleware ------------------ diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index b3e9d9a1d..68d87d04f 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -229,6 +229,7 @@ Those are: * :reqmeta:`redirect_urls` * :reqmeta:`bindaddress` * :reqmeta:`dont_obey_robotstxt` +* :reqmeta:`download_timeout` .. reqmeta:: bindaddress @@ -237,6 +238,15 @@ bindaddress The IP of the outgoing IP address to use for the performing the request. +.. reqmeta:: download_timeout + +download_timeout +---------------- + +The amount of time (in secs) that the downloader will wait before timing out. +See also: :setting:`DOWNLOAD_TIMEOUT`. + + .. _topics-request-response-ref-request-subclasses: Request subclasses diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 89ee7605a..9000f0242 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -416,6 +416,12 @@ Default: ``180`` The amount of time (in secs) that the downloader will wait before timing out. +.. note:: + + This timeout can be set per spider using :attr:`download_timeout` + spider attribute and per-request using :reqmeta:`download_timeout` + Request.meta key. + .. setting:: DUPEFILTER_CLASS DUPEFILTER_CLASS From db2474f7e7ef85809528a6f92aff3127be55734c Mon Sep 17 00:00:00 2001 From: Jonas Brunsgaard Date: Tue, 7 Oct 2014 13:54:04 +0200 Subject: [PATCH 0060/2974] Deleted bin folder from root, fixes #913 --- bin/scrapy | 4 ---- 1 file changed, 4 deletions(-) delete mode 100755 bin/scrapy diff --git a/bin/scrapy b/bin/scrapy deleted file mode 100755 index 918ea7fbd..000000000 --- a/bin/scrapy +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env python - -from scrapy.cmdline import execute -execute() From 22278056199244c3c87128ab9e5cf4743bb908c3 Mon Sep 17 00:00:00 2001 From: Nikolaos-Digenis Karagiannis Date: Wed, 8 Oct 2014 17:46:07 +0300 Subject: [PATCH 0061/2974] Compatibility with .15 leftover --- scrapy/core/engine.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scrapy/core/engine.py b/scrapy/core/engine.py index bd1a9f04b..4ef1d0fc6 100644 --- a/scrapy/core/engine.py +++ b/scrapy/core/engine.py @@ -263,10 +263,8 @@ class ExecutionEngine(object): dfd.addBoth(lambda _: slot.scheduler.close(reason)) dfd.addErrback(log.err, spider=spider) - # XXX: spider_stats argument was added for backwards compatibility with - # stats collection refactoring added in 0.15. it should be removed in 0.17. - dfd.addBoth(lambda _: self.signals.send_catch_log_deferred(signal=signals.spider_closed, \ - spider=spider, reason=reason, spider_stats=self.crawler.stats.get_stats())) + dfd.addBoth(lambda _: self.signals.send_catch_log_deferred( + signal=signals.spider_closed, spider=spider, reason=reason)) dfd.addErrback(log.err, spider=spider) dfd.addBoth(lambda _: self.crawler.stats.close_spider(spider, reason=reason)) From 38dcf50cd6a6328f5ec293015dbe53733343991a Mon Sep 17 00:00:00 2001 From: Lazar-T Date: Sat, 25 Oct 2014 00:21:16 +0200 Subject: [PATCH 0062/2974] comma instead of fullstop --- docs/topics/extensions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/extensions.rst b/docs/topics/extensions.rst index c23e783bf..2c2f7fb9c 100644 --- a/docs/topics/extensions.rst +++ b/docs/topics/extensions.rst @@ -204,7 +204,7 @@ Memory usage extension Monitors the memory used by the Scrapy process that runs the spider and: -1, sends a notification e-mail when it exceeds a certain value +1. sends a notification e-mail when it exceeds a certain value 2. closes the spider when it exceeds a certain value The notification e-mails can be triggered when a certain warning value is From 6cb89957312730dd4d3740c9daacb1f1a07a5f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Wed, 5 Nov 2014 22:49:40 -0200 Subject: [PATCH 0063/2974] Update install.rst fixes #937 --- docs/intro/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro/install.rst b/docs/intro/install.rst index ffba0e2b3..867bc2da7 100644 --- a/docs/intro/install.rst +++ b/docs/intro/install.rst @@ -43,7 +43,7 @@ Windows the Python executable and additional scripts. The following paths need to be added to ``PATH``:: - C:\Python2.7\;C:\Python2.7\Scripts\; + C:\Python27\;C:\Python27\Scripts\; To update the ``PATH`` open a Command prompt and run:: From 2c67bd6c57cec359b6a7894f0adeb10d9e2ab77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Wed, 5 Nov 2014 23:05:51 -0200 Subject: [PATCH 0064/2974] pywin32 is required by Twisted. closes #937 see: * http://twistedmatrix.com/trac/ticket/6032 * https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2028 --- docs/intro/install.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/intro/install.rst b/docs/intro/install.rst index 867bc2da7..1d786efe7 100644 --- a/docs/intro/install.rst +++ b/docs/intro/install.rst @@ -54,6 +54,10 @@ Windows python --version +* Install `pywin32` from http://sourceforge.net/projects/pywin32/ + + Be sure you download the architecture (win32 or amd64) that matches your system + * Install `pip`_ from https://pip.pypa.io/en/latest/installing.html Now open a Command prompt to check ``pip`` is installed correctly:: From b21a28cc9a4d0cfc3375974b857123b6be0386f4 Mon Sep 17 00:00:00 2001 From: HalfCrazy Date: Thu, 6 Nov 2014 00:48:11 +0800 Subject: [PATCH 0065/2974] Afterwords->Afterwards --- docs/topics/loaders.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/loaders.rst b/docs/topics/loaders.rst index d571d564d..236725309 100644 --- a/docs/topics/loaders.rst +++ b/docs/topics/loaders.rst @@ -61,7 +61,7 @@ In other words, data is being collected by extracting it from two XPath locations, using the :meth:`~ItemLoader.add_xpath` method. This is the data that will be assigned to the ``name`` field later. -Afterwords, similar calls are used for ``price`` and ``stock`` fields +Afterwards, similar calls are used for ``price`` and ``stock`` fields (the later using a CSS selector with the :meth:`~ItemLoader.add_css` method), and finally the ``last_update`` field is populated directly with a literal value (``today``) using a different method: :meth:`~ItemLoader.add_value`. From 13f83f0da0c03dabbbdeee3410e1d5f776bd2677 Mon Sep 17 00:00:00 2001 From: Lazar-T Date: Sun, 9 Nov 2014 00:04:22 +0100 Subject: [PATCH 0066/2974] typo --- docs/topics/autothrottle.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/autothrottle.rst b/docs/topics/autothrottle.rst index e7b900876..475946a54 100644 --- a/docs/topics/autothrottle.rst +++ b/docs/topics/autothrottle.rst @@ -11,7 +11,7 @@ Design goals 1. be nicer to sites instead of using default download delay of zero 2. automatically adjust scrapy to the optimum crawling speed, so the user doesn't have to tune the download delays and concurrent requests to find the - optimum one. the user only needs to specify the maximum concurrent requests + optimum one. The user only needs to specify the maximum concurrent requests it allows, and the extension does the rest. How it works From b422312a38b517d77f920ace68f24cbe0f60faaf Mon Sep 17 00:00:00 2001 From: Jeff Paine Date: Sun, 9 Nov 2014 21:08:27 -0500 Subject: [PATCH 0067/2974] Update docs copyright year range --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 7acf7c7fa..fdd3025db 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,7 @@ master_doc = 'index' # General information about the project. project = u'Scrapy' -copyright = u'2008-2013, Scrapy developers' +copyright = u'2008-2014, Scrapy developers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From fa84730e702a1d94ddeee0acbb81ac7394348b98 Mon Sep 17 00:00:00 2001 From: tpeng Date: Wed, 12 Nov 2014 12:28:02 +0100 Subject: [PATCH 0068/2974] avoid download large response introduce DOWNLOAD_MAXSIZE and DOWNLOAD_WARNSIZE in settings and download_maxsize/download_warnsize in spider/request meta, so downloader stop downloading as soon as the received data exceed the limit. also check the twsisted response's length in advance to stop downloading as early as possible. --- docs/topics/settings.rst | 34 +++++++++++++ scrapy/core/downloader/handlers/http11.py | 45 +++++++++++++++-- scrapy/settings/default_settings.py | 3 ++ tests/test_downloader_handlers.py | 60 +++++++++++++++++++++++ 4 files changed, 137 insertions(+), 5 deletions(-) diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 9000f0242..402226743 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -422,6 +422,40 @@ The amount of time (in secs) that the downloader will wait before timing out. spider attribute and per-request using :reqmeta:`download_timeout` Request.meta key. +.. setting:: DOWNLOAD_MAXSIZE + +DOWNLOAD_MAXSIZE +---------------- + +Default: `1073741824` (1024Mb) + +The maximum response size (in bytes) that downloader will download. + +If you want to disable it set to 0. + +.. note:: + + This size can be set per spider using :attr:`download_maxsize` + spider attribute and per-request using :reqmeta:`download_maxsize` + Request.meta key. + +.. setting:: DOWNLOAD_WARNSIZE + +DOWNLOAD_WARNSIZE +---------------- + +Default: `33554432` (32Mb) + +The response size (in bytes) that downloader will start to warn. + +If you want to disable it set to 0. + +.. note:: + + This size can be set per spider using :attr:`download_warnsize` + spider attribute and per-request using :reqmeta:`download_warnsize` + Request.meta key. + .. setting:: DUPEFILTER_CLASS DUPEFILTER_CLASS diff --git a/scrapy/core/downloader/handlers/http11.py b/scrapy/core/downloader/handlers/http11.py index 23cd07c51..dd3ad488b 100644 --- a/scrapy/core/downloader/handlers/http11.py +++ b/scrapy/core/downloader/handlers/http11.py @@ -9,7 +9,7 @@ from six.moves.urllib.parse import urldefrag from zope.interface import implements from twisted.internet import defer, reactor, protocol from twisted.web.http_headers import Headers as TxHeaders -from twisted.web.iweb import IBodyProducer +from twisted.web.iweb import IBodyProducer, UNKNOWN_LENGTH from twisted.internet.error import TimeoutError from twisted.web.http import PotentialDataLoss from scrapy.xlib.tx import Agent, ProxyAgent, ResponseDone, \ @@ -19,6 +19,7 @@ from scrapy.http import Headers from scrapy.responsetypes import responsetypes from scrapy.core.downloader.webclient import _parse from scrapy.utils.misc import load_object +from scrapy import log class HTTP11DownloadHandler(object): @@ -29,10 +30,14 @@ class HTTP11DownloadHandler(object): self._pool._factory.noisy = False self._contextFactoryClass = load_object(settings['DOWNLOADER_CLIENTCONTEXTFACTORY']) self._contextFactory = self._contextFactoryClass() + self._default_maxsize = settings.getint('DOWNLOAD_MAXSIZE') + self._default_warnsize = settings.getint('DOWNLOAD_WARNSIZE') def download_request(self, request, spider): """Return a deferred for the HTTP download""" - agent = ScrapyAgent(contextFactory=self._contextFactory, pool=self._pool) + agent = ScrapyAgent(contextFactory=self._contextFactory, pool=self._pool, + maxsize=getattr(spider, 'download_maxsize', self._default_maxsize), + warnsize=getattr(spider, 'download_warnsize', self._default_warnsize)) return agent.download_request(request) def close(self): @@ -131,11 +136,14 @@ class ScrapyAgent(object): _ProxyAgent = ProxyAgent _TunnelingAgent = TunnelingAgent - def __init__(self, contextFactory=None, connectTimeout=10, bindAddress=None, pool=None): + def __init__(self, contextFactory=None, connectTimeout=10, bindAddress=None, pool=None, + maxsize=0, warnsize=0): self._contextFactory = contextFactory self._connectTimeout = connectTimeout self._bindAddress = bindAddress self._pool = pool + self._maxsize = maxsize + self._warnsize = warnsize def _get_agent(self, request, timeout): bindaddress = request.meta.get('bindaddress') or self._bindAddress @@ -197,11 +205,25 @@ class ScrapyAgent(object): if txresponse.length == 0: return txresponse, '', None + maxsize = request.meta.get('download_maxsize', self._maxsize) + warnsize = request.meta.get('download_warnsize', self._warnsize) + expected_size = txresponse.length if txresponse.length != UNKNOWN_LENGTH else -1 + + if maxsize and expected_size > maxsize: + log.msg("Expected response size (%s) larger than download max size (%s)." % (expected_size, maxsize), + logLevel=log.ERROR) + txresponse._transport._producer.loseConnection() + raise defer.CancelledError() + + if warnsize and expected_size > warnsize: + log.msg("Expected response size (%s) larger than downlod warn size (%s)." % (expected_size, warnsize), + logLevel=log.WARNING) + def _cancel(_): txresponse._transport._producer.loseConnection() d = defer.Deferred(_cancel) - txresponse.deliverBody(_ResponseReader(d, txresponse, request)) + txresponse.deliverBody(_ResponseReader(d, txresponse, request, maxsize, warnsize)) return d def _cb_bodydone(self, result, request, url): @@ -232,14 +254,27 @@ class _RequestBodyProducer(object): class _ResponseReader(protocol.Protocol): - def __init__(self, finished, txresponse, request): + def __init__(self, finished, txresponse, request, maxsize, warnsize): self._finished = finished self._txresponse = txresponse self._request = request self._bodybuf = BytesIO() + self._maxsize = maxsize + self._warnsize = warnsize + self._bytes_received = 0 def dataReceived(self, bodyBytes): self._bodybuf.write(bodyBytes) + self._bytes_received += len(bodyBytes) + + if self._maxsize and self._bytes_received > self._maxsize: + log.msg("Received (%s) bytes larger than download max size (%s)." % (self._bytes_received, self._maxsize), + logLevel=log.ERROR) + self._finished.cancel() + + if self._warnsize and self._bytes_received > self._warnsize: + log.msg("Received (%s) bytes larger than download warn size (%s)." % (self._bytes_received, self._warnsize), + logLevel=log.WARNING) def connectionLost(self, reason): if self._finished.called: diff --git a/scrapy/settings/default_settings.py b/scrapy/settings/default_settings.py index f01203c42..1b7b3bf29 100644 --- a/scrapy/settings/default_settings.py +++ b/scrapy/settings/default_settings.py @@ -66,6 +66,9 @@ DOWNLOAD_HANDLERS_BASE = { DOWNLOAD_TIMEOUT = 180 # 3mins +DOWNLOAD_MAXSIZE = 1073741824 # 1024m +DOWNLOAD_WARNSIZE = 33554432 # 32m + DOWNLOADER = 'scrapy.core.downloader.Downloader' DOWNLOADER_HTTPCLIENTFACTORY = 'scrapy.core.downloader.webclient.ScrapyHTTPClientFactory' diff --git a/tests/test_downloader_handlers.py b/tests/test_downloader_handlers.py index c444d35fa..55bb7ccf7 100644 --- a/tests/test_downloader_handlers.py +++ b/tests/test_downloader_handlers.py @@ -30,6 +30,8 @@ from scrapy import optional_features from scrapy.utils.test import get_crawler from scrapy.exceptions import NotConfigured +from tests.mockserver import MockServer +from tests.spiders import SingleRequestSpider class DummyDH(object): @@ -211,6 +213,64 @@ class Http11TestCase(HttpTestCase): if 'http11' not in optional_features: skip = 'HTTP1.1 not supported in twisted < 11.1.0' + def test_download_without_maxsize_limit(self): + request = Request(self.getURL('file')) + d = self.download_request(request, Spider('foo')) + d.addCallback(lambda r: r.body) + d.addCallback(self.assertEquals, "0123456789") + return d + + @defer.inlineCallbacks + def test_download_with_maxsize_per_req(self): + meta = {'download_maxsize': 2} + request = Request(self.getURL('file'), meta=meta) + d = self.download_request(request, Spider('foo')) + yield self.assertFailure(d, defer.CancelledError, error.ConnectionAborted) + + @defer.inlineCallbacks + def test_download_with_small_maxsize_per_spider(self): + request = Request(self.getURL('file')) + d = self.download_request(request, Spider('foo', download_maxsize=2)) + yield self.assertFailure(d, defer.CancelledError, error.ConnectionAborted) + + def test_download_with_large_maxsize_per_spider(self): + request = Request(self.getURL('file')) + d = self.download_request(request, Spider('foo', download_maxsize=100)) + d.addCallback(lambda r: r.body) + d.addCallback(self.assertEquals, "0123456789") + return d + + +class Http11MockServerTestCase(unittest.TestCase): + """HTTP 1.1 test case with MockServer""" + if 'http11' not in optional_features: + skip = 'HTTP1.1 not supported in twisted < 11.1.0' + + def setUp(self): + self.mockserver = MockServer() + self.mockserver.__enter__() + + def tearDown(self): + self.mockserver.__exit__(None, None, None) + + @defer.inlineCallbacks + def test_download_with_content_length(self): + crawler = get_crawler(SingleRequestSpider) + # http://localhost:8998/partial set Content-Length to 1024, use download_maxsize= 1000 to avoid + # download it + yield crawler.crawl(seed=Request(url='http://localhost:8998/partial', meta={'download_maxsize': 1000})) + failure = crawler.spider.meta['failure'] + self.assertIsInstance(failure.value, defer.CancelledError) + + @defer.inlineCallbacks + def test_download(self): + crawler = get_crawler(SingleRequestSpider) + yield crawler.crawl(seed=Request(url='http://localhost:8998')) + failure = crawler.spider.meta.get('failure') + self.assertTrue(failure == None) + reason = crawler.spider.meta['close_reason'] + self.assertTrue(reason, 'finished') + class UriResource(resource.Resource): """Return the full uri that was requested""" From a69f042d1064c4beea46fa70084835dd6c91c143 Mon Sep 17 00:00:00 2001 From: tpeng Date: Wed, 19 Nov 2014 11:50:07 +0100 Subject: [PATCH 0069/2974] add 2 more test cases and minor doc fixes --- docs/topics/settings.rst | 10 ++++++--- scrapy/settings/default_settings.py | 4 ++-- tests/mockserver.py | 7 ++++-- tests/test_downloader_handlers.py | 34 +++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 402226743..0e9e53de4 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -427,7 +427,7 @@ The amount of time (in secs) that the downloader will wait before timing out. DOWNLOAD_MAXSIZE ---------------- -Default: `1073741824` (1024Mb) +Default: `1073741824` (1024MB) The maximum response size (in bytes) that downloader will download. @@ -439,12 +439,14 @@ If you want to disable it set to 0. spider attribute and per-request using :reqmeta:`download_maxsize` Request.meta key. + This feature needs Twisted >= 11.1. + .. setting:: DOWNLOAD_WARNSIZE DOWNLOAD_WARNSIZE ----------------- +----------------- -Default: `33554432` (32Mb) +Default: `33554432` (32MB) The response size (in bytes) that downloader will start to warn. @@ -456,6 +458,8 @@ If you want to disable it set to 0. spider attribute and per-request using :reqmeta:`download_warnsize` Request.meta key. + This feature needs Twisted >= 11.1. + .. setting:: DUPEFILTER_CLASS DUPEFILTER_CLASS diff --git a/scrapy/settings/default_settings.py b/scrapy/settings/default_settings.py index 1b7b3bf29..cf216385a 100644 --- a/scrapy/settings/default_settings.py +++ b/scrapy/settings/default_settings.py @@ -66,8 +66,8 @@ DOWNLOAD_HANDLERS_BASE = { DOWNLOAD_TIMEOUT = 180 # 3mins -DOWNLOAD_MAXSIZE = 1073741824 # 1024m -DOWNLOAD_WARNSIZE = 33554432 # 32m +DOWNLOAD_MAXSIZE = 1024*1024*1024 # 1024m +DOWNLOAD_WARNSIZE = 32*1024*1024 # 32m DOWNLOADER = 'scrapy.core.downloader.Downloader' diff --git a/tests/mockserver.py b/tests/mockserver.py index 6910532b6..2c0ad66fb 100644 --- a/tests/mockserver.py +++ b/tests/mockserver.py @@ -1,9 +1,10 @@ from __future__ import print_function import sys, time, random, urllib, os, json from subprocess import Popen, PIPE -from twisted.web.server import Site, NOT_DONE_YET -from twisted.web.resource import Resource +from twisted.web.server import Site, NOT_DONE_YET, GzipEncoderFactory +from twisted.web.resource import Resource, EncodingResourceWrapper from twisted.internet import reactor, defer, ssl +from twisted.web.test.test_webclient import PayloadResource from scrapy import twisted_version @@ -167,6 +168,8 @@ class Root(Resource): self.putChild("drop", Drop()) self.putChild("raw", Raw()) self.putChild("echo", Echo()) + self.putChild('payload', PayloadResource()) + self.putChild("xpayload", EncodingResourceWrapper(PayloadResource(), [GzipEncoderFactory()])) def getChild(self, name, request): return self diff --git a/tests/test_downloader_handlers.py b/tests/test_downloader_handlers.py index 55bb7ccf7..127c4a4bd 100644 --- a/tests/test_downloader_handlers.py +++ b/tests/test_downloader_handlers.py @@ -220,6 +220,20 @@ class Http11TestCase(HttpTestCase): d.addCallback(self.assertEquals, "0123456789") return d + @defer.inlineCallbacks + def test_download_with_maxsize(self): + request = Request(self.getURL('file')) + + # 10 is minimal size for this request and the limit is only counted on + # response body. (regardless of headers) + d = self.download_request(request, Spider('foo', download_maxsize=10)) + d.addCallback(lambda r: r.body) + d.addCallback(self.assertEquals, "0123456789") + yield d + + d = self.download_request(request, Spider('foo', download_maxsize=9)) + yield self.assertFailure(d, defer.CancelledError, error.ConnectionAborted) + @defer.inlineCallbacks def test_download_with_maxsize_per_req(self): meta = {'download_maxsize': 2} @@ -271,6 +285,26 @@ class Http11MockServerTestCase(unittest.TestCase): reason = crawler.spider.meta['close_reason'] self.assertTrue(reason, 'finished') + @defer.inlineCallbacks + def test_download_gzip_response(self): + crawler = get_crawler(SingleRequestSpider) + body = '1'*100 # PayloadResource requires body length to be 100 + request = Request('http://localhost:8998/payload', method='POST', body=body, meta={'download_maxsize': 50}) + yield crawler.crawl(seed=request) + failure = crawler.spider.meta['failure'] + # download_maxsize < 100, hence the CancelledError + self.assertIsInstance(failure.value, defer.CancelledError) + + request.headers.setdefault('Accept-Encoding', 'gzip,deflate') + request = request.replace(url='http://localhost:8998/xpayload') + yield crawler.crawl(seed=request) + + # download_maxsize = 50 is enough for the gzipped response + failure = crawler.spider.meta.get('failure') + self.assertTrue(failure == None) + reason = crawler.spider.meta['close_reason'] + self.assertTrue(reason, 'finished') + class UriResource(resource.Resource): """Return the full uri that was requested""" From 7910fa017243c6f64ecb55b4272894f6eb5d35ff Mon Sep 17 00:00:00 2001 From: Martin Olveyra Date: Fri, 21 Nov 2014 01:09:32 -0200 Subject: [PATCH 0070/2974] Force to read DOWNLOAD_TIMEOUT as int (for example to pass using environment variable) --- scrapy/contrib/downloadermiddleware/downloadtimeout.py | 2 +- tests/test_downloadermiddleware_downloadtimeout.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scrapy/contrib/downloadermiddleware/downloadtimeout.py b/scrapy/contrib/downloadermiddleware/downloadtimeout.py index 612b081ec..18123cfce 100644 --- a/scrapy/contrib/downloadermiddleware/downloadtimeout.py +++ b/scrapy/contrib/downloadermiddleware/downloadtimeout.py @@ -14,7 +14,7 @@ class DownloadTimeoutMiddleware(object): @classmethod def from_crawler(cls, crawler): - o = cls(crawler.settings['DOWNLOAD_TIMEOUT']) + o = cls(crawler.settings.getfloat('DOWNLOAD_TIMEOUT')) crawler.signals.connect(o.spider_opened, signal=signals.spider_opened) return o diff --git a/tests/test_downloadermiddleware_downloadtimeout.py b/tests/test_downloadermiddleware_downloadtimeout.py index 3e3ff2401..aba06686a 100644 --- a/tests/test_downloadermiddleware_downloadtimeout.py +++ b/tests/test_downloadermiddleware_downloadtimeout.py @@ -8,8 +8,8 @@ from scrapy.utils.test import get_crawler class DownloadTimeoutMiddlewareTest(unittest.TestCase): - def get_request_spider_mw(self): - crawler = get_crawler(Spider) + def get_request_spider_mw(self, settings=None): + crawler = get_crawler(Spider, settings) spider = crawler._create_spider('foo') request = Request('http://scrapytest.org/') return request, spider, DownloadTimeoutMiddleware.from_crawler(crawler) @@ -20,6 +20,12 @@ class DownloadTimeoutMiddlewareTest(unittest.TestCase): assert mw.process_request(req, spider) is None self.assertEquals(req.meta.get('download_timeout'), 180) + def test_string_download_timeout(self): + req, spider, mw = self.get_request_spider_mw({'DOWNLOAD_TIMEOUT': '20.1'}) + mw.spider_opened(spider) + assert mw.process_request(req, spider) is None + self.assertEquals(req.meta.get('download_timeout'), 20.1) + def test_spider_has_download_timeout(self): req, spider, mw = self.get_request_spider_mw() spider.download_timeout = 2 From 314db3db8b7b84ee3541e8afdd1026681f410de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Fri, 21 Nov 2014 10:54:43 -0200 Subject: [PATCH 0071/2974] pin mitmproxy 0.10.1 as >0.11 does not work with tests --- tests/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index b7d6a0a56..cd8a4d471 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,4 @@ # Tests requirements mock -mitmproxy >= 0.10 +mitmproxy==0.10.1 pytest-twisted From 8d8e1b2c0c0ba35d8496e503909d7f29dcd1906a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Fri, 21 Nov 2014 12:15:02 -0200 Subject: [PATCH 0072/2974] mitmproxy 0.10.1 needs netlib 0.10.1 too --- tests/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/requirements.txt b/tests/requirements.txt index cd8a4d471..0c1b65aa8 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,5 @@ # Tests requirements mock mitmproxy==0.10.1 +netlib==0.10.1 pytest-twisted From cd193827546d2e20029c28961622ae7def7d541d Mon Sep 17 00:00:00 2001 From: tpeng Date: Tue, 25 Nov 2014 14:09:51 +0100 Subject: [PATCH 0073/2974] attemp to fix travis fails --- tests/mockserver.py | 15 ++++++++----- tests/test_downloader_handlers.py | 36 ++++++++++++++++++------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/tests/mockserver.py b/tests/mockserver.py index 2c0ad66fb..b73208c5c 100644 --- a/tests/mockserver.py +++ b/tests/mockserver.py @@ -1,10 +1,10 @@ from __future__ import print_function import sys, time, random, urllib, os, json +import six from subprocess import Popen, PIPE -from twisted.web.server import Site, NOT_DONE_YET, GzipEncoderFactory -from twisted.web.resource import Resource, EncodingResourceWrapper +from twisted.web.server import Site, NOT_DONE_YET +from twisted.web.resource import Resource from twisted.internet import reactor, defer, ssl -from twisted.web.test.test_webclient import PayloadResource from scrapy import twisted_version @@ -168,8 +168,13 @@ class Root(Resource): self.putChild("drop", Drop()) self.putChild("raw", Raw()) self.putChild("echo", Echo()) - self.putChild('payload', PayloadResource()) - self.putChild("xpayload", EncodingResourceWrapper(PayloadResource(), [GzipEncoderFactory()])) + + if six.PY2 and twisted_version > (12, 3, 0): + from twisted.web.test.test_webclient import PayloadResource + from twisted.web.server import GzipEncoderFactory + from twisted.web.resource import EncodingResourceWrapper + self.putChild('payload', PayloadResource()) + self.putChild("xpayload", EncodingResourceWrapper(PayloadResource(), [GzipEncoderFactory()])) def getChild(self, name, request): return self diff --git a/tests/test_downloader_handlers.py b/tests/test_downloader_handlers.py index 127c4a4bd..9021af3b4 100644 --- a/tests/test_downloader_handlers.py +++ b/tests/test_downloader_handlers.py @@ -1,5 +1,6 @@ import os import twisted +import six from twisted.trial import unittest from twisted.protocols.policies import WrappingFactory @@ -287,23 +288,28 @@ class Http11MockServerTestCase(unittest.TestCase): @defer.inlineCallbacks def test_download_gzip_response(self): - crawler = get_crawler(SingleRequestSpider) - body = '1'*100 # PayloadResource requires body length to be 100 - request = Request('http://localhost:8998/payload', method='POST', body=body, meta={'download_maxsize': 50}) - yield crawler.crawl(seed=request) - failure = crawler.spider.meta['failure'] - # download_maxsize < 100, hence the CancelledError - self.assertIsInstance(failure.value, defer.CancelledError) - request.headers.setdefault('Accept-Encoding', 'gzip,deflate') - request = request.replace(url='http://localhost:8998/xpayload') - yield crawler.crawl(seed=request) + if six.PY2 and twisted_version > (12, 3, 0): - # download_maxsize = 50 is enough for the gzipped response - failure = crawler.spider.meta.get('failure') - self.assertTrue(failure == None) - reason = crawler.spider.meta['close_reason'] - self.assertTrue(reason, 'finished') + crawler = get_crawler(SingleRequestSpider) + body = '1'*100 # PayloadResource requires body length to be 100 + request = Request('http://localhost:8998/payload', method='POST', body=body, meta={'download_maxsize': 50}) + yield crawler.crawl(seed=request) + failure = crawler.spider.meta['failure'] + # download_maxsize < 100, hence the CancelledError + self.assertIsInstance(failure.value, defer.CancelledError) + + request.headers.setdefault('Accept-Encoding', 'gzip,deflate') + request = request.replace(url='http://localhost:8998/xpayload') + yield crawler.crawl(seed=request) + + # download_maxsize = 50 is enough for the gzipped response + failure = crawler.spider.meta.get('failure') + self.assertTrue(failure == None) + reason = crawler.spider.meta['close_reason'] + self.assertTrue(reason, 'finished') + else: + raise unittest.SkipTest("xpayload and payload endpoint only enabled for twisted > 12.3.0 and python 2.x") class UriResource(resource.Resource): From e04b0aff74632702d78f7729b855c0737d50dfcf Mon Sep 17 00:00:00 2001 From: Lev Berman Date: Thu, 27 Nov 2014 15:10:15 +0300 Subject: [PATCH 0074/2974] An attempt to resolve #977, add signal to be sent when request is dropped by the scheduler --- docs/topics/signals.rst | 17 +++++++++++++++++ scrapy/core/engine.py | 4 +++- scrapy/core/scheduler.py | 3 ++- scrapy/signals.py | 1 + tests/test_engine.py | 10 ++++++++++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/topics/signals.rst b/docs/topics/signals.rst index 5407141db..405b131ed 100644 --- a/docs/topics/signals.rst +++ b/docs/topics/signals.rst @@ -200,6 +200,23 @@ request_scheduled :param spider: the spider that yielded the request :type spider: :class:`~scrapy.spider.Spider` object +request_dropped +----------------- + +.. signal:: request_dropped +.. function:: request_dropped(request, spider) + + Sent when a :class:`~scrapy.http.Request`, scheduled by the engine to be + downloaded later, is rejected by the scheduler. + + The signal does not support returning deferreds from their handlers. + + :param request: the request that reached the scheduler + :type request: :class:`~scrapy.http.Request` object + + :param spider: the spider that yielded the request + :type spider: :class:`~scrapy.spider.Spider` object + response_received ----------------- diff --git a/scrapy/core/engine.py b/scrapy/core/engine.py index 4ef1d0fc6..b009898a3 100644 --- a/scrapy/core/engine.py +++ b/scrapy/core/engine.py @@ -173,7 +173,9 @@ class ExecutionEngine(object): def schedule(self, request, spider): self.signals.send_catch_log(signal=signals.request_scheduled, request=request, spider=spider) - return self.slot.scheduler.enqueue_request(request) + if not self.slot.scheduler.enqueue_request(request): + self.signals.send_catch_log(signal=signals.request_dropped, + request=request, spider=spider) def download(self, request, spider): slot = self.slot diff --git a/scrapy/core/scheduler.py b/scrapy/core/scheduler.py index ba2ca5a03..232bc6a40 100644 --- a/scrapy/core/scheduler.py +++ b/scrapy/core/scheduler.py @@ -47,7 +47,7 @@ class Scheduler(object): def enqueue_request(self, request): if not request.dont_filter and self.df.request_seen(request): self.df.log(request, self.spider) - return + return False dqok = self._dqpush(request) if dqok: self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider) @@ -55,6 +55,7 @@ class Scheduler(object): self._mqpush(request) self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider) self.stats.inc_value('scheduler/enqueued', spider=self.spider) + return True def next_request(self): request = self.mqs.pop() diff --git a/scrapy/signals.py b/scrapy/signals.py index 11bbae945..de0886fb6 100644 --- a/scrapy/signals.py +++ b/scrapy/signals.py @@ -12,6 +12,7 @@ spider_idle = object() spider_closed = object() spider_error = object() request_scheduled = object() +request_dropped = object() response_received = object() response_downloaded = object() item_scraped = object() diff --git a/tests/test_engine.py b/tests/test_engine.py index 67fb8ae79..6ed700caa 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -79,6 +79,7 @@ class CrawlerRun(object): self.spider = None self.respplug = [] self.reqplug = [] + self.reqdropped = [] self.itemresp = [] self.signals_catched = {} @@ -95,6 +96,7 @@ class CrawlerRun(object): self.crawler = get_crawler(TestSpider) self.crawler.signals.connect(self.item_scraped, signals.item_scraped) self.crawler.signals.connect(self.request_scheduled, signals.request_scheduled) + self.crawler.signals.connect(self.request_dropped, signals.request_dropped) self.crawler.signals.connect(self.response_downloaded, signals.response_downloaded) self.crawler.crawl(start_urls=start_urls) self.spider = self.crawler.spider @@ -123,6 +125,9 @@ class CrawlerRun(object): def request_scheduled(self, request, spider): self.reqplug.append((request, spider)) + def request_dropped(self, request, spider): + self.reqdropped.append((request, spider)) + def response_downloaded(self, response, spider): self.respplug.append((response, spider)) @@ -161,6 +166,11 @@ class EngineTest(unittest.TestCase): urls_requested = set([rq[0].url for rq in self.run.reqplug]) urls_expected = set([self.run.geturl(p) for p in paths_expected]) assert urls_expected <= urls_requested + scheduled_requests_count = len(self.run.reqplug) + dropped_requests_count = len(self.run.reqdropped) + responses_count = len(self.run.respplug) + self.assertEqual(scheduled_requests_count, + dropped_requests_count + responses_count) def _assert_downloaded_responses(self): # response tests From fdb6bb07c0cbe2cf664993555c376eeff750f28d Mon Sep 17 00:00:00 2001 From: Lev Berman Date: Fri, 28 Nov 2014 10:53:33 +0300 Subject: [PATCH 0075/2974] #977 - test dropping requests --- tests/test_engine.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index 6ed700caa..52c8e5752 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -59,6 +59,12 @@ class TestSpider(Spider): item['price'] = m.group(1) return item + +class TestDupeFilterSpider(TestSpider): + def make_requests_from_url(self, url): + return Request(url) # dont_filter=False + + def start_test_site(debug=False): root_dir = os.path.join(tests_datadir, "test_site") r = static.File(root_dir) @@ -75,25 +81,28 @@ def start_test_site(debug=False): class CrawlerRun(object): """A class to run the crawler and keep track of events occurred""" - def __init__(self): + def __init__(self, with_dupefilter=False): self.spider = None self.respplug = [] self.reqplug = [] self.reqdropped = [] self.itemresp = [] self.signals_catched = {} + self.spider_class = TestSpider if not with_dupefilter else \ + TestDupeFilterSpider def run(self): self.port = start_test_site() self.portno = self.port.getHost().port - start_urls = [self.geturl("/"), self.geturl("/redirect")] + start_urls = [self.geturl("/"), self.geturl("/redirect"), + self.geturl("/redirect")] # a duplicate for name, signal in vars(signals).items(): if not name.startswith('_'): dispatcher.connect(self.record_signal, signal) - self.crawler = get_crawler(TestSpider) + self.crawler = get_crawler(self.spider_class) self.crawler.signals.connect(self.item_scraped, signals.item_scraped) self.crawler.signals.connect(self.request_scheduled, signals.request_scheduled) self.crawler.signals.connect(self.request_dropped, signals.request_dropped) @@ -146,10 +155,14 @@ class EngineTest(unittest.TestCase): self.run = CrawlerRun() yield self.run.run() self._assert_visited_urls() - self._assert_scheduled_requests() + self._assert_scheduled_requests(urls_to_visit=8) self._assert_downloaded_responses() self._assert_scraped_items() self._assert_signals_catched() + self.run = CrawlerRun(with_dupefilter=True) + yield self.run.run() + self._assert_scheduled_requests(urls_to_visit=7) + self._assert_dropped_requests() def _assert_visited_urls(self): must_be_visited = ["/", "/redirect", "/redirected", @@ -158,8 +171,8 @@ class EngineTest(unittest.TestCase): urls_expected = set([self.run.geturl(p) for p in must_be_visited]) assert urls_expected <= urls_visited, "URLs not visited: %s" % list(urls_expected - urls_visited) - def _assert_scheduled_requests(self): - self.assertEqual(6, len(self.run.reqplug)) + def _assert_scheduled_requests(self, urls_to_visit=None): + self.assertEqual(urls_to_visit, len(self.run.reqplug)) paths_expected = ['/item999.html', '/item2.html', '/item1.html'] @@ -172,9 +185,12 @@ class EngineTest(unittest.TestCase): self.assertEqual(scheduled_requests_count, dropped_requests_count + responses_count) + def _assert_dropped_requests(self): + self.assertEqual(len(self.run.reqdropped), 1) + def _assert_downloaded_responses(self): # response tests - self.assertEqual(6, len(self.run.respplug)) + self.assertEqual(8, len(self.run.respplug)) for response, _ in self.run.respplug: if self.run.getpath(response.url) == '/item999.html': From 3602fc4fcb906a432126c87978c07bef42cea527 Mon Sep 17 00:00:00 2001 From: Stefan Date: Wed, 10 Dec 2014 22:48:09 +0100 Subject: [PATCH 0076/2974] fixed the variable types in mailsender documentation --- docs/topics/email.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/email.rst b/docs/topics/email.rst index e73c74753..d99589413 100644 --- a/docs/topics/email.rst +++ b/docs/topics/email.rst @@ -63,10 +63,10 @@ uses `Twisted non-blocking IO`_, like the rest of the framework. :type smtpport: int :param smtptls: enforce using SMTP STARTTLS - :type smtpport: boolean + :type smtptls: boolean :param smtpssl: enforce using a secure SSL connection - :type smtpport: boolean + :type smtpssl: boolean .. classmethod:: from_settings(settings) From 82b187f283b51c9c71e97d240e115b4abe09deec Mon Sep 17 00:00:00 2001 From: immerrr Date: Thu, 11 Dec 2014 17:49:20 +0300 Subject: [PATCH 0077/2974] S3DownloadHandler: fix auth for requests with quoted paths/query params --- scrapy/core/downloader/handlers/s3.py | 6 ++++-- tests/test_downloader_handlers.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/scrapy/core/downloader/handlers/s3.py b/scrapy/core/downloader/handlers/s3.py index 09a76b7b7..f1e2c7767 100644 --- a/scrapy/core/downloader/handlers/s3.py +++ b/scrapy/core/downloader/handlers/s3.py @@ -1,3 +1,5 @@ +from urlparse import unquote + from scrapy import optional_features from scrapy.exceptions import NotConfigured from scrapy.utils.httpobj import urlparse_cached @@ -54,8 +56,8 @@ class S3DownloadHandler(object): signed_headers = self.conn.make_request( method=request.method, bucket=bucket, - key=p.path, - query_args=p.query, + key=unquote(p.path), + query_args=unquote(p.query), headers=request.headers, data=request.body) httpreq = request.replace(url=url, headers=signed_headers) diff --git a/tests/test_downloader_handlers.py b/tests/test_downloader_handlers.py index 9021af3b4..62fc280ee 100644 --- a/tests/test_downloader_handlers.py +++ b/tests/test_downloader_handlers.py @@ -482,6 +482,21 @@ class S3TestCase(unittest.TestCase): self.assertEqual(httpreq.headers['Authorization'], \ 'AWS 0PN5J17HBGZHT7JJ3X82:C0FlOtU8Ylb9KDTpZqYkZPX91iI=') + def test_request_signing7(self): + # ensure that spaces are quoted properly before signing + req = Request( + ("s3://johnsmith/photos/my puppy.jpg" + "?response-content-disposition=my puppy.jpg"), + method='GET', + headers={ + 'Date': 'Tue, 27 Mar 2007 19:42:41 +0000', + }) + httpreq = self.download_request(req, self.spider) + self.assertEqual( + httpreq.headers['Authorization'], + 'AWS 0PN5J17HBGZHT7JJ3X82:+CfvG8EZ3YccOrRVMXNaK2eKZmM=') + + class FTPTestCase(unittest.TestCase): username = "scrapy" From d4cb03eded95dbd27af8dd90495fa4c277ef0b48 Mon Sep 17 00:00:00 2001 From: Artur Gaspar Date: Thu, 11 Dec 2014 16:45:20 -0200 Subject: [PATCH 0078/2974] add CSS support for link extractors --- scrapy/contrib/linkextractors/lxmlhtml.py | 8 ++++---- scrapy/contrib/linkextractors/sgml.py | 8 ++++---- scrapy/linkextractor.py | 8 +++++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/scrapy/contrib/linkextractors/lxmlhtml.py b/scrapy/contrib/linkextractors/lxmlhtml.py index b6de74f33..f747fa99b 100644 --- a/scrapy/contrib/linkextractors/lxmlhtml.py +++ b/scrapy/contrib/linkextractors/lxmlhtml.py @@ -81,8 +81,8 @@ class LxmlParserLinkExtractor(object): class LxmlLinkExtractor(FilteringLinkExtractor): def __init__(self, allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(), - tags=('a', 'area'), attrs=('href',), canonicalize=True, unique=True, process_value=None, - deny_extensions=None): + restrict_css=(), tags=('a', 'area'), attrs=('href',), canonicalize=True, + unique=True, process_value=None, deny_extensions=None): tags, attrs = set(arg_to_iter(tags)), set(arg_to_iter(attrs)) tag_func = lambda x: x in tags attr_func = lambda x: x in attrs @@ -90,8 +90,8 @@ class LxmlLinkExtractor(FilteringLinkExtractor): unique=unique, process=process_value) super(LxmlLinkExtractor, self).__init__(lx, allow, deny, - allow_domains, deny_domains, restrict_xpaths, canonicalize, - deny_extensions) + allow_domains, deny_domains, restrict_xpaths, restrict_css, + canonicalize, deny_extensions) def extract_links(self, response): html = Selector(response) diff --git a/scrapy/contrib/linkextractors/sgml.py b/scrapy/contrib/linkextractors/sgml.py index 3eb5fd91f..3a8fdbb69 100644 --- a/scrapy/contrib/linkextractors/sgml.py +++ b/scrapy/contrib/linkextractors/sgml.py @@ -98,8 +98,8 @@ class BaseSgmlLinkExtractor(SGMLParser): class SgmlLinkExtractor(FilteringLinkExtractor): def __init__(self, allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(), - tags=('a', 'area'), attrs=('href',), canonicalize=True, unique=True, process_value=None, - deny_extensions=None): + restrict_css=(), tags=('a', 'area'), attrs=('href',), canonicalize=True, unique=True, + process_value=None, deny_extensions=None): warnings.warn( "SgmlLinkExtractor is deprecated and will be removed in future releases. " @@ -116,8 +116,8 @@ class SgmlLinkExtractor(FilteringLinkExtractor): unique=unique, process_value=process_value) super(SgmlLinkExtractor, self).__init__(lx, allow, deny, - allow_domains, deny_domains, restrict_xpaths, canonicalize, - deny_extensions) + allow_domains, deny_domains, restrict_xpaths, restrict_css, + canonicalize, deny_extensions) # FIXME: was added to fix a RegexLinkExtractor testcase self.base_url = None diff --git a/scrapy/linkextractor.py b/scrapy/linkextractor.py index 5badea5e5..52271959f 100644 --- a/scrapy/linkextractor.py +++ b/scrapy/linkextractor.py @@ -5,6 +5,7 @@ scrapy.contrib.linkextractor). import re from six.moves.urllib.parse import urlparse +from scrapy.selector.csstranslator import ScrapyHTMLTranslator from scrapy.utils.url import url_is_from_any_domain from scrapy.utils.url import canonicalize_url, url_is_from_any_domain, url_has_any_extension from scrapy.utils.misc import arg_to_iter @@ -38,8 +39,10 @@ _is_valid_url = lambda url: url.split('://', 1)[0] in set(['http', 'https', 'fil class FilteringLinkExtractor(object): + _csstranslator = ScrapyHTMLTranslator() + def __init__(self, link_extractor, allow, deny, allow_domains, deny_domains, - restrict_xpaths, canonicalize, deny_extensions): + restrict_xpaths, restrict_css, canonicalize, deny_extensions): self.link_extractor = link_extractor @@ -50,6 +53,9 @@ class FilteringLinkExtractor(object): self.deny_domains = set(arg_to_iter(deny_domains)) self.restrict_xpaths = tuple(arg_to_iter(restrict_xpaths)) + self.restrict_xpaths += tuple(map(self._csstranslator.css_to_xpath, + arg_to_iter(restrict_css))) + self.canonicalize = canonicalize if deny_extensions is None: deny_extensions = IGNORED_EXTENSIONS From 403fc686b861b193a13a9abe038eda92aebf5ce8 Mon Sep 17 00:00:00 2001 From: Artur Gaspar Date: Thu, 11 Dec 2014 18:20:30 -0200 Subject: [PATCH 0079/2974] tests for CSS support in link extractors --- tests/test_contrib_linkextractors.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_contrib_linkextractors.py b/tests/test_contrib_linkextractors.py index 3902d4c50..a624f9e66 100644 --- a/tests/test_contrib_linkextractors.py +++ b/tests/test_contrib_linkextractors.py @@ -284,6 +284,21 @@ class SgmlLinkExtractorTestCase(unittest.TestCase): [Link(url='http://example.org/foo', text=u'>\u4eac<\u4e1c', fragment='', nofollow=False)]) + def test_restrict_css(self): + lx = self.extractor_cls(restrict_css=('#subwrapper a',)) + self.assertEqual(lx.extract_links(self.response), [ + Link(url='http://example.com/sample2.html', text=u'sample 2') + ]) + + def test_restrict_css_and_restrict_xpaths_together(self): + lx = self.extractor_cls(restrict_xpaths=('//div[@id="subwrapper"]', ), + restrict_css=('#subwrapper + a', )) + self.assertEqual([link for link in lx.extract_links(self.response)], [ + Link(url='http://example.com/sample1.html', text=u''), + Link(url='http://example.com/sample2.html', text=u'sample 2'), + Link(url='http://example.com/sample3.html', text=u'sample 3 text'), + ]) + def test_area_tag_with_unicode_present(self): body = """\xbe\xa9""" response = HtmlResponse("http://example.org", body=body, encoding='utf-8') From b0730a1d16b608a34d12218ed1fd36c6720eee61 Mon Sep 17 00:00:00 2001 From: Artur Gaspar Date: Thu, 11 Dec 2014 18:22:08 -0200 Subject: [PATCH 0080/2974] documentation for CSS support in link extractors --- docs/topics/link-extractors.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/topics/link-extractors.rst b/docs/topics/link-extractors.rst index 43740adcc..9758c2f35 100644 --- a/docs/topics/link-extractors.rst +++ b/docs/topics/link-extractors.rst @@ -51,7 +51,7 @@ LxmlLinkExtractor :synopsis: lxml's HTMLParser-based link extractors -.. class:: LxmlLinkExtractor(allow=(), deny=(), allow_domains=(), deny_domains=(), deny_extensions=None, restrict_xpaths=(), tags=('a', 'area'), attrs=('href',), canonicalize=True, unique=True, process_value=None) +.. class:: LxmlLinkExtractor(allow=(), deny=(), allow_domains=(), deny_domains=(), deny_extensions=None, restrict_xpaths=(), restrict_css=(), tags=('a', 'area'), attrs=('href',), canonicalize=True, unique=True, process_value=None) LxmlLinkExtractor is the recommended link extractor with handy filtering options. It is implemented using lxml's robust HTMLParser. @@ -88,6 +88,11 @@ LxmlLinkExtractor links. See examples below. :type restrict_xpaths: str or list + :param restrict_css: a CSS selector (or list of selectors) which defines + regions inside the response where links should be extracted from. + Has the same behaviour as ``restrict_xpaths``. + :type restrict_css: str or list + :param tags: a tag or a list of tags to consider when extracting links. Defaults to ``('a', 'area')``. :type tags: str or list From 22247cf79152dbeb635b77f445ebd359c6d910db Mon Sep 17 00:00:00 2001 From: Artur Gaspar Date: Mon, 15 Dec 2014 09:18:15 -0200 Subject: [PATCH 0081/2974] move restrict_css argument to end of argument list in link extractors for backwards compatibility, use keyword arguments in link extractor super().__init__() calls --- scrapy/contrib/linkextractors/lxmlhtml.py | 11 ++++++----- scrapy/contrib/linkextractors/sgml.py | 11 ++++++----- scrapy/linkextractor.py | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/scrapy/contrib/linkextractors/lxmlhtml.py b/scrapy/contrib/linkextractors/lxmlhtml.py index f747fa99b..1ff8e4d36 100644 --- a/scrapy/contrib/linkextractors/lxmlhtml.py +++ b/scrapy/contrib/linkextractors/lxmlhtml.py @@ -81,17 +81,18 @@ class LxmlParserLinkExtractor(object): class LxmlLinkExtractor(FilteringLinkExtractor): def __init__(self, allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(), - restrict_css=(), tags=('a', 'area'), attrs=('href',), canonicalize=True, - unique=True, process_value=None, deny_extensions=None): + tags=('a', 'area'), attrs=('href',), canonicalize=True, + unique=True, process_value=None, deny_extensions=None, restrict_css=()): tags, attrs = set(arg_to_iter(tags)), set(arg_to_iter(attrs)) tag_func = lambda x: x in tags attr_func = lambda x: x in attrs lx = LxmlParserLinkExtractor(tag=tag_func, attr=attr_func, unique=unique, process=process_value) - super(LxmlLinkExtractor, self).__init__(lx, allow, deny, - allow_domains, deny_domains, restrict_xpaths, restrict_css, - canonicalize, deny_extensions) + super(LxmlLinkExtractor, self).__init__(lx, allow=allow, deny=deny, + allow_domains=allow_domains, deny_domains=deny_domains, + restrict_xpaths=restrict_xpaths, restrict_css=restrict_css, + canonicalize=canonicalize, deny_extensions=deny_extensions) def extract_links(self, response): html = Selector(response) diff --git a/scrapy/contrib/linkextractors/sgml.py b/scrapy/contrib/linkextractors/sgml.py index 3a8fdbb69..335773db1 100644 --- a/scrapy/contrib/linkextractors/sgml.py +++ b/scrapy/contrib/linkextractors/sgml.py @@ -98,8 +98,8 @@ class BaseSgmlLinkExtractor(SGMLParser): class SgmlLinkExtractor(FilteringLinkExtractor): def __init__(self, allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(), - restrict_css=(), tags=('a', 'area'), attrs=('href',), canonicalize=True, unique=True, - process_value=None, deny_extensions=None): + tags=('a', 'area'), attrs=('href',), canonicalize=True, unique=True, + process_value=None, deny_extensions=None, restrict_css=()): warnings.warn( "SgmlLinkExtractor is deprecated and will be removed in future releases. " @@ -115,9 +115,10 @@ class SgmlLinkExtractor(FilteringLinkExtractor): lx = BaseSgmlLinkExtractor(tag=tag_func, attr=attr_func, unique=unique, process_value=process_value) - super(SgmlLinkExtractor, self).__init__(lx, allow, deny, - allow_domains, deny_domains, restrict_xpaths, restrict_css, - canonicalize, deny_extensions) + super(SgmlLinkExtractor, self).__init__(lx, allow=allow, deny=deny, + allow_domains=allow_domains, deny_domains=deny_domains, + restrict_xpaths=restrict_xpaths, restrict_css=restrict_css, + canonicalize=canonicalize, deny_extensions=deny_extensions) # FIXME: was added to fix a RegexLinkExtractor testcase self.base_url = None diff --git a/scrapy/linkextractor.py b/scrapy/linkextractor.py index 52271959f..227d79b46 100644 --- a/scrapy/linkextractor.py +++ b/scrapy/linkextractor.py @@ -42,7 +42,7 @@ class FilteringLinkExtractor(object): _csstranslator = ScrapyHTMLTranslator() def __init__(self, link_extractor, allow, deny, allow_domains, deny_domains, - restrict_xpaths, restrict_css, canonicalize, deny_extensions): + restrict_xpaths, canonicalize, deny_extensions, restrict_css): self.link_extractor = link_extractor From 82d138e87e68b1e198dfdc3b0d7f62d0de2e1ceb Mon Sep 17 00:00:00 2001 From: tpeng Date: Mon, 1 Dec 2014 14:15:15 +0100 Subject: [PATCH 0082/2974] support namespace prefix in xmliter_lxml --- scrapy/contrib_exp/iterators.py | 6 +++--- tests/test_utils_iterators.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/scrapy/contrib_exp/iterators.py b/scrapy/contrib_exp/iterators.py index 7cf9103fd..d96105fb9 100644 --- a/scrapy/contrib_exp/iterators.py +++ b/scrapy/contrib_exp/iterators.py @@ -2,18 +2,18 @@ from scrapy.http import Response from scrapy.selector import Selector -def xmliter_lxml(obj, nodename, namespace=None): +def xmliter_lxml(obj, nodename, namespace=None, prefix='x'): from lxml import etree reader = _StreamReader(obj) tag = '{%s}%s' % (namespace, nodename) if namespace else nodename iterable = etree.iterparse(reader, tag=tag, encoding=reader.encoding) - selxpath = '//' + ('x:%s' % nodename if namespace else nodename) + selxpath = '//' + ('%s:%s' % (prefix, nodename) if namespace else nodename) for _, node in iterable: nodetext = etree.tostring(node) node.clear() xs = Selector(text=nodetext, type='xml') if namespace: - xs.register_namespace('x', namespace) + xs.register_namespace(prefix, namespace) yield xs.xpath(selxpath)[0] diff --git a/tests/test_utils_iterators.py b/tests/test_utils_iterators.py index 840f4c596..d8faa810c 100644 --- a/tests/test_utils_iterators.py +++ b/tests/test_utils_iterators.py @@ -124,6 +124,38 @@ class LxmlXmliterTestCase(XmliterTestCase): node = next(namespace_iter) self.assertEqual(node.xpath('text()').extract(), ['http://www.mydummycompany.com/images/item2.jpg']) + def test_xmliter_namespaces_prefix(self): + body = """\ + + + + + Apples + Bananas + + + + + African Coffee Table + 80 + 120 + + + + """ + response = XmlResponse(url='http://mydummycompany.com', body=body) + my_iter = self.xmliter(response, 'table', 'http://www.w3.org/TR/html4/', 'h') + + node = next(my_iter) + self.assertEqual(len(node.xpath('h:tr/h:td').extract()), 2) + self.assertEqual(node.xpath('h:tr/h:td[1]/text()').extract(), ['Apples']) + self.assertEqual(node.xpath('h:tr/h:td[2]/text()').extract(), ['Bananas']) + + my_iter = self.xmliter(response, 'table', 'http://www.w3schools.com/furniture', 'f') + + node = next(my_iter) + self.assertEqual(node.xpath('f:name/text()').extract(), ['African Coffee Table']) + class UtilsCsvTestCase(unittest.TestCase): sample_feeds_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'sample_data', 'feeds') From 880c9e52b5c0a88df39e5fda6546771f4729bc47 Mon Sep 17 00:00:00 2001 From: nyov Date: Tue, 16 Dec 2014 19:28:39 +0000 Subject: [PATCH 0083/2974] t.i.b.ThreadedResolver is now a new-style class since 2009 / twisted-9.0.0 https://github.com/twisted/twisted/commit/663d669dce6ee5009eee8c6d2a81f5199855178b --- scrapy/resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/resolver.py b/scrapy/resolver.py index 7d9811727..e1a7aedce 100644 --- a/scrapy/resolver.py +++ b/scrapy/resolver.py @@ -13,7 +13,7 @@ class CachingThreadedResolver(ThreadedResolver): def getHostByName(self, name, timeout = (1, 3, 11, 45)): if name in dnscache: return defer.succeed(dnscache[name]) - d = ThreadedResolver.getHostByName(self, name, timeout) + d = super(CachingThreadedResolver, self).getHostByName(name, timeout) d.addCallback(self._cache_result, name) return d From 09ba4ff68a7ecb00036d02ffda9ee48f48b69402 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Tue, 30 Dec 2014 19:53:50 -0300 Subject: [PATCH 0084/2974] Patches Twisted issue while closing the connection pool on HTTPDownloadHandler --- scrapy/core/downloader/handlers/http11.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/scrapy/core/downloader/handlers/http11.py b/scrapy/core/downloader/handlers/http11.py index dd3ad488b..19db71bd1 100644 --- a/scrapy/core/downloader/handlers/http11.py +++ b/scrapy/core/downloader/handlers/http11.py @@ -32,6 +32,7 @@ class HTTP11DownloadHandler(object): self._contextFactory = self._contextFactoryClass() self._default_maxsize = settings.getint('DOWNLOAD_MAXSIZE') self._default_warnsize = settings.getint('DOWNLOAD_WARNSIZE') + self._disconnect_timeout = 1 def download_request(self, request, spider): """Return a deferred for the HTTP download""" @@ -41,7 +42,24 @@ class HTTP11DownloadHandler(object): return agent.download_request(request) def close(self): - return self._pool.closeCachedConnections() + d = self._pool.closeCachedConnections() + # closeCachedConnections will hang on network or server issues, so + # we'll manually timeout the deferred. + # + # Twisted issue addressing this problem can be found here: + # https://twistedmatrix.com/trac/ticket/7738. + # + # closeCachedConnections doesn't handle external errbacks, so we'll + # issue a callback after `_disconnect_timeout` seconds. + delayed_call = reactor.callLater(self._disconnect_timeout, d.callback, []) + + def cancel_delayed_call(result): + if delayed_call.active(): + delayed_call.cancel() + return result + + d.addBoth(cancel_delayed_call) + return d class TunnelError(Exception): From d68615a5af6df2f972c7d46ab3fb8950f15747e6 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Mon, 19 Jan 2015 10:28:25 -0300 Subject: [PATCH 0085/2974] Test the parse command locally instead of against an external url --- scrapy/utils/testsite.py | 2 ++ tests/test_commands.py | 28 +++++++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/scrapy/utils/testsite.py b/scrapy/utils/testsite.py index 9f8419814..7d4d4b2bd 100644 --- a/scrapy/utils/testsite.py +++ b/scrapy/utils/testsite.py @@ -7,10 +7,12 @@ from twisted.web import server, resource, static, util class SiteTest(object): def setUp(self): + super(SiteTest, self).setUp() self.site = reactor.listenTCP(0, test_site(), interface="127.0.0.1") self.baseurl = "http://localhost:%d/" % self.site.getHost().port def tearDown(self): + super(SiteTest, self).tearDown() self.site.stopListening() def url(self, path): diff --git a/tests/test_commands.py b/tests/test_commands.py index eefda833e..70b4e74dc 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -8,9 +8,13 @@ from shutil import rmtree from tempfile import mkdtemp from twisted.trial import unittest +from twisted.internet import defer from scrapy.utils.python import retry_on_eintr from scrapy.utils.test import get_testenv +from scrapy.utils.testsite import SiteTest +from scrapy.utils.testproc import ProcessTest + class ProjectTest(unittest.TestCase): project_name = 'testproject' @@ -177,7 +181,9 @@ from scrapy.spider import Spider self.assert_("Unable to load" in log) -class ParseCommandTest(CommandTest): +class ParseCommandTest(ProcessTest, SiteTest, CommandTest): + + command = 'parse' def setUp(self): super(ParseCommandTest, self).setUp() @@ -217,17 +223,21 @@ class MyPipeline(object): ITEM_PIPELINES = {'%s.pipelines.MyPipeline': 1} """ % self.project_name) + @defer.inlineCallbacks def test_spider_arguments(self): - p = self.proc('parse', '--spider', self.spider_name, '-a', 'test_arg=1', - '-c', 'parse', 'http://scrapinghub.com') - log = p.stderr.read() - self.assert_("[parse_spider] DEBUG: It Works!" in log, log) + _, _, stderr = yield self.execute(['--spider', self.spider_name, + '-a', 'test_arg=1', + '-c', 'parse', + self.url('/html')]) + self.assert_("[parse_spider] DEBUG: It Works!" in stderr, stderr) + @defer.inlineCallbacks def test_pipelines(self): - p = self.proc('parse', '--spider', self.spider_name, '--pipelines', - '-c', 'parse', 'http://scrapinghub.com') - log = p.stderr.read() - self.assert_("[scrapy] INFO: It Works!" in log, log) + _, _, stderr = yield self.execute(['--spider', self.spider_name, + '--pipelines', + '-c', 'parse', + self.url('/html')]) + self.assert_("[scrapy] INFO: It Works!" in stderr, stderr) class BenchCommandTest(CommandTest): From 73e6b35622ec98a6520aa606c610b28d1b116e82 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Mon, 19 Jan 2015 22:02:46 +0500 Subject: [PATCH 0086/2974] DOC fix a reference --- docs/topics/spider-middleware.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/spider-middleware.rst b/docs/topics/spider-middleware.rst index 92dc6ac47..93878943c 100644 --- a/docs/topics/spider-middleware.rst +++ b/docs/topics/spider-middleware.rst @@ -94,7 +94,7 @@ following methods: :param response: the response which generated this output from the spider - :type response: class:`~scrapy.http.Response` object + :type response: :class:`~scrapy.http.Response` object :param result: the result returned by the spider :type result: an iterable of :class:`~scrapy.http.Request` or From 283d6a5344c97d54091a8a79ba23e7cd819e7a4e Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Mon, 19 Jan 2015 22:07:03 +0500 Subject: [PATCH 0087/2974] DOC a couple more references are fixed --- docs/topics/spider-middleware.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/spider-middleware.rst b/docs/topics/spider-middleware.rst index 93878943c..173c4a80c 100644 --- a/docs/topics/spider-middleware.rst +++ b/docs/topics/spider-middleware.rst @@ -101,7 +101,7 @@ following methods: :class:`~scrapy.item.Item` objects :param spider: the spider whose result is being processed - :type spider: :class:`~scrapy.item.Spider` object + :type spider: :class:`~scrapy.spider.Spider` object .. method:: process_spider_exception(response, exception, spider) @@ -156,7 +156,7 @@ following methods: :type start_requests: an iterable of :class:`~scrapy.http.Request` :param spider: the spider to whom the start requests belong - :type spider: :class:`~scrapy.item.Spider` object + :type spider: :class:`~scrapy.spider.Spider` object .. _Exception: http://docs.python.org/library/exceptions.html#exceptions.Exception From 4bc14da59ed9f68a66e5fcc623bfcb471d98386a Mon Sep 17 00:00:00 2001 From: Capi Etheriel Date: Mon, 19 Jan 2015 17:21:55 -0200 Subject: [PATCH 0088/2974] Updates documentation on dynamic item classes. Fixes #398 --- docs/topics/practices.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/topics/practices.rst b/docs/topics/practices.rst index e9c7a94bf..13dde52a3 100644 --- a/docs/topics/practices.rst +++ b/docs/topics/practices.rst @@ -199,8 +199,6 @@ classes instead of manually coding them. from scrapy.item import DictItem, Field def create_item_class(class_name, field_list): - field_dict = {} - for field_name in field_list: - field_dict[field_name] = Field() + fields = {field_name: Field() for field_name in field_list} - return type(class_name, (DictItem,), field_dict) + return type(class_name, (DictItem,), {'fields': fields}) From bd5d99a2d2dd61868d8c69acf410f27238443abb Mon Sep 17 00:00:00 2001 From: Jonas Tingeborn Date: Wed, 21 Jan 2015 20:18:11 +0100 Subject: [PATCH 0089/2974] add gzip compression to filesystem http cache backend --- docs/topics/downloader-middleware.rst | 12 +++++++++++ scrapy/contrib/httpcache.py | 21 +++++++++++--------- scrapy/settings/default_settings.py | 1 + tests/test_downloadermiddleware_httpcache.py | 5 +++++ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index 835af2e74..e74889dd9 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -560,6 +560,18 @@ Default: ``'scrapy.contrib.httpcache.DummyPolicy'`` The class which implements the cache policy. +.. setting:: HTTPCACHE_GZIP + +HTTPCACHE_GZIP +^^^^^^^^^^^^^^ + +.. versionadded:: 0.25 + +Default: ``False`` + +If enabled, will compress all cached data with gzip. +This setting is specific to the Filesystem backend. + HttpCompressionMiddleware ------------------------- diff --git a/scrapy/contrib/httpcache.py b/scrapy/contrib/httpcache.py index c5cb3023f..3173656fe 100644 --- a/scrapy/contrib/httpcache.py +++ b/scrapy/contrib/httpcache.py @@ -1,5 +1,6 @@ from __future__ import print_function import os +import gzip from six.moves import cPickle as pickle from importlib import import_module from time import time @@ -220,6 +221,8 @@ class FilesystemCacheStorage(object): def __init__(self, settings): self.cachedir = data_path(settings['HTTPCACHE_DIR']) self.expiration_secs = settings.getint('HTTPCACHE_EXPIRATION_SECS') + self.use_gzip = settings.getbool('HTTPCACHE_GZIP') + self._open = gzip.open if self.use_gzip else open def open_spider(self, spider): pass @@ -233,9 +236,9 @@ class FilesystemCacheStorage(object): if metadata is None: return # not cached rpath = self._get_request_path(spider, request) - with open(os.path.join(rpath, 'response_body'), 'rb') as f: + with self._open(os.path.join(rpath, 'response_body'), 'rb') as f: body = f.read() - with open(os.path.join(rpath, 'response_headers'), 'rb') as f: + with self._open(os.path.join(rpath, 'response_headers'), 'rb') as f: rawheaders = f.read() url = metadata.get('response_url') status = metadata['status'] @@ -256,17 +259,17 @@ class FilesystemCacheStorage(object): 'response_url': response.url, 'timestamp': time(), } - with open(os.path.join(rpath, 'meta'), 'wb') as f: + with self._open(os.path.join(rpath, 'meta'), 'wb') as f: f.write(repr(metadata)) - with open(os.path.join(rpath, 'pickled_meta'), 'wb') as f: + with self._open(os.path.join(rpath, 'pickled_meta'), 'wb') as f: pickle.dump(metadata, f, protocol=2) - with open(os.path.join(rpath, 'response_headers'), 'wb') as f: + with self._open(os.path.join(rpath, 'response_headers'), 'wb') as f: f.write(headers_dict_to_raw(response.headers)) - with open(os.path.join(rpath, 'response_body'), 'wb') as f: + with self._open(os.path.join(rpath, 'response_body'), 'wb') as f: f.write(response.body) - with open(os.path.join(rpath, 'request_headers'), 'wb') as f: + with self._open(os.path.join(rpath, 'request_headers'), 'wb') as f: f.write(headers_dict_to_raw(request.headers)) - with open(os.path.join(rpath, 'request_body'), 'wb') as f: + with self._open(os.path.join(rpath, 'request_body'), 'wb') as f: f.write(request.body) def _get_request_path(self, spider, request): @@ -281,7 +284,7 @@ class FilesystemCacheStorage(object): mtime = os.stat(rpath).st_mtime if 0 < self.expiration_secs < time() - mtime: return # expired - with open(metapath, 'rb') as f: + with self._open(metapath, 'rb') as f: return pickle.load(f) diff --git a/scrapy/settings/default_settings.py b/scrapy/settings/default_settings.py index cf216385a..0342b1ada 100644 --- a/scrapy/settings/default_settings.py +++ b/scrapy/settings/default_settings.py @@ -154,6 +154,7 @@ HTTPCACHE_IGNORE_HTTP_CODES = [] HTTPCACHE_IGNORE_SCHEMES = ['file'] HTTPCACHE_DBM_MODULE = 'anydbm' HTTPCACHE_POLICY = 'scrapy.contrib.httpcache.DummyPolicy' +HTTPCACHE_GZIP = False ITEM_PROCESSOR = 'scrapy.contrib.pipeline.ItemPipelineManager' diff --git a/tests/test_downloadermiddleware_httpcache.py b/tests/test_downloadermiddleware_httpcache.py index 1e22ae661..7ac5d5d11 100644 --- a/tests/test_downloadermiddleware_httpcache.py +++ b/tests/test_downloadermiddleware_httpcache.py @@ -136,6 +136,11 @@ class FilesystemStorageTest(DefaultStorageTest): storage_class = 'scrapy.contrib.httpcache.FilesystemCacheStorage' +class FilesystemStorageGzipTest(FilesystemStorageTest): + + def _get_settings(self, **new_settings): + new_settings.setdefault('HTTPCACHE_GZIP', True) + return super(FilesystemStorageTest, self)._get_settings(**new_settings) class LeveldbStorageTest(DefaultStorageTest): From a07b4353d6038ac0782b96a88659718f5fa6d61d Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Thu, 22 Jan 2015 22:58:10 +0500 Subject: [PATCH 0090/2974] DOC reorder topics * duplicate "topics/commands" link is removed; * Request/Response docs are moved to "Basic Concepts"; * settings docs are moved to "Basic Concepts"; * exceptions docs are moved to "Basic Concepts"; * "signals" and "exporters" docs are moved to "Extending Scrapy"; * "Reference" section is dropped because it is empty now. --- docs/index.rst | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 2a1ae037b..0384dae3d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -63,7 +63,11 @@ Basic concepts topics/shell topics/item-pipeline topics/feed-exports + topics/request-response topics/link-extractors + topics/settings + topics/exceptions + :doc:`topics/commands` Learn about the command-line tool used to manage your Scrapy project. @@ -89,9 +93,19 @@ Basic concepts :doc:`topics/feed-exports` Output your scraped data using different formats and storages. +:doc:`topics/request-response` + Understand the classes used to represent HTTP requests and responses. + :doc:`topics/link-extractors` Convenient classes to extract links to follow from pages. +:doc:`topics/settings` + Learn how to configure Scrapy and see all :ref:`available settings `. + +:doc:`topics/exceptions` + See all available exceptions and their meaning. + + Built-in services ================= @@ -106,7 +120,7 @@ Built-in services :doc:`topics/logging` Understand the simple logging facility provided by Scrapy. - + :doc:`topics/stats` Collect statistics about your scraping crawler. @@ -200,6 +214,9 @@ Extending Scrapy topics/spider-middleware topics/extensions topics/api + topics/signals + topics/exporters + :doc:`topics/architecture` Understand the Scrapy architecture. @@ -216,33 +233,9 @@ Extending Scrapy :doc:`topics/api` Use it on extensions and middlewares to extend Scrapy functionality -Reference -========= - -.. toctree:: - :hidden: - - topics/request-response - topics/settings - topics/signals - topics/exceptions - topics/exporters - -:doc:`topics/commands` - Learn about the command-line tool and see all :ref:`available commands `. - -:doc:`topics/request-response` - Understand the classes used to represent HTTP requests and responses. - -:doc:`topics/settings` - Learn how to configure Scrapy and see all :ref:`available settings `. - :doc:`topics/signals` See all available signals and how to work with them. -:doc:`topics/exceptions` - See all available exceptions and their meaning. - :doc:`topics/exporters` Quickly export your scraped items to a file (XML, CSV, etc). From 074b4a9315620801159e5a3427d9cf77d133f501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Thu, 29 Jan 2015 14:53:37 -0200 Subject: [PATCH 0091/2974] Contribute to master branch ref #975 --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6624b43b6..2309bb1f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,2 +1,2 @@ The guidelines for contributing are available here: -http://doc.scrapy.org/en/latest/contributing.html +http://doc.scrapy.org/en/master/contributing.html From f0bdc14522aac0b7e52a1409847e7d57530d56ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Thu, 29 Jan 2015 15:40:12 -0200 Subject: [PATCH 0092/2974] Tentative attention message about what document to read for contributions --- docs/contributing.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/contributing.rst b/docs/contributing.rst index d7a47a746..f8d965df8 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -4,6 +4,11 @@ Contributing to Scrapy ====================== +.. important:: + + Double check you are reading the most recent version of this document at + http://doc.scrapy.org/en/master/contributing.html + There are many ways to contribute to Scrapy. Here are some of them: * Blog about Scrapy. Tell the world how you're using Scrapy. This will help From e42a1ac1a1ead692a25f9c12fa19614ec5c4c090 Mon Sep 17 00:00:00 2001 From: Sudhanshu Shekhar Date: Wed, 14 Jan 2015 02:31:03 +0530 Subject: [PATCH 0093/2974] Reset items_scraped instead of item_count items_scraped is the counter that needs to be reset each time we have scraped a specific number of items in the code instead of item_count (which represents the specific number of items needed before a message is logged). Updating the source code to reflect this. Removed some irrelevant words from the log message. Signed-off-by: Sudhanshu Shekhar --- docs/topics/extensions.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/topics/extensions.rst b/docs/topics/extensions.rst index 2c2f7fb9c..1824bcc3a 100644 --- a/docs/topics/extensions.rst +++ b/docs/topics/extensions.rst @@ -140,9 +140,9 @@ Here is the code of such extension:: def item_scraped(self, item, spider): self.items_scraped += 1 - if self.items_scraped == self.item_count: - spider.log("scraped %d items, resetting counter" % self.items_scraped) - self.item_count = 0 + if self.items_scraped % self.item_count == 0: + spider.log("scraped %d items" % self.items_scraped) + .. _topics-extensions-ref: From 721d8d5daedf543a51a074e5d8691312e63acbd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maik=20R=C3=B6der?= Date: Wed, 4 Feb 2015 11:38:37 +0100 Subject: [PATCH 0094/2974] Update form.py Typo fixed --- scrapy/http/request/form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/http/request/form.py b/scrapy/http/request/form.py index 3aa9db960..02e77ce29 100644 --- a/scrapy/http/request/form.py +++ b/scrapy/http/request/form.py @@ -1,5 +1,5 @@ """ -This module implements the FormRequest class which is a more covenient class +This module implements the FormRequest class which is a more convenient class (than Request) to generate Requests based on form data. See documentation in docs/topics/request-response.rst From 5ea4a72b8eecdae6a2ed29adbbc96df9987509ef Mon Sep 17 00:00:00 2001 From: klangner Date: Mon, 9 Feb 2015 10:28:22 +0100 Subject: [PATCH 0095/2974] py3-ignores.txt supports comments --- .gitignore | 2 ++ conftest.py | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4eb80012f..97535ccaf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/.vagrant +/scrapy.iml *.pyc _trial_temp* dropin.cache diff --git a/conftest.py b/conftest.py index aa27ddd2b..1c221491e 100644 --- a/conftest.py +++ b/conftest.py @@ -9,9 +9,10 @@ if 'django' not in optional_features: collect_ignore.append("tests/test_djangoitem/models.py") if six.PY3: - for fn in open('tests/py3-ignores.txt'): - if fn.strip(): - collect_ignore.append(fn.strip()) + for line in open('tests/py3-ignores.txt'): + filePath = line.strip() + if len(filePath) > 0 and filePath[0] != '#': + collect_ignore.append(filePath) class LogObservers: """Class for keeping track of log observers across test modules""" From a0299d97f56d1d034ac5178d1c1ef9da57aa0cde Mon Sep 17 00:00:00 2001 From: "klangner@gmail.com" Date: Tue, 10 Feb 2015 10:34:44 +0100 Subject: [PATCH 0096/2974] fixed variable name --- conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index 1c221491e..258a670dd 100644 --- a/conftest.py +++ b/conftest.py @@ -10,9 +10,9 @@ if 'django' not in optional_features: if six.PY3: for line in open('tests/py3-ignores.txt'): - filePath = line.strip() - if len(filePath) > 0 and filePath[0] != '#': - collect_ignore.append(filePath) + file_path = line.strip() + if len(file_path) > 0 and file_path[0] != '#': + collect_ignore.append(file_path) class LogObservers: """Class for keeping track of log observers across test modules""" From d67ca77e61020802c593c8b60a977e26bebfd7c6 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Sun, 15 Feb 2015 21:14:47 -0300 Subject: [PATCH 0097/2974] Support new _getEndpoint Agent signatures on Twisted 15.0.0 --- scrapy/core/downloader/handlers/http11.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scrapy/core/downloader/handlers/http11.py b/scrapy/core/downloader/handlers/http11.py index 19db71bd1..634c6398b 100644 --- a/scrapy/core/downloader/handlers/http11.py +++ b/scrapy/core/downloader/handlers/http11.py @@ -19,7 +19,7 @@ from scrapy.http import Headers from scrapy.responsetypes import responsetypes from scrapy.core.downloader.webclient import _parse from scrapy.utils.misc import load_object -from scrapy import log +from scrapy import log, twisted_version class HTTP11DownloadHandler(object): @@ -142,10 +142,19 @@ class TunnelingAgent(Agent): self._proxyConf = proxyConf self._contextFactory = contextFactory - def _getEndpoint(self, scheme, host, port): - return TunnelingTCP4ClientEndpoint(self._reactor, host, port, - self._proxyConf, self._contextFactory, self._connectTimeout, - self._bindAddress) + if twisted_version >= (15, 0, 0): + def _getEndpoint(self, uri): + return TunnelingTCP4ClientEndpoint( + self._reactor, uri.host, uri.port, self._proxyConf, + self._contextFactory, self._endpointFactory._connectTimeout, + self._endpointFactory._bindAddress) + else: + def _getEndpoint(self, scheme, host, port): + return TunnelingTCP4ClientEndpoint( + self._reactor, host, port, self._proxyConf, + self._contextFactory, self._connectTimeout, + self._bindAddress) + class ScrapyAgent(object): From 839ffba971c29b8aa5875ca60d5ea280acd95ae3 Mon Sep 17 00:00:00 2001 From: Sudhanshu Shekhar Date: Wed, 14 Jan 2015 02:31:03 +0530 Subject: [PATCH 0098/2974] Added the first version of SelectJmes Utilizes jmespath. Also, added tests and documentation for the same. --- docs/topics/loaders.rst | 24 ++++++++++++++++++++++++ scrapy/contrib/loader/processor.py | 21 +++++++++++++++++++++ tests/requirements.txt | 1 + tests/test_contrib_loader.py | 28 ++++++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/docs/topics/loaders.rst b/docs/topics/loaders.rst index 236725309..9df8e117d 100644 --- a/docs/topics/loaders.rst +++ b/docs/topics/loaders.rst @@ -675,3 +675,27 @@ Here is a list of all built-in processors: constructor keyword arguments are used as default context values. See :class:`Compose` processor for more info. +.. class:: SelectJmes(json_path) + + Queries the value using the json path provided to the constructor and returns the output. + Requires jmespath (https://github.com/jmespath/jmespath) to run. + This processor takes only one input at a time. + + Example:: + + >>> from scrapy.contrib.loader.processor import SelectJmes, Compose, MapCompose + >>> proc = SelectJmes("foo") #for direct use on lists and dictionaries + >>> proc({'foo': 'bar'}) + 'bar' + >>> proc({'foo': {'bar': 'baz'}}) + {'bar': 'baz'} + + Working with Json:: + + >>> import json + >>> proc_single_json_str = Compose(json.loads, SelectJmes("foo")) + >>> proc_single_json_str('{"foo": "bar"}') + u'bar' + >>> proc_json_list = Compose(json.loads, MapCompose(SelectJmes('foo'))) + >>> proc_json_list('[{"foo":"bar"}, {"baz":"tar"}]') + [u'bar'] diff --git a/scrapy/contrib/loader/processor.py b/scrapy/contrib/loader/processor.py index 8c74c5358..3b221acaf 100644 --- a/scrapy/contrib/loader/processor.py +++ b/scrapy/contrib/loader/processor.py @@ -8,6 +8,7 @@ from scrapy.utils.misc import arg_to_iter from scrapy.utils.datatypes import MergeDict from .common import wrap_loader_context + class MapCompose(object): def __init__(self, *functions, **default_loader_context): @@ -63,6 +64,26 @@ class Identity(object): return values +class SelectJmes(object): + """ + Query the input string for the jmespath (given at instantiation), + and return the answer + Requires : jmespath(https://github.com/jmespath/jmespath) + Note: SelectJmes accepts only one input element at a time. + """ + def __init__(self, json_path): + self.json_path = json_path + import jmespath + self.compiled_path = jmespath.compile(self.json_path) + + def __call__(self, value): + """Query value for the jmespath query and return answer + :param str value: a string with JSON data to extract from + :return: Element extracted according to jmespath query + """ + return self.compiled_path.search(value) + + class Join(object): def __init__(self, separator=u' '): diff --git a/tests/requirements.txt b/tests/requirements.txt index 0c1b65aa8..4da8e30ea 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -3,3 +3,4 @@ mock mitmproxy==0.10.1 netlib==0.10.1 pytest-twisted +jmespath diff --git a/tests/test_contrib_loader.py b/tests/test_contrib_loader.py index 3330b6105..6af4cf70c 100644 --- a/tests/test_contrib_loader.py +++ b/tests/test_contrib_loader.py @@ -3,12 +3,11 @@ from functools import partial from scrapy.contrib.loader import ItemLoader from scrapy.contrib.loader.processor import Join, Identity, TakeFirst, \ - Compose, MapCompose + Compose, MapCompose, SelectJmes from scrapy.item import Item, Field from scrapy.selector import Selector from scrapy.http import HtmlResponse - # test items class NameItem(Item): name = Field() @@ -579,5 +578,30 @@ class SelectortemLoaderTest(unittest.TestCase): self.assertEqual(l.get_output_value('url'), [u'scrapy.org']) +class SelectJmesTestCase(unittest.TestCase): + test_list_equals = { + 'simple': ('foo.bar', {"foo": {"bar": "baz"}}, "baz"), + 'invalid': ('foo.bar.baz', {"foo": {"bar": "baz"}}, None), + 'top_level': ('foo', {"foo": {"bar": "baz"}}, {"bar": "baz"}), + 'double_vs_single_quote_string': ('foo.bar', {"foo": {"bar": "baz"}}, "baz"), + 'dict': ( + 'foo.bar[*].name', + {"foo": {"bar": [{"name": "one"}, {"name": "two"}]}}, + ['one', 'two'] + ), + 'list': ('[1]', [1, 2], 2) + } + + def test_output(self): + for l in self.test_list_equals: + expr, test_list, expected = self.test_list_equals[l] + test = SelectJmes(expr)(test_list) + self.assertEqual( + test, + expected, + msg='test "{}" got {} expected {}'.format(l, test, expected) + ) + + if __name__ == "__main__": unittest.main() From c9d7386a32aeb4bc7fe9654d194651eee1ede56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Wed, 25 Feb 2015 18:03:53 -0200 Subject: [PATCH 0099/2974] Add 0.24.5 release notes --- docs/news.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/news.rst b/docs/news.rst index d246e98bc..0f5e78e8c 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -3,6 +3,32 @@ Release notes ============= +0.24.5 (2015-02-25) +------------------- + +- Support new _getEndpoint Agent signatures on Twisted 15.0.0 (:commit:`540b9bc`) +- DOC a couple more references are fixed (:commit:`b4c454b`) +- DOC fix a reference (:commit:`e3c1260`) +- t.i.b.ThreadedResolver is now a new-style class (:commit:`9e13f42`) +- S3DownloadHandler: fix auth for requests with quoted paths/query params (:commit:`cdb9 +- fixed the variable types in mailsender documentation (:commit:`bb3a848`) +- Reset items_scraped instead of item_count (:commit:`edb07a4`) +- Tentative attention message about what document to read for contributions (:commit:`7e +- mitmproxy 0.10.1 needs netlib 0.10.1 too (:commit:`874fcdd`) +- pin mitmproxy 0.10.1 as >0.11 does not work with tests (:commit:`c6b21f0`) +- Test the parse command locally instead of against an external url (:commit:`c3a6628`) +- Patches Twisted issue while closing the connection pool on HTTPDownloadHandler (:commi +- Updates documentation on dynamic item classes. (:commit:`eeb589a`) +- Merge pull request #943 from Lazar-T/patch-3 (:commit:`5fdab02`) +- typo (:commit:`b0ae199`) +- pywin32 is required by Twisted. closes #937 (:commit:`5cb0cfb`) +- Update install.rst (:commit:`781286b`) +- Merge pull request #928 from Lazar-T/patch-1 (:commit:`b415d04`) +- comma instead of fullstop (:commit:`627b9ba`) +- Merge pull request #885 from jsma/patch-1 (:commit:`de909ad`) +- Update request-response.rst (:commit:`3f3263d`) +- SgmlLinkExtractor - fix for parsing tag with Unicode present (:commit:`49b40f0` + 0.24.4 (2014-08-09) ------------------- From 31e5f164d41ad4781bb610db53f7ab7bb5ddc4f9 Mon Sep 17 00:00:00 2001 From: Berker Peksag Date: Fri, 6 Mar 2015 15:45:04 +0200 Subject: [PATCH 0100/2974] Import unittest.mock if available. mock is in the stdlib since Python 3.3. --- tests/__init__.py | 5 +++++ tests/test_crawl.py | 2 +- tests/test_downloadermiddleware_robotstxt.py | 2 +- tests/test_pipeline_files.py | 3 ++- tests/test_settings/__init__.py | 5 +---- tests/test_spider.py | 6 ++---- tests/test_utils_deprecate.py | 3 ++- tox.ini | 1 - 8 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 54e79b318..c6dd45181 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,6 +6,11 @@ see http://doc.scrapy.org/en/latest/contributing.html#running-tests import os +try: + import unittest.mock as mock +except ImportError: + import mock + tests_datadir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'sample_data') def get_testdata(*paths): diff --git a/tests/test_crawl.py b/tests/test_crawl.py index 48931d6ff..0fcc7c31e 100644 --- a/tests/test_crawl.py +++ b/tests/test_crawl.py @@ -1,9 +1,9 @@ import json import socket -import mock from twisted.internet import defer from twisted.trial.unittest import TestCase from scrapy.utils.test import get_crawler, get_testlog +from tests import mock from tests.spiders import FollowAllSpider, DelaySpider, SimpleSpider, \ BrokenStartRequestsSpider, SingleRequestSpider, DuplicateStartRequestsSpider from tests.mockserver import MockServer diff --git a/tests/test_downloadermiddleware_robotstxt.py b/tests/test_downloadermiddleware_robotstxt.py index eba337cbd..8b7ac7a6b 100644 --- a/tests/test_downloadermiddleware_robotstxt.py +++ b/tests/test_downloadermiddleware_robotstxt.py @@ -1,6 +1,5 @@ from __future__ import absolute_import import re -import mock from twisted.internet import reactor from twisted.internet.defer import Deferred from twisted.trial import unittest @@ -8,6 +7,7 @@ from scrapy.contrib.downloadermiddleware.robotstxt import RobotsTxtMiddleware from scrapy.exceptions import IgnoreRequest, NotConfigured from scrapy.http import Request, Response from scrapy.settings import Settings +from tests import mock class RobotsTxtMiddlewareTest(unittest.TestCase): diff --git a/tests/test_pipeline_files.py b/tests/test_pipeline_files.py index d1bcf5f79..0a1737c44 100644 --- a/tests/test_pipeline_files.py +++ b/tests/test_pipeline_files.py @@ -1,4 +1,3 @@ -import mock import os import time import hashlib @@ -14,6 +13,8 @@ from scrapy.item import Item, Field from scrapy.http import Request, Response from scrapy.settings import Settings +from tests import mock + def _mocked_download_func(request, info): response = request.meta.get('response') diff --git a/tests/test_settings/__init__.py b/tests/test_settings/__init__.py index 38797ad45..092806d20 100644 --- a/tests/test_settings/__init__.py +++ b/tests/test_settings/__init__.py @@ -1,12 +1,9 @@ import six import unittest import warnings -try: - from unittest import mock -except ImportError: - import mock from scrapy.settings import Settings, SettingsAttribute, CrawlerSettings +from tests import mock from . import default_settings diff --git a/tests/test_spider.py b/tests/test_spider.py index 585b4b53b..56bcb2b1e 100644 --- a/tests/test_spider.py +++ b/tests/test_spider.py @@ -3,10 +3,6 @@ import inspect import warnings from io import BytesIO from twisted.trial import unittest -try: - from unittest import mock -except ImportError: - import mock from scrapy import signals from scrapy.spider import Spider, BaseSpider @@ -20,6 +16,8 @@ from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.utils.trackref import object_ref from scrapy.utils.test import get_crawler +from tests import mock + class SpiderTest(unittest.TestCase): diff --git a/tests/test_utils_deprecate.py b/tests/test_utils_deprecate.py index d31b1d1f3..3d94bc1a0 100644 --- a/tests/test_utils_deprecate.py +++ b/tests/test_utils_deprecate.py @@ -3,9 +3,10 @@ from __future__ import absolute_import import inspect import unittest import warnings -import mock from scrapy.utils.deprecate import create_deprecated_class +from tests import mock + class MyWarning(UserWarning): pass diff --git a/tox.ini b/tox.ini index 624f550e1..2dff74973 100644 --- a/tox.ini +++ b/tox.ini @@ -50,7 +50,6 @@ deps = w3lib>=1.8.0 Pillow # tests requirements - mock pytest>=2.6.0 pytest-twisted From c86e1bebec9d1e994ca4afdc13610c0fb59d6c53 Mon Sep 17 00:00:00 2001 From: Berker Peksag Date: Sun, 8 Mar 2015 15:18:49 +0200 Subject: [PATCH 0101/2974] assertItemsEqual was renamed to assertCountEqual in Python 3. --- tests/test_settings/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_settings/__init__.py b/tests/test_settings/__init__.py index 38797ad45..127e282ea 100644 --- a/tests/test_settings/__init__.py +++ b/tests/test_settings/__init__.py @@ -33,6 +33,9 @@ class SettingsAttributeTest(unittest.TestCase): class SettingsTest(unittest.TestCase): + if six.PY3: + assertItemsEqual = unittest.TestCase.assertCountEqual + def setUp(self): self.settings = Settings() From f7031c08ffadf3a78182d38c46c4d4897feccaa0 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Tue, 10 Mar 2015 22:29:07 -0300 Subject: [PATCH 0102/2974] updating list of Request.meta special keys --- docs/topics/downloader-middleware.rst | 2 ++ docs/topics/request-response.rst | 3 +++ docs/topics/settings.rst | 2 ++ docs/topics/spider-middleware.rst | 5 ++++- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index 835af2e74..fbc5391fa 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -603,6 +603,8 @@ HttpProxyMiddleware .. versionadded:: 0.8 +.. reqmeta:: proxy + .. class:: HttpProxyMiddleware This middleware sets the HTTP proxy to use for requests, by setting the diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index 68d87d04f..f2c37c2e6 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -224,12 +224,15 @@ Those are: * :reqmeta:`dont_redirect` * :reqmeta:`dont_retry` * :reqmeta:`handle_httpstatus_list` +* :reqmeta:`handle_httpstatus_all` * ``dont_merge_cookies`` (see ``cookies`` parameter of :class:`Request` constructor) * :reqmeta:`cookiejar` * :reqmeta:`redirect_urls` * :reqmeta:`bindaddress` * :reqmeta:`dont_obey_robotstxt` * :reqmeta:`download_timeout` +* :reqmeta:`download_maxsize` +* :reqmeta:`proxy` .. reqmeta:: bindaddress diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 0e9e53de4..ea2c208bb 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -433,6 +433,8 @@ The maximum response size (in bytes) that downloader will download. If you want to disable it set to 0. +.. reqmeta:: download_maxsize + .. note:: This size can be set per spider using :attr:`download_maxsize` diff --git a/docs/topics/spider-middleware.rst b/docs/topics/spider-middleware.rst index 92dc6ac47..fc707a87a 100644 --- a/docs/topics/spider-middleware.rst +++ b/docs/topics/spider-middleware.rst @@ -225,9 +225,12 @@ this:: .. reqmeta:: handle_httpstatus_list +.. reqmeta:: handle_httpstatus_all + The ``handle_httpstatus_list`` key of :attr:`Request.meta ` can also be used to specify which response codes to -allow on a per-request basis. +allow on a per-request basis. You can also set the meta key ``handle_httpstatus_all`` +to ``True`` if you want to allow any response code for a request. Keep in mind, however, that it's usually a bad idea to handle non-200 responses, unless you really know what you're doing. From 57a5ee0097f5ec9ba45726d28ec943b88ec47fde Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Thu, 12 Mar 2015 23:20:44 -0300 Subject: [PATCH 0103/2974] added example value to set for proxy meta key --- docs/topics/downloader-middleware.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index fbc5391fa..a3647fd9b 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -608,7 +608,7 @@ HttpProxyMiddleware .. class:: HttpProxyMiddleware This middleware sets the HTTP proxy to use for requests, by setting the - ``proxy`` meta value to :class:`~scrapy.http.Request` objects. + ``proxy`` meta value for :class:`~scrapy.http.Request` objects. Like the Python standard library modules `urllib`_ and `urllib2`_, it obeys the following environment variables: @@ -617,6 +617,9 @@ HttpProxyMiddleware * ``https_proxy`` * ``no_proxy`` + You can also set the meta key ``proxy`` per-request, to a value like + ``http://some_proxy_server:port``. + .. _urllib: http://docs.python.org/library/urllib.html .. _urllib2: http://docs.python.org/library/urllib2.html From 05a88152affd1d153c7accb37370919d1374faa5 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Sat, 14 Mar 2015 16:12:37 -0300 Subject: [PATCH 0104/2974] adding more settings to project template --- .../templates/project/module/settings.py.tmpl | 57 ++++++++++++++++++- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/scrapy/templates/project/module/settings.py.tmpl b/scrapy/templates/project/module/settings.py.tmpl index e9f1ba0e4..572519412 100644 --- a/scrapy/templates/project/module/settings.py.tmpl +++ b/scrapy/templates/project/module/settings.py.tmpl @@ -2,16 +2,67 @@ # Scrapy settings for $project_name project # -# For simplicity, this file contains only the most important settings by -# default. All the other settings are documented here: +# For simplicity, this file contains only settings considered important or +# commonly used. You can find more settings consulting the documentation: # # http://doc.scrapy.org/en/latest/topics/settings.html -# +# http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html +# http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html BOT_NAME = '$project_name' SPIDER_MODULES = ['$project_name.spiders'] NEWSPIDER_MODULE = '$project_name.spiders' + # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = '$project_name (+http://www.yourdomain.com)' + + +# Configure a delay for requests for the same website +# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay +#DOWNLOAD_DELAY=3 +# The download delay setting will honor only one of: +#CONCURRENT_REQUESTS_PER_DOMAIN=16 +#CONCURRENT_REQUESTS_PER_IP=16 + +# Disable cookies (enabled by default) +# COOKIES_ENABLED=False + +# Disable Telnet Console (enabled by default) +# TELNETCONSOLE_ENABLED=False + +# Override the default request headers: +# DEFAULT_REQUEST_HEADERS = { +# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', +# 'Accept-Language': 'en', +# } + +# Enable or disable spider middlewares +# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html +# SPIDER_MIDDLEWARES = { +# '$project_name.middlewares.MyCustomSpiderMiddleware': 543, +# } + +# Enable or disable downloader middlewares +# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html +# DOWNLOADER_MIDDLEWARES = { +# '$project_name.middlewares.MyCustomDownloaderMiddleware': 543, +# } + +# Enable and configure the AutoThrottle extension (disabled by default) +# See http://doc.scrapy.org/en/latest/topics/autothrottle.html +#AUTOTHROTTLE_ENABLED=True +# The initial download delay (default: 5) +#AUTOTHROTTLE_START_DELAY=3 +# The maximum download delay to be set in case of high latencies (default: 60) +#AUTOTHROTTLE_MAX_DELAY=90 +# Enable showing throttling stats for every response received: +#AUTOTHROTTLE_DEBUG=True + +# Enable and configure HTTP caching (disabled by default) +# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings +#HTTPCACHE_ENABLED=True +#HTTPCACHE_EXPIRATION_SECS=3600 +#HTTPCACHE_DIR='httpcache' +#HTTPCACHE_IGNORE_HTTP_CODES=[404] From c13e23641bbc66c5d1a760310c6eb97e35c12c11 Mon Sep 17 00:00:00 2001 From: nramirezuy Date: Mon, 28 Jul 2014 17:58:56 -0300 Subject: [PATCH 0105/2974] httpcache dont_cache meta #19 #689 --- docs/topics/downloader-middleware.rst | 3 +++ docs/topics/request-response.rst | 1 + scrapy/contrib/downloadermiddleware/httpcache.py | 6 ++++++ tests/test_downloadermiddleware_httpcache.py | 12 ++++++++++++ 4 files changed, 22 insertions(+) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index a3647fd9b..9ef09785f 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -328,6 +328,9 @@ HttpCacheMiddleware You can change the HTTP cache policy with the :setting:`HTTPCACHE_POLICY` setting. Or you can also implement your own policy. + .. reqmeta:: dont_cache + + You can also avoid caching a response on every policy using :reqmeta:`dont_cache` meta key equals `True`. .. _httpcache-policy-dummy: diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index f2c37c2e6..b6b165d50 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -227,6 +227,7 @@ Those are: * :reqmeta:`handle_httpstatus_all` * ``dont_merge_cookies`` (see ``cookies`` parameter of :class:`Request` constructor) * :reqmeta:`cookiejar` + :reqmeta:`dont_cache` * :reqmeta:`redirect_urls` * :reqmeta:`bindaddress` * :reqmeta:`dont_obey_robotstxt` diff --git a/scrapy/contrib/downloadermiddleware/httpcache.py b/scrapy/contrib/downloadermiddleware/httpcache.py index 90aa6cab7..7b4b53f7c 100644 --- a/scrapy/contrib/downloadermiddleware/httpcache.py +++ b/scrapy/contrib/downloadermiddleware/httpcache.py @@ -28,6 +28,9 @@ class HttpCacheMiddleware(object): self.storage.close_spider(spider) def process_request(self, request, spider): + if request.meta.get('dont_cache', False): + return + # Skip uncacheable requests if not self.policy.should_cache_request(request): request.meta['_dont_cache'] = True # flag as uncacheable @@ -53,6 +56,9 @@ class HttpCacheMiddleware(object): request.meta['cached_response'] = cachedresponse def process_response(self, request, response, spider): + if request.meta.get('dont_cache', False): + return response + # Skip cached responses and uncacheable requests if 'cached' in response.flags or '_dont_cache' in request.meta: request.meta.pop('_dont_cache', None) diff --git a/tests/test_downloadermiddleware_httpcache.py b/tests/test_downloadermiddleware_httpcache.py index 1e22ae661..ea811cb3b 100644 --- a/tests/test_downloadermiddleware_httpcache.py +++ b/tests/test_downloadermiddleware_httpcache.py @@ -89,6 +89,18 @@ class _BaseTest(unittest.TestCase): assert any(h in request2.headers for h in ('If-None-Match', 'If-Modified-Since')) self.assertEqual(request1.body, request2.body) + def test_dont_cache(self): + with self._middleware() as mw: + self.request.meta['dont_cache'] = True + mw.process_response(self.request, self.response, self.spider) + self.assertEqual(mw.storage.retrieve_response(self.spider, self.request), None) + + with self._middleware() as mw: + self.request.meta['dont_cache'] = False + mw.process_response(self.request, self.response, self.spider) + if mw.policy.should_cache_response(self.response, self.request): + self.assertIsInstance(mw.storage.retrieve_response(self.spider, self.request), self.response.__class__) + class DefaultStorageTest(_BaseTest): From aa56dd30ea31d6ac1785e4fb383375250b2010c9 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Mon, 16 Mar 2015 17:36:46 -0300 Subject: [PATCH 0106/2974] updating new settings to template, as pointed in PR review --- .../templates/project/module/settings.py.tmpl | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scrapy/templates/project/module/settings.py.tmpl b/scrapy/templates/project/module/settings.py.tmpl index 572519412..f4832dd27 100644 --- a/scrapy/templates/project/module/settings.py.tmpl +++ b/scrapy/templates/project/module/settings.py.tmpl @@ -18,9 +18,12 @@ NEWSPIDER_MODULE = '$project_name.spiders' # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = '$project_name (+http://www.yourdomain.com)' +# Configure maximum concurrent requests performed by Scrapy (default: 16) +# CONCURRENT_REQUESTS=32 -# Configure a delay for requests for the same website +# Configure a delay for requests for the same website (default: 0) # See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay +# See also autothrottle settings and docs #DOWNLOAD_DELAY=3 # The download delay setting will honor only one of: #CONCURRENT_REQUESTS_PER_DOMAIN=16 @@ -50,8 +53,21 @@ NEWSPIDER_MODULE = '$project_name.spiders' # '$project_name.middlewares.MyCustomDownloaderMiddleware': 543, # } +# Enable or disable extensions +# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html +# EXTENSIONS = { +# 'scrapy.telnet.TelnetConsole': None, +# } + +# Configure item pipelines +# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html +# ITEM_PIPELINES = { +# '$project_name.pipelines.SomePipeline': 300, +# } + # Enable and configure the AutoThrottle extension (disabled by default) # See http://doc.scrapy.org/en/latest/topics/autothrottle.html +# NOTE: AutoThrottle will honour the standard settings for concurrency and delay #AUTOTHROTTLE_ENABLED=True # The initial download delay (default: 5) #AUTOTHROTTLE_START_DELAY=3 From 64082b46405c401a1e5accc7ae297c146619df3f Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Tue, 17 Mar 2015 01:46:45 +0500 Subject: [PATCH 0107/2974] DOC contribute to master branch See GH-975 and GH-1029. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6020a3670..7e5dd173f 100644 --- a/README.rst +++ b/README.rst @@ -58,7 +58,7 @@ See http://scrapy.org/community/ Contributing ============ -See http://doc.scrapy.org/en/latest/contributing.html +See http://doc.scrapy.org/en/master/contributing.html Companies using Scrapy ====================== From 05cb31d3060c543e4266f0e3b67eb332ac4abe0f Mon Sep 17 00:00:00 2001 From: Alexander Chekunkov Date: Fri, 19 Dec 2014 21:06:58 +0200 Subject: [PATCH 0108/2974] pydispatch - pep8 reformat files - proper spacing --- scrapy/xlib/pydispatch/dispatcher.py | 833 +++++++++++++------------- scrapy/xlib/pydispatch/errors.py | 17 +- scrapy/xlib/pydispatch/robust.py | 89 ++- scrapy/xlib/pydispatch/robustapply.py | 19 +- scrapy/xlib/pydispatch/saferef.py | 314 +++++----- 5 files changed, 655 insertions(+), 617 deletions(-) diff --git a/scrapy/xlib/pydispatch/dispatcher.py b/scrapy/xlib/pydispatch/dispatcher.py index f5a9a611b..7d588dd20 100644 --- a/scrapy/xlib/pydispatch/dispatcher.py +++ b/scrapy/xlib/pydispatch/dispatcher.py @@ -6,24 +6,24 @@ system. Module attributes of note: - Any -- Singleton used to signal either "Any Sender" or - "Any Signal". See documentation of the _Any class. - Anonymous -- Singleton used to signal "Anonymous Sender" - See documentation of the _Anonymous class. + Any -- Singleton used to signal either "Any Sender" or + "Any Signal". See documentation of the _Any class. + Anonymous -- Singleton used to signal "Anonymous Sender" + See documentation of the _Anonymous class. Internal attributes: - WEAKREF_TYPES -- tuple of types/classes which represent - weak references to receivers, and thus must be de- - referenced on retrieval to retrieve the callable - object - connections -- { senderkey (id) : { signal : [receivers...]}} - senders -- { senderkey (id) : weakref(sender) } - used for cleaning up sender references on sender - deletion - sendersBack -- { receiverkey (id) : [senderkey (id)...] } - used for cleaning up receiver references on receiver - deletion, (considerably speeds up the cleanup process - vs. the original code.) + WEAKREF_TYPES -- tuple of types/classes which represent + weak references to receivers, and thus must be de- + referenced on retrieval to retrieve the callable + object + connections -- { senderkey (id) : { signal : [receivers...]}} + senders -- { senderkey (id) : weakref(sender) } + used for cleaning up sender references on sender + deletion + sendersBack -- { receiverkey (id) : [senderkey (id)...] } + used for cleaning up receiver references on receiver + deletion, (considerably speeds up the cleanup process + vs. the original code.) """ from __future__ import generators import types, weakref, six @@ -33,39 +33,47 @@ __author__ = "Patrick K. O'Brien " __cvsid__ = "$Id: dispatcher.py,v 1.1.1.1 2006/07/07 15:59:38 mcfletch Exp $" __version__ = "$Revision: 1.1.1.1 $"[11:-2] + class _Parameter: - """Used to represent default parameter values.""" - def __repr__(self): - return self.__class__.__name__ + """Used to represent default parameter values.""" + + def __repr__(self): + return self.__class__.__name__ + class _Any(_Parameter): - """Singleton used to signal either "Any Sender" or "Any Signal" + """Singleton used to signal either "Any Sender" or "Any Signal" + + The Any object can be used with connect, disconnect, + send, or sendExact to signal that the parameter given + Any should react to all senders/signals, not just + a particular sender/signal. + """ + - The Any object can be used with connect, disconnect, - send, or sendExact to signal that the parameter given - Any should react to all senders/signals, not just - a particular sender/signal. - """ Any = _Any() + class _Anonymous(_Parameter): - """Singleton used to signal "Anonymous Sender" + """Singleton used to signal "Anonymous Sender" + + The Anonymous object is used to signal that the sender + of a message is not specified (as distinct from being + "any sender"). Registering callbacks for Anonymous + will only receive messages sent without senders. Sending + with anonymous will only send messages to those receivers + registered for Any or Anonymous. + + Note: + The default sender for connect is Any, while the + default sender for send is Anonymous. This has + the effect that if you do not specify any senders + in either function then all messages are routed + as though there was a single sender (Anonymous) + being used everywhere. + """ - The Anonymous object is used to signal that the sender - of a message is not specified (as distinct from being - "any sender"). Registering callbacks for Anonymous - will only receive messages sent without senders. Sending - with anonymous will only send messages to those receivers - registered for Any or Anonymous. - Note: - The default sender for connect is Any, while the - default sender for send is Anonymous. This has - the effect that if you do not specify any senders - in either function then all messages are routed - as though there was a single sender (Anonymous) - being used everywhere. - """ Anonymous = _Anonymous() WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref) @@ -76,417 +84,428 @@ sendersBack = {} def connect(receiver, signal=Any, sender=Any, weak=True): - """Connect receiver to sender for signal + """Connect receiver to sender for signal - receiver -- a callable Python object which is to receive - messages/signals/events. Receivers must be hashable - objects. + receiver -- a callable Python object which is to receive + messages/signals/events. Receivers must be hashable + objects. - if weak is True, then receiver must be weak-referencable - (more precisely saferef.safeRef() must be able to create - a reference to the receiver). - - Receivers are fairly flexible in their specification, - as the machinery in the robustApply module takes care - of most of the details regarding figuring out appropriate - subsets of the sent arguments to apply to a given - receiver. + if weak is True, then receiver must be weak-referencable + (more precisely saferef.safeRef() must be able to create + a reference to the receiver). - Note: - if receiver is itself a weak reference (a callable), - it will be de-referenced by the system's machinery, - so *generally* weak references are not suitable as - receivers, though some use might be found for the - facility whereby a higher-level library passes in - pre-weakrefed receiver references. + Receivers are fairly flexible in their specification, + as the machinery in the robustApply module takes care + of most of the details regarding figuring out appropriate + subsets of the sent arguments to apply to a given + receiver. - signal -- the signal to which the receiver should respond - - if Any, receiver will receive any signal from the - indicated sender (which might also be Any, but is not - necessarily Any). - - Otherwise must be a hashable Python object other than - None (DispatcherError raised on None). - - sender -- the sender to which the receiver should respond - - if Any, receiver will receive the indicated signals - from any sender. - - if Anonymous, receiver will only receive indicated - signals from send/sendExact which do not specify a - sender, or specify Anonymous explicitly as the sender. + Note: + if receiver is itself a weak reference (a callable), + it will be de-referenced by the system's machinery, + so *generally* weak references are not suitable as + receivers, though some use might be found for the + facility whereby a higher-level library passes in + pre-weakrefed receiver references. - Otherwise can be any python object. - - weak -- whether to use weak references to the receiver - By default, the module will attempt to use weak - references to the receiver objects. If this parameter - is false, then strong references will be used. + signal -- the signal to which the receiver should respond - returns None, may raise DispatcherTypeError - """ - if signal is None: - raise errors.DispatcherTypeError( - 'Signal cannot be None (receiver=%r sender=%r)'%( receiver,sender) - ) - if weak: - receiver = saferef.safeRef(receiver, onDelete=_removeReceiver) - senderkey = id(sender) - if senderkey in connections: - signals = connections[senderkey] - else: - connections[senderkey] = signals = {} - # Keep track of senders for cleanup. - # Is Anonymous something we want to clean up? - if sender not in (None, Anonymous, Any): - def remove(object, senderkey=senderkey): - _removeSender(senderkey=senderkey) - # Skip objects that can not be weakly referenced, which means - # they won't be automatically cleaned up, but that's too bad. - try: - weakSender = weakref.ref(sender, remove) - senders[senderkey] = weakSender - except: - pass - - receiverID = id(receiver) - # get current set, remove any current references to - # this receiver in the set, including back-references - if signal in signals: - receivers = signals[signal] - _removeOldBackRefs(senderkey, signal, receiver, receivers) - else: - receivers = signals[signal] = [] - try: - current = sendersBack.get( receiverID ) - if current is None: - sendersBack[ receiverID ] = current = [] - if senderkey not in current: - current.append(senderkey) - except: - pass + if Any, receiver will receive any signal from the + indicated sender (which might also be Any, but is not + necessarily Any). - receivers.append(receiver) + Otherwise must be a hashable Python object other than + None (DispatcherError raised on None). + sender -- the sender to which the receiver should respond + + if Any, receiver will receive the indicated signals + from any sender. + + if Anonymous, receiver will only receive indicated + signals from send/sendExact which do not specify a + sender, or specify Anonymous explicitly as the sender. + + Otherwise can be any python object. + + weak -- whether to use weak references to the receiver + By default, the module will attempt to use weak + references to the receiver objects. If this parameter + is false, then strong references will be used. + + returns None, may raise DispatcherTypeError + """ + if signal is None: + raise errors.DispatcherTypeError( + 'Signal cannot be None (receiver=%r sender=%r)' % ( + receiver, sender) + ) + if weak: + receiver = saferef.safeRef(receiver, onDelete=_removeReceiver) + senderkey = id(sender) + if senderkey in connections: + signals = connections[senderkey] + else: + connections[senderkey] = signals = {} + # Keep track of senders for cleanup. + # Is Anonymous something we want to clean up? + if sender not in (None, Anonymous, Any): + def remove(object, senderkey=senderkey): + _removeSender(senderkey=senderkey) + + # Skip objects that can not be weakly referenced, which means + # they won't be automatically cleaned up, but that's too bad. + try: + weakSender = weakref.ref(sender, remove) + senders[senderkey] = weakSender + except: + pass + + receiverID = id(receiver) + # get current set, remove any current references to + # this receiver in the set, including back-references + if signal in signals: + receivers = signals[signal] + _removeOldBackRefs(senderkey, signal, receiver, receivers) + else: + receivers = signals[signal] = [] + try: + current = sendersBack.get(receiverID) + if current is None: + sendersBack[receiverID] = current = [] + if senderkey not in current: + current.append(senderkey) + except: + pass + + receivers.append(receiver) def disconnect(receiver, signal=Any, sender=Any, weak=True): - """Disconnect receiver from sender for signal + """Disconnect receiver from sender for signal - receiver -- the registered receiver to disconnect - signal -- the registered signal to disconnect - sender -- the registered sender to disconnect - weak -- the weakref state to disconnect + receiver -- the registered receiver to disconnect + signal -- the registered signal to disconnect + sender -- the registered sender to disconnect + weak -- the weakref state to disconnect - disconnect reverses the process of connect, - the semantics for the individual elements are - logically equivalent to a tuple of - (receiver, signal, sender, weak) used as a key - to be deleted from the internal routing tables. - (The actual process is slightly more complex - but the semantics are basically the same). + disconnect reverses the process of connect, + the semantics for the individual elements are + logically equivalent to a tuple of + (receiver, signal, sender, weak) used as a key + to be deleted from the internal routing tables. + (The actual process is slightly more complex + but the semantics are basically the same). - Note: - Using disconnect is not required to cleanup - routing when an object is deleted, the framework - will remove routes for deleted objects - automatically. It's only necessary to disconnect - if you want to stop routing to a live object. - - returns None, may raise DispatcherTypeError or - DispatcherKeyError - """ - if signal is None: - raise errors.DispatcherTypeError( - 'Signal cannot be None (receiver=%r sender=%r)'%( receiver,sender) - ) - if weak: receiver = saferef.safeRef(receiver) - senderkey = id(sender) - try: - signals = connections[senderkey] - receivers = signals[signal] - except KeyError: - raise errors.DispatcherKeyError( - """No receivers found for signal %r from sender %r""" %( - signal, - sender - ) - ) - try: - # also removes from receivers - _removeOldBackRefs(senderkey, signal, receiver, receivers) - except ValueError: - raise errors.DispatcherKeyError( - """No connection to receiver %s for signal %s from sender %s""" %( - receiver, - signal, - sender - ) - ) - _cleanupConnections(senderkey, signal) + Note: + Using disconnect is not required to cleanup + routing when an object is deleted, the framework + will remove routes for deleted objects + automatically. It's only necessary to disconnect + if you want to stop routing to a live object. -def getReceivers( sender = Any, signal = Any ): - """Get list of receivers from global tables + returns None, may raise DispatcherTypeError or + DispatcherKeyError + """ + if signal is None: + raise errors.DispatcherTypeError( + 'Signal cannot be None (receiver=%r sender=%r)' % ( + receiver, sender) + ) + if weak: receiver = saferef.safeRef(receiver) + senderkey = id(sender) + try: + signals = connections[senderkey] + receivers = signals[signal] + except KeyError: + raise errors.DispatcherKeyError( + """No receivers found for signal %r from sender %r""" % ( + signal, + sender + ) + ) + try: + # also removes from receivers + _removeOldBackRefs(senderkey, signal, receiver, receivers) + except ValueError: + raise errors.DispatcherKeyError( + """No connection to receiver %s for signal %s from sender %s""" % ( + receiver, + signal, + sender + ) + ) + _cleanupConnections(senderkey, signal) - This utility function allows you to retrieve the - raw list of receivers from the connections table - for the given sender and signal pair. - Note: - there is no guarantee that this is the actual list - stored in the connections table, so the value - should be treated as a simple iterable/truth value - rather than, for instance a list to which you - might append new records. +def getReceivers(sender=Any, signal=Any): + """Get list of receivers from global tables + + This utility function allows you to retrieve the + raw list of receivers from the connections table + for the given sender and signal pair. + + Note: + there is no guarantee that this is the actual list + stored in the connections table, so the value + should be treated as a simple iterable/truth value + rather than, for instance a list to which you + might append new records. + + Normally you would use liveReceivers( getReceivers( ...)) + to retrieve the actual receiver objects as an iterable + object. + """ + try: + return connections[id(sender)][signal] + except KeyError: + return [] - Normally you would use liveReceivers( getReceivers( ...)) - to retrieve the actual receiver objects as an iterable - object. - """ - try: - return connections[id(sender)][signal] - except KeyError: - return [] def liveReceivers(receivers): - """Filter sequence of receivers to get resolved, live receivers + """Filter sequence of receivers to get resolved, live receivers - This is a generator which will iterate over - the passed sequence, checking for weak references - and resolving them, then returning all live - receivers. - """ - for receiver in receivers: - if isinstance( receiver, WEAKREF_TYPES): - # Dereference the weak reference. - receiver = receiver() - if receiver is not None: - yield receiver - else: - yield receiver + This is a generator which will iterate over + the passed sequence, checking for weak references + and resolving them, then returning all live + receivers. + """ + for receiver in receivers: + if isinstance(receiver, WEAKREF_TYPES): + # Dereference the weak reference. + receiver = receiver() + if receiver is not None: + yield receiver + else: + yield receiver +def getAllReceivers(sender=Any, signal=Any): + """Get list of all receivers from global tables -def getAllReceivers( sender = Any, signal = Any ): - """Get list of all receivers from global tables + This gets all receivers which should receive + the given signal from sender, each receiver should + be produced only once by the resulting generator + """ + receivers = {} + for set in ( + # Get receivers that receive *this* signal from *this* sender. + getReceivers(sender, signal), + # Add receivers that receive *any* signal from *this* sender. + getReceivers(sender, Any), + # Add receivers that receive *this* signal from *any* sender. + getReceivers(Any, signal), + # Add receivers that receive *any* signal from *any* sender. + getReceivers(Any, Any), + ): + for receiver in set: + if receiver: # filter out dead instance-method weakrefs + try: + if receiver not in receivers: + receivers[receiver] = 1 + yield receiver + except TypeError: + # dead weakrefs raise TypeError on hash... + pass - This gets all receivers which should receive - the given signal from sender, each receiver should - be produced only once by the resulting generator - """ - receivers = {} - for set in ( - # Get receivers that receive *this* signal from *this* sender. - getReceivers( sender, signal ), - # Add receivers that receive *any* signal from *this* sender. - getReceivers( sender, Any ), - # Add receivers that receive *this* signal from *any* sender. - getReceivers( Any, signal ), - # Add receivers that receive *any* signal from *any* sender. - getReceivers( Any, Any ), - ): - for receiver in set: - if receiver: # filter out dead instance-method weakrefs - try: - if receiver not in receivers: - receivers[receiver] = 1 - yield receiver - except TypeError: - # dead weakrefs raise TypeError on hash... - pass def send(signal=Any, sender=Anonymous, *arguments, **named): - """Send signal from sender to all connected receivers. - - signal -- (hashable) signal value, see connect for details + """Send signal from sender to all connected receivers. - sender -- the sender of the signal - - if Any, only receivers registered for Any will receive - the message. + signal -- (hashable) signal value, see connect for details - if Anonymous, only receivers registered to receive - messages from Anonymous or Any will receive the message + sender -- the sender of the signal - Otherwise can be any python object (normally one - registered with a connect if you actually want - something to occur). + if Any, only receivers registered for Any will receive + the message. - arguments -- positional arguments which will be passed to - *all* receivers. Note that this may raise TypeErrors - if the receivers do not allow the particular arguments. - Note also that arguments are applied before named - arguments, so they should be used with care. + if Anonymous, only receivers registered to receive + messages from Anonymous or Any will receive the message - named -- named arguments which will be filtered according - to the parameters of the receivers to only provide those - acceptable to the receiver. + Otherwise can be any python object (normally one + registered with a connect if you actually want + something to occur). - Return a list of tuple pairs [(receiver, response), ... ] + arguments -- positional arguments which will be passed to + *all* receivers. Note that this may raise TypeErrors + if the receivers do not allow the particular arguments. + Note also that arguments are applied before named + arguments, so they should be used with care. - if any receiver raises an error, the error propagates back - through send, terminating the dispatch loop, so it is quite - possible to not have all receivers called if a raises an - error. - """ - # Call each receiver with whatever arguments it can accept. - # Return a list of tuple pairs [(receiver, response), ... ]. - responses = [] - for receiver in liveReceivers(getAllReceivers(sender, signal)): - response = robustapply.robustApply( - receiver, - signal=signal, - sender=sender, - *arguments, - **named - ) - responses.append((receiver, response)) - return responses -def sendExact( signal=Any, sender=Anonymous, *arguments, **named ): - """Send signal only to those receivers registered for exact message + named -- named arguments which will be filtered according + to the parameters of the receivers to only provide those + acceptable to the receiver. + + Return a list of tuple pairs [(receiver, response), ... ] + + if any receiver raises an error, the error propagates back + through send, terminating the dispatch loop, so it is quite + possible to not have all receivers called if a raises an + error. + """ + # Call each receiver with whatever arguments it can accept. + # Return a list of tuple pairs [(receiver, response), ... ]. + responses = [] + for receiver in liveReceivers(getAllReceivers(sender, signal)): + response = robustapply.robustApply( + receiver, + signal=signal, + sender=sender, + *arguments, + **named + ) + responses.append((receiver, response)) + return responses + + +def sendExact(signal=Any, sender=Anonymous, *arguments, **named): + """Send signal only to those receivers registered for exact message + + sendExact allows for avoiding Any/Anonymous registered + handlers, sending only to those receivers explicitly + registered for a particular signal on a particular + sender. + """ + responses = [] + for receiver in liveReceivers(getReceivers(sender, signal)): + response = robustapply.robustApply( + receiver, + signal=signal, + sender=sender, + *arguments, + **named + ) + responses.append((receiver, response)) + return responses - sendExact allows for avoiding Any/Anonymous registered - handlers, sending only to those receivers explicitly - registered for a particular signal on a particular - sender. - """ - responses = [] - for receiver in liveReceivers(getReceivers(sender, signal)): - response = robustapply.robustApply( - receiver, - signal=signal, - sender=sender, - *arguments, - **named - ) - responses.append((receiver, response)) - return responses - def _removeReceiver(receiver): - """Remove receiver from connections.""" - if not sendersBack: - # During module cleanup the mapping will be replaced with None - return False - backKey = id(receiver) - try: - backSet = sendersBack.pop(backKey) - except KeyError as err: - return False - else: - for senderkey in backSet: - try: - signals = connections[senderkey].keys() - except KeyError as err: - pass - else: - for signal in signals: - try: - receivers = connections[senderkey][signal] - except KeyError: - pass - else: - try: - receivers.remove( receiver ) - except Exception as err: - pass - _cleanupConnections(senderkey, signal) + """Remove receiver from connections.""" + if not sendersBack: + # During module cleanup the mapping will be replaced with None + return False + backKey = id(receiver) + try: + backSet = sendersBack.pop(backKey) + except KeyError as err: + return False + else: + for senderkey in backSet: + try: + signals = connections[senderkey].keys() + except KeyError as err: + pass + else: + for signal in signals: + try: + receivers = connections[senderkey][signal] + except KeyError: + pass + else: + try: + receivers.remove(receiver) + except Exception as err: + pass + _cleanupConnections(senderkey, signal) + def _cleanupConnections(senderkey, signal): - """Delete any empty signals for senderkey. Delete senderkey if empty.""" - try: - receivers = connections[senderkey][signal] - except: - pass - else: - if not receivers: - # No more connected receivers. Therefore, remove the signal. - try: - signals = connections[senderkey] - except KeyError: - pass - else: - del signals[signal] - if not signals: - # No more signal connections. Therefore, remove the sender. - _removeSender(senderkey) + """Delete any empty signals for senderkey. Delete senderkey if empty.""" + try: + receivers = connections[senderkey][signal] + except: + pass + else: + if not receivers: + # No more connected receivers. Therefore, remove the signal. + try: + signals = connections[senderkey] + except KeyError: + pass + else: + del signals[signal] + if not signals: + # No more signal connections. Therefore, remove the sender. + _removeSender(senderkey) + def _removeSender(senderkey): - """Remove senderkey from connections.""" - _removeBackrefs(senderkey) - try: - del connections[senderkey] - except KeyError: - pass - # Senderkey will only be in senders dictionary if sender - # could be weakly referenced. - try: - del senders[senderkey] - except: - pass + """Remove senderkey from connections.""" + _removeBackrefs(senderkey) + try: + del connections[senderkey] + except KeyError: + pass + # Senderkey will only be in senders dictionary if sender + # could be weakly referenced. + try: + del senders[senderkey] + except: + pass -def _removeBackrefs( senderkey): - """Remove all back-references to this senderkey""" - try: - signals = connections[senderkey] - except KeyError: - signals = None - else: - items = signals.items() - def allReceivers( ): - for signal,set in items: - for item in set: - yield item - for receiver in allReceivers(): - _killBackref( receiver, senderkey ) +def _removeBackrefs(senderkey): + """Remove all back-references to this senderkey""" + try: + signals = connections[senderkey] + except KeyError: + signals = None + else: + items = signals.items() + + def allReceivers(): + for signal, set in items: + for item in set: + yield item + + for receiver in allReceivers(): + _killBackref(receiver, senderkey) + def _removeOldBackRefs(senderkey, signal, receiver, receivers): - """Kill old sendersBack references from receiver + """Kill old sendersBack references from receiver - This guards against multiple registration of the same - receiver for a given signal and sender leaking memory - as old back reference records build up. + This guards against multiple registration of the same + receiver for a given signal and sender leaking memory + as old back reference records build up. - Also removes old receiver instance from receivers - """ - try: - index = receivers.index(receiver) - # need to scan back references here and remove senderkey - except ValueError: - return False - else: - oldReceiver = receivers[index] - del receivers[index] - found = 0 - signals = connections.get(signal) - if signals is not None: - for sig, recs in six.iteritems(connections.get(signal,{})): - if sig != signal: - for rec in recs: - if rec is oldReceiver: - found = 1 - break - if not found: - _killBackref( oldReceiver, senderkey ) - return True - return False - - -def _killBackref( receiver, senderkey ): - """Do the actual removal of back reference from receiver to senderkey""" - receiverkey = id(receiver) - set = sendersBack.get( receiverkey, () ) - while senderkey in set: - try: - set.remove( senderkey ) - except: - break - if not set: - try: - del sendersBack[ receiverkey ] - except KeyError: - pass - return True + Also removes old receiver instance from receivers + """ + try: + index = receivers.index(receiver) + # need to scan back references here and remove senderkey + except ValueError: + return False + else: + oldReceiver = receivers[index] + del receivers[index] + found = 0 + signals = connections.get(signal) + if signals is not None: + for sig, recs in six.iteritems(connections.get(signal, {})): + if sig != signal: + for rec in recs: + if rec is oldReceiver: + found = 1 + break + if not found: + _killBackref(oldReceiver, senderkey) + return True + return False + + +def _killBackref(receiver, senderkey): + """Do the actual removal of back reference from receiver to senderkey""" + receiverkey = id(receiver) + set = sendersBack.get(receiverkey, ()) + while senderkey in set: + try: + set.remove(senderkey) + except: + break + if not set: + try: + del sendersBack[receiverkey] + except KeyError: + pass + return True diff --git a/scrapy/xlib/pydispatch/errors.py b/scrapy/xlib/pydispatch/errors.py index a2eb32ed7..c5540d8f7 100644 --- a/scrapy/xlib/pydispatch/errors.py +++ b/scrapy/xlib/pydispatch/errors.py @@ -1,10 +1,15 @@ """Error types for dispatcher mechanism """ -class DispatcherError(Exception): - """Base class for all Dispatcher errors""" -class DispatcherKeyError(KeyError, DispatcherError): - """Error raised when unknown (sender,signal) set specified""" -class DispatcherTypeError(TypeError, DispatcherError): - """Error raised when inappropriate signal-type specified (None)""" + +class DispatcherError(Exception): + """Base class for all Dispatcher errors""" + + +class DispatcherKeyError(KeyError, DispatcherError): + """Error raised when unknown (sender,signal) set specified""" + + +class DispatcherTypeError(TypeError, DispatcherError): + """Error raised when inappropriate signal-type specified (None)""" diff --git a/scrapy/xlib/pydispatch/robust.py b/scrapy/xlib/pydispatch/robust.py index f829dbfd9..a28f127e2 100644 --- a/scrapy/xlib/pydispatch/robust.py +++ b/scrapy/xlib/pydispatch/robust.py @@ -2,56 +2,53 @@ from scrapy.xlib.pydispatch.dispatcher import Any, Anonymous, liveReceivers, getAllReceivers from scrapy.xlib.pydispatch.robustapply import robustApply -def sendRobust( - signal=Any, - sender=Anonymous, - *arguments, **named -): - """Send signal from sender to all connected receivers catching errors - - signal -- (hashable) signal value, see connect for details - sender -- the sender of the signal - - if Any, only receivers registered for Any will receive - the message. +def sendRobust(signal=Any, sender=Anonymous, *arguments, **named): + """Send signal from sender to all connected receivers catching errors - if Anonymous, only receivers registered to receive - messages from Anonymous or Any will receive the message + signal -- (hashable) signal value, see connect for details - Otherwise can be any python object (normally one - registered with a connect if you actually want - something to occur). + sender -- the sender of the signal - arguments -- positional arguments which will be passed to - *all* receivers. Note that this may raise TypeErrors - if the receivers do not allow the particular arguments. - Note also that arguments are applied before named - arguments, so they should be used with care. + if Any, only receivers registered for Any will receive + the message. - named -- named arguments which will be filtered according - to the parameters of the receivers to only provide those - acceptable to the receiver. + if Anonymous, only receivers registered to receive + messages from Anonymous or Any will receive the message - Return a list of tuple pairs [(receiver, response), ... ] + Otherwise can be any python object (normally one + registered with a connect if you actually want + something to occur). - if any receiver raises an error (specifically any subclass of Exception), - the error instance is returned as the result for that receiver. - """ - # Call each receiver with whatever arguments it can accept. - # Return a list of tuple pairs [(receiver, response), ... ]. - responses = [] - for receiver in liveReceivers(getAllReceivers(sender, signal)): - try: - response = robustApply( - receiver, - signal=signal, - sender=sender, - *arguments, - **named - ) - except Exception as err: - responses.append((receiver, err)) - else: - responses.append((receiver, response)) - return responses + arguments -- positional arguments which will be passed to + *all* receivers. Note that this may raise TypeErrors + if the receivers do not allow the particular arguments. + Note also that arguments are applied before named + arguments, so they should be used with care. + + named -- named arguments which will be filtered according + to the parameters of the receivers to only provide those + acceptable to the receiver. + + Return a list of tuple pairs [(receiver, response), ... ] + + if any receiver raises an error (specifically any subclass of Exception), + the error instance is returned as the result for that receiver. + """ + # Call each receiver with whatever arguments it can accept. + # Return a list of tuple pairs [(receiver, response), ... ]. + responses = [] + for receiver in liveReceivers(getAllReceivers(sender, signal)): + try: + response = robustApply( + receiver, + signal=signal, + sender=sender, + *arguments, + **named + ) + except Exception as err: + responses.append((receiver, err)) + else: + responses.append((receiver, response)) + return responses diff --git a/scrapy/xlib/pydispatch/robustapply.py b/scrapy/xlib/pydispatch/robustapply.py index 5deda3c42..9977ac6b7 100644 --- a/scrapy/xlib/pydispatch/robustapply.py +++ b/scrapy/xlib/pydispatch/robustapply.py @@ -5,9 +5,9 @@ what arguments a given callable object can take, and subset the given arguments to match only those which are acceptable. """ - import inspect + def function(receiver): """Get function-like callable object for given receiver @@ -20,32 +20,35 @@ def function(receiver): # receiver is a class instance; assume it is callable. # Reassign receiver to the actual method that will be called. if hasattr(receiver.__call__, 'im_func') or \ - hasattr(receiver.__call__, 'im_code'): + hasattr(receiver.__call__, 'im_code'): receiver = receiver.__call__ - if hasattr( receiver, 'im_func' ): + if hasattr(receiver, 'im_func'): # an instance-method... return receiver, receiver.im_func.func_code, 1 elif not hasattr(receiver, 'func_code'): - raise ValueError('unknown receiver type %s %s'%(receiver, type(receiver))) + raise ValueError( + 'unknown receiver type %s %s' % (receiver, type(receiver))) return receiver, receiver.func_code, 0 + def robustApply(receiver, *arguments, **named): """Call receiver with arguments and an appropriate subset of named """ receiver, codeObject, startIndex = function(receiver) - acceptable = codeObject.co_varnames[startIndex+len(arguments):codeObject.co_argcount] - for name in codeObject.co_varnames[startIndex:startIndex+len(arguments)]: + acceptable = codeObject.co_varnames[ + startIndex + len(arguments):codeObject.co_argcount] + for name in codeObject.co_varnames[startIndex:startIndex + len(arguments)]: if name in named: raise TypeError( - """Argument %r specified both positionally and as a keyword for calling %r"""% ( + """Argument %r specified both positionally and as a keyword for calling %r""" % ( name, receiver, ) ) if not (codeObject.co_flags & 8): - # fc does not have a **kwds type parameter, therefore + # fc does not have a **kwds type parameter, therefore # remove unacceptable arguments. for arg in named.keys(): if arg not in acceptable: diff --git a/scrapy/xlib/pydispatch/saferef.py b/scrapy/xlib/pydispatch/saferef.py index f1b8b1f9b..bd9659673 100644 --- a/scrapy/xlib/pydispatch/saferef.py +++ b/scrapy/xlib/pydispatch/saferef.py @@ -2,165 +2,179 @@ from __future__ import print_function import weakref, traceback -def safeRef(target, onDelete = None): - """Return a *safe* weak reference to a callable target - target -- the object to be weakly referenced, if it's a - bound method reference, will create a BoundMethodWeakref, - otherwise creates a simple weakref. - onDelete -- if provided, will have a hard reference stored - to the callable to be called after the safe reference - goes out of scope with the reference object, (either a - weakref or a BoundMethodWeakref) as argument. - """ - if hasattr(target, 'im_self'): - if target.im_self is not None: - # Turn a bound method into a BoundMethodWeakref instance. - # Keep track of these instances for lookup by disconnect(). - assert hasattr(target, 'im_func'), """safeRef target %r has im_self, but no im_func, don't know how to create reference"""%( target,) - reference = BoundMethodWeakref( - target=target, - onDelete=onDelete - ) - return reference - if onDelete is not None: - return weakref.ref(target, onDelete) - else: - return weakref.ref( target ) +def safeRef(target, onDelete=None): + """Return a *safe* weak reference to a callable target + + target -- the object to be weakly referenced, if it's a + bound method reference, will create a BoundMethodWeakref, + otherwise creates a simple weakref. + onDelete -- if provided, will have a hard reference stored + to the callable to be called after the safe reference + goes out of scope with the reference object, (either a + weakref or a BoundMethodWeakref) as argument. + """ + if hasattr(target, 'im_self'): + if target.im_self is not None: + # Turn a bound method into a BoundMethodWeakref instance. + # Keep track of these instances for lookup by disconnect(). + assert hasattr(target, 'im_func'), """safeRef target %r has im_self, but no im_func, don't know how to create reference"""%( target,) + reference = BoundMethodWeakref( + target=target, + onDelete=onDelete + ) + return reference + if onDelete is not None: + return weakref.ref(target, onDelete) + else: + return weakref.ref(target) + class BoundMethodWeakref(object): - """'Safe' and reusable weak references to instance methods + """'Safe' and reusable weak references to instance methods - BoundMethodWeakref objects provide a mechanism for - referencing a bound method without requiring that the - method object itself (which is normally a transient - object) is kept alive. Instead, the BoundMethodWeakref - object keeps weak references to both the object and the - function which together define the instance method. + BoundMethodWeakref objects provide a mechanism for + referencing a bound method without requiring that the + method object itself (which is normally a transient + object) is kept alive. Instead, the BoundMethodWeakref + object keeps weak references to both the object and the + function which together define the instance method. - Attributes: - key -- the identity key for the reference, calculated - by the class's calculateKey method applied to the - target instance method - deletionMethods -- sequence of callable objects taking - single argument, a reference to this object which - will be called when *either* the target object or - target function is garbage collected (i.e. when - this object becomes invalid). These are specified - as the onDelete parameters of safeRef calls. - weakSelf -- weak reference to the target object - weakFunc -- weak reference to the target function + Attributes: + key -- the identity key for the reference, calculated + by the class's calculateKey method applied to the + target instance method + deletionMethods -- sequence of callable objects taking + single argument, a reference to this object which + will be called when *either* the target object or + target function is garbage collected (i.e. when + this object becomes invalid). These are specified + as the onDelete parameters of safeRef calls. + weakSelf -- weak reference to the target object + weakFunc -- weak reference to the target function - Class Attributes: - _allInstances -- class attribute pointing to all live - BoundMethodWeakref objects indexed by the class's - calculateKey(target) method applied to the target - objects. This weak value dictionary is used to - short-circuit creation so that multiple references - to the same (object, function) pair produce the - same BoundMethodWeakref instance. + Class Attributes: + _allInstances -- class attribute pointing to all live + BoundMethodWeakref objects indexed by the class's + calculateKey(target) method applied to the target + objects. This weak value dictionary is used to + short-circuit creation so that multiple references + to the same (object, function) pair produce the + same BoundMethodWeakref instance. - """ - _allInstances = weakref.WeakValueDictionary() - def __new__( cls, target, onDelete=None, *arguments,**named ): - """Create new instance or return current instance + """ + _allInstances = weakref.WeakValueDictionary() - Basically this method of construction allows us to - short-circuit creation of references to already- - referenced instance methods. The key corresponding - to the target is calculated, and if there is already - an existing reference, that is returned, with its - deletionMethods attribute updated. Otherwise the - new instance is created and registered in the table - of already-referenced methods. - """ - key = cls.calculateKey(target) - current =cls._allInstances.get(key) - if current is not None: - current.deletionMethods.append( onDelete) - return current - else: - base = super( BoundMethodWeakref, cls).__new__( cls ) - cls._allInstances[key] = base - base.__init__( target, onDelete, *arguments,**named) - return base - def __init__(self, target, onDelete=None): - """Return a weak-reference-like instance for a bound method + def __new__(cls, target, onDelete=None, *arguments, **named): + """Create new instance or return current instance - target -- the instance-method target for the weak - reference, must have im_self and im_func attributes - and be reconstructable via: - target.im_func.__get__( target.im_self ) - which is true of built-in instance methods. - onDelete -- optional callback which will be called - when this weak reference ceases to be valid - (i.e. either the object or the function is garbage - collected). Should take a single argument, - which will be passed a pointer to this object. - """ - def remove(weak, self=self): - """Set self.isDead to true when method or instance is destroyed""" - methods = self.deletionMethods[:] - del self.deletionMethods[:] - try: - del self.__class__._allInstances[ self.key ] - except KeyError: - pass - for function in methods: - try: - if callable( function ): - function( self ) - except Exception as e: - try: - traceback.print_exc() - except AttributeError as err: - print('''Exception during saferef %s cleanup function %s: %s'''%( - self, function, e - )) - self.deletionMethods = [onDelete] - self.key = self.calculateKey( target ) - self.weakSelf = weakref.ref(target.im_self, remove) - self.weakFunc = weakref.ref(target.im_func, remove) - self.selfName = target.im_self.__class__.__name__ - self.funcName = str(target.im_func.__name__) - def calculateKey( cls, target ): - """Calculate the reference key for this reference + Basically this method of construction allows us to + short-circuit creation of references to already- + referenced instance methods. The key corresponding + to the target is calculated, and if there is already + an existing reference, that is returned, with its + deletionMethods attribute updated. Otherwise the + new instance is created and registered in the table + of already-referenced methods. + """ + key = cls.calculateKey(target) + current = cls._allInstances.get(key) + if current is not None: + current.deletionMethods.append(onDelete) + return current + else: + base = super(BoundMethodWeakref, cls).__new__(cls) + cls._allInstances[key] = base + base.__init__(target, onDelete, *arguments, **named) + return base - Currently this is a two-tuple of the id()'s of the - target object and the target function respectively. - """ - return (id(target.im_self),id(target.im_func)) - calculateKey = classmethod( calculateKey ) - def __str__(self): - """Give a friendly representation of the object""" - return """%s( %s.%s )"""%( - self.__class__.__name__, - self.selfName, - self.funcName, - ) - __repr__ = __str__ - def __nonzero__( self ): - """Whether we are still a valid reference""" - return self() is not None - def __cmp__( self, other ): - """Compare with another reference""" - if not isinstance (other,self.__class__): - return cmp( self.__class__, type(other) ) - return cmp( self.key, other.key) - def __call__(self): - """Return a strong reference to the bound method + def __init__(self, target, onDelete=None): + """Return a weak-reference-like instance for a bound method - If the target cannot be retrieved, then will - return None, otherwise returns a bound instance - method for our object and function. + target -- the instance-method target for the weak + reference, must have im_self and im_func attributes + and be reconstructable via: + target.im_func.__get__( target.im_self ) + which is true of built-in instance methods. + onDelete -- optional callback which will be called + when this weak reference ceases to be valid + (i.e. either the object or the function is garbage + collected). Should take a single argument, + which will be passed a pointer to this object. + """ - Note: - You may call this method any number of times, - as it does not invalidate the reference. - """ - target = self.weakSelf() - if target is not None: - function = self.weakFunc() - if function is not None: - return function.__get__(target) - return None + def remove(weak, self=self): + """Set self.isDead to true when method or instance is destroyed""" + methods = self.deletionMethods[:] + del self.deletionMethods[:] + try: + del self.__class__._allInstances[self.key] + except KeyError: + pass + for function in methods: + try: + if callable(function): + function(self) + except Exception as e: + try: + traceback.print_exc() + except AttributeError as err: + print( + '''Exception during saferef %s cleanup function %s: %s''' % ( + self, function, e + )) + + self.deletionMethods = [onDelete] + self.key = self.calculateKey(target) + self.weakSelf = weakref.ref(target.im_self, remove) + self.weakFunc = weakref.ref(target.im_func, remove) + self.selfName = target.im_self.__class__.__name__ + self.funcName = str(target.im_func.__name__) + + def calculateKey(cls, target): + """Calculate the reference key for this reference + + Currently this is a two-tuple of the id()'s of the + target object and the target function respectively. + """ + return (id(target.im_self), id(target.im_func)) + + calculateKey = classmethod(calculateKey) + + def __str__(self): + """Give a friendly representation of the object""" + return """%s( %s.%s )""" % ( + self.__class__.__name__, + self.selfName, + self.funcName, + ) + + __repr__ = __str__ + + def __nonzero__(self): + """Whether we are still a valid reference""" + return self() is not None + + def __cmp__(self, other): + """Compare with another reference""" + if not isinstance(other, self.__class__): + return cmp(self.__class__, type(other)) + return cmp(self.key, other.key) + + def __call__(self): + """Return a strong reference to the bound method + + If the target cannot be retrieved, then will + return None, otherwise returns a bound instance + method for our object and function. + + Note: + You may call this method any number of times, + as it does not invalidate the reference. + """ + target = self.weakSelf() + if target is not None: + function = self.weakFunc() + if function is not None: + return function.__get__(target) + return None From fd67fe273aa68acbc290d2f067230516bcebcade Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Tue, 17 Mar 2015 09:34:54 -0300 Subject: [PATCH 0109/2974] using default values for settings that are off by default --- scrapy/templates/project/module/settings.py.tmpl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scrapy/templates/project/module/settings.py.tmpl b/scrapy/templates/project/module/settings.py.tmpl index f4832dd27..302d96b17 100644 --- a/scrapy/templates/project/module/settings.py.tmpl +++ b/scrapy/templates/project/module/settings.py.tmpl @@ -69,16 +69,17 @@ NEWSPIDER_MODULE = '$project_name.spiders' # See http://doc.scrapy.org/en/latest/topics/autothrottle.html # NOTE: AutoThrottle will honour the standard settings for concurrency and delay #AUTOTHROTTLE_ENABLED=True -# The initial download delay (default: 5) -#AUTOTHROTTLE_START_DELAY=3 -# The maximum download delay to be set in case of high latencies (default: 60) -#AUTOTHROTTLE_MAX_DELAY=90 +# The initial download delay +#AUTOTHROTTLE_START_DELAY=5 +# The maximum download delay to be set in case of high latencies +#AUTOTHROTTLE_MAX_DELAY=60 # Enable showing throttling stats for every response received: -#AUTOTHROTTLE_DEBUG=True +#AUTOTHROTTLE_DEBUG=False # Enable and configure HTTP caching (disabled by default) # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings #HTTPCACHE_ENABLED=True -#HTTPCACHE_EXPIRATION_SECS=3600 +#HTTPCACHE_EXPIRATION_SECS=0 #HTTPCACHE_DIR='httpcache' -#HTTPCACHE_IGNORE_HTTP_CODES=[404] +#HTTPCACHE_IGNORE_HTTP_CODES=[] +#HTTPCACHE_STORAGE='scrapy.contrib.httpcache.FilesystemCacheStorage' From 8ce4ad06151273fe67c28a95ec59269439d95025 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Tue, 17 Mar 2015 23:07:39 +0500 Subject: [PATCH 0110/2974] remove unnecessary check from scrapy.utils.spider.iter_spider_output arg_to_iter handles Items since https://github.com/scrapy/scrapy/commit/2bbd92742b796e1a565d4914a77889c884dd01ac --- scrapy/utils/spider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapy/utils/spider.py b/scrapy/utils/spider.py index b81cf2b9b..40ebbefc3 100644 --- a/scrapy/utils/spider.py +++ b/scrapy/utils/spider.py @@ -3,13 +3,13 @@ import inspect import six from scrapy import log -from scrapy.item import BaseItem from scrapy.spider import Spider from scrapy.utils.misc import arg_to_iter def iterate_spider_output(result): - return [result] if isinstance(result, BaseItem) else arg_to_iter(result) + return arg_to_iter(result) + def iter_spider_classes(module): """Return an iterator over all spider classes defined in the given module From 6974902323f4b4cecbe663e6e0ff4c67c0557029 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Tue, 17 Mar 2015 23:31:29 +0500 Subject: [PATCH 0111/2974] CSVFeedSpider cleanup: use iterate_spider_output A similar fix was made for XMLFeedSpider in https://github.com/scrapy/scrapy/commit/95fde0a4987acaa75a6749223c8b7f9bd7081c23 --- scrapy/contrib/spiders/feed.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/scrapy/contrib/spiders/feed.py b/scrapy/contrib/spiders/feed.py index 1a95c5c35..d83ee605e 100644 --- a/scrapy/contrib/spiders/feed.py +++ b/scrapy/contrib/spiders/feed.py @@ -5,8 +5,6 @@ for scraping from an XML feed. See documentation in docs/topics/spiders.rst """ from scrapy.spider import Spider -from scrapy.item import BaseItem -from scrapy.http import Request from scrapy.utils.iterators import xmliter, csviter from scrapy.utils.spider import iterate_spider_output from scrapy.selector import Selector @@ -92,6 +90,7 @@ class XMLFeedSpider(Spider): for (prefix, uri) in self.namespaces: selector.register_namespace(prefix, uri) + class CSVFeedSpider(Spider): """Spider for parsing CSV feeds. It receives a CSV file in a response; iterates through each of its rows, @@ -125,11 +124,7 @@ class CSVFeedSpider(Spider): """ for row in csviter(response, self.delimiter, self.headers, self.quotechar): - ret = self.parse_row(response, row) - if isinstance(ret, (BaseItem, Request)): - ret = [ret] - if not isinstance(ret, (list, tuple)): - raise TypeError('You cannot return an "%s" object from a spider' % type(ret).__name__) + ret = iterate_spider_output(self.parse_row(response, row)) for result_item in self.process_results(response, ret): yield result_item From da90449edfa13b5be1550b3acc212dbf3a8c6e69 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Wed, 18 Mar 2015 07:24:15 +0500 Subject: [PATCH 0112/2974] typo fix in scrapy.contrib.pipeline.media --- scrapy/contrib/pipeline/media.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/contrib/pipeline/media.py b/scrapy/contrib/pipeline/media.py index 82270e15d..012b7979a 100644 --- a/scrapy/contrib/pipeline/media.py +++ b/scrapy/contrib/pipeline/media.py @@ -117,7 +117,7 @@ class MediaPipeline(object): def item_completed(self, results, item, info): """Called per item when all media requests has been processed""" if self.LOG_FAILED_RESULTS: - msg = '%s found errors proessing %s' % (self.__class__.__name__, item) + msg = '%s found errors processing %s' % (self.__class__.__name__, item) for ok, value in results: if not ok: log.err(value, msg, spider=info.spider) From 39635e5f55bfbbc88d051778db6ba6f391630619 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Wed, 18 Mar 2015 07:26:56 +0500 Subject: [PATCH 0113/2974] Allow spiders to return dicts. See GH-1064. --- docs/topics/exporters.rst | 5 + scrapy/commands/parse.py | 2 +- scrapy/contracts/default.py | 6 +- scrapy/contrib/exporter/__init__.py | 21 ++- scrapy/contrib/pipeline/files.py | 2 +- scrapy/contrib/pipeline/images.py | 2 +- scrapy/core/scraper.py | 4 +- tests/spiders.py | 1 + tests/test_commands.py | 20 ++- tests/test_contracts.py | 51 ++++++ tests/test_contrib_exporter.py | 243 +++++++++++++++++----------- tests/test_engine.py | 34 ++-- tests/test_pipeline_files.py | 43 ++--- tests/test_pipeline_images.py | 43 ++--- 14 files changed, 310 insertions(+), 167 deletions(-) diff --git a/docs/topics/exporters.rst b/docs/topics/exporters.rst index c472f5b96..43931544f 100644 --- a/docs/topics/exporters.rst +++ b/docs/topics/exporters.rst @@ -197,12 +197,17 @@ BaseItemExporter Some exporters (like :class:`CsvItemExporter`) respect the order of the fields defined in this attribute. + Some exporters may require fields_to_export list in order to export the + data properly when spiders return dicts (not :class:`~Item` instances). + .. attribute:: export_empty_fields Whether to include empty/unpopulated item fields in the exported data. Defaults to ``False``. Some exporters (like :class:`CsvItemExporter`) ignore this attribute and always export all empty fields. + This option is ignored for dict items. + .. attribute:: encoding The encoding that will be used to encode unicode values. This only diff --git a/scrapy/commands/parse.py b/scrapy/commands/parse.py index 01c7fff0a..b8cc140d4 100644 --- a/scrapy/commands/parse.py +++ b/scrapy/commands/parse.py @@ -107,7 +107,7 @@ class Command(ScrapyCommand): items, requests = [], [] for x in iterate_spider_output(cb(response)): - if isinstance(x, BaseItem): + if isinstance(x, (BaseItem, dict)): items.append(x) elif isinstance(x, Request): requests.append(x) diff --git a/scrapy/contracts/default.py b/scrapy/contracts/default.py index 1d8367f82..20582503d 100644 --- a/scrapy/contracts/default.py +++ b/scrapy/contracts/default.py @@ -35,8 +35,8 @@ class ReturnsContract(Contract): objects = { 'request': Request, 'requests': Request, - 'item': BaseItem, - 'items': BaseItem, + 'item': (BaseItem, dict), + 'items': (BaseItem, dict), } def __init__(self, *args, **kwargs): @@ -83,7 +83,7 @@ class ScrapesContract(Contract): def post_process(self, output): for x in output: - if isinstance(x, BaseItem): + if isinstance(x, (BaseItem, dict)): for arg in self.args: if not arg in x: raise ContractFail("'%s' field is missing" % arg) diff --git a/scrapy/contrib/exporter/__init__.py b/scrapy/contrib/exporter/__init__.py index cc88f8792..7e1d01a0a 100644 --- a/scrapy/contrib/exporter/__init__.py +++ b/scrapy/contrib/exporter/__init__.py @@ -9,6 +9,7 @@ import marshal import six from six.moves import cPickle as pickle from xml.sax.saxutils import XMLGenerator + from scrapy.utils.serialize import ScrapyJSONEncoder from scrapy.item import BaseItem @@ -50,13 +51,13 @@ class BaseItemExporter(object): return value.encode(self.encoding) if isinstance(value, unicode) else value def _get_serialized_fields(self, item, default_value=None, include_empty=None): - """Return the fields to export as an iterable of tuples (name, - serialized_value) + """Return the fields to export as an iterable of tuples + (name, serialized_value) """ if include_empty is None: include_empty = self.export_empty_fields if self.fields_to_export is None: - if include_empty: + if include_empty and not isinstance(item, dict): field_iter = six.iterkeys(item.fields) else: field_iter = six.iterkeys(item) @@ -64,12 +65,11 @@ class BaseItemExporter(object): if include_empty: field_iter = self.fields_to_export else: - nonempty_fields = set(item.keys()) - field_iter = (x for x in self.fields_to_export if x in - nonempty_fields) + field_iter = (x for x in self.fields_to_export if x in item) + for field_name in field_iter: if field_name in item: - field = item.fields[field_name] + field = {} if isinstance(item, dict) else item.fields[field_name] value = self.serialize_field(field, field_name, item[field_name]) else: value = default_value @@ -191,7 +191,12 @@ class CsvItemExporter(BaseItemExporter): def _write_headers_and_set_fields_to_export(self, item): if self.include_headers_line: if not self.fields_to_export: - self.fields_to_export = item.fields.keys() + if isinstance(item, dict): + # for dicts try using fields of the first item + self.fields_to_export = list(item.keys()) + else: + # use fields declared in Item + self.fields_to_export = list(item.fields.keys()) self.csv_writer.writerow(self.fields_to_export) diff --git a/scrapy/contrib/pipeline/files.py b/scrapy/contrib/pipeline/files.py index db8cf8b76..9e803aca0 100644 --- a/scrapy/contrib/pipeline/files.py +++ b/scrapy/contrib/pipeline/files.py @@ -267,7 +267,7 @@ class FilesPipeline(MediaPipeline): return checksum def item_completed(self, results, item, info): - if self.FILES_RESULT_FIELD in item.fields: + if isinstance(item, dict) or self.FILES_RESULT_FIELD in item.fields: item[self.FILES_RESULT_FIELD] = [x for ok, x in results if ok] return item diff --git a/scrapy/contrib/pipeline/images.py b/scrapy/contrib/pipeline/images.py index 9c1a54455..b12995f09 100644 --- a/scrapy/contrib/pipeline/images.py +++ b/scrapy/contrib/pipeline/images.py @@ -109,7 +109,7 @@ class ImagesPipeline(FilesPipeline): return [Request(x) for x in item.get(self.IMAGES_URLS_FIELD, [])] def item_completed(self, results, item, info): - if self.IMAGES_RESULT_FIELD in item.fields: + if isinstance(item, dict) or self.IMAGES_RESULT_FIELD in item.fields: item[self.IMAGES_RESULT_FIELD] = [x for ok, x in results if ok] return item diff --git a/scrapy/core/scraper.py b/scrapy/core/scraper.py index 3409a0e7c..b301aa962 100644 --- a/scrapy/core/scraper.py +++ b/scrapy/core/scraper.py @@ -174,7 +174,7 @@ class Scraper(object): """ if isinstance(output, Request): self.crawler.engine.crawl(request=output, spider=spider) - elif isinstance(output, BaseItem): + elif isinstance(output, (BaseItem, dict)): self.slot.itemproc_size += 1 dfd = self.itemproc.process_item(output, spider) dfd.addBoth(self._itemproc_finished, output, response, spider) @@ -183,7 +183,7 @@ class Scraper(object): pass else: typename = type(output).__name__ - log.msg(format='Spider must return Request, BaseItem or None, ' + log.msg(format='Spider must return Request, BaseItem, dict or None, ' 'got %(typename)r in %(request)s', level=log.ERROR, spider=spider, request=request, typename=typename) diff --git a/tests/spiders.py b/tests/spiders.py index 83d767f5c..86ace9d6e 100644 --- a/tests/spiders.py +++ b/tests/spiders.py @@ -85,6 +85,7 @@ class ItemSpider(FollowAllSpider): for request in super(ItemSpider, self).parse(response): yield request yield Item() + yield {} class DefaultError(Exception): diff --git a/tests/test_commands.py b/tests/test_commands.py index 70b4e74dc..eb3556b62 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -127,6 +127,7 @@ class MiscCommandsTest(CommandTest): def test_list(self): self.assertEqual(0, self.call('list')) + class RunSpiderCommandTest(CommandTest): def test_runspider(self): @@ -135,10 +136,10 @@ class RunSpiderCommandTest(CommandTest): fname = abspath(join(tmpdir, 'myspider.py')) with open(fname, 'w') as f: f.write(""" +import scrapy from scrapy import log -from scrapy.spider import Spider -class MySpider(Spider): +class MySpider(scrapy.Spider): name = 'myspider' def start_requests(self): @@ -192,16 +193,15 @@ class ParseCommandTest(ProcessTest, SiteTest, CommandTest): with open(fname, 'w') as f: f.write(""" from scrapy import log -from scrapy.spider import Spider -from scrapy.item import Item +import scrapy -class MySpider(Spider): +class MySpider(scrapy.Spider): name = '{0}' def parse(self, response): if getattr(self, 'test_arg', None): self.log('It Works!') - return [Item()] + return [scrapy.Item(), dict(foo='bar')] """.format(self.spider_name)) fname = abspath(join(self.proj_mod_path, 'pipelines.py')) @@ -239,6 +239,14 @@ ITEM_PIPELINES = {'%s.pipelines.MyPipeline': 1} self.url('/html')]) self.assert_("[scrapy] INFO: It Works!" in stderr, stderr) + @defer.inlineCallbacks + def test_parse_items(self): + status, out, stderr = yield self.execute( + ['--spider', self.spider_name, '-c', 'parse', self.url('/html')] + ) + self.assertIn("""[{}, {'foo': 'bar'}]""", out) + + class BenchCommandTest(CommandTest): diff --git a/tests/test_contracts.py b/tests/test_contracts.py index a651576a5..d7732f55d 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -39,6 +39,13 @@ class TestSpider(Spider): """ return TestItem(url=response.url) + def returns_dict_item(self, response): + """ method which returns item + @url http://scrapy.org + @returns items 1 1 + """ + return {"url": response.url} + def returns_fail(self, response): """ method which returns item @url http://scrapy.org @@ -46,6 +53,13 @@ class TestSpider(Spider): """ return TestItem(url=response.url) + def returns_dict_fail(self, response): + """ method which returns item + @url http://scrapy.org + @returns items 0 0 + """ + return {'url': response.url} + def scrapes_item_ok(self, response): """ returns item with name and url @url http://scrapy.org @@ -54,6 +68,14 @@ class TestSpider(Spider): """ return TestItem(name='test', url=response.url) + def scrapes_dict_item_ok(self, response): + """ returns item with name and url + @url http://scrapy.org + @returns items 1 1 + @scrapes name url + """ + return {'name': 'test', 'url': response.url} + def scrapes_item_fail(self, response): """ returns item with no name @url http://scrapy.org @@ -62,6 +84,14 @@ class TestSpider(Spider): """ return TestItem(url=response.url) + def scrapes_dict_item_fail(self, response): + """ returns item with no name + @url http://scrapy.org + @returns items 1 1 + @scrapes name url + """ + return {'url': response.url} + def parse_no_url(self, response): """ method with no url @returns items 1 1 @@ -110,6 +140,11 @@ class ContractsManagerTest(unittest.TestCase): request.callback(response) self.should_succeed() + # returns_dict_item + request = self.conman.from_method(spider.returns_dict_item, self.results) + request.callback(response) + self.should_succeed() + # returns_request request = self.conman.from_method(spider.returns_request, self.results) request.callback(response) @@ -120,6 +155,11 @@ class ContractsManagerTest(unittest.TestCase): request.callback(response) self.should_fail() + # returns_dict_fail + request = self.conman.from_method(spider.returns_dict_fail, self.results) + request.callback(response) + self.should_fail() + def test_scrapes(self): spider = TestSpider() response = ResponseMock() @@ -129,8 +169,19 @@ class ContractsManagerTest(unittest.TestCase): request.callback(response) self.should_succeed() + # scrapes_dict_item_ok + request = self.conman.from_method(spider.scrapes_dict_item_ok, self.results) + request.callback(response) + self.should_succeed() + # scrapes_item_fail request = self.conman.from_method(spider.scrapes_item_fail, self.results) request.callback(response) self.should_fail() + + # scrapes_dict_item_fail + request = self.conman.from_method(spider.scrapes_dict_item_fail, + self.results) + request.callback(response) + self.should_fail() diff --git a/tests/test_contrib_exporter.py b/tests/test_contrib_exporter.py index 9092007e5..746aeb65b 100644 --- a/tests/test_contrib_exporter.py +++ b/tests/test_contrib_exporter.py @@ -1,14 +1,19 @@ -import unittest, json +from __future__ import absolute_import +import re +import json +import unittest from io import BytesIO from six.moves import cPickle as pickle + import lxml.etree -import re from scrapy.item import Item, Field from scrapy.utils.python import str_to_unicode -from scrapy.contrib.exporter import BaseItemExporter, PprintItemExporter, \ - PickleItemExporter, CsvItemExporter, XmlItemExporter, JsonLinesItemExporter, \ - JsonItemExporter, PythonItemExporter +from scrapy.contrib.exporter import ( + BaseItemExporter, PprintItemExporter, PickleItemExporter, CsvItemExporter, + XmlItemExporter, JsonLinesItemExporter, JsonItemExporter, PythonItemExporter +) + class TestItem(Item): name = Field() @@ -33,21 +38,28 @@ class BaseItemExporterTest(unittest.TestCase): exported_dict[k] = str_to_unicode(v) self.assertEqual(self.i, exported_dict) - def test_export_item(self): + def assertItemExportWorks(self, item): self.ie.start_exporting() try: - self.ie.export_item(self.i) + self.ie.export_item(item) except NotImplementedError: if self.ie.__class__ is not BaseItemExporter: raise self.ie.finish_exporting() self._check_output() + def test_export_item(self): + self.assertItemExportWorks(self.i) + + def test_export_dict_item(self): + self.assertItemExportWorks(dict(self.i)) + def test_serialize_field(self): - self.assertEqual(self.ie.serialize_field( \ - self.i.fields['name'], 'name', self.i['name']), 'John\xc2\xa3') - self.assertEqual( \ - self.ie.serialize_field(self.i.fields['age'], 'age', self.i['age']), '22') + res = self.ie.serialize_field(self.i.fields['name'], 'name', self.i['name']) + self.assertEqual(res, 'John\xc2\xa3') + + res = self.ie.serialize_field(self.i.fields['age'], 'age', self.i['age']) + self.assertEqual(res, '22') def test_fields_to_export(self): ie = self._get_exporter(fields_to_export=['name']) @@ -72,13 +84,14 @@ class BaseItemExporterTest(unittest.TestCase): self.assertEqual(ie.serialize_field(i.fields['name'], 'name', i['name']), 'John\xc2\xa3') self.assertEqual(ie.serialize_field(i.fields['age'], 'age', i['age']), '24') + class PythonItemExporterTest(BaseItemExporterTest): def _get_exporter(self, **kwargs): return PythonItemExporter(**kwargs) def test_nested_item(self): i1 = TestItem(name=u'Joseph', age='22') - i2 = TestItem(name=u'Maria', age=i1) + i2 = dict(name=u'Maria', age=i1) i3 = TestItem(name=u'Jesus', age=i2) ie = self._get_exporter() exported = ie.export_item(i3) @@ -107,6 +120,7 @@ class PythonItemExporterTest(BaseItemExporterTest): self.assertEqual(type(exported['age'][0]), dict) self.assertEqual(type(exported['age'][0]['age'][0]), dict) + class PprintItemExporterTest(BaseItemExporterTest): def _get_exporter(self, **kwargs): @@ -115,6 +129,7 @@ class PprintItemExporterTest(BaseItemExporterTest): def _check_output(self): self._assert_expected_item(eval(self.output.getvalue())) + class PickleItemExporterTest(BaseItemExporterTest): def _get_exporter(self, **kwargs): @@ -150,48 +165,65 @@ class CsvItemExporterTest(BaseItemExporterTest): def _check_output(self): self.assertCsvEqual(self.output.getvalue(), 'age,name\r\n22,John\xc2\xa3\r\n') - def test_header(self): - output = BytesIO() - ie = CsvItemExporter(output, fields_to_export=self.i.fields.keys()) + def assertExportResult(self, item, expected, **kwargs): + fp = BytesIO() + ie = CsvItemExporter(fp, **kwargs) ie.start_exporting() - ie.export_item(self.i) + ie.export_item(item) ie.finish_exporting() - self.assertCsvEqual(output.getvalue(), 'age,name\r\n22,John\xc2\xa3\r\n') + self.assertCsvEqual(fp.getvalue(), expected) - output = BytesIO() - ie = CsvItemExporter(output, fields_to_export=['age']) - ie.start_exporting() - ie.export_item(self.i) - ie.finish_exporting() - self.assertCsvEqual(output.getvalue(), 'age\r\n22\r\n') + def test_header_export_all(self): + self.assertExportResult( + item=self.i, + fields_to_export=self.i.fields.keys(), + expected='age,name\r\n22,John\xc2\xa3\r\n', + ) - output = BytesIO() - ie = CsvItemExporter(output) - ie.start_exporting() - ie.export_item(self.i) - ie.export_item(self.i) - ie.finish_exporting() - self.assertCsvEqual(output.getvalue(), 'age,name\r\n22,John\xc2\xa3\r\n22,John\xc2\xa3\r\n') + def test_header_export_all_dict(self): + self.assertExportResult( + item=dict(self.i), + expected='age,name\r\n22,John\xc2\xa3\r\n', + ) - output = BytesIO() - ie = CsvItemExporter(output, include_headers_line=False) - ie.start_exporting() - ie.export_item(self.i) - ie.finish_exporting() - self.assertCsvEqual(output.getvalue(), '22,John\xc2\xa3\r\n') + def test_header_export_single_field(self): + for item in [self.i, dict(self.i)]: + self.assertExportResult( + item=item, + fields_to_export=['age'], + expected='age\r\n22\r\n', + ) + + def test_header_export_two_items(self): + for item in [self.i, dict(self.i)]: + output = BytesIO() + ie = CsvItemExporter(output) + ie.start_exporting() + ie.export_item(item) + ie.export_item(item) + ie.finish_exporting() + self.assertCsvEqual(output.getvalue(), 'age,name\r\n22,John\xc2\xa3\r\n22,John\xc2\xa3\r\n') + + def test_header_no_header_line(self): + for item in [self.i, dict(self.i)]: + self.assertExportResult( + item=item, + include_headers_line=False, + expected='22,John\xc2\xa3\r\n', + ) def test_join_multivalue(self): class TestItem2(Item): name = Field() friends = Field() - i = TestItem2(name='John', friends=['Mary', 'Paul']) - output = BytesIO() - ie = CsvItemExporter(output, include_headers_line=False) - ie.start_exporting() - ie.export_item(i) - ie.finish_exporting() - self.assertCsvEqual(output.getvalue(), '"Mary,Paul",John\r\n') + for cls in TestItem2, dict: + self.assertExportResult( + item=cls(name='John', friends=['Mary', 'Paul']), + include_headers_line=False, + expected='"Mary,Paul",John\r\n', + ) + class XmlItemExporterTest(BaseItemExporterTest): @@ -211,60 +243,62 @@ class XmlItemExporterTest(BaseItemExporterTest): return xmltuple(doc) return self.assertEqual(xmlsplit(first), xmlsplit(second), msg) + def assertExportResult(self, item, expected_value): + fp = BytesIO() + ie = XmlItemExporter(fp) + ie.start_exporting() + ie.export_item(item) + ie.finish_exporting() + self.assertXmlEquivalent(fp.getvalue(), expected_value) + def _check_output(self): expected_value = '\n22John\xc2\xa3' self.assertXmlEquivalent(self.output.getvalue(), expected_value) def test_multivalued_fields(self): - output = BytesIO() - item = TestItem(name=[u'John\xa3', u'Doe']) - ie = XmlItemExporter(output) - ie.start_exporting() - ie.export_item(item) - ie.finish_exporting() - expected_value = '\nJohn\xc2\xa3Doe' - self.assertXmlEquivalent(output.getvalue(), expected_value) + self.assertExportResult( + TestItem(name=[u'John\xa3', u'Doe']), + '\nJohn\xc2\xa3Doe' + ) def test_nested_item(self): - output = BytesIO() i1 = TestItem(name=u'foo\xa3hoo', age='22') - i2 = TestItem(name=u'bar', age=i1) + i2 = dict(name=u'bar', age=i1) i3 = TestItem(name=u'buz', age=i2) - ie = XmlItemExporter(output) - ie.start_exporting() - ie.export_item(i3) - ie.finish_exporting() - expected_value = '\n'\ - ''\ - ''\ - ''\ - '22'\ - 'foo\xc2\xa3hoo'\ - ''\ - 'bar'\ - ''\ - 'buz'\ - '' - self.assertXmlEquivalent(output.getvalue(), expected_value) + + self.assertExportResult(i3, + '\n' + '' + '' + '' + '' + '22' + 'foo\xc2\xa3hoo' + '' + 'bar' + '' + 'buz' + '' + '' + ) def test_nested_list_item(self): - output = BytesIO() i1 = TestItem(name=u'foo') - i2 = TestItem(name=u'bar') + i2 = dict(name=u'bar', v2={"egg": ["spam"]}) i3 = TestItem(name=u'buz', age=[i1, i2]) - ie = XmlItemExporter(output) - ie.start_exporting() - ie.export_item(i3) - ie.finish_exporting() - expected_value = '\n'\ - ''\ - ''\ - 'foo'\ - 'bar'\ - ''\ - 'buz'\ - '' - self.assertXmlEquivalent(output.getvalue(), expected_value) + + self.assertExportResult(i3, + '\n' + '' + '' + '' + 'foo' + 'barspam' + '' + 'buz' + '' + '' + ) class JsonLinesItemExporterTest(BaseItemExporterTest): @@ -280,7 +314,7 @@ class JsonLinesItemExporterTest(BaseItemExporterTest): def test_nested_item(self): i1 = TestItem(name=u'Joseph', age='22') - i2 = TestItem(name=u'Maria', age=i1) + i2 = dict(name=u'Maria', age=i1) i3 = TestItem(name=u'Jesus', age=i2) self.ie.start_exporting() self.ie.export_item(i3) @@ -306,13 +340,19 @@ class JsonItemExporterTest(JsonLinesItemExporterTest): exported = json.loads(self.output.getvalue().strip()) self.assertEqual(exported, [dict(self.i)]) - def test_two_items(self): + def assertTwoItemsExported(self, item): self.ie.start_exporting() - self.ie.export_item(self.i) - self.ie.export_item(self.i) + self.ie.export_item(item) + self.ie.export_item(item) self.ie.finish_exporting() exported = json.loads(self.output.getvalue()) - self.assertEqual(exported, [dict(self.i), dict(self.i)]) + self.assertEqual(exported, [dict(item), dict(item)]) + + def test_two_items(self): + self.assertTwoItemsExported(self.i) + + def test_two_dict_items(self): + self.assertTwoItemsExported(dict(self.i)) def test_nested_item(self): i1 = TestItem(name=u'Joseph\xa3', age='22') @@ -325,6 +365,18 @@ class JsonItemExporterTest(JsonLinesItemExporterTest): expected = {'name': u'Jesus', 'age': {'name': 'Maria', 'age': dict(i1)}} self.assertEqual(exported, [expected]) + def test_nested_dict_item(self): + i1 = dict(name=u'Joseph\xa3', age='22') + i2 = TestItem(name=u'Maria', age=i1) + i3 = dict(name=u'Jesus', age=i2) + self.ie.start_exporting() + self.ie.export_item(i3) + self.ie.finish_exporting() + exported = json.loads(self.output.getvalue()) + expected = {'name': u'Jesus', 'age': {'name': 'Maria', 'age': i1}} + self.assertEqual(exported, [expected]) + + class CustomItemExporterTest(unittest.TestCase): def test_exporter_custom_serializer(self): @@ -333,16 +385,17 @@ class CustomItemExporterTest(unittest.TestCase): if name == 'age': return str(int(value) + 1) else: - return super(CustomItemExporter, self).serialize_field(field, \ - name, value) + return super(CustomItemExporter, self).serialize_field(field, name, value) i = TestItem(name=u'John', age='22') ie = CustomItemExporter() - self.assertEqual( \ - ie.serialize_field(i.fields['name'], 'name', i['name']), 'John') - self.assertEqual( - ie.serialize_field(i.fields['age'], 'age', i['age']), '23') + self.assertEqual(ie.serialize_field(i.fields['name'], 'name', i['name']), 'John') + self.assertEqual(ie.serialize_field(i.fields['age'], 'age', i['age']), '23') + + i2 = {'name': u'John', 'age': '22'} + self.assertEqual(ie.serialize_field({}, 'name', i2['name']), 'John') + self.assertEqual(ie.serialize_field({}, 'age', i2['age']), '23') if __name__ == '__main__': diff --git a/tests/test_engine.py b/tests/test_engine.py index 52c8e5752..04fae02c0 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -28,11 +28,13 @@ from scrapy.contrib.linkextractors import LinkExtractor from scrapy.http import Request from scrapy.utils.signal import disconnect_all + class TestItem(Item): name = Field() url = Field() price = Field() + class TestSpider(Spider): name = "scrapytest.org" allowed_domains = ["scrapytest.org", "localhost"] @@ -41,6 +43,8 @@ class TestSpider(Spider): name_re = re.compile("

(.*?)

", re.M) price_re = re.compile(">Price: \$(.*?)<", re.M) + item_cls = TestItem + def parse(self, response): xlink = LinkExtractor() itemre = re.compile(self.itemurl_re) @@ -49,7 +53,7 @@ class TestSpider(Spider): yield Request(url=link.url, callback=self.parse_item) def parse_item(self, response): - item = TestItem() + item = self.item_cls() m = self.name_re.search(response.body) if m: item['name'] = m.group(1) @@ -65,6 +69,10 @@ class TestDupeFilterSpider(TestSpider): return Request(url) # dont_filter=False +class DictItemsSpider(TestSpider): + item_cls = dict + + def start_test_site(debug=False): root_dir = os.path.join(tests_datadir, "test_site") r = static.File(root_dir) @@ -81,15 +89,14 @@ def start_test_site(debug=False): class CrawlerRun(object): """A class to run the crawler and keep track of events occurred""" - def __init__(self, with_dupefilter=False): + def __init__(self, spider_class): self.spider = None self.respplug = [] self.reqplug = [] self.reqdropped = [] self.itemresp = [] self.signals_catched = {} - self.spider_class = TestSpider if not with_dupefilter else \ - TestDupeFilterSpider + self.spider_class = spider_class def run(self): self.port = start_test_site() @@ -152,14 +159,17 @@ class EngineTest(unittest.TestCase): @defer.inlineCallbacks def test_crawler(self): - self.run = CrawlerRun() - yield self.run.run() - self._assert_visited_urls() - self._assert_scheduled_requests(urls_to_visit=8) - self._assert_downloaded_responses() - self._assert_scraped_items() - self._assert_signals_catched() - self.run = CrawlerRun(with_dupefilter=True) + + for spider in TestSpider, DictItemsSpider: + self.run = CrawlerRun(spider) + yield self.run.run() + self._assert_visited_urls() + self._assert_scheduled_requests(urls_to_visit=8) + self._assert_downloaded_responses() + self._assert_scraped_items() + self._assert_signals_catched() + + self.run = CrawlerRun(TestDupeFilterSpider) yield self.run.run() self._assert_scheduled_requests(urls_to_visit=7) self._assert_dropped_requests() diff --git a/tests/test_pipeline_files.py b/tests/test_pipeline_files.py index 0a1737c44..84fe4927d 100644 --- a/tests/test_pipeline_files.py +++ b/tests/test_pipeline_files.py @@ -142,35 +142,40 @@ class DeprecatedFilesPipelineTestCase(unittest.TestCase): class FilesPipelineTestCaseFields(unittest.TestCase): def test_item_fields_default(self): - from scrapy.contrib.pipeline.files import FilesPipeline class TestItem(Item): name = Field() file_urls = Field() files = Field() - url = 'http://www.example.com/files/1.txt' - item = TestItem({'name': 'item1', 'file_urls': [url]}) - pipeline = FilesPipeline.from_settings(Settings({'FILES_STORE': 's3://example/files/'})) - requests = list(pipeline.get_media_requests(item, None)) - self.assertEqual(requests[0].url, url) - results = [(True, {'url': url})] - pipeline.item_completed(results, item, None) - self.assertEqual(item['files'], [results[0][1]]) + + for cls in TestItem, dict: + url = 'http://www.example.com/files/1.txt' + item = cls({'name': 'item1', 'file_urls': [url]}) + pipeline = FilesPipeline.from_settings(Settings({'FILES_STORE': 's3://example/files/'})) + requests = list(pipeline.get_media_requests(item, None)) + self.assertEqual(requests[0].url, url) + results = [(True, {'url': url})] + pipeline.item_completed(results, item, None) + self.assertEqual(item['files'], [results[0][1]]) def test_item_fields_override_settings(self): - from scrapy.contrib.pipeline.files import FilesPipeline class TestItem(Item): name = Field() files = Field() stored_file = Field() - url = 'http://www.example.com/files/1.txt' - item = TestItem({'name': 'item1', 'files': [url]}) - pipeline = FilesPipeline.from_settings(Settings({'FILES_STORE': 's3://example/files/', - 'FILES_URLS_FIELD': 'files', 'FILES_RESULT_FIELD': 'stored_file'})) - requests = list(pipeline.get_media_requests(item, None)) - self.assertEqual(requests[0].url, url) - results = [(True, {'url': url})] - pipeline.item_completed(results, item, None) - self.assertEqual(item['stored_file'], [results[0][1]]) + + for cls in TestItem, dict: + url = 'http://www.example.com/files/1.txt' + item = cls({'name': 'item1', 'files': [url]}) + pipeline = FilesPipeline.from_settings(Settings({ + 'FILES_STORE': 's3://example/files/', + 'FILES_URLS_FIELD': 'files', + 'FILES_RESULT_FIELD': 'stored_file' + })) + requests = list(pipeline.get_media_requests(item, None)) + self.assertEqual(requests[0].url, url) + results = [(True, {'url': url})] + pipeline.item_completed(results, item, None) + self.assertEqual(item['stored_file'], [results[0][1]]) class ItemWithFiles(Item): diff --git a/tests/test_pipeline_images.py b/tests/test_pipeline_images.py index a3b1059ef..f5750b4fc 100644 --- a/tests/test_pipeline_images.py +++ b/tests/test_pipeline_images.py @@ -168,35 +168,40 @@ class DeprecatedImagesPipelineTestCase(unittest.TestCase): class ImagesPipelineTestCaseFields(unittest.TestCase): def test_item_fields_default(self): - from scrapy.contrib.pipeline.images import ImagesPipeline class TestItem(Item): name = Field() image_urls = Field() images = Field() - url = 'http://www.example.com/images/1.jpg' - item = TestItem({'name': 'item1', 'image_urls': [url]}) - pipeline = ImagesPipeline.from_settings(Settings({'IMAGES_STORE': 's3://example/images/'})) - requests = list(pipeline.get_media_requests(item, None)) - self.assertEqual(requests[0].url, url) - results = [(True, {'url': url})] - pipeline.item_completed(results, item, None) - self.assertEqual(item['images'], [results[0][1]]) + + for cls in TestItem, dict: + url = 'http://www.example.com/images/1.jpg' + item = cls({'name': 'item1', 'image_urls': [url]}) + pipeline = ImagesPipeline.from_settings(Settings({'IMAGES_STORE': 's3://example/images/'})) + requests = list(pipeline.get_media_requests(item, None)) + self.assertEqual(requests[0].url, url) + results = [(True, {'url': url})] + pipeline.item_completed(results, item, None) + self.assertEqual(item['images'], [results[0][1]]) def test_item_fields_override_settings(self): - from scrapy.contrib.pipeline.images import ImagesPipeline class TestItem(Item): name = Field() image = Field() stored_image = Field() - url = 'http://www.example.com/images/1.jpg' - item = TestItem({'name': 'item1', 'image': [url]}) - pipeline = ImagesPipeline.from_settings(Settings({'IMAGES_STORE': 's3://example/images/', - 'IMAGES_URLS_FIELD': 'image', 'IMAGES_RESULT_FIELD': 'stored_image'})) - requests = list(pipeline.get_media_requests(item, None)) - self.assertEqual(requests[0].url, url) - results = [(True, {'url': url})] - pipeline.item_completed(results, item, None) - self.assertEqual(item['stored_image'], [results[0][1]]) + + for cls in TestItem, dict: + url = 'http://www.example.com/images/1.jpg' + item = cls({'name': 'item1', 'image': [url]}) + pipeline = ImagesPipeline.from_settings(Settings({ + 'IMAGES_STORE': 's3://example/images/', + 'IMAGES_URLS_FIELD': 'image', + 'IMAGES_RESULT_FIELD': 'stored_image' + })) + requests = list(pipeline.get_media_requests(item, None)) + self.assertEqual(requests[0].url, url) + results = [(True, {'url': url})] + pipeline.item_completed(results, item, None) + self.assertEqual(item['stored_image'], [results[0][1]]) def _create_image(format, *a, **kw): From 5846d6154ca3adf87dfee7ba7294ef35949177ab Mon Sep 17 00:00:00 2001 From: Pablo Hoffman Date: Wed, 18 Mar 2015 00:24:16 -0300 Subject: [PATCH 0114/2974] emphasize web crawling over screen scraping on scrapy description. closes #586 --- README.rst | 2 +- debian/control | 4 ++-- scrapy/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 7e5dd173f..bf043e1ab 100644 --- a/README.rst +++ b/README.rst @@ -15,7 +15,7 @@ Scrapy Overview ======== -Scrapy is a fast high-level screen scraping and web crawling framework, used to +Scrapy is a fast high-level web crawling and screen scraping framework, used to crawl websites and extract structured data from their pages. It can be used for a wide range of purposes, from data mining to monitoring and automated testing. diff --git a/debian/control b/debian/control index 4be62895f..c3dae341e 100644 --- a/debian/control +++ b/debian/control @@ -13,8 +13,8 @@ Depends: ${python:Depends}, python-lxml, python-twisted, python-openssl, Recommends: python-setuptools Conflicts: python-scrapy, scrapy, scrapy-0.11 Provides: python-scrapy, scrapy -Description: Python web crawling and scraping framework - Scrapy is a fast high-level screen scraping and web crawling framework, +Description: Python web crawling and screen scraping framework + Scrapy is a fast high-level web crawling and screen scraping framework, used to crawl websites and extract structured data from their pages. It can be used for a wide range of purposes, from data mining to monitoring and automated testing. diff --git a/scrapy/__init__.py b/scrapy/__init__.py index 10e9091b6..d60b239a0 100644 --- a/scrapy/__init__.py +++ b/scrapy/__init__.py @@ -1,5 +1,5 @@ """ -Scrapy - a screen scraping framework written in Python +Scrapy - a web crawling and screen scraping framework written for Python """ __all__ = ['__version__', 'version_info', 'optional_features', 'twisted_version', diff --git a/setup.py b/setup.py index b5732cbc2..d463bccd9 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='Scrapy', version=version, url='http://scrapy.org', - description='A high-level Python Screen Scraping framework', + description='A high-level Web Crawling and Screen Scraping framework', long_description=open('README.rst').read(), author='Scrapy developers', maintainer='Pablo Hoffman', From 776616bdec4680e31071debbfe94cad77c3ff148 Mon Sep 17 00:00:00 2001 From: Kevin Yap Date: Tue, 17 Mar 2015 22:20:45 -0700 Subject: [PATCH 0115/2974] Use Shields.io SVGs for README badges - Use SVGs for badges (more friendly to retina displays). - Add alt text to PyPI version and build status badges. --- README.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 7e5dd173f..396bb14f6 100644 --- a/README.rst +++ b/README.rst @@ -2,15 +2,17 @@ Scrapy ====== -.. image:: https://badge.fury.io/py/Scrapy.png - :target: http://badge.fury.io/py/Scrapy +.. image:: https://img.shields.io/pypi/v/Scrapy.svg + :target: https://pypi.python.org/pypi/Scrapy + :alt: PyPI Version -.. image:: https://secure.travis-ci.org/scrapy/scrapy.png?branch=master +.. image:: https://img.shields.io/travis/scrapy/scrapy/master.svg :target: http://travis-ci.org/scrapy/scrapy + :alt: Build Status -.. image:: https://pypip.in/wheel/Scrapy/badge.png - :target: https://pypi.python.org/pypi/Scrapy/ - :alt: Wheel Status +.. image:: https://img.shields.io/badge/wheel-yes-brightgreen.svg + :target: https://pypi.python.org/pypi/Scrapy + :alt: Wheel Status Overview ======== From 5a58d6413195d00484d5e206e006906231e5890d Mon Sep 17 00:00:00 2001 From: Shadab Zafar Date: Fri, 6 Feb 2015 22:46:18 +0530 Subject: [PATCH 0116/2974] Fix some redirection links in documentation Fixes #606 --- docs/contributing.rst | 4 ++-- docs/faq.rst | 8 ++++---- docs/index.rst | 4 ++-- docs/intro/examples.rst | 2 +- docs/intro/install.rst | 6 +++--- docs/intro/overview.rst | 2 +- docs/intro/tutorial.rst | 4 ++-- docs/news.rst | 8 ++++---- docs/topics/commands.rst | 2 +- docs/topics/downloader-middleware.rst | 8 ++++---- docs/topics/email.rst | 2 +- docs/topics/exporters.rst | 8 ++++---- docs/topics/extensions.rst | 2 +- docs/topics/firebug.rst | 4 ++-- docs/topics/firefox.rst | 10 +++++----- docs/topics/images.rst | 4 ++-- docs/topics/items.rst | 10 +++++----- docs/topics/leaks.rst | 10 +++++----- docs/topics/logging.rst | 2 +- docs/topics/request-response.rst | 2 +- docs/topics/scrapyd.rst | 2 +- docs/topics/selectors.rst | 8 ++++---- docs/topics/settings.rst | 2 +- docs/topics/spider-middleware.rst | 2 +- docs/topics/spiders.rst | 4 ++-- 25 files changed, 60 insertions(+), 60 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index f8d965df8..f49bc536e 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -173,10 +173,10 @@ And their unit-tests are in:: tests/test_contrib_loader.py .. _issue tracker: https://github.com/scrapy/scrapy/issues -.. _scrapy-users: http://groups.google.com/group/scrapy-users +.. _scrapy-users: https://groups.google.com/forum/#!forum/scrapy-users .. _Twisted unit-testing framework: http://twistedmatrix.com/documents/current/core/development/policy/test-standard.html .. _AUTHORS: https://github.com/scrapy/scrapy/blob/master/AUTHORS .. _tests/: https://github.com/scrapy/scrapy/tree/master/tests .. _open issues: https://github.com/scrapy/scrapy/issues -.. _pull request: http://help.github.com/send-pull-requests/ +.. _pull request: https://help.github.com/send-pull-requests/ .. _tox: https://pypi.python.org/pypi/tox diff --git a/docs/faq.rst b/docs/faq.rst index 1d6c56d97..71d9e4c4e 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -21,8 +21,8 @@ comparing `jinja2`_ to `Django`_. .. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/ .. _lxml: http://lxml.de/ -.. _jinja2: http://jinja.pocoo.org/2/ -.. _Django: http://www.djangoproject.com +.. _jinja2: http://jinja.pocoo.org/ +.. _Django: https://www.djangoproject.com/ .. _faq-python-versions: @@ -57,7 +57,7 @@ focus on the real problems we need to solve. We'd be proud if Scrapy serves as an inspiration for other projects. Feel free to steal from us! -.. _Django: http://www.djangoproject.com +.. _Django: https://www.djangoproject.com/ Does Scrapy work with HTTP proxies? ----------------------------------- @@ -221,7 +221,7 @@ more info on how it works see `this page`_. Also, here's an `example spider`_ which scrapes one of these sites. .. _this page: http://search.cpan.org/~ecarroll/HTML-TreeBuilderX-ASP_NET-0.09/lib/HTML/TreeBuilderX/ASP_NET.pm -.. _example spider: http://github.com/AmbientLighter/rpn-fas/blob/master/fas/spiders/rnp.py +.. _example spider: https://github.com/AmbientLighter/rpn-fas/blob/master/fas/spiders/rnp.py What's the best way to parse big XML/CSV data feeds? ---------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 0384dae3d..507b9bea9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,8 +18,8 @@ Having trouble? We'd like to help! * Ask a question in the `#scrapy IRC channel`_. * Report bugs with Scrapy in our `issue tracker`_. -.. _archives of the scrapy-users mailing list: http://groups.google.com/group/scrapy-users/ -.. _post a question: http://groups.google.com/group/scrapy-users/ +.. _archives of the scrapy-users mailing list: https://groups.google.com/forum/#!forum/scrapy-users +.. _post a question: https://groups.google.com/forum/#!forum/scrapy-users .. _#scrapy IRC channel: irc://irc.freenode.net/scrapy .. _issue tracker: https://github.com/scrapy/scrapy/issues diff --git a/docs/intro/examples.rst b/docs/intro/examples.rst index 40a124679..c56348714 100644 --- a/docs/intro/examples.rst +++ b/docs/intro/examples.rst @@ -21,5 +21,5 @@ middlewares, extensions, or scripts. Feel free (and encouraged!) to share any code there. .. _dirbot: https://github.com/scrapy/dirbot -.. _Downloads: https://github.com/scrapy/dirbot/archives/master +.. _Downloads: https://github.com/scrapy/dirbot/downloads .. _scrapy tag on Snipplr: http://snipplr.com/all/tags/scrapy/ diff --git a/docs/intro/install.rst b/docs/intro/install.rst index 1d786efe7..fbed84055 100644 --- a/docs/intro/install.rst +++ b/docs/intro/install.rst @@ -37,7 +37,7 @@ Platform specific installation notes Windows ------- -* Install Python 2.7 from http://python.org/download/ +* Install Python 2.7 from https://www.python.org/downloads/ You need to adjust ``PATH`` environment variable to include paths to the Python executable and additional scripts. The following paths need to be @@ -87,8 +87,8 @@ You can follow the generic instructions or install Scrapy from `AUR Scrapy packa yaourt -S scrapy -.. _Python: http://www.python.org -.. _pip: http://www.pip-installer.org/en/latest/installing.html +.. _Python: https://www.python.org/ +.. _pip: https://pip.pypa.io/en/latest/installing.html .. _easy_install: http://pypi.python.org/pypi/setuptools .. _Control Panel: http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/sysdm_advancd_environmnt_addchange_variable.mspx .. _lxml: http://lxml.de/ diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index 289e975b8..c30963db8 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -258,7 +258,7 @@ interest! .. _the community: http://scrapy.org/community/ .. _screen scraping: http://en.wikipedia.org/wiki/Screen_scraping .. _web scraping: http://en.wikipedia.org/wiki/Web_scraping -.. _Amazon Associates Web Services: http://aws.amazon.com/associates/ +.. _Amazon Associates Web Services: https://affiliate-program.amazon.com/gp/advertising/api/detail/main.html .. _Mininova: http://www.mininova.org .. _XPath: http://www.w3.org/TR/xpath .. _XPath reference: http://www.w3.org/TR/xpath diff --git a/docs/intro/tutorial.rst b/docs/intro/tutorial.rst index a4248d7aa..ad808316b 100644 --- a/docs/intro/tutorial.rst +++ b/docs/intro/tutorial.rst @@ -26,8 +26,8 @@ Python quickly, we recommend `Learn Python The Hard Way`_. If you're new to pro and want to start with Python, take a look at `this list of Python resources for non-programmers`_. -.. _Python: http://www.python.org -.. _this list of Python resources for non-programmers: http://wiki.python.org/moin/BeginnersGuide/NonProgrammers +.. _Python: https://www.python.org/ +.. _this list of Python resources for non-programmers: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers .. _Learn Python The Hard Way: http://learnpythonthehardway.org/book/ Creating a project diff --git a/docs/news.rst b/docs/news.rst index 0f5e78e8c..383f59760 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -578,7 +578,7 @@ Scrapy changes: ------ - added precise to supported ubuntu distros (:commit:`b7e46df`) -- fixed bug in json-rpc webservice reported in https://groups.google.com/d/topic/scrapy-users/qgVBmFybNAQ/discussion. also removed no longer supported 'run' command from extras/scrapy-ws.py (:commit:`340fbdb`) +- fixed bug in json-rpc webservice reported in https://groups.google.com/forum/#!topic/scrapy-users/qgVBmFybNAQ/discussion. also removed no longer supported 'run' command from extras/scrapy-ws.py (:commit:`340fbdb`) - meta tag attributes for content-type http equiv can be in any order. #123 (:commit:`0cb68af`) - replace "import Image" by more standard "from PIL import Image". closes #88 (:commit:`4d17048`) - return trial status as bin/runtests.sh exit value. #118 (:commit:`b7b2e7f`) @@ -902,14 +902,14 @@ Backwards-incompatible changes First release of Scrapy. -.. _AJAX crawleable urls: http://code.google.com/web/ajaxcrawling/docs/getting-started.html +.. _AJAX crawleable urls: https://developers.google.com/webmasters/ajax-crawling/docs/getting-started?csw=1 .. _chunked transfer encoding: http://en.wikipedia.org/wiki/Chunked_transfer_encoding .. _w3lib: https://github.com/scrapy/w3lib .. _scrapely: https://github.com/scrapy/scrapely -.. _marshal: http://docs.python.org/library/marshal.html +.. _marshal: https://docs.python.org/2/library/marshal.html .. _w3lib.encoding: https://github.com/scrapy/w3lib/blob/master/w3lib/encoding.py .. _lxml: http://lxml.de/ .. _ClientForm: http://wwwsearch.sourceforge.net/old/ClientForm/ -.. _resource: http://docs.python.org/library/resource.html +.. _resource: https://docs.python.org/2/library/resource.html .. _queuelib: https://github.com/scrapy/queuelib .. _cssselect: https://github.com/SimonSapin/cssselect diff --git a/docs/topics/commands.rst b/docs/topics/commands.rst index 545a2f165..5c0de0d6d 100644 --- a/docs/topics/commands.rst +++ b/docs/topics/commands.rst @@ -484,7 +484,7 @@ You can also add your custom project commands by using the :setting:`COMMANDS_MODULE` setting. See the Scrapy commands in `scrapy/commands`_ for examples on how to implement your commands. -.. _scrapy/commands: https://github.com/scrapy/scrapy/blob/master/scrapy/commands +.. _scrapy/commands: https://github.com/scrapy/scrapy/tree/master/scrapy/commands .. setting:: COMMANDS_MODULE COMMANDS_MODULE diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index 424d52a73..5b38f048c 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -451,7 +451,7 @@ In order to use this storage backend: * install `LevelDB python bindings`_ like ``pip install leveldb`` .. _LevelDB: http://code.google.com/p/leveldb/ -.. _leveldb python bindings: http://pypi.python.org/pypi/leveldb +.. _leveldb python bindings: https://pypi.python.org/pypi/leveldb HTTPCache middleware settings @@ -635,8 +635,8 @@ HttpProxyMiddleware You can also set the meta key ``proxy`` per-request, to a value like ``http://some_proxy_server:port``. -.. _urllib: http://docs.python.org/library/urllib.html -.. _urllib2: http://docs.python.org/library/urllib2.html +.. _urllib: https://docs.python.org/2/library/urllib.html +.. _urllib2: https://docs.python.org/2/library/urllib2.html RedirectMiddleware ------------------ @@ -890,5 +890,5 @@ enable it for :ref:`broad crawls `. .. _DBM: http://en.wikipedia.org/wiki/Dbm -.. _anydbm: http://docs.python.org/library/anydbm.html +.. _anydbm: https://docs.python.org/2/library/anydbm.html .. _chunked transfer encoding: http://en.wikipedia.org/wiki/Chunked_transfer_encoding diff --git a/docs/topics/email.rst b/docs/topics/email.rst index d99589413..789fbd4fb 100644 --- a/docs/topics/email.rst +++ b/docs/topics/email.rst @@ -14,7 +14,7 @@ interfering with the non-blocking IO of the crawler. It also provides a simple API for sending attachments and it's very easy to configure, with a few :ref:`settings `. -.. _smtplib: http://docs.python.org/library/smtplib.html +.. _smtplib: https://docs.python.org/2/library/smtplib.html .. _Twisted non-blocking IO: http://twistedmatrix.com/documents/current/core/howto/defer-intro.html Quick example diff --git a/docs/topics/exporters.rst b/docs/topics/exporters.rst index c472f5b96..f7feed4af 100644 --- a/docs/topics/exporters.rst +++ b/docs/topics/exporters.rst @@ -297,7 +297,7 @@ CsvItemExporter Color TV,1200 DVD player,200 -.. _csv.writer: http://docs.python.org/library/csv.html#csv.writer +.. _csv.writer: https://docs.python.org/2/library/csv.html#csv.writer PickleItemExporter ------------------ @@ -318,7 +318,7 @@ PickleItemExporter Pickle isn't a human readable format, so no output examples are provided. -.. _pickle module documentation: http://docs.python.org/library/pickle.html +.. _pickle module documentation: https://docs.python.org/2/library/pickle.html PprintItemExporter ------------------ @@ -367,7 +367,7 @@ JsonItemExporter stream-friendly format, consider using :class:`JsonLinesItemExporter` instead, or splitting the output in multiple chunks. -.. _JSONEncoder: http://docs.python.org/library/json.html#json.JSONEncoder +.. _JSONEncoder: https://docs.python.org/2/library/json.html#json.JSONEncoder JsonLinesItemExporter --------------------- @@ -390,4 +390,4 @@ JsonLinesItemExporter Unlike the one produced by :class:`JsonItemExporter`, the format produced by this exporter is well suited for serializing large amounts of data. -.. _JSONEncoder: http://docs.python.org/library/json.html#json.JSONEncoder +.. _JSONEncoder: https://docs.python.org/2/library/json.html#json.JSONEncoder diff --git a/docs/topics/extensions.rst b/docs/topics/extensions.rst index 1824bcc3a..8cd588c4a 100644 --- a/docs/topics/extensions.rst +++ b/docs/topics/extensions.rst @@ -368,5 +368,5 @@ For more info see `Debugging in Python`. This extension only works on POSIX-compliant platforms (ie. not Windows). -.. _Python debugger: http://docs.python.org/library/pdb.html +.. _Python debugger: https://docs.python.org/2/library/pdb.html .. _Debugging in Python: http://www.ferg.org/papers/debugging_in_python.html diff --git a/docs/topics/firebug.rst b/docs/topics/firebug.rst index 7dd5a2b76..ad3f26b50 100644 --- a/docs/topics/firebug.rst +++ b/docs/topics/firebug.rst @@ -32,7 +32,7 @@ you to inspect the HTML code of the different page elements just by hovering your mouse over them. Otherwise you would have to search for the tags manually through the HTML body which can be a very tedious task. -.. _Inspect Element: http://www.youtube.com/watch?v=-pT_pDe54aA +.. _Inspect Element: https://www.youtube.com/watch?v=-pT_pDe54aA In the following screenshot you can see the `Inspect Element`_ tool in action. @@ -164,4 +164,4 @@ elements. or tags which Therefer in page HTML sources may on Firebug inspects the live DOM -.. _has been shut down by Google: http://searchenginewatch.com/article/2096661/Google-Directory-Has-Been-Shut-Down +.. _has been shut down by Google: http://searchenginewatch.com/sew/news/2096661/google-directory-shut diff --git a/docs/topics/firefox.rst b/docs/topics/firefox.rst index f0b8eb594..beda3b8db 100644 --- a/docs/topics/firefox.rst +++ b/docs/topics/firefox.rst @@ -74,9 +74,9 @@ extension to create a new cookie, delete existing cookies, see a list of cookies for the current site, manage cookies permissions and a lot more. .. _Firebug: http://getfirebug.com -.. _Inspect Element: http://www.youtube.com/watch?v=-pT_pDe54aA -.. _XPather: https://addons.mozilla.org/firefox/addon/1192 -.. _XPath Checker: https://addons.mozilla.org/firefox/addon/1095 -.. _Tamper Data: http://addons.mozilla.org/firefox/addon/966 -.. _Firecookie: https://addons.mozilla.org/firefox/addon/6683 +.. _Inspect Element: https://www.youtube.com/watch?v=-pT_pDe54aA +.. _XPather: https://addons.mozilla.org/en-US/firefox/addon/xpather/ +.. _XPath Checker: https://addons.mozilla.org/en-US/firefox/addon/xpath-checker/ +.. _Tamper Data: https://addons.mozilla.org/en-US/firefox/addon/tamper-data/ +.. _Firecookie: https://addons.mozilla.org/en-US/firefox/addon/firecookie/ diff --git a/docs/topics/images.rst b/docs/topics/images.rst index 91e59d2a9..4b07300eb 100644 --- a/docs/topics/images.rst +++ b/docs/topics/images.rst @@ -30,7 +30,7 @@ so you need to install this library in order to use the images pipeline. is known to cause troubles in some setups, so we recommend to use `Pillow`_ instead of `PIL `_. -.. _Pillow: https://github.com/python-imaging/Pillow +.. _Pillow: https://github.com/python-pillow/Pillow .. _Python Imaging Library: http://www.pythonware.com/products/pil/ Using the Images Pipeline @@ -104,7 +104,7 @@ Images Storage File system is currently the only officially supported storage, but there is also (undocumented) support for `Amazon S3`_. -.. _Amazon S3: https://s3.amazonaws.com/ +.. _Amazon S3: http://aws.amazon.com/s3/ File system storage ------------------- diff --git a/docs/topics/items.rst b/docs/topics/items.rst index ee604a7f1..17f10a88c 100644 --- a/docs/topics/items.rst +++ b/docs/topics/items.rst @@ -15,7 +15,7 @@ purpose. They provide a `dictionary-like`_ API with a convenient syntax for declaring their available fields. -.. _dictionary-like: http://docs.python.org/library/stdtypes.html#dict +.. _dictionary-like: https://docs.python.org/2/library/stdtypes.html#dict .. _topics-items-declaring: @@ -37,8 +37,8 @@ objects. Here is an example:: declared similar to `Django Models`_, except that Scrapy Items are much simpler as there is no concept of different field types. -.. _Django: http://www.djangoproject.com/ -.. _Django Models: http://docs.djangoproject.com/en/dev/topics/db/models/ +.. _Django: https://www.djangoproject.com/ +.. _Django Models: https://docs.djangoproject.com/en/dev/topics/db/models/ .. _topics-items-fields: @@ -214,7 +214,7 @@ Item objects :class:`Field` objects used in the :ref:`Item declaration `. -.. _dict API: http://docs.python.org/library/stdtypes.html#dict +.. _dict API: https://docs.python.org/2/library/stdtypes.html#dict Field objects ============= @@ -227,6 +227,6 @@ Field objects to support the :ref:`item declaration syntax ` based on class attributes. -.. _dict: http://docs.python.org/library/stdtypes.html#dict +.. _dict: https://docs.python.org/2/library/stdtypes.html#dict diff --git a/docs/topics/leaks.rst b/docs/topics/leaks.rst index 372691c8e..21cfcf9bc 100644 --- a/docs/topics/leaks.rst +++ b/docs/topics/leaks.rst @@ -203,7 +203,7 @@ other cases where the memory leaks could come from other (more or less obscure) objects. If this is your case, and you can't find your leaks using ``trackref``, you still have another resource: the `Guppy library`_. -.. _Guppy library: http://pypi.python.org/pypi/guppy +.. _Guppy library: https://pypi.python.org/pypi/guppy If you use ``pip``, you can install Guppy with the following command:: @@ -264,9 +264,9 @@ though neither Scrapy nor your project are leaking memory. This is due to a (not so well) known problem of Python, which may not return released memory to the operating system in some cases. For more information on this issue see: -* `Python Memory Management `_ -* `Python Memory Management Part 2 `_ -* `Python Memory Management Part 3 `_ +* `Python Memory Management `_ +* `Python Memory Management Part 2 `_ +* `Python Memory Management Part 3 `_ The improvements proposed by Evan Jones, which are detailed in `this paper`_, got merged in Python 2.5, but this only reduces the problem, it doesn't fix it @@ -280,7 +280,7 @@ completely. To quote the paper: to move to a compacting garbage collector, which is able to move objects in memory. This would require significant changes to the Python interpreter.* -.. _this paper: http://evanjones.ca/memoryallocator/ +.. _this paper: http://www.evanjones.ca/memoryallocator/ To keep memory consumption reasonable you can split the job into several smaller jobs or enable :ref:`persistent job queue ` diff --git a/docs/topics/logging.rst b/docs/topics/logging.rst index 819884ac2..e9266cd6a 100644 --- a/docs/topics/logging.rst +++ b/docs/topics/logging.rst @@ -8,7 +8,7 @@ Scrapy provides a logging facility which can be used through the :mod:`scrapy.log` module. The current underlying implementation uses `Twisted logging`_ but this may change in the future. -.. _Twisted logging: http://twistedmatrix.com/projects/core/documentation/howto/logging.html +.. _Twisted logging: http://twistedmatrix.com/documents/current/core/howto/logging.html The logging service must be explicitly started through the :func:`scrapy.log.start` function to catch the top level Scrapy's log messages. diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index b6b165d50..302ed4f3b 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -157,7 +157,7 @@ Request objects ``copy()`` or ``replace()`` methods, and can also be accessed, in your spider, from the ``response.meta`` attribute. - .. _shallow copied: http://docs.python.org/library/copy.html + .. _shallow copied: https://docs.python.org/2/library/copy.html .. method:: Request.copy() diff --git a/docs/topics/scrapyd.rst b/docs/topics/scrapyd.rst index 2b7ded9dd..85d27a99e 100644 --- a/docs/topics/scrapyd.rst +++ b/docs/topics/scrapyd.rst @@ -8,4 +8,4 @@ Scrapyd has been moved into a separate project. Its documentation is now hosted at: - http://scrapyd.readthedocs.org/ + http://scrapyd.readthedocs.org/en/latest/ diff --git a/docs/topics/selectors.rst b/docs/topics/selectors.rst index d966a67d2..00ed8152c 100644 --- a/docs/topics/selectors.rst +++ b/docs/topics/selectors.rst @@ -38,7 +38,7 @@ For a complete reference of the selectors API see .. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/ .. _lxml: http://lxml.de/ -.. _ElementTree: http://docs.python.org/library/xml.etree.elementtree.html +.. _ElementTree: https://docs.python.org/2/library/xml.etree.elementtree.html .. _cssselect: https://pypi.python.org/pypi/cssselect/ .. _XPath: http://www.w3.org/TR/xpath .. _CSS: http://www.w3.org/TR/selectors @@ -403,9 +403,9 @@ Here we first iterate over ``itemscope`` elements, and for each one, we look for all ``itemprops`` elements and exclude those that are themselves inside another ``itemscope``. -.. _EXSLT: http://www.exslt.org/ -.. _regular expressions: http://www.exslt.org/regexp/index.html -.. _set manipulation: http://www.exslt.org/set/index.html +.. _EXSLT: http://exslt.org/ +.. _regular expressions: http://exslt.org/regexp/index.html +.. _set manipulation: http://exslt.org/set/index.html Some XPath tips diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index ea2c208bb..5e11e473f 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -26,7 +26,7 @@ The value of ``SCRAPY_SETTINGS_MODULE`` should be in Python path syntax, e.g. ``myproject.settings``. Note that the settings module should be on the Python `import search path`_. -.. _import search path: http://docs.python.org/2/tutorial/modules.html#the-module-search-path +.. _import search path: https://docs.python.org/2/tutorial/modules.html#the-module-search-path Populating the settings ======================= diff --git a/docs/topics/spider-middleware.rst b/docs/topics/spider-middleware.rst index 79cfbcf5d..6f14567fc 100644 --- a/docs/topics/spider-middleware.rst +++ b/docs/topics/spider-middleware.rst @@ -159,7 +159,7 @@ following methods: :type spider: :class:`~scrapy.spider.Spider` object -.. _Exception: http://docs.python.org/library/exceptions.html#exceptions.Exception +.. _Exception: https://docs.python.org/2/library/exceptions.html#exceptions.Exception .. _topics-spider-middleware-ref: diff --git a/docs/topics/spiders.rst b/docs/topics/spiders.rst index cb3f6caeb..a7e7d2746 100644 --- a/docs/topics/spiders.rst +++ b/docs/topics/spiders.rst @@ -706,7 +706,7 @@ Combine SitemapSpider with other sources of urls:: pass # ... scrape other here ... .. _Sitemaps: http://www.sitemaps.org -.. _Sitemap index files: http://www.sitemaps.org/protocol.php#index +.. _Sitemap index files: http://www.sitemaps.org/protocol.html#index .. _robots.txt: http://www.robotstxt.org/ .. _TLD: http://en.wikipedia.org/wiki/Top-level_domain -.. _Scrapyd documentation: http://scrapyd.readthedocs.org/ +.. _Scrapyd documentation: http://scrapyd.readthedocs.org/en/latest/ From c05f5f175e7027752357fa4c931ce4677e7f1c6e Mon Sep 17 00:00:00 2001 From: Shadab Zafar Date: Thu, 12 Mar 2015 06:57:47 +0530 Subject: [PATCH 0117/2974] Added linkfix script to docs/utils https://github.com/scrapy/scrapy/pull/1041#issuecomment-78143576 --- docs/utils/linkfix.py | 63 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100755 docs/utils/linkfix.py diff --git a/docs/utils/linkfix.py b/docs/utils/linkfix.py new file mode 100755 index 000000000..40316968f --- /dev/null +++ b/docs/utils/linkfix.py @@ -0,0 +1,63 @@ +#!/usr/bin/python + +""" + +Linkfix - a companion to sphinx's linkcheck builder. + +Uses the linkcheck's output file to fix links in docs. + +Originally created for this issue: +https://github.com/scrapy/scrapy/issues/606 + +Author: dufferzafar +""" + +import re + +# Used for remembering the file (and its contents) +# so we don't have to open the same file again. +_filename = None +_contents = None + +# A regex that matches standard linkcheck output lines +line_re = re.compile(ur'(.*)\:\d+\:\s\[(.*)\]\s(?:(.*)\sto\s(.*)|(.*))') + +# Read lines from the linkcheck output file +try: + with open("build/linkcheck/output.txt") as out: + output_lines = out.readlines() +except IOError: + print("linkcheck output not found; please run linkcheck first.") + exit(1) + +# For every line, fix the respective file +for line in output_lines: + match = re.match(line_re, line) + + if match: + newfilename = match.group(1) + errortype = match.group(2) + + # Broken links can't be fixed and + # I am not sure what do with the local ones. + if errortype.lower() in ["broken", "local"]: + print("Not Fixed: " + line) + else: + # If this is a new file + if newfilename != _filename: + + # Update the previous file + if _filename: + with open(_filename, "w") as _file: + _file.write(_contents) + + _filename = newfilename + + # Read the new file to memory + with open(_filename) as _file: + _contents = _file.read() + + _contents = _contents.replace(match.group(3), match.group(4)) + else: + # We don't understand what the current line means! + print("Not Understood: " + line) From ff987fb5a571ba91cb4e8bd0472e94579acbf2a2 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Wed, 18 Mar 2015 20:00:03 -0300 Subject: [PATCH 0118/2974] Add linkfix rule to docs Makefile --- docs/Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index c6e4dd64d..4289690f0 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -26,7 +26,7 @@ help: build: mkdir -p build/$(BUILDER) build/doctrees - sphinx-build $(ALLSPHINXOPTS) + -sphinx-build $(ALLSPHINXOPTS) @echo @@ -58,6 +58,11 @@ linkcheck: build @echo "Link check complete; look for any errors in the above output " \ "or in build/$(BUILDER)/output.txt" +linkfix: linkcheck + $(PYTHON) utils/linkfix.py + @echo "Fixing redirecting links in docs has finished; check all " \ + "replacements before committing them" + doctest: BUILDER = doctest doctest: build @echo "Testing of doctests in the sources finished, look at the " \ From 4fb818a250ef48fd2e7708c569dfa6b4474a173e Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Wed, 18 Mar 2015 20:04:14 -0300 Subject: [PATCH 0119/2974] Run linkfix over current docs --- docs/topics/downloader-middleware.rst | 2 +- docs/topics/loaders.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index 5b38f048c..149eef59e 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -376,7 +376,7 @@ what is implemented: what is missing: -* `Pragma: no-cache` support http://www.mnot.net/cache_docs/#PRAGMA +* `Pragma: no-cache` support https://www.mnot.net/cache_docs/#PRAGMA * `Vary` header support http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6 * Invalidation after updates or deletes http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10 * ... probably others .. diff --git a/docs/topics/loaders.rst b/docs/topics/loaders.rst index 9df8e117d..b2c8a018d 100644 --- a/docs/topics/loaders.rst +++ b/docs/topics/loaders.rst @@ -678,7 +678,7 @@ Here is a list of all built-in processors: .. class:: SelectJmes(json_path) Queries the value using the json path provided to the constructor and returns the output. - Requires jmespath (https://github.com/jmespath/jmespath) to run. + Requires jmespath (https://github.com/jmespath/jmespath.py) to run. This processor takes only one input at a time. Example:: From 12eedd90d19fda91f7d2f985ee48f3e338175b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Wed, 18 Mar 2015 20:46:18 -0300 Subject: [PATCH 0120/2974] fix truncated 0.24.5 release notes. closes #1084 --- docs/news.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/news.rst b/docs/news.rst index 383f59760..bb1b374ce 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -10,14 +10,14 @@ Release notes - DOC a couple more references are fixed (:commit:`b4c454b`) - DOC fix a reference (:commit:`e3c1260`) - t.i.b.ThreadedResolver is now a new-style class (:commit:`9e13f42`) -- S3DownloadHandler: fix auth for requests with quoted paths/query params (:commit:`cdb9 +- S3DownloadHandler: fix auth for requests with quoted paths/query params (:commit:`cdb9a0b`) - fixed the variable types in mailsender documentation (:commit:`bb3a848`) - Reset items_scraped instead of item_count (:commit:`edb07a4`) -- Tentative attention message about what document to read for contributions (:commit:`7e +- Tentative attention message about what document to read for contributions (:commit:`7ee6f7a`) - mitmproxy 0.10.1 needs netlib 0.10.1 too (:commit:`874fcdd`) - pin mitmproxy 0.10.1 as >0.11 does not work with tests (:commit:`c6b21f0`) - Test the parse command locally instead of against an external url (:commit:`c3a6628`) -- Patches Twisted issue while closing the connection pool on HTTPDownloadHandler (:commi +- Patches Twisted issue while closing the connection pool on HTTPDownloadHandler (:commit:`d0bf957`) - Updates documentation on dynamic item classes. (:commit:`eeb589a`) - Merge pull request #943 from Lazar-T/patch-3 (:commit:`5fdab02`) - typo (:commit:`b0ae199`) @@ -27,7 +27,7 @@ Release notes - comma instead of fullstop (:commit:`627b9ba`) - Merge pull request #885 from jsma/patch-1 (:commit:`de909ad`) - Update request-response.rst (:commit:`3f3263d`) -- SgmlLinkExtractor - fix for parsing tag with Unicode present (:commit:`49b40f0` +- SgmlLinkExtractor - fix for parsing tag with Unicode present (:commit:`49b40f0`) 0.24.4 (2014-08-09) ------------------- From 6c7bd54fc37d2f21bbbd0927eab2da827f070852 Mon Sep 17 00:00:00 2001 From: Mateusz Golewski Date: Thu, 30 Jan 2014 21:33:46 +0100 Subject: [PATCH 0121/2974] Add extract_first() method to SelectorList --- scrapy/selector/unified.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scrapy/selector/unified.py b/scrapy/selector/unified.py index b8a3678a8..7b877153d 100644 --- a/scrapy/selector/unified.py +++ b/scrapy/selector/unified.py @@ -178,6 +178,10 @@ class SelectorList(list): def extract(self): return [x.extract() for x in self] + def extract_first(self): + for x in self.extract(): + return x + @deprecated(use_instead='.extract()') def extract_unquoted(self): return [x.extract_unquoted() for x in self] From bd126be3569ddd77c458c79ef9e066cacf3a3af1 Mon Sep 17 00:00:00 2001 From: Mateusz Golewski Date: Thu, 30 Jan 2014 21:48:50 +0100 Subject: [PATCH 0122/2974] Optimize extract_first() --- scrapy/selector/unified.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapy/selector/unified.py b/scrapy/selector/unified.py index 7b877153d..3d9435665 100644 --- a/scrapy/selector/unified.py +++ b/scrapy/selector/unified.py @@ -179,8 +179,8 @@ class SelectorList(list): return [x.extract() for x in self] def extract_first(self): - for x in self.extract(): - return x + for x in self: + return x.extract() @deprecated(use_instead='.extract()') def extract_unquoted(self): From 2742b4d8c26f946c20767fd7fd3f227d00002597 Mon Sep 17 00:00:00 2001 From: Mateusz Golewski Date: Thu, 30 Jan 2014 23:10:53 +0100 Subject: [PATCH 0123/2974] Add tests to extract_first() --- tests/test_selector.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_selector.py b/tests/test_selector.py index 6fbb451a6..80a9a4672 100644 --- a/tests/test_selector.py +++ b/tests/test_selector.py @@ -55,6 +55,23 @@ class SelectorTestCase(unittest.TestCase): [""] ) + def test_extract_first(self): + """Test if extract_first() returns first element""" + body = '
  • 1
  • 2
' + response = TextResponse(url="http://example.com", body=body) + sel = self.sscls(response) + + self.assertEqual(sel.xpath('//ul/li/text()').extract_first(), + sel.xpath('//ul/li/text()').extract()[0]) + + self.assertEqual(sel.xpath('//ul/li[@id="1"]/text()').extract_first(), + sel.xpath('//ul/li[@id="1"]/text()').extract()[0]) + + self.assertEqual(sel.xpath('//ul/li[2]/text()').extract_first(), + sel.xpath('//ul/li/text()').extract()[1]) + + self.assertEqual(sel.xpath('/ul/li[@id="doesnt-exist"]/text()').extract_first(), None) + def test_select_unicode_query(self): body = u"

" response = TextResponse(url="http://example.com", body=body, encoding='utf8') From 012211accda0fb5ce3af2d4010e2b51db33bdd02 Mon Sep 17 00:00:00 2001 From: Mateusz Golewski Date: Thu, 30 Jan 2014 23:39:15 +0100 Subject: [PATCH 0124/2974] Add docs for extract_first() --- docs/topics/selectors.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/topics/selectors.rst b/docs/topics/selectors.rst index 00ed8152c..0ce0f084e 100644 --- a/docs/topics/selectors.rst +++ b/docs/topics/selectors.rst @@ -139,6 +139,16 @@ method, as follows:: >>> response.xpath('//title/text()').extract() [u'Example website'] +If you want to extract only first matched element, you must call the selector ``.extract_first()`` + + >>> sel.xpath('//ul/li').extract_first() + u'First list element' + +It returns ``None`` if no element was found: + + >>> sel.xpath('//ul/li[999]').extract_first() + None + Notice that CSS selectors can select text or attribute nodes using CSS3 pseudo-elements:: From 127c6c694a75e1448ddbd3d0f699ca8074c46761 Mon Sep 17 00:00:00 2001 From: Mateusz Golewski Date: Sun, 2 Feb 2014 15:02:25 +0100 Subject: [PATCH 0125/2974] Fix extract_first() docs --- docs/topics/selectors.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/topics/selectors.rst b/docs/topics/selectors.rst index 0ce0f084e..92e092246 100644 --- a/docs/topics/selectors.rst +++ b/docs/topics/selectors.rst @@ -139,15 +139,15 @@ method, as follows:: >>> response.xpath('//title/text()').extract() [u'Example website'] -If you want to extract only first matched element, you must call the selector ``.extract_first()`` +If you want to extract only first matched element, you can call the selector ``.extract_first()`` - >>> sel.xpath('//ul/li').extract_first() - u'First list element' + >>> sel.xpath('//div[@id="images"]/a/text()').extract_first() + u'Name: My image 1 ' It returns ``None`` if no element was found: - >>> sel.xpath('//ul/li[999]').extract_first() - None + >>> sel.xpath('//div/[id="not-exists"]/text()').extract_first() is None + True Notice that CSS selectors can select text or attribute nodes using CSS3 pseudo-elements:: From f92bc09bf433c43ab5669f7bc14108ac6fd49e5c Mon Sep 17 00:00:00 2001 From: Mateusz Golewski Date: Sun, 2 Feb 2014 15:45:43 +0100 Subject: [PATCH 0126/2974] Add re_first() to SelectorList and iflatten() to utils.python --- scrapy/selector/unified.py | 6 +++++- scrapy/utils/python.py | 15 +++++++++++---- tests/test_selector.py | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/scrapy/selector/unified.py b/scrapy/selector/unified.py index 3d9435665..889c349e3 100644 --- a/scrapy/selector/unified.py +++ b/scrapy/selector/unified.py @@ -6,7 +6,7 @@ from lxml import etree from scrapy.utils.misc import extract_regex from scrapy.utils.trackref import object_ref -from scrapy.utils.python import unicode_to_str, flatten +from scrapy.utils.python import unicode_to_str, flatten, iflatten from scrapy.utils.decorator import deprecated from scrapy.http import HtmlResponse, XmlResponse from .lxmldocument import LxmlDocument @@ -175,6 +175,10 @@ class SelectorList(list): def re(self, regex): return flatten([x.re(regex) for x in self]) + def re_first(self, regex): + for el in iflatten((x.re(regex) for x in self)): + return el + def extract(self): return [x.extract() for x in self] diff --git a/scrapy/utils/python.py b/scrapy/utils/python.py index 551d337eb..b6100f899 100644 --- a/scrapy/utils/python.py +++ b/scrapy/utils/python.py @@ -27,13 +27,20 @@ def flatten(x): >>> flatten([[[1,2,3], (42,None)], [4,5], [6], 7, (8,9,10)]) [1, 2, 3, 42, None, 4, 5, 6, 7, 8, 9, 10]""" - result = [] + return list(iflatten(x)) + + +def iflatten(x): + """iflatten(sequence) -> iterator + + Similar to ``.flatten()``, but returns iterator instead""" + for el in x: if hasattr(el, "__iter__"): - result.extend(flatten(el)) + for el_ in flatten(el): + yield el_ else: - result.append(el) - return result + yield el def unique(list_, key=lambda x: x): diff --git a/tests/test_selector.py b/tests/test_selector.py index 80a9a4672..9b8613319 100644 --- a/tests/test_selector.py +++ b/tests/test_selector.py @@ -72,6 +72,24 @@ class SelectorTestCase(unittest.TestCase): self.assertEqual(sel.xpath('/ul/li[@id="doesnt-exist"]/text()').extract_first(), None) + def test_re_first(self): + """Test if re_first() returns first matched element""" + body = '
  • 1
  • 2
' + response = TextResponse(url="http://example.com", body=body) + sel = self.sscls(response) + + self.assertEqual(sel.xpath('//ul/li/text()').re_first('\d'), + sel.xpath('//ul/li/text()').re('\d')[0]) + + self.assertEqual(sel.xpath('//ul/li[@id="1"]/text()').re_first('\d'), + sel.xpath('//ul/li[@id="1"]/text()').re('\d')[0]) + + self.assertEqual(sel.xpath('//ul/li[2]/text()').re_first('\d'), + sel.xpath('//ul/li/text()').re('\d')[1]) + + self.assertEqual(sel.xpath('/ul/li/text()').re_first('\w+'), None) + self.assertEqual(sel.xpath('/ul/li[@id="doesnt-exist"]/text()').re_first('\d'), None) + def test_select_unicode_query(self): body = u"

" response = TextResponse(url="http://example.com", body=body, encoding='utf8') From 0dade7315bc59ce6b2cfacdd17895985882f5ae1 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Wed, 18 Mar 2015 20:50:17 -0300 Subject: [PATCH 0127/2974] Use generator sintax in re_first --- scrapy/selector/unified.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/selector/unified.py b/scrapy/selector/unified.py index 889c349e3..c0eefb85e 100644 --- a/scrapy/selector/unified.py +++ b/scrapy/selector/unified.py @@ -176,7 +176,7 @@ class SelectorList(list): return flatten([x.re(regex) for x in self]) def re_first(self, regex): - for el in iflatten((x.re(regex) for x in self)): + for el in iflatten(x.re(regex) for x in self): return el def extract(self): From 959aaad20554f1ad89704229594d2efb7a835bd3 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Wed, 18 Mar 2015 21:04:15 -0300 Subject: [PATCH 0128/2974] Document `re_first` --- docs/topics/selectors.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/topics/selectors.rst b/docs/topics/selectors.rst index 92e092246..33958cee5 100644 --- a/docs/topics/selectors.rst +++ b/docs/topics/selectors.rst @@ -236,6 +236,12 @@ Here's an example used to extract images names from the :ref:`HTML code u'My image 4', u'My image 5'] +There's an additional helper reciprocating ``.extract_first()`` for ``.re()``, +named ``.re_first()``. Use it to extract just the first matching string:: + + >>> response.xpath('//a[contains(@href, "image")]/text()').re_first(r'Name:\s*(.*)') + u'My image 1' + .. _topics-selectors-relative-xpaths: Working with relative XPaths From 817dbc6cbd04d1ee8644ccc22d1b109afcf5a892 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Thu, 19 Mar 2015 05:16:14 +0500 Subject: [PATCH 0129/2974] DOC mention dicts in documentation; explain better what are Items for --- docs/topics/architecture.rst | 4 ++-- docs/topics/exporters.rst | 13 +++++++------ docs/topics/images.rst | 13 +++++++++---- docs/topics/item-pipeline.rst | 15 ++++++++------- docs/topics/items.rst | 17 ++++++++++++----- docs/topics/practices.rst | 7 +++---- docs/topics/signals.rst | 4 ++-- docs/topics/spider-middleware.rst | 9 +++++---- 8 files changed, 48 insertions(+), 34 deletions(-) diff --git a/docs/topics/architecture.rst b/docs/topics/architecture.rst index 80ccd42dd..55df1cc98 100644 --- a/docs/topics/architecture.rst +++ b/docs/topics/architecture.rst @@ -102,10 +102,10 @@ this: 6. The Engine receives the Response from the Downloader and sends it to the Spider for processing, passing through the Spider Middleware (input direction). -7. The Spider processes the Response and returns scraped Items and new Requests +7. The Spider processes the Response and returns scraped items and new Requests (to follow) to the Engine. -8. The Engine sends scraped Items (returned by the Spider) to the Item Pipeline +8. The Engine sends scraped items (returned by the Spider) to the Item Pipeline and Requests (returned by spider) to the Scheduler 9. The process repeats (from step 2) until there are no more requests from the diff --git a/docs/topics/exporters.rst b/docs/topics/exporters.rst index 43931544f..7455b28da 100644 --- a/docs/topics/exporters.rst +++ b/docs/topics/exporters.rst @@ -7,7 +7,7 @@ Item Exporters .. module:: scrapy.contrib.exporter :synopsis: Item Exporters -Once you have scraped your Items, you often want to persist or export those +Once you have scraped your items, you often want to persist or export those items, to use the data in some other application. That is, after all, the whole purpose of the scraping process. @@ -90,9 +90,9 @@ described next. 1. Declaring a serializer in the field -------------------------------------- -You can declare a serializer in the :ref:`field metadata -`. The serializer must be a callable which receives a -value and returns its serialized form. +If you use :class:`~.Item` you can declare a serializer in the +:ref:`field metadata `. The serializer must be +a callable which receives a value and returns its serialized form. Example:: @@ -167,8 +167,9 @@ BaseItemExporter value unchanged except for ``unicode`` values which are encoded to ``str`` using the encoding declared in the :attr:`encoding` attribute. - :param field: the field being serialized - :type field: :class:`~scrapy.item.Field` object + :param field: the field being serialized. If a raw dict is being + exported (not :class:`~.Item`) *field* value is an empty dict. + :type field: :class:`~scrapy.item.Field` object or an empty dict :param name: the name of the field being serialized :type name: str diff --git a/docs/topics/images.rst b/docs/topics/images.rst index 91e59d2a9..2cbff2e84 100644 --- a/docs/topics/images.rst +++ b/docs/topics/images.rst @@ -63,9 +63,14 @@ this: Usage example ============= -In order to use the image pipeline you just need to :ref:`enable it -` and define an item with the ``image_urls`` and -``images`` fields:: +In order to use the image pipeline first +:ref:`enable it `. + +Then, if a spider returns a dict with 'image_urls' key, +the pipeline will put the results under 'images' key. + +If you prefer to use :class:`~.Item` then define a custom +item with the ``image_urls`` and ``images`` fields:: import scrapy @@ -74,7 +79,7 @@ In order to use the image pipeline you just need to :ref:`enable it # ... other item fields ... image_urls = scrapy.Field() images = scrapy.Field() - + If you need something more complex and want to override the custom images pipeline behaviour, see :ref:`topics-images-override`. diff --git a/docs/topics/item-pipeline.rst b/docs/topics/item-pipeline.rst index 7b66753b8..973c77516 100644 --- a/docs/topics/item-pipeline.rst +++ b/docs/topics/item-pipeline.rst @@ -8,8 +8,8 @@ After an item has been scraped by a spider, it is sent to the Item Pipeline which process it through several components that are executed sequentially. Each item pipeline component (sometimes referred as just "Item Pipeline") is a -Python class that implements a simple method. They receive an Item and perform -an action over it, also deciding if the Item should continue through the +Python class that implements a simple method. They receive an item and perform +an action over it, also deciding if the item should continue through the pipeline or be dropped and no longer processed. Typical use for item pipelines are: @@ -28,12 +28,12 @@ Each item pipeline component is a Python class that must implement the following .. method:: process_item(self, item, spider) This method is called for every item pipeline component and must either return - a :class:`~scrapy.item.Item` (or any descendant class) object or raise a - :exc:`~scrapy.exceptions.DropItem` exception. Dropped items are no longer + a dict with data, :class:`~scrapy.item.Item` (or any descendant class) object + or raise a :exc:`~scrapy.exceptions.DropItem` exception. Dropped items are no longer processed by further pipeline components. :param item: the item scraped - :type item: :class:`~scrapy.item.Item` object + :type item: :class:`~scrapy.item.Item` object or a dict :param spider: the spider which scraped the item :type spider: :class:`~scrapy.spider.Spider` object @@ -135,6 +135,8 @@ method and how to clean up the resources properly. import pymongo class MongoPipeline(object): + + collection_name = 'scrapy_items' def __init__(self, mongo_uri, mongo_db): self.mongo_uri = mongo_uri @@ -155,8 +157,7 @@ method and how to clean up the resources properly. self.client.close() def process_item(self, item, spider): - collection_name = item.__class__.__name__ - self.db[collection_name].insert(dict(item)) + self.db[self.collection_name].insert(dict(item)) return item .. _MongoDB: http://www.mongodb.org/ diff --git a/docs/topics/items.rst b/docs/topics/items.rst index ee604a7f1..3fda20494 100644 --- a/docs/topics/items.rst +++ b/docs/topics/items.rst @@ -8,12 +8,21 @@ Items :synopsis: Item and Field classes The main goal in scraping is to extract structured data from unstructured -sources, typically, web pages. Scrapy provides the :class:`Item` class for this -purpose. +sources, typically, web pages. Scrapy spiders can return the extracted data +as Python dicts. While convenient and familiar, Python dicts lack structure: +it is easy to make a typo in a field name or return inconsistent data, +especially in a larger project with many spiders. +To define common output data format Scrapy provides the :class:`Item` class. :class:`Item` objects are simple containers used to collect the scraped data. They provide a `dictionary-like`_ API with a convenient syntax for declaring -their available fields. +their available fields. + +Various Scrapy components use extra information provided by Items: +exporters look at declared fields to figure out columns to export, +serialization can be customized using Item fields metadata, :mod:`trackref` +tracks Item instances to help finding memory leaks +(see :ref:`topics-leaks-trackrefs`_), etc. .. _dictionary-like: http://docs.python.org/library/stdtypes.html#dict @@ -64,8 +73,6 @@ It's important to note that the :class:`Field` objects used to declare the item do not stay assigned as class attributes. Instead, they can be accessed through the :attr:`Item.fields` attribute. -And that's all you need to know about declaring items. - Working with Items ================== diff --git a/docs/topics/practices.rst b/docs/topics/practices.rst index 13dde52a3..9e65c07be 100644 --- a/docs/topics/practices.rst +++ b/docs/topics/practices.rst @@ -190,11 +190,10 @@ Dynamic Creation of Item Classes ================================ For applications in which the structure of item class is to be determined by -user input, or other changing conditions, you can dynamically create item -classes instead of manually coding them. - -:: +user input or other changing conditions you can return regular Python +dicts from spiders. +Another option is to dynamically create Item classes:: from scrapy.item import DictItem, Field diff --git a/docs/topics/signals.rst b/docs/topics/signals.rst index 405b131ed..85cf43c76 100644 --- a/docs/topics/signals.rst +++ b/docs/topics/signals.rst @@ -71,7 +71,7 @@ item_scraped This signal supports returning deferreds from their handlers. :param item: the item scraped - :type item: :class:`~scrapy.item.Item` object + :type item: dict or :class:`~scrapy.item.Item` object :param spider: the spider which scraped the item :type spider: :class:`~scrapy.spider.Spider` object @@ -91,7 +91,7 @@ item_dropped This signal supports returning deferreds from their handlers. :param item: the item dropped from the :ref:`topics-item-pipeline` - :type item: :class:`~scrapy.item.Item` object + :type item: dict or :class:`~scrapy.item.Item` object :param spider: the spider which scraped the item :type spider: :class:`~scrapy.spider.Spider` object diff --git a/docs/topics/spider-middleware.rst b/docs/topics/spider-middleware.rst index 79cfbcf5d..0104ae552 100644 --- a/docs/topics/spider-middleware.rst +++ b/docs/topics/spider-middleware.rst @@ -90,15 +90,16 @@ following methods: it has processed the response. :meth:`process_spider_output` must return an iterable of - :class:`~scrapy.http.Request` or :class:`~scrapy.item.Item` objects. + :class:`~scrapy.http.Request`, dict or :class:`~scrapy.item.Item` + objects. :param response: the response which generated this output from the spider :type response: :class:`~scrapy.http.Response` object :param result: the result returned by the spider - :type result: an iterable of :class:`~scrapy.http.Request` or - :class:`~scrapy.item.Item` objects + :type result: an iterable of :class:`~scrapy.http.Request`, dict + or :class:`~scrapy.item.Item` objects :param spider: the spider whose result is being processed :type spider: :class:`~scrapy.spider.Spider` object @@ -110,7 +111,7 @@ following methods: method (from other spider middleware) raises an exception. :meth:`process_spider_exception` should return either ``None`` or an - iterable of :class:`~scrapy.http.Response` or + iterable of :class:`~scrapy.http.Response`, dict or :class:`~scrapy.item.Item` objects. If it returns ``None``, Scrapy will continue processing this exception, From f16a33f34e8226a38a2990c0609a8ad15a8cf011 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Thu, 19 Mar 2015 05:25:15 +0500 Subject: [PATCH 0130/2974] DOC change structure of spider docs: * start with scrapy.Spider, then mention spider arguments, then describe generic spiders; * change wording regarding start_urls/start_requests; * show an example of start_requests vs start_urls; * show an example of dicts as items; * as defining Item is an optional step now, docs for Items are moved below Spider docs. --- docs/index.rst | 8 +- docs/topics/spiders.rst | 171 +++++++++++++++++++++++----------------- 2 files changed, 102 insertions(+), 77 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 0384dae3d..0474cd14b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -56,9 +56,9 @@ Basic concepts :hidden: topics/commands - topics/items topics/spiders topics/selectors + topics/items topics/loaders topics/shell topics/item-pipeline @@ -72,9 +72,6 @@ Basic concepts :doc:`topics/commands` Learn about the command-line tool used to manage your Scrapy project. -:doc:`topics/items` - Define the data you want to scrape. - :doc:`topics/spiders` Write the rules to crawl your websites. @@ -84,6 +81,9 @@ Basic concepts :doc:`topics/shell` Test your extraction code in an interactive environment. +:doc:`topics/items` + Define the data you want to scrape. + :doc:`topics/loaders` Populate your items with the extracted data. diff --git a/docs/topics/spiders.rst b/docs/topics/spiders.rst index cb3f6caeb..036c4e744 100644 --- a/docs/topics/spiders.rst +++ b/docs/topics/spiders.rst @@ -24,8 +24,9 @@ For spiders, the scraping cycle goes through something like this: Requests. 2. In the callback function, you parse the response (web page) and return either - :class:`~scrapy.item.Item` objects, :class:`~scrapy.http.Request` objects, - or an iterable of both. Those Requests will also contain a callback (maybe + dicts with extracted data, :class:`~scrapy.item.Item` objects, + :class:`~scrapy.http.Request` objects, or an iterable of these objects. + Those Requests will also contain a callback (maybe the same) and will then be downloaded by Scrapy and then their response handled by the specified callback. @@ -41,70 +42,19 @@ Even though this cycle applies (more or less) to any kind of spider, there are different kinds of default spiders bundled into Scrapy for different purposes. We will talk about those types here. -.. _spiderargs: - -Spider arguments -================ - -Spiders can receive arguments that modify their behaviour. Some common uses for -spider arguments are to define the start URLs or to restrict the crawl to -certain sections of the site, but they can be used to configure any -functionality of the spider. - -Spider arguments are passed through the :command:`crawl` command using the -``-a`` option. For example:: - - scrapy crawl myspider -a category=electronics - -Spiders receive arguments in their constructors:: - - import scrapy - - class MySpider(scrapy.Spider): - name = 'myspider' - - def __init__(self, category=None, *args, **kwargs): - super(MySpider, self).__init__(*args, **kwargs) - self.start_urls = ['http://www.example.com/categories/%s' % category] - # ... - -Spider arguments can also be passed through the Scrapyd ``schedule.json`` API. -See `Scrapyd documentation`_. - .. _topics-spiders-ref: -Built-in spiders reference -========================== - -Scrapy comes with some useful generic spiders that you can use, to subclass -your spiders from. Their aim is to provide convenient functionality for a few -common scraping cases, like following all links on a site based on certain -rules, crawling from `Sitemaps`_, or parsing a XML/CSV feed. - -For the examples used in the following spiders, we'll assume you have a project -with a ``TestItem`` declared in a ``myproject.items`` module:: - - import scrapy - - class TestItem(scrapy.Item): - id = scrapy.Field() - name = scrapy.Field() - description = scrapy.Field() - - -.. module:: scrapy.spider - :synopsis: Spiders base class, spider manager and spider middleware - -Spider ------- +scrapy.Spider +============= .. class:: Spider() This is the simplest spider, and the one from which every other spider must inherit from (either the ones that come bundled with Scrapy, or the ones that you write yourself). It doesn't provide any special functionality. It just - requests the given ``start_urls``/``start_requests``, and calls the spider's - method ``parse`` for each of the resulting responses. + provides a default :meth:`start_requests` implementation which sends requests from + the :attr:`start_urls` spider attribute and calls the spider's method ``parse`` + for each of the resulting responses. .. attribute:: name @@ -198,15 +148,18 @@ Spider the method to override. For example, if you need to start by logging in using a POST request, you could do:: - def start_requests(self): - return [scrapy.FormRequest("http://www.example.com/login", - formdata={'user': 'john', 'pass': 'secret'}, - callback=self.logged_in)] + class MySpider(scrapy.Spider): + name = 'myspider' + + def start_requests(self): + return [scrapy.FormRequest("http://www.example.com/login", + formdata={'user': 'john', 'pass': 'secret'}, + callback=self.logged_in)] - def logged_in(self, response): - # here you would extract links to follow and return Requests for - # each of them, with another callback - pass + def logged_in(self, response): + # here you would extract links to follow and return Requests for + # each of them, with another callback + pass .. method:: make_requests_from_url(url) @@ -231,7 +184,7 @@ Spider This method, as well as any other Request callback, must return an iterable of :class:`~scrapy.http.Request` and/or - :class:`~scrapy.item.Item` objects. + dicts or :class:`~scrapy.item.Item` objects. :param response: the response to parse :type response: :class:~scrapy.http.Response` @@ -247,10 +200,6 @@ Spider Called when the spider closes. This method provides a shortcut to signals.connect() for the :signal:`spider_closed` signal. - -Spider example -~~~~~~~~~~~~~~ - Let's see an example:: import scrapy @@ -268,10 +217,9 @@ Let's see an example:: def parse(self, response): self.log('A response from %s just arrived!' % response.url) -Another example returning multiple Requests and Items from a single callback:: +Return multiple Requests and items from a single callback:: import scrapy - from myproject.items import MyItem class MySpider(scrapy.Spider): name = 'example.com' @@ -282,12 +230,89 @@ Another example returning multiple Requests and Items from a single callback:: 'http://www.example.com/3.html', ] + def parse(self, response): + for h3 in response.xpath('//h3').extract(): + yield {"title": h3} + + for url in response.xpath('//a/@href').extract(): + yield scrapy.Request(url, callback=self.parse) + +Instead of :attr:`~.start_urls` you can use :meth:`~.start_requests` directly; +to give data more structure you can use :ref:`topics-items`:: + + import scrapy + from myproject.items import MyItem + + class MySpider(scrapy.Spider): + name = 'example.com' + allowed_domains = ['example.com'] + + def start_requests(self): + yield scrapy.Request('http://www.example.com/1.html', self.parse) + yield scrapy.Request('http://www.example.com/2.html', self.parse) + yield scrapy.Request('http://www.example.com/3.html', self.parse) + def parse(self, response): for h3 in response.xpath('//h3').extract(): yield MyItem(title=h3) for url in response.xpath('//a/@href').extract(): yield scrapy.Request(url, callback=self.parse) + +.. _spiderargs: + +Spider arguments +================ + +Spiders can receive arguments that modify their behaviour. Some common uses for +spider arguments are to define the start URLs or to restrict the crawl to +certain sections of the site, but they can be used to configure any +functionality of the spider. + +Spider arguments are passed through the :command:`crawl` command using the +``-a`` option. For example:: + + scrapy crawl myspider -a category=electronics + +Spiders receive arguments in their constructors:: + + import scrapy + + class MySpider(scrapy.Spider): + name = 'myspider' + + def __init__(self, category=None, *args, **kwargs): + super(MySpider, self).__init__(*args, **kwargs) + self.start_urls = ['http://www.example.com/categories/%s' % category] + # ... + +Spider arguments can also be passed through the Scrapyd ``schedule.json`` API. +See `Scrapyd documentation`_. + +.. _builtin-spiders: + +Generic Spiders +=============== + +Scrapy comes with some useful generic spiders that you can use, to subclass +your spiders from. Their aim is to provide convenient functionality for a few +common scraping cases, like following all links on a site based on certain +rules, crawling from `Sitemaps`_, or parsing a XML/CSV feed. + +For the examples used in the following spiders, we'll assume you have a project +with a ``TestItem`` declared in a ``myproject.items`` module:: + + import scrapy + + class TestItem(scrapy.Item): + id = scrapy.Field() + name = scrapy.Field() + description = scrapy.Field() + + +.. module:: scrapy.spider + :synopsis: Spiders base class, spider manager and spider middleware + .. module:: scrapy.contrib.spiders :synopsis: Collection of generic spiders From 643984e1b4b573db737af161f1f4975a5af712fa Mon Sep 17 00:00:00 2001 From: Faisal Anees Date: Sun, 23 Mar 2014 01:00:37 +0530 Subject: [PATCH 0131/2974] Updated architecture.rst Added http://krondo.com/blog/?page_id=1327 as a resource --- docs/topics/architecture.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/architecture.rst b/docs/topics/architecture.rst index 80ccd42dd..700e6d92d 100644 --- a/docs/topics/architecture.rst +++ b/docs/topics/architecture.rst @@ -123,8 +123,10 @@ links: * `Introduction to Deferreds in Twisted`_ * `Twisted - hello, asynchronous programming`_ +* `Twisted Introduction - Krondo`_ .. _Twisted: http://twistedmatrix.com/trac/ .. _Introduction to Deferreds in Twisted: http://twistedmatrix.com/documents/current/core/howto/defer-intro.html .. _Twisted - hello, asynchronous programming: http://jessenoller.com/2009/02/11/twisted-hello-asynchronous-programming/ +.. _Twisted Introduction - Krondo: http://krondo.com/blog/?page_id=1327/ From 8ac397670f319c541e9bfe25db0de34215f633c7 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Thu, 19 Mar 2015 21:41:36 +0500 Subject: [PATCH 0132/2974] DOC move .. module: declaration to a proper place --- docs/topics/spiders.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/topics/spiders.rst b/docs/topics/spiders.rst index 036c4e744..2985a7a89 100644 --- a/docs/topics/spiders.rst +++ b/docs/topics/spiders.rst @@ -42,6 +42,9 @@ Even though this cycle applies (more or less) to any kind of spider, there are different kinds of default spiders bundled into Scrapy for different purposes. We will talk about those types here. +.. module:: scrapy.spider + :synopsis: Spiders base class, spider manager and spider middleware + .. _topics-spiders-ref: scrapy.Spider @@ -310,10 +313,6 @@ with a ``TestItem`` declared in a ``myproject.items`` module:: description = scrapy.Field() -.. module:: scrapy.spider - :synopsis: Spiders base class, spider manager and spider middleware - - .. module:: scrapy.contrib.spiders :synopsis: Collection of generic spiders From d7cb2b9a918dcf4cf4594a05c0c1a3b88fdca413 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Thu, 19 Mar 2015 15:41:43 -0300 Subject: [PATCH 0133/2974] making commented code indentation consistent --- .../templates/project/module/settings.py.tmpl | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/scrapy/templates/project/module/settings.py.tmpl b/scrapy/templates/project/module/settings.py.tmpl index 302d96b17..37cbb4d32 100644 --- a/scrapy/templates/project/module/settings.py.tmpl +++ b/scrapy/templates/project/module/settings.py.tmpl @@ -19,7 +19,7 @@ NEWSPIDER_MODULE = '$project_name.spiders' #USER_AGENT = '$project_name (+http://www.yourdomain.com)' # Configure maximum concurrent requests performed by Scrapy (default: 16) -# CONCURRENT_REQUESTS=32 +#CONCURRENT_REQUESTS=32 # Configure a delay for requests for the same website (default: 0) # See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay @@ -30,40 +30,40 @@ NEWSPIDER_MODULE = '$project_name.spiders' #CONCURRENT_REQUESTS_PER_IP=16 # Disable cookies (enabled by default) -# COOKIES_ENABLED=False +#COOKIES_ENABLED=False # Disable Telnet Console (enabled by default) -# TELNETCONSOLE_ENABLED=False +#TELNETCONSOLE_ENABLED=False # Override the default request headers: -# DEFAULT_REQUEST_HEADERS = { -# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', -# 'Accept-Language': 'en', -# } +#DEFAULT_REQUEST_HEADERS = { +# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', +# 'Accept-Language': 'en', +#} # Enable or disable spider middlewares # See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html -# SPIDER_MIDDLEWARES = { -# '$project_name.middlewares.MyCustomSpiderMiddleware': 543, -# } +#SPIDER_MIDDLEWARES = { +# '$project_name.middlewares.MyCustomSpiderMiddleware': 543, +#} # Enable or disable downloader middlewares # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html -# DOWNLOADER_MIDDLEWARES = { -# '$project_name.middlewares.MyCustomDownloaderMiddleware': 543, -# } +#DOWNLOADER_MIDDLEWARES = { +# '$project_name.middlewares.MyCustomDownloaderMiddleware': 543, +#} # Enable or disable extensions # See http://scrapy.readthedocs.org/en/latest/topics/extensions.html -# EXTENSIONS = { -# 'scrapy.telnet.TelnetConsole': None, -# } +#EXTENSIONS = { +# 'scrapy.telnet.TelnetConsole': None, +#} # Configure item pipelines # See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html -# ITEM_PIPELINES = { -# '$project_name.pipelines.SomePipeline': 300, -# } +#ITEM_PIPELINES = { +# '$project_name.pipelines.SomePipeline': 300, +#} # Enable and configure the AutoThrottle extension (disabled by default) # See http://doc.scrapy.org/en/latest/topics/autothrottle.html From c81eefaf8148af70ca9214a0a3aa776eeb9b436c Mon Sep 17 00:00:00 2001 From: Pablo Hoffman Date: Thu, 19 Mar 2015 17:42:48 -0300 Subject: [PATCH 0134/2974] fix doc links --- docs/topics/downloader-middleware.rst | 2 +- docs/topics/telnetconsole.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index 149eef59e..df679daea 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -376,7 +376,7 @@ what is implemented: what is missing: -* `Pragma: no-cache` support https://www.mnot.net/cache_docs/#PRAGMA +* `Pragma: no-cache` support http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1 * `Vary` header support http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6 * Invalidation after updates or deletes http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10 * ... probably others .. diff --git a/docs/topics/telnetconsole.rst b/docs/topics/telnetconsole.rst index 267e6e1d8..b1ff88771 100644 --- a/docs/topics/telnetconsole.rst +++ b/docs/topics/telnetconsole.rst @@ -66,7 +66,7 @@ convenience: | ``hpy`` | for memory debugging (see :ref:`topics-leaks`) | +----------------+-------------------------------------------------------------------+ -.. _pprint.pprint: http://docs.python.org/library/pprint.html#pprint.pprint +.. _pprint.pprint: https://docs.python.org/library/pprint.html#pprint.pprint Telnet console usage examples ============================= From cda39225073ac06ef5563b7a0fd8f8a09194ad69 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 19 Mar 2015 16:59:52 -0300 Subject: [PATCH 0135/2974] Add Response.urljoin() helper --- docs/topics/request-response.rst | 12 ++++++++++++ scrapy/http/response/__init__.py | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/docs/topics/request-response.rst b/docs/topics/request-response.rst index 302ed4f3b..5b4ced992 100644 --- a/docs/topics/request-response.rst +++ b/docs/topics/request-response.rst @@ -493,6 +493,18 @@ Response objects given new values by whichever keyword arguments are specified. The attribute :attr:`Response.meta` is copied by default. + .. method:: Response.urljoin(url) + + Constructs an absolute url by combining the Response's :attr:`url` with + a possible relative url. + + This is a wrapper over `urlparse.urljoin`_, it's merely an alias for + making this call:: + + urlparse.urljoin(response.url, url) + +.. _urlparse.urljoin: https://docs.python.org/2/library/urlparse.html#urlparse.urljoin + .. _topics-request-response-ref-response-subclasses: Response subclasses diff --git a/scrapy/http/response/__init__.py b/scrapy/http/response/__init__.py index 7ff683eb6..e138d5cc8 100644 --- a/scrapy/http/response/__init__.py +++ b/scrapy/http/response/__init__.py @@ -7,6 +7,8 @@ See documentation in docs/topics/request-response.rst import copy +from six.moves.urllib.parse import urljoin + from scrapy.http.headers import Headers from scrapy.utils.trackref import object_ref from scrapy.http.common import obsolete_setter @@ -75,3 +77,8 @@ class Response(object_ref): kwargs.setdefault(x, getattr(self, x)) cls = kwargs.pop('cls', self.__class__) return cls(*args, **kwargs) + + def urljoin(self, url): + """Join this Response's url with a possible relative url to form an + absolute interpretation of the latter.""" + return urljoin(self.url, url) From 1b6d5a011a5485f3f2c01b32fa425b512d90d7ef Mon Sep 17 00:00:00 2001 From: drack3800 Date: Sat, 21 Mar 2015 04:02:51 +0300 Subject: [PATCH 0136/2974] Added webclient test for checking Content-Length header in response for POST request with no given body --- tests/test_webclient.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_webclient.py b/tests/test_webclient.py index a16eb1ccf..e0b46286a 100644 --- a/tests/test_webclient.py +++ b/tests/test_webclient.py @@ -132,6 +132,18 @@ class ScrapyHTTPPageGetterTests(unittest.TestCase): "\r\n" "name=value") + # test a POST method with no body provided + factory = client.ScrapyHTTPClientFactory(Request( + method='POST', + url='http://foo/bar' + )) + + self._test(factory, + "POST /bar HTTP/1.0\r\n" + "Host: foo\r\n" + "Content-Length: 0\r\n" + "\r\n") + # test with single and multivalued headers factory = client.ScrapyHTTPClientFactory(Request( url='http://foo/bar', From deb5bb530cfe14993f92d3031820153ae53e0edb Mon Sep 17 00:00:00 2001 From: drack3800 Date: Sun, 22 Mar 2015 19:25:08 +0300 Subject: [PATCH 0137/2974] Fixed bug with no specified Content-Length header by ScrapyHTTPClientFactory for POST request with no given body --- scrapy/core/downloader/webclient.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scrapy/core/downloader/webclient.py b/scrapy/core/downloader/webclient.py index 2c6a61b8a..93ab8a391 100644 --- a/scrapy/core/downloader/webclient.py +++ b/scrapy/core/downloader/webclient.py @@ -112,12 +112,14 @@ class ScrapyHTTPClientFactory(HTTPClientFactory): # set Host header based on url self.headers.setdefault('Host', self.netloc) - # set Content-Length based len of body if self.body is not None: self.headers['Content-Length'] = len(self.body) # just in case a broken http/1.1 decides to keep connection alive self.headers.setdefault("Connection", "close") + # Content-Length must be specified in POST method even with no body + elif self.method == 'POST': + self.headers['Content-Length'] = 0 def _build_response(self, body, request): request.meta['download_latency'] = self.headers_time-self.start_time From 549882590bfdb3d66b623ea7414fac5097560a96 Mon Sep 17 00:00:00 2001 From: drack3800 Date: Sun, 22 Mar 2015 19:25:08 +0300 Subject: [PATCH 0138/2974] Fixed bug with no specified Content-Length header by ScrapyHTTPClientFactory for POST request with no given body --- scrapy/core/downloader/webclient.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scrapy/core/downloader/webclient.py b/scrapy/core/downloader/webclient.py index 2c6a61b8a..22c461b15 100644 --- a/scrapy/core/downloader/webclient.py +++ b/scrapy/core/downloader/webclient.py @@ -112,12 +112,15 @@ class ScrapyHTTPClientFactory(HTTPClientFactory): # set Host header based on url self.headers.setdefault('Host', self.netloc) - + # set Content-Length based len of body if self.body is not None: self.headers['Content-Length'] = len(self.body) # just in case a broken http/1.1 decides to keep connection alive self.headers.setdefault("Connection", "close") + # Content-Length must be specified in POST method even with no body + elif self.method == 'POST': + self.headers['Content-Length'] = 0 def _build_response(self, body, request): request.meta['download_latency'] = self.headers_time-self.start_time From 5ac91e488339c71c9fef9cb41a320e6ee9ed8278 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Mon, 23 Mar 2015 18:11:35 +0500 Subject: [PATCH 0139/2974] DOC remove Dynamic Creation of Item Classes section It was a hack, and dicts-as-items cover most use cases. Dicts don't allow to attach metadata to fields, but e.g. adding "_meta" key and removing it in a custom serializer is no worse than creating classes dynamically. --- docs/topics/practices.rst | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/docs/topics/practices.rst b/docs/topics/practices.rst index 9e65c07be..3ec7bc29b 100644 --- a/docs/topics/practices.rst +++ b/docs/topics/practices.rst @@ -183,21 +183,3 @@ If you are still unable to prevent your bot getting banned, consider contacting .. _testspiders: https://github.com/scrapinghub/testspiders .. _Twisted Reactor Overview: http://twistedmatrix.com/documents/current/core/howto/reactor-basics.html .. _Crawlera: http://crawlera.com - -.. _dynamic-item-classes: - -Dynamic Creation of Item Classes -================================ - -For applications in which the structure of item class is to be determined by -user input or other changing conditions you can return regular Python -dicts from spiders. - -Another option is to dynamically create Item classes:: - - from scrapy.item import DictItem, Field - - def create_item_class(class_name, field_list): - fields = {field_name: Field() for field_name in field_list} - - return type(class_name, (DictItem,), {'fields': fields}) From aaeb837db409442579d260f703e2f2ca705020ca Mon Sep 17 00:00:00 2001 From: nyov Date: Tue, 24 Mar 2015 07:11:48 +0000 Subject: [PATCH 0140/2974] handle TLS SNI if we have twisted>=14.0 (closes #981, #1101) --- scrapy/core/downloader/contextfactory.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scrapy/core/downloader/contextfactory.py b/scrapy/core/downloader/contextfactory.py index e20830c71..0b39b89d8 100644 --- a/scrapy/core/downloader/contextfactory.py +++ b/scrapy/core/downloader/contextfactory.py @@ -1,11 +1,17 @@ from OpenSSL import SSL from twisted.internet.ssl import ClientContextFactory +try: + # available since twisted 14.0 + from twisted.internet._sslverify import ClientTLSOptions +except ImportError: + ClientTLSOptions = None class ScrapyClientContextFactory(ClientContextFactory): "A SSL context factory which is more permissive against SSL bugs." # see https://github.com/scrapy/scrapy/issues/82 # and https://github.com/scrapy/scrapy/issues/26 + # and https://github.com/scrapy/scrapy/issues/981 def __init__(self): # see this issue on why we use TLSv1_METHOD by default @@ -17,4 +23,6 @@ class ScrapyClientContextFactory(ClientContextFactory): # Enable all workarounds to SSL bugs as documented by # http://www.openssl.org/docs/ssl/SSL_CTX_set_options.html ctx.set_options(SSL.OP_ALL) + if hostname and ClientTLSOptions is not None: # workaround for TLS SNI + ClientTLSOptions(hostname, ctx) return ctx From ca2575001ef29af46b337ca8576cc93a4d2f8a73 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Wed, 25 Mar 2015 18:32:20 -0300 Subject: [PATCH 0141/2974] Add missing callback arg in jobs topic example. --- docs/topics/jobs.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/jobs.rst b/docs/topics/jobs.rst index 4e09000d2..38d77534a 100644 --- a/docs/topics/jobs.rst +++ b/docs/topics/jobs.rst @@ -90,7 +90,7 @@ But this will:: def some_callback(self, response): somearg = 'test' - return scrapy.Request('http://www.example.com', meta={'somearg': somearg}) + return scrapy.Request('http://www.example.com', , callback=self.other_callback, meta={'somearg': somearg}) def other_callback(self, response): somearg = response.meta['somearg'] From 933dbc6be6c1dac64fe31e34c7943e050eff254a Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Wed, 25 Mar 2015 18:33:17 -0300 Subject: [PATCH 0142/2974] Oops --- docs/topics/jobs.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/jobs.rst b/docs/topics/jobs.rst index 38d77534a..303076015 100644 --- a/docs/topics/jobs.rst +++ b/docs/topics/jobs.rst @@ -90,7 +90,7 @@ 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, meta={'somearg': somearg}) def other_callback(self, response): somearg = response.meta['somearg'] From 32423d4a330e8af72ae077fac6347812d57bca08 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Wed, 25 Mar 2015 19:24:36 -0300 Subject: [PATCH 0143/2974] some improvements to overview page --- docs/intro/overview.rst | 253 +++++++++++----------------------------- 1 file changed, 65 insertions(+), 188 deletions(-) diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index c30963db8..9a3015ddc 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -13,172 +13,83 @@ precisely, `web scraping`_), it can also be used to extract data using APIs (such as `Amazon Associates Web Services`_) or as a general purpose web crawler. -The purpose of this document is to introduce you to the concepts behind Scrapy -so you can get an idea of how it works and decide if Scrapy is what you need. -When you're ready to start a project, you can :ref:`start with the tutorial -`. +Walk-through of an example spider +================================= -Pick a website -============== +In order to show you what Scrapy brings to the table, we'll walk you +through an example of a Scrapy Spider using the simplest way to run a spider. -So you need to extract some information from a website, but the website doesn't -provide any API or mechanism to access that info programmatically. Scrapy can -help you extract that information. +Once you're ready to dive in more, you can :ref:`follow the tutorial +and build a full-blown Scrapy project `. -Let's say we want to extract the URL, name, description and size of all torrent -files added today in the `Mininova`_ site. - -The list of all torrents added today can be found on this page: - - http://www.mininova.org/today - -.. _intro-overview-item: - -Define the data you want to scrape -================================== - -The first thing is to define the data we want to scrape. In Scrapy, this is -done through :ref:`Scrapy Items ` (Torrent files, in this case). - -This would be our Item:: +So, here's the code for a spider that follows the links to the top +voted questions on StackOverflow and scrapes some data from each page:: import scrapy - class TorrentItem(scrapy.Item): - url = scrapy.Field() - name = scrapy.Field() - description = scrapy.Field() - size = scrapy.Field() -Write a Spider to extract the data -================================== + class StackOverflowSpider(scrapy.Spider): + name = 'stackoverflow' + start_urls = ['http://stackoverflow.com/questions?sort=votes'] -The next thing is to write a Spider which defines the start URL -(http://www.mininova.org/today), the rules for following links and the rules -for extracting the data from pages. + def parse(self, response): + for href in response.css('.question-summary h3 a::attr(href)'): + full_url = response.urljoin(href.extract()) + yield scrapy.Request(full_url, callback=self.parse_question) -If we take a look at that page content we'll see that all torrent URLs are like -``http://www.mininova.org/tor/NUMBER`` where ``NUMBER`` is an integer. We'll use -that to construct the regular expression for the links to follow: ``/tor/\d+``. - -We'll use `XPath`_ for selecting the data to extract from the web page HTML -source. Let's take one of those torrent pages: - - http://www.mininova.org/tor/2676093 - -And look at the page HTML source to construct the XPath to select the data we -want which is: torrent name, description and size. - -.. highlight:: html - -By looking at the page HTML source we can see that the file name is contained -inside a ``

`` tag:: - -

Darwin - The Evolution Of An Exhibition

- -.. highlight:: none - -An XPath expression to extract the name could be:: - - //h1/text() - -.. highlight:: html - -And the description is contained inside a ``
`` tag with ``id="description"``:: - -

Description:

- -
- Short documentary made for Plymouth City Museum and Art Gallery regarding the setup of an exhibit about Charles Darwin in conjunction with the 200th anniversary of his birth. - - ... - -.. highlight:: none - -An XPath expression to select the description could be:: - - //div[@id='description'] - -.. highlight:: html - -Finally, the file size is contained in the second ``

`` tag inside the ``

`` -tag with ``id=specifications``:: - -
- -

- Category: - Movies > Documentary -

- -

- Total size: - 150.62 megabyte

+ def parse_question(self, response): + title = response.css('h1 a::text').extract_first() + votes = response.css('.question .vote-count-post::text').extract_first() + tags = response.css('.question .post-tag::text').extract() + body = response.css('.question .post-text').extract_first() + yield { + 'title': title, + 'votes': votes, + 'body': body, + 'tags': tags, + 'link': response.url, + } -.. highlight:: none +Put this in a file, name it to something like ``stackoverflow_spider.py`` +and run the spider using the :command:`runspider` command:: -An XPath expression to select the file size could be:: + scrapy runspider stackoverflow_spider.py -o top-stackoverflow-questions.json - //div[@id='specifications']/p[2]/text()[2] -.. highlight:: python +When this finishes you will have in the ``top-stackoverflow-questions.json`` file +a list of the most upvoted questions in StackOverflow in JSON format, containing the +title, link, number of upvotes, a list of the tags and the question content in HTML. -For more information about XPath see the `XPath reference`_. -Finally, here's the spider code:: +What just happened? +------------------- - from scrapy.contrib.spiders import CrawlSpider, Rule - from scrapy.contrib.linkextractors import LinkExtractor +When you ran the command ``scrapy runspider somefile.py``, Scrapy looked +for a Spider definition inside it and ran it through its crawler engine. - class MininovaSpider(CrawlSpider): +The crawl started by making requests to the URLs defined in the ``start_urls`` +attribute (in this case, only the URL for StackOverflow top questions page), +and then called the default callback method ``parse`` passing the response +object as an argument. - name = 'mininova' - allowed_domains = ['mininova.org'] - start_urls = ['http://www.mininova.org/today'] - rules = [Rule(LinkExtractor(allow=['/tor/\d+']), 'parse_torrent')] +In the ``parse`` callback, we scrape the links to the questions and +yield a few more requests to be processed, registering for them +the method ``parse_question`` as the callback to be called when the +requests are complete. - def parse_torrent(self, response): - torrent = TorrentItem() - torrent['url'] = response.url - torrent['name'] = response.xpath("//h1/text()").extract() - torrent['description'] = response.xpath("//div[@id='description']").extract() - torrent['size'] = response.xpath("//div[@id='info-left']/p[2]/text()[2]").extract() - return torrent +Finally, the ``parse_question`` callback scrapes the question data +for each page yielding a dict, which Scrapy then collects and +writes to a JSON file as requested in the command line. -The ``TorrentItem`` class is :ref:`defined above `. +.. note:: -Run the spider to extract the data -================================== + This is using :ref:`feed exports ` to generate the + JSON file, you can easily change the export format (XML or CSV, for example) or the + storage backend (FTP or `Amazon S3`_, for example). You can also write an + :ref:`item pipeline ` to store the items in a database. -Finally, we'll run the spider to crawl the site and output the file -``scraped_data.json`` with the scraped data in JSON format:: - - scrapy crawl mininova -o scraped_data.json - -This uses :ref:`feed exports ` to generate the JSON file. -You can easily change the export format (XML or CSV, for example) or the -storage backend (FTP or `Amazon S3`_, for example). - -You can also write an :ref:`item pipeline ` to store the -items in a database very easily. - -Review scraped data -=================== - -If you check the ``scraped_data.json`` file after the process finishes, you'll -see the scraped items there:: - - [{"url": "http://www.mininova.org/tor/2676093", "name": ["Darwin - The Evolution Of An Exhibition"], "description": ["Short documentary made for Plymouth ..."], "size": ["150.62 megabyte"]}, - # ... other items ... - ] - -You'll notice that all field values (except for the ``url`` which was assigned -directly) are actually lists. This is because the :ref:`selectors -` return lists. You may want to store single values, or -perform some additional parsing/cleansing to the values. That's what -:ref:`Item Loaders ` are for. .. _topics-whatelse: @@ -189,68 +100,37 @@ You've seen how to extract and store items from a website using Scrapy, but this is just the surface. Scrapy provides a lot of powerful features for making scraping easy and efficient, such as: -* Built-in support for :ref:`selecting and extracting ` data - from HTML and XML sources - -* Built-in support for cleaning and sanitizing the scraped data using a - collection of reusable filters (called :ref:`Item Loaders `) - shared between all the spiders. +* An :ref:`interactive shell console ` (IPython aware) for trying + out the CSS and XPath expressions to scrape data, very useful when writing or + debugging your spiders. * Built-in support for :ref:`generating feed exports ` in multiple formats (JSON, CSV, XML) and storing them in multiple backends (FTP, S3, local filesystem) -* A media pipeline for :ref:`automatically downloading images ` - (or any other media) associated with the scraped items - -* Support for :ref:`extending Scrapy ` by plugging - your own functionality using :ref:`signals ` and a - well-defined API (middlewares, :ref:`extensions `, and - :ref:`pipelines `). - -* Wide range of built-in middlewares and extensions for: - - * cookies and session handling - * HTTP compression - * HTTP authentication - * HTTP cache - * user-agent spoofing - * robots.txt - * crawl depth restriction - * and more - * Robust encoding support and auto-detection, for dealing with foreign, non-standard and broken encoding declarations. -* Support for creating spiders based on pre-defined templates, to speed up - spider creation and make their code more consistent on large projects. See - :command:`genspider` command for more details. - -* Extensible :ref:`stats collection ` for multiple spider - metrics, useful for monitoring the performance of your spiders and detecting - when they get broken - -* An :ref:`Interactive shell console ` for trying XPaths, very - useful for writing and debugging your spiders - -* A :ref:`System service ` designed to ease the deployment and - run of your spiders in production. +* Strong :ref:`extensibility support ` and lots of built-in + extensions and middlewares to handle things like cookies, crawl throttling, + HTTP caching, HTTP compression, user-agent spoofing, robots.txt, + stats collection and many more. * A :ref:`Telnet console ` for hooking into a Python console running inside your Scrapy process, to introspect and debug your crawler -* :ref:`Logging ` facility that you can hook on to for catching - errors during the scraping process. +* A caching DNS resolver * Support for crawling based on URLs discovered through `Sitemaps`_ -* A caching DNS resolver +* A media pipeline for :ref:`automatically downloading images ` + (or any other media) associated with the scraped items What's next? ============ -The next obvious steps are for you to `download Scrapy`_, read :ref:`the +The next obvious steps for you are to `download Scrapy`_, read :ref:`the tutorial ` and join `the community`_. Thanks for your interest! @@ -258,9 +138,6 @@ interest! .. _the community: http://scrapy.org/community/ .. _screen scraping: http://en.wikipedia.org/wiki/Screen_scraping .. _web scraping: http://en.wikipedia.org/wiki/Web_scraping -.. _Amazon Associates Web Services: https://affiliate-program.amazon.com/gp/advertising/api/detail/main.html -.. _Mininova: http://www.mininova.org -.. _XPath: http://www.w3.org/TR/xpath -.. _XPath reference: http://www.w3.org/TR/xpath +.. _Amazon Associates Web Services: http://aws.amazon.com/associates/ .. _Amazon S3: http://aws.amazon.com/s3/ .. _Sitemaps: http://www.sitemaps.org From 8f4a268f3757fe663de49e80dfd9325c260fcc73 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Thu, 26 Mar 2015 12:14:56 -0300 Subject: [PATCH 0144/2974] added bit about async requests, improved phrasing --- docs/intro/overview.rst | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index 9a3015ddc..295208bd2 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -74,10 +74,16 @@ attribute (in this case, only the URL for StackOverflow top questions page), and then called the default callback method ``parse`` passing the response object as an argument. -In the ``parse`` callback, we scrape the links to the questions and -yield a few more requests to be processed, registering for them -the method ``parse_question`` as the callback to be called when the -requests are complete. +Here you notice one of the main advantages about Scrapy: requests are +scheduled and processed asynchronously. This means that Scrapy doesn't +need to wait for a request to be finished and processed, it can send +another request or do other things in the meantime, which results in much +faster crawlings. + +So, in the ``parse`` callback, we scrape the links to the questions and +yield a few more requests to be done, registering for them the method +``parse_question`` as the callback to be called for each of them as +they finish. Finally, the ``parse_question`` callback scrapes the question data for each page yielding a dict, which Scrapy then collects and From 76e3bf12508f51c8c7b532b1a55880e55e144f19 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Thu, 26 Mar 2015 14:26:20 -0300 Subject: [PATCH 0145/2974] addressing comments from the review plus further editing --- docs/intro/overview.rst | 83 +++++++++++++++++++++++------------- docs/topics/autothrottle.rst | 2 + 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index 295208bd2..d93797cae 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -39,15 +39,11 @@ voted questions on StackOverflow and scrapes some data from each page:: yield scrapy.Request(full_url, callback=self.parse_question) def parse_question(self, response): - title = response.css('h1 a::text').extract_first() - votes = response.css('.question .vote-count-post::text').extract_first() - tags = response.css('.question .post-tag::text').extract() - body = response.css('.question .post-text').extract_first() yield { - 'title': title, - 'votes': votes, - 'body': body, - 'tags': tags, + 'title': response.css('h1 a::text').extract()[0], + 'votes': response.css('.question .vote-count-post::text').extract()[0], + 'body': response.css('.question .post-text').extract()[0], + 'tags': response.css('.question .post-tag::text').extract(), 'link': response.url, } @@ -66,28 +62,36 @@ title, link, number of upvotes, a list of the tags and the question content in H What just happened? ------------------- -When you ran the command ``scrapy runspider somefile.py``, Scrapy looked -for a Spider definition inside it and ran it through its crawler engine. +When you ran the command ``scrapy runspider somefile.py``, Scrapy looked for a +Spider definition inside it and ran it through its crawler engine. The crawl started by making requests to the URLs defined in the ``start_urls`` attribute (in this case, only the URL for StackOverflow top questions page), -and then called the default callback method ``parse`` passing the response -object as an argument. +and called the default callback method ``parse`` passing the response object as +an argument. In the ``parse`` callback, we extract the links to the +question pages using a CSS Selector with a custom extension that allows to get +the value for an attribute. Then, we yield a few more requests to be sent, +registering the method ``parse_question`` as the callback to be called for each +of them as they finish. Here you notice one of the main advantages about Scrapy: requests are -scheduled and processed asynchronously. This means that Scrapy doesn't -need to wait for a request to be finished and processed, it can send -another request or do other things in the meantime, which results in much -faster crawlings. +:ref:`scheduled and processed asynchronously `. This +means that Scrapy doesn't need to wait for a request to be finished and +processed, it can send another request or do other things in the meantime. This +also means that other requests can keep going even if some request fails or an +error happens while handling it. -So, in the ``parse`` callback, we scrape the links to the questions and -yield a few more requests to be done, registering for them the method -``parse_question`` as the callback to be called for each of them as -they finish. +While this enables you to do very fast crawlings sending multiple concurrent +requests at the same time in a fault-tolerant way, Scrapy also gives you +control over the politeness of the crawl through :ref:`a few settings +`. You can do things like setting a download delay between +each request, limit amount of concurrent requests per domain or per IP, and +even :ref:`use an auto-throttling extension ` that tries +to figure out these automatically. -Finally, the ``parse_question`` callback scrapes the question data -for each page yielding a dict, which Scrapy then collects and -writes to a JSON file as requested in the command line. +Finally, the ``parse_question`` callback scrapes the question data for each +page yielding a dict, which Scrapy then collects and writes to a JSON file as +requested in the command line. .. note:: @@ -96,6 +100,25 @@ writes to a JSON file as requested in the command line. storage backend (FTP or `Amazon S3`_, for example). You can also write an :ref:`item pipeline ` to store the items in a database. +The data in the file will look like this (note: formatted for easier reading):: + + + [{ + "body": "... LONG HTML HERE ...", + "link": "http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array", + "tags": ["java", "c++", "performance", "optimization"], + "title": "Why is processing a sorted array faster than an unsorted array?", + "votes": "9924" + }, + { + "body": "... LONG HTML HERE ...", + "link": "http://stackoverflow.com/questions/1260748/how-do-i-remove-a-git-submodule", + "tags": ["git", "git-submodules"], + "title": "How do I remove a Git submodule?", + "votes": "1764" + }, + ...] + .. _topics-whatelse: @@ -106,6 +129,10 @@ You've seen how to extract and store items from a website using Scrapy, but this is just the surface. Scrapy provides a lot of powerful features for making scraping easy and efficient, such as: +* Built-in support for :ref:`selecting and extracting ` data + from HTML/XML sources using CSS selectors extended and XPath expressions, + with helper methods to extract using regular expressions. + * An :ref:`interactive shell console ` (IPython aware) for trying out the CSS and XPath expressions to scrape data, very useful when writing or debugging your spiders. @@ -126,12 +153,10 @@ scraping easy and efficient, such as: console running inside your Scrapy process, to introspect and debug your crawler -* A caching DNS resolver - -* Support for crawling based on URLs discovered through `Sitemaps`_ - -* A media pipeline for :ref:`automatically downloading images ` - (or any other media) associated with the scraped items +* Plus other goodies like reusable spiders to crawl sites from `Sitemaps`_ and + XML/CSV feeds, a media pipeline for :ref:`automatically downloading images ` + (or any other media) associated with the scraped items, a caching DNS resolver, + and much more! What's next? ============ diff --git a/docs/topics/autothrottle.rst b/docs/topics/autothrottle.rst index 475946a54..8073ec6e0 100644 --- a/docs/topics/autothrottle.rst +++ b/docs/topics/autothrottle.rst @@ -1,3 +1,5 @@ +.. _topics-autothrottle: + ====================== AutoThrottle extension ====================== From 13d0ecde77cc33a8f81d7165a47ed2dc9c2c5a58 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Thu, 26 Mar 2015 15:26:16 -0300 Subject: [PATCH 0146/2974] addressing more review comments, to avoid ambiguity on desired reading flow --- docs/intro/overview.rst | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index d93797cae..65b26613d 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -17,11 +17,8 @@ crawler. Walk-through of an example spider ================================= -In order to show you what Scrapy brings to the table, we'll walk you -through an example of a Scrapy Spider using the simplest way to run a spider. - -Once you're ready to dive in more, you can :ref:`follow the tutorial -and build a full-blown Scrapy project `. +In order to show you what Scrapy brings to the table, we'll walk you through an +example of a Scrapy Spider using the simplest way to run a spider. So, here's the code for a spider that follows the links to the top voted questions on StackOverflow and scrapes some data from each page:: @@ -144,10 +141,18 @@ scraping easy and efficient, such as: * Robust encoding support and auto-detection, for dealing with foreign, non-standard and broken encoding declarations. -* Strong :ref:`extensibility support ` and lots of built-in - extensions and middlewares to handle things like cookies, crawl throttling, - HTTP caching, HTTP compression, user-agent spoofing, robots.txt, - stats collection and many more. +* :ref:`Strong extensibility support `, allowing you to plug + in your own functionality using :ref:`signals ` and a + well-defined API (middlewares, :ref:`extensions `, and + :ref:`pipelines `). + +* Wide range of built-in extensions and middlewares for handling: + * cookies and session handling + * HTTP features like compression, authentication, caching + * user-agent spoofing + * robots.txt + * crawl depth restriction + * and more * A :ref:`Telnet console ` for hooking into a Python console running inside your Scrapy process, to introspect and debug your @@ -161,11 +166,11 @@ scraping easy and efficient, such as: What's next? ============ -The next obvious steps for you are to `download Scrapy`_, read :ref:`the -tutorial ` and join `the community`_. Thanks for your +The next steps for you are to :ref:`install Scrapy `, +:ref:`follow through the tutorial ` to learn how to organize +your code in Scrapy projects and `join the community`_. Thanks for your interest! -.. _download Scrapy: http://scrapy.org/download/ .. _the community: http://scrapy.org/community/ .. _screen scraping: http://en.wikipedia.org/wiki/Screen_scraping .. _web scraping: http://en.wikipedia.org/wiki/Web_scraping From 729861c8644b0ec3b21dd2b4d2ba15952e274197 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Thu, 26 Mar 2015 15:31:42 -0300 Subject: [PATCH 0147/2974] fixing indentation --- docs/intro/overview.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index 65b26613d..395e1227c 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -97,24 +97,24 @@ requested in the command line. storage backend (FTP or `Amazon S3`_, for example). You can also write an :ref:`item pipeline ` to store the items in a database. -The data in the file will look like this (note: formatted for easier reading):: +The data in the file will look like this (note: reformatted for easier reading):: [{ - "body": "... LONG HTML HERE ...", - "link": "http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array", - "tags": ["java", "c++", "performance", "optimization"], - "title": "Why is processing a sorted array faster than an unsorted array?", - "votes": "9924" - }, - { - "body": "... LONG HTML HERE ...", - "link": "http://stackoverflow.com/questions/1260748/how-do-i-remove-a-git-submodule", - "tags": ["git", "git-submodules"], - "title": "How do I remove a Git submodule?", - "votes": "1764" - }, - ...] + "body": "... LONG HTML HERE ...", + "link": "http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array", + "tags": ["java", "c++", "performance", "optimization"], + "title": "Why is processing a sorted array faster than an unsorted array?", + "votes": "9924" + }, + { + "body": "... LONG HTML HERE ...", + "link": "http://stackoverflow.com/questions/1260748/how-do-i-remove-a-git-submodule", + "tags": ["git", "git-submodules"], + "title": "How do I remove a Git submodule?", + "votes": "1764" + }, + ...] .. _topics-whatelse: From 7402e27230958ee2c89275676969e0cc844d6e4b Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Thu, 26 Mar 2015 15:35:31 -0300 Subject: [PATCH 0148/2974] fix community link --- docs/intro/overview.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index 395e1227c..6b0a2c2bf 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -171,7 +171,7 @@ The next steps for you are to :ref:`install Scrapy `, your code in Scrapy projects and `join the community`_. Thanks for your interest! -.. _the community: http://scrapy.org/community/ +.. _join the community: http://scrapy.org/community/ .. _screen scraping: http://en.wikipedia.org/wiki/Screen_scraping .. _web scraping: http://en.wikipedia.org/wiki/Web_scraping .. _Amazon Associates Web Services: http://aws.amazon.com/associates/ From 4dcecc98f919103b9886fddbcc5e6273378854cb Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Thu, 26 Mar 2015 15:45:17 -0300 Subject: [PATCH 0149/2974] moved example data to a better place --- docs/intro/overview.rst | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index 6b0a2c2bf..18a877cdc 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -53,7 +53,25 @@ and run the spider using the :command:`runspider` command:: When this finishes you will have in the ``top-stackoverflow-questions.json`` file a list of the most upvoted questions in StackOverflow in JSON format, containing the -title, link, number of upvotes, a list of the tags and the question content in HTML. +title, link, number of upvotes, a list of the tags and the question content in HTML, +looking like this (reformatted for easier reading):: + + [{ + "body": "... LONG HTML HERE ...", + "link": "http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array", + "tags": ["java", "c++", "performance", "optimization"], + "title": "Why is processing a sorted array faster than an unsorted array?", + "votes": "9924" + }, + { + "body": "... LONG HTML HERE ...", + "link": "http://stackoverflow.com/questions/1260748/how-do-i-remove-a-git-submodule", + "tags": ["git", "git-submodules"], + "title": "How do I remove a Git submodule?", + "votes": "1764" + }, + ...] + What just happened? @@ -97,25 +115,6 @@ requested in the command line. storage backend (FTP or `Amazon S3`_, for example). You can also write an :ref:`item pipeline ` to store the items in a database. -The data in the file will look like this (note: reformatted for easier reading):: - - - [{ - "body": "... LONG HTML HERE ...", - "link": "http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array", - "tags": ["java", "c++", "performance", "optimization"], - "title": "Why is processing a sorted array faster than an unsorted array?", - "votes": "9924" - }, - { - "body": "... LONG HTML HERE ...", - "link": "http://stackoverflow.com/questions/1260748/how-do-i-remove-a-git-submodule", - "tags": ["git", "git-submodules"], - "title": "How do I remove a Git submodule?", - "votes": "1764" - }, - ...] - .. _topics-whatelse: From 475766c73a4fd6f65caa5c5c25e713eab8eac84b Mon Sep 17 00:00:00 2001 From: Peter Bronez Date: Thu, 26 Mar 2015 15:34:30 -0400 Subject: [PATCH 0150/2974] Converted sel.xpath() calls to response.xpath() in Extracting the data --- docs/intro/tutorial.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/intro/tutorial.rst b/docs/intro/tutorial.rst index ad808316b..286a591ee 100644 --- a/docs/intro/tutorial.rst +++ b/docs/intro/tutorial.rst @@ -315,19 +315,19 @@ is inside a ``
    `` element, in fact the *second* ``
      `` element. So we can select each ``
    • `` element belonging to the sites list with this code:: - sel.xpath('//ul/li') + response.xpath('//ul/li') And from them, the sites descriptions:: - sel.xpath('//ul/li/text()').extract() + response.xpath('//ul/li/text()').extract() The sites titles:: - sel.xpath('//ul/li/a/text()').extract() + response.xpath('//ul/li/a/text()').extract() And the sites links:: - sel.xpath('//ul/li/a/@href').extract() + response.xpath('//ul/li/a/@href').extract() As we've said before, each ``.xpath()`` call returns a list of selectors, so we can concatenate further ``.xpath()`` calls to dig deeper into a node. We are going to use From 1134a9cab055969b3232a16037d16e5ab93759f0 Mon Sep 17 00:00:00 2001 From: nyov Date: Tue, 24 Mar 2015 03:12:58 +0000 Subject: [PATCH 0151/2974] config: look in ~/.config/scrapy.cfg as well --- docs/topics/commands.rst | 21 +++++++++++++++++++++ docs/topics/settings.rst | 2 ++ scrapy/utils/conf.py | 3 +++ 3 files changed, 26 insertions(+) diff --git a/docs/topics/commands.rst b/docs/topics/commands.rst index 5c0de0d6d..27d0655ef 100644 --- a/docs/topics/commands.rst +++ b/docs/topics/commands.rst @@ -13,6 +13,27 @@ just call "commands" or "Scrapy commands". The Scrapy tool provides several commands, for multiple purposes, and each one accepts a different set of arguments and options. +Configuration settings +====================== + +Scrapy will look for configuration parameters in ini-style ``scrapy.cfg`` files +in standard locations: + +1. ``/etc/scrapy.cfg`` or ``c:\scrapy\scrapy.cfg`` (system-wide), +2. ``~/.config/scrapy.cfg`` (``$XDG_CONFIG_HOME``) and ``~/.scrapy.cfg`` (``$HOME``) + for global (user-wide) settings, and +3. ``scrapy.cfg`` inside a scrapy project's root (see next section). + +Settings from these files are merged in the listed order of preference: +user-defined values have higher priority than system-wide defaults +and project-wide settings will override all others, when defined. + +Scrapy also understands, and can be configured through, a number of environment +variables. Currently these are: + +* ``SCRAPY_SETTINGS_MODULE`` (See :ref:`topics-settings-module-envvar`) +* ``SCRAPY_PROJECT`` + .. _topics-project-structure: Default structure of Scrapy projects diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 5e11e473f..0f41c427b 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -16,6 +16,8 @@ project (in case you have many). For a list of available built-in settings see: :ref:`topics-settings-ref`. +.. _topics-settings-module-envvar: + Designating the settings ======================== diff --git a/scrapy/utils/conf.py b/scrapy/utils/conf.py index caa80a5a1..b883923b9 100644 --- a/scrapy/utils/conf.py +++ b/scrapy/utils/conf.py @@ -63,7 +63,10 @@ def get_config(use_closest=True): def get_sources(use_closest=True): + xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \ + os.path.expanduser('~/.config') sources = ['/etc/scrapy.cfg', r'c:\scrapy\scrapy.cfg', + xdg_config_home + '/scrapy.cfg', os.path.expanduser('~/.scrapy.cfg')] if use_closest: sources.append(closest_scrapy_cfg()) From 84edc2ebc7afcdfd6b4789a6e1725b83eb9076ab Mon Sep 17 00:00:00 2001 From: nyov Date: Tue, 24 Mar 2015 11:42:33 +0000 Subject: [PATCH 0152/2974] Add Response.urljoin() testcase and add evaluation of base-url for HtmlResponse. --- scrapy/http/response/__init__.py | 2 +- scrapy/http/response/text.py | 8 ++++++++ scrapy/utils/response.py | 2 +- tests/test_http_response.py | 23 +++++++++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/scrapy/http/response/__init__.py b/scrapy/http/response/__init__.py index e138d5cc8..5b1d34c6d 100644 --- a/scrapy/http/response/__init__.py +++ b/scrapy/http/response/__init__.py @@ -55,7 +55,7 @@ class Response(object_ref): elif body is None: self._body = '' else: - raise TypeError("Response body must either str or unicode. Got: '%s'" \ + raise TypeError("Response body must either be str or unicode. Got: '%s'" \ % type(body).__name__) body = property(_get_body, obsolete_setter(_set_body, 'body')) diff --git a/scrapy/http/response/text.py b/scrapy/http/response/text.py index 14030d8e5..9a435f194 100644 --- a/scrapy/http/response/text.py +++ b/scrapy/http/response/text.py @@ -5,9 +5,12 @@ discovering (through HTTP headers) to base Response class. See documentation in docs/topics/request-response.rst """ +from six.moves.urllib.parse import urljoin + from w3lib.encoding import html_to_unicode, resolve_encoding, \ html_body_declared_encoding, http_content_type_encoding from scrapy.http.response import Response +from scrapy.utils.response import get_base_url from scrapy.utils.python import memoizemethod_noargs @@ -63,6 +66,11 @@ class TextResponse(Response): self._cached_ubody = html_to_unicode(charset, self.body)[1] return self._cached_ubody + def urljoin(self, url): + """Join this Response's url with a possible relative url to form an + absolute interpretation of the latter.""" + return urljoin(get_base_url(self), url) + @memoizemethod_noargs def _headers_encoding(self): content_type = self.headers.get('Content-Type') diff --git a/scrapy/utils/response.py b/scrapy/utils/response.py index 61f43535f..1d79ec0e3 100644 --- a/scrapy/utils/response.py +++ b/scrapy/utils/response.py @@ -13,7 +13,6 @@ from twisted.web import http from twisted.web.http import RESPONSES from w3lib import html -from scrapy.http import HtmlResponse, TextResponse from scrapy.utils.decorator import deprecated @@ -73,6 +72,7 @@ def open_in_browser(response, _openfunc=webbrowser.open): """Open the given response in a local web browser, populating the tag for external links to work """ + from scrapy.http import HtmlResponse, TextResponse # XXX: this implementation is a bit dirty and could be improved body = response.body if isinstance(response, HtmlResponse): diff --git a/tests/test_http_response.py b/tests/test_http_response.py index 26a628182..b0b5b82c4 100644 --- a/tests/test_http_response.py +++ b/tests/test_http_response.py @@ -113,6 +113,12 @@ class BaseResponseTest(unittest.TestCase): self.assertRaises(AttributeError, setattr, r, 'url', 'http://example2.com') self.assertRaises(AttributeError, setattr, r, 'body', 'xxx') + def test_urljoin(self): + """Test urljoin shortcut (only for existence, since behavior equals urljoin)""" + joined = self.response_class('http://www.example.com').urljoin('/test') + absolute = 'http://www.example.com/test' + self.assertEqual(joined, absolute) + class ResponseText(BaseResponseTest): @@ -295,6 +301,23 @@ class TextResponseTest(BaseResponseTest): response.selector.css("title::text").extract(), ) + def test_urljoin_with_base_url(self): + """Test urljoin shortcut which also evaluates base-url through get_base_url().""" + body = '' + joined = self.response_class('http://www.example.com', body=body).urljoin('/test') + absolute = 'https://example.net/test' + self.assertEqual(joined, absolute) + + body = '' + joined = self.response_class('http://www.example.com', body=body).urljoin('test') + absolute = 'http://www.example.com/test' + self.assertEqual(joined, absolute) + + body = '' + joined = self.response_class('http://www.example.com', body=body).urljoin('test') + absolute = 'http://www.example.com/elsewhere/test' + self.assertEqual(joined, absolute) + class HtmlResponseTest(TextResponseTest): From 7135c4e694bc4dc970311f67386dfe8660c09012 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Fri, 27 Mar 2015 17:56:56 -0300 Subject: [PATCH 0153/2974] some minor grammar fixes --- docs/intro/overview.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index 18a877cdc..577e769b2 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -82,7 +82,7 @@ Spider definition inside it and ran it through its crawler engine. The crawl started by making requests to the URLs defined in the ``start_urls`` attribute (in this case, only the URL for StackOverflow top questions page), -and called the default callback method ``parse`` passing the response object as +and called the default callback method ``parse``, passing the response object as an argument. In the ``parse`` callback, we extract the links to the question pages using a CSS Selector with a custom extension that allows to get the value for an attribute. Then, we yield a few more requests to be sent, @@ -96,12 +96,12 @@ processed, it can send another request or do other things in the meantime. This also means that other requests can keep going even if some request fails or an error happens while handling it. -While this enables you to do very fast crawlings sending multiple concurrent -requests at the same time in a fault-tolerant way, Scrapy also gives you +While this enables you to do very fast crawlings (sending multiple concurrent +requests at the same time, in a fault-tolerant way) Scrapy also gives you control over the politeness of the crawl through :ref:`a few settings `. You can do things like setting a download delay between -each request, limit amount of concurrent requests per domain or per IP, and -even :ref:`use an auto-throttling extension ` that tries +each request, limiting amount of concurrent requests per domain or per IP, and +even :ref:`using an auto-throttling extension ` that tries to figure out these automatically. Finally, the ``parse_question`` callback scrapes the question data for each @@ -126,7 +126,7 @@ this is just the surface. Scrapy provides a lot of powerful features for making scraping easy and efficient, such as: * Built-in support for :ref:`selecting and extracting ` data - from HTML/XML sources using CSS selectors extended and XPath expressions, + from HTML/XML sources using extended CSS selectors and XPath expressions, with helper methods to extract using regular expressions. * An :ref:`interactive shell console ` (IPython aware) for trying From 18c5bc75c27f88f05fe7f1686dfa491b82a7f558 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Fri, 27 Mar 2015 21:00:21 -0300 Subject: [PATCH 0154/2974] some more minor grammar fixes --- docs/intro/overview.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index 577e769b2..8777ac891 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -81,11 +81,11 @@ When you ran the command ``scrapy runspider somefile.py``, Scrapy looked for a Spider definition inside it and ran it through its crawler engine. The crawl started by making requests to the URLs defined in the ``start_urls`` -attribute (in this case, only the URL for StackOverflow top questions page), +attribute (in this case, only the URL for StackOverflow top questions page) and called the default callback method ``parse``, passing the response object as -an argument. In the ``parse`` callback, we extract the links to the +an argument. In the ``parse`` callback we extract the links to the question pages using a CSS Selector with a custom extension that allows to get -the value for an attribute. Then, we yield a few more requests to be sent, +the value for an attribute. Then we yield a few more requests to be sent, registering the method ``parse_question`` as the callback to be called for each of them as they finish. @@ -96,7 +96,7 @@ processed, it can send another request or do other things in the meantime. This also means that other requests can keep going even if some request fails or an error happens while handling it. -While this enables you to do very fast crawlings (sending multiple concurrent +While this enables you to do very fast crawls (sending multiple concurrent requests at the same time, in a fault-tolerant way) Scrapy also gives you control over the politeness of the crawl through :ref:`a few settings `. You can do things like setting a download delay between From c1cd019f56b703509bff5cf97da7e2c70fb466b4 Mon Sep 17 00:00:00 2001 From: Alexander Sibiryakov Date: Tue, 31 Mar 2015 10:56:31 +0200 Subject: [PATCH 0155/2974] Setting maximum thread pool size in reactor. --- scrapy/crawler.py | 3 +++ scrapy/settings/default_settings.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 6866be809..41716852c 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -12,6 +12,7 @@ from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.utils.ossignal import install_shutdown_handlers, signal_names from scrapy.utils.misc import load_object from scrapy import log, signals +from pdb import set_trace class Crawler(object): @@ -152,6 +153,8 @@ class CrawlerProcess(CrawlerRunner): if self.settings.getbool('DNSCACHE_ENABLED'): reactor.installResolver(CachingThreadedResolver(reactor)) + tp = reactor.getThreadPool() + tp.adjustPoolsize(maxthreads=self.settings.getint('REACTOR_THREADPOOL_MAXSIZE')) reactor.addSystemEventTrigger('before', 'shutdown', self.stop) reactor.run(installSignalHandlers=False) # blocking call diff --git a/scrapy/settings/default_settings.py b/scrapy/settings/default_settings.py index 0342b1ada..5b8dc4eaa 100644 --- a/scrapy/settings/default_settings.py +++ b/scrapy/settings/default_settings.py @@ -194,6 +194,8 @@ NEWSPIDER_MODULE = '' RANDOMIZE_DOWNLOAD_DELAY = True +REACTOR_THREADPOOL_MAXSIZE = 10 + REDIRECT_ENABLED = True REDIRECT_MAX_TIMES = 20 # uses Firefox default setting REDIRECT_PRIORITY_ADJUST = +2 From 5864d291d4ff96351661ce01c414f2bdae43efe0 Mon Sep 17 00:00:00 2001 From: Alexander Sibiryakov Date: Tue, 31 Mar 2015 11:10:56 +0200 Subject: [PATCH 0156/2974] Setting documentation. --- docs/topics/settings.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 5e11e473f..4812de8b9 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -742,6 +742,17 @@ If :setting:`DOWNLOAD_DELAY` is zero (default) this option has no effect. .. _wget: http://www.gnu.org/software/wget/manual/wget.html +.. setting:: REACTOR_THREADPOOL_MAXSIZE + +REACTOR_THREADPOOL_MAXSIZE +------------------ + +Default: ``10`` + +The maximum limit for Twisted Reactor thread pool size. This is common multi-purpose thread pool used by various +Scrapy components. Threaded DNS Resolver, BlockingFeedStorage, S3FilesStore just to name a few. Increase this value if +you're experiencing problems with insufficient blocking IO. + .. setting:: REDIRECT_MAX_TIMES REDIRECT_MAX_TIMES From 2d142d64014d7094119a4556337c90b5f4b0b900 Mon Sep 17 00:00:00 2001 From: Richard Dowinton Date: Tue, 31 Mar 2015 12:17:31 +0100 Subject: [PATCH 0157/2974] Added deployment section covering scrapyd-deploy and shub --- docs/index.rst | 4 ++-- docs/topics/deployment.rst | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 docs/topics/deployment.rst diff --git a/docs/index.rst b/docs/index.rst index 507b9bea9..cea652b89 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -150,7 +150,7 @@ Solving specific problems topics/leaks topics/images topics/ubuntu - topics/scrapyd + topics/deployment topics/autothrottle topics/benchmarking topics/jobs @@ -186,7 +186,7 @@ Solving specific problems :doc:`topics/ubuntu` Install latest Scrapy packages easily on Ubuntu -:doc:`topics/scrapyd` +:doc:`topics/deployment` Deploying your Scrapy project in production. :doc:`topics/autothrottle` diff --git a/docs/topics/deployment.rst b/docs/topics/deployment.rst new file mode 100644 index 000000000..8f8c55c2f --- /dev/null +++ b/docs/topics/deployment.rst @@ -0,0 +1,29 @@ +.. _topics-deployment: + +========== +Deployment +========== + +The recommended way to deploy Scrapy projects to a server is through `Scrapyd`_. + +.. _Scrapyd: https://github.com/scrapy/scrapyd + +Deploying to a Scrapyd Server +============================= + +You can deploy to a Scrapyd server using the `Scrapyd client `_. You can add targets to your ``scrapy.cfg`` file which can be deployed to using the ``scrapyd-deploy`` command. + +The basic syntax is as follows: + + scrapyd-deploy -p + +For more information please refer to the `Deploying your project`_ section. + +.. _Deploying your project: https://scrapyd.readthedocs.org/en/latest/deploy.html + +Deploying to Scrapinghub +======================== + +You can deploy to Scrapinghub using Scrapinghub's command line client, `shub`_. The configuration is read from the ``scrapy.cfg`` file just like ``scrapyd-deploy``. + +.. _shub: https://github.com/scrapinghub/shub From 5916df64cd20ccc5b7fe02410b7ea485892a39da Mon Sep 17 00:00:00 2001 From: Alexander Sibiryakov Date: Wed, 1 Apr 2015 11:24:12 +0200 Subject: [PATCH 0158/2974] Removing unnecessary import. --- scrapy/crawler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 41716852c..52d4069ac 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -12,7 +12,6 @@ from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.utils.ossignal import install_shutdown_handlers, signal_names from scrapy.utils.misc import load_object from scrapy import log, signals -from pdb import set_trace class Crawler(object): From 94fceb4c1500a7add5b462beab83ea0562479880 Mon Sep 17 00:00:00 2001 From: Alexander Sibiryakov Date: Wed, 1 Apr 2015 11:25:10 +0200 Subject: [PATCH 0159/2974] Fixing underscore size. --- docs/topics/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 4812de8b9..bbe822046 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -745,7 +745,7 @@ If :setting:`DOWNLOAD_DELAY` is zero (default) this option has no effect. .. setting:: REACTOR_THREADPOOL_MAXSIZE REACTOR_THREADPOOL_MAXSIZE ------------------- +-------------------------- Default: ``10`` From e7b274edf3aa8320edf91a652e1c221654e26aa6 Mon Sep 17 00:00:00 2001 From: Alexander Sibiryakov Date: Wed, 1 Apr 2015 11:49:55 +0200 Subject: [PATCH 0160/2974] Reformat to 80 characters per line. --- docs/topics/settings.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index bbe822046..3a2876a7a 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -749,9 +749,10 @@ REACTOR_THREADPOOL_MAXSIZE Default: ``10`` -The maximum limit for Twisted Reactor thread pool size. This is common multi-purpose thread pool used by various -Scrapy components. Threaded DNS Resolver, BlockingFeedStorage, S3FilesStore just to name a few. Increase this value if -you're experiencing problems with insufficient blocking IO. +The maximum limit for Twisted Reactor thread pool size. This is common +multi-purpose thread pool used by various Scrapy components. Threaded +DNS Resolver, BlockingFeedStorage, S3FilesStore just to name a few. Increase +this value if you're experiencing problems with insufficient blocking IO. .. setting:: REDIRECT_MAX_TIMES From b794cdaf4bf5d43d5eb5d3802eb46976d5d55d14 Mon Sep 17 00:00:00 2001 From: Alexander Sibiryakov Date: Wed, 1 Apr 2015 12:07:03 +0200 Subject: [PATCH 0161/2974] Broad crawls notes. --- docs/topics/broad-crawls.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/topics/broad-crawls.rst b/docs/topics/broad-crawls.rst index b95974f5d..aaf46bc92 100644 --- a/docs/topics/broad-crawls.rst +++ b/docs/topics/broad-crawls.rst @@ -57,6 +57,27 @@ To increase the global concurrency use:: CONCURRENT_REQUESTS = 100 +Increase Twisted IO thread pool maximum size +============================================ + +Currently Scrapy does DNS resolution in a blocking way with usage of thread +pool. With higher concurrency levels the crawling could be slow or even fail +hitting DNS resolver timeouts. Possible solution to increase the number of +threads handling DNS queries. The DNS queue will be processed faster speeding +up establishing of connection and crawling overall. + +To increase maximum thread pool size use:: + + REACTOR_THREADPOOL_MAXSIZE = 20 + +Setup your own DNS +================== + +If you have multiple crawling processes and single central DNS, it can act +like DoS attack on the DNS server resulting to slow down of entire network or +even blocking your machines. To avoid this setup your own DNS server with +local cache and upstream to some large DNS like OpenDNS or Verizon. + Reduce log level ================ From 92b574309e3102d0887fe1eba19447fbf3c57c34 Mon Sep 17 00:00:00 2001 From: nyov Date: Wed, 1 Apr 2015 19:46:21 +0000 Subject: [PATCH 0162/2974] documentation build warning fixes --- docs/topics/commands.rst | 2 +- docs/topics/items.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/commands.rst b/docs/topics/commands.rst index 27d0655ef..478746d3c 100644 --- a/docs/topics/commands.rst +++ b/docs/topics/commands.rst @@ -21,7 +21,7 @@ in standard locations: 1. ``/etc/scrapy.cfg`` or ``c:\scrapy\scrapy.cfg`` (system-wide), 2. ``~/.config/scrapy.cfg`` (``$XDG_CONFIG_HOME``) and ``~/.scrapy.cfg`` (``$HOME``) - for global (user-wide) settings, and + for global (user-wide) settings, and 3. ``scrapy.cfg`` inside a scrapy project's root (see next section). Settings from these files are merged in the listed order of preference: diff --git a/docs/topics/items.rst b/docs/topics/items.rst index ac3eb6699..21ec0ed8c 100644 --- a/docs/topics/items.rst +++ b/docs/topics/items.rst @@ -22,7 +22,7 @@ Various Scrapy components use extra information provided by Items: exporters look at declared fields to figure out columns to export, serialization can be customized using Item fields metadata, :mod:`trackref` tracks Item instances to help finding memory leaks -(see :ref:`topics-leaks-trackrefs`_), etc. +(see :ref:`topics-leaks-trackrefs`), etc. .. _dictionary-like: https://docs.python.org/2/library/stdtypes.html#dict From dc88be75e26cf970f65cd5c3c7f5908cb4b68834 Mon Sep 17 00:00:00 2001 From: nyov Date: Mon, 30 Mar 2015 14:56:02 +0000 Subject: [PATCH 0163/2974] more python3 compatibility changes for urllib --- scrapy/commands/deploy.py | 25 ++++++++++--------- .../contrib/downloadermiddleware/httpproxy.py | 8 ++++-- scrapy/http/request/form.py | 5 ++-- scrapy/utils/url.py | 8 +++--- tests/mockserver.py | 5 ++-- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/scrapy/commands/deploy.py b/scrapy/commands/deploy.py index 051ce661a..b7d989f69 100644 --- a/scrapy/commands/deploy.py +++ b/scrapy/commands/deploy.py @@ -5,10 +5,11 @@ import glob import tempfile import shutil import time -import urllib2 import netrc import json +from six.moves.urllib import request from six.moves.urllib.parse import urlparse, urljoin +from six.moves.urllib.error import HTTPError, URLError from subprocess import Popen, PIPE, check_call from w3lib.form import encode_multipart @@ -69,7 +70,7 @@ class Command(ScrapyCommand): except ImportError: raise UsageError("setuptools not installed") - urllib2.install_opener(urllib2.build_opener(HTTPRedirectHandler)) + request.install_opener(request.build_opener(HTTPRedirectHandler)) if opts.list_targets: for name, target in _get_targets().items(): @@ -78,9 +79,9 @@ class Command(ScrapyCommand): if opts.list_projects: target = _get_target(opts.list_projects) - req = urllib2.Request(_url(target, 'listprojects.json')) + req = request.Request(_url(target, 'listprojects.json')) _add_auth_header(req, target) - f = urllib2.urlopen(req) + f = request.urlopen(req) projects = json.loads(f.read())['projects'] print(os.linesep.join(projects)) return @@ -188,7 +189,7 @@ def _upload_egg(target, eggpath, project, version): 'Content-Type': 'multipart/form-data; boundary=%s' % boundary, 'Content-Length': str(len(body)), } - req = urllib2.Request(url, body, headers) + req = request.Request(url, body, headers) _add_auth_header(req, target) _log('Deploying to project "%s" in %s' % (project, url)) return _http_post(req) @@ -207,14 +208,14 @@ def _add_auth_header(request, target): def _http_post(request): try: - f = urllib2.urlopen(request) + f = request.urlopen(request) _log("Server response (%s):" % f.code) print(f.read()) return True - except urllib2.HTTPError as e: + except HTTPError as e: _log("Deploy failed (%s):" % e.code) print(e.read()) - except urllib2.URLError as e: + except URLError as e: _log("Deploy failed: %s" % e) def _build_egg(): @@ -237,12 +238,12 @@ def _create_default_setup_py(**kwargs): f.write(_SETUP_PY_TEMPLATE % kwargs) -class HTTPRedirectHandler(urllib2.HTTPRedirectHandler): +class HTTPRedirectHandler(request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): newurl = newurl.replace(' ', '%20') if code in (301, 307): - return urllib2.Request(newurl, + return request.Request(newurl, data=req.get_data(), headers=req.headers, origin_req_host=req.get_origin_req_host(), @@ -250,9 +251,9 @@ class HTTPRedirectHandler(urllib2.HTTPRedirectHandler): elif code in (302, 303): newheaders = dict((k, v) for k, v in req.headers.items() if k.lower() not in ("content-length", "content-type")) - return urllib2.Request(newurl, + return request.Request(newurl, headers=newheaders, origin_req_host=req.get_origin_req_host(), unverifiable=True) else: - raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) + raise HTTPError(req.get_full_url(), code, msg, headers, fp) diff --git a/scrapy/contrib/downloadermiddleware/httpproxy.py b/scrapy/contrib/downloadermiddleware/httpproxy.py index ce09655d0..48a877c91 100644 --- a/scrapy/contrib/downloadermiddleware/httpproxy.py +++ b/scrapy/contrib/downloadermiddleware/httpproxy.py @@ -1,6 +1,10 @@ import base64 -from urllib import getproxies, unquote, proxy_bypass -from urllib2 import _parse_proxy +from six.moves.urllib.request import getproxies, proxy_bypass +from six.moves.urllib.parse import unquote +try: + from urllib2 import _parse_proxy +except ImportError: + from urllib.request import _parse_proxy from six.moves.urllib.parse import urlunparse from scrapy.utils.httpobj import urlparse_cached diff --git a/scrapy/http/request/form.py b/scrapy/http/request/form.py index 02e77ce29..a4695f1a2 100644 --- a/scrapy/http/request/form.py +++ b/scrapy/http/request/form.py @@ -5,8 +5,7 @@ This module implements the FormRequest class which is a more convenient class See documentation in docs/topics/request-response.rst """ -import urllib -from six.moves.urllib.parse import urljoin +from six.moves.urllib.parse import urljoin, urlencode import lxml.html import six from scrapy.http.request import Request @@ -52,7 +51,7 @@ def _urlencode(seq, enc): values = [(unicode_to_str(k, enc), unicode_to_str(v, enc)) for k, vs in seq for v in (vs if hasattr(vs, '__iter__') else [vs])] - return urllib.urlencode(values, doseq=1) + return urlencode(values, doseq=1) def _get_form(response, formname, formnumber, formxpath): diff --git a/scrapy/utils/url.py b/scrapy/utils/url.py index 36490a39d..5c645aaf0 100644 --- a/scrapy/utils/url.py +++ b/scrapy/utils/url.py @@ -7,8 +7,8 @@ to the w3lib.url module. Always import those from there instead. """ import posixpath from six.moves.urllib.parse import (ParseResult, urlunparse, urldefrag, - urlparse, parse_qsl) -import urllib + urlparse, parse_qsl, urlencode, + unquote) # scrapy.utils.url was moved to w3lib.url and import * ensures this move doesn't break old code from w3lib.url import * @@ -56,7 +56,7 @@ def canonicalize_url(url, keep_blank_values=True, keep_fragments=False, scheme, netloc, path, params, query, fragment = parse_url(url) keyvals = parse_qsl(query, keep_blank_values) keyvals.sort() - query = urllib.urlencode(keyvals) + query = urlencode(keyvals) path = safe_url_string(_unquotepath(path)) or '/' fragment = '' if not keep_fragments else fragment return urlunparse((scheme, netloc.lower(), path, params, query, fragment)) @@ -65,7 +65,7 @@ def canonicalize_url(url, keep_blank_values=True, keep_fragments=False, def _unquotepath(path): for reserved in ('2f', '2F', '3f', '3F'): path = path.replace('%' + reserved, '%25' + reserved.upper()) - return urllib.unquote(path) + return unquote(path) def parse_url(url, encoding=None): diff --git a/tests/mockserver.py b/tests/mockserver.py index b73208c5c..d2ba92485 100644 --- a/tests/mockserver.py +++ b/tests/mockserver.py @@ -1,6 +1,7 @@ from __future__ import print_function -import sys, time, random, urllib, os, json +import sys, time, random, os, json import six +from six.moves.urllib.parse import urlencode from subprocess import Popen, PIPE from twisted.web.server import Site, NOT_DONE_YET from twisted.web.resource import Resource @@ -73,7 +74,7 @@ class Follow(LeafResource): args = request.args.copy() for nl in nlist: args["n"] = [str(nl)] - argstr = urllib.urlencode(args, doseq=True) + argstr = urlencode(args, doseq=True) s += "follow %d
      " % (argstr, nl) s += """""" request.write(s) From 6f9265b44be6f52a2af94458a86fb4ba503fa74d Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Wed, 1 Apr 2015 23:29:10 -0300 Subject: [PATCH 0164/2974] Move spider settings population to Crawler.__init__ --- scrapy/crawler.py | 12 ++++++------ tests/test_crawler.py | 21 ++++++--------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 52d4069ac..692a896be 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -18,13 +18,17 @@ class Crawler(object): def __init__(self, spidercls, settings): self.spidercls = spidercls - self.settings = settings + self.settings = settings.copy() + self.signals = SignalManager(self) self.stats = load_object(self.settings['STATS_CLASS'])(self) lf_cls = load_object(self.settings['LOG_FORMATTER']) self.logformatter = lf_cls.from_crawler(self) self.extensions = ExtensionManager.from_crawler(self) + self.spidercls.update_settings(self.settings) + self.settings.freeze() + self.crawling = False self.spider = None self.engine = None @@ -95,11 +99,7 @@ class CrawlerRunner(object): def _create_crawler(self, spidercls): if isinstance(spidercls, six.string_types): spidercls = self.spiders.load(spidercls) - - crawler_settings = self.settings.copy() - spidercls.update_settings(crawler_settings) - crawler_settings.freeze() - return Crawler(spidercls, crawler_settings) + return Crawler(spidercls, self.settings) def _setup_crawler_logging(self, crawler): log_observer = log.start_from_crawler(crawler) diff --git a/tests/test_crawler.py b/tests/test_crawler.py index 5128ea6e9..34fc1a367 100644 --- a/tests/test_crawler.py +++ b/tests/test_crawler.py @@ -25,16 +25,6 @@ class CrawlerTestCase(unittest.TestCase): self.crawler.spiders self.assertEqual(len(w), 1, "Warn deprecated access only once") - -class CrawlerRunnerTest(unittest.TestCase): - - def setUp(self): - self.crawler_runner = CrawlerRunner(Settings()) - - def tearDown(self): - return self.crawler_runner.stop() - - @defer.inlineCallbacks def test_populate_spidercls_settings(self): spider_settings = {'TEST1': 'spider', 'TEST2': 'spider'} project_settings = {'TEST1': 'project', 'TEST3': 'project'} @@ -42,12 +32,13 @@ class CrawlerRunnerTest(unittest.TestCase): class CustomSettingsSpider(DefaultSpider): custom_settings = spider_settings - self.crawler_runner.settings.setdict(project_settings, - priority='project') + settings = Settings() + settings.setdict(project_settings, priority='project') + crawler = Crawler(CustomSettingsSpider, settings) - d = self.crawler_runner.crawl(CustomSettingsSpider) - crawler = list(self.crawler_runner.crawlers)[0] - yield d self.assertEqual(crawler.settings.get('TEST1'), 'spider') self.assertEqual(crawler.settings.get('TEST2'), 'spider') self.assertEqual(crawler.settings.get('TEST3'), 'project') + + self.assertFalse(settings.frozen) + self.assertTrue(crawler.settings.frozen) From 85aa3c7596c6e9c66daaa5503faadd03a16e1d59 Mon Sep 17 00:00:00 2001 From: Alexander Sibiryakov Date: Thu, 2 Apr 2015 18:30:59 +0200 Subject: [PATCH 0165/2974] Dns cache size and timeout options --- docs/topics/settings.rst | 18 ++++++++++++++++++ scrapy/crawler.py | 6 +++--- scrapy/resolver.py | 9 +++++++-- scrapy/settings/default_settings.py | 2 ++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index c39601b14..1986b2f3e 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -290,6 +290,24 @@ Default: ``True`` Whether to enable DNS in-memory cache. +.. setting:: DNSCACHE_SIZE + +DNSCACHE_SIZE +---------------- + +Default: ``10000`` + +DNS in-memory cache size. + +.. setting:: DNS_TIMEOUT + +DNS_TIMEOUT +---------------- + +Default: ``60`` + +Timeout for processing of DNS queries in seconds. Float is supported. + .. setting:: DOWNLOADER DOWNLOADER diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 52d4069ac..efb181fa4 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -149,9 +149,9 @@ class CrawlerProcess(CrawlerRunner): return d.addBoth(lambda _: self._stop_reactor()) - if self.settings.getbool('DNSCACHE_ENABLED'): - reactor.installResolver(CachingThreadedResolver(reactor)) - + cache_size = self.settings.getint('DNSCACHE_SIZE') if self.settings.getbool('DNSCACHE_ENABLED') else 0 + reactor.installResolver(CachingThreadedResolver(reactor, cache_size, + self.settings.getfloat('DNS_TIMEOUT'))) tp = reactor.getThreadPool() tp.adjustPoolsize(maxthreads=self.settings.getint('REACTOR_THREADPOOL_MAXSIZE')) reactor.addSystemEventTrigger('before', 'shutdown', self.stop) diff --git a/scrapy/resolver.py b/scrapy/resolver.py index e1a7aedce..3954fd977 100644 --- a/scrapy/resolver.py +++ b/scrapy/resolver.py @@ -4,15 +4,20 @@ from twisted.internet.base import ThreadedResolver from scrapy.utils.datatypes import LocalCache # TODO: cache misses -# TODO: make cache size a setting dnscache = LocalCache(10000) class CachingThreadedResolver(ThreadedResolver): + def __init__(self, reactor, cache_size, timeout): + super(CachingThreadedResolver, self).__init__(reactor) + dnscache.limit = cache_size + self.timeout = timeout - def getHostByName(self, name, timeout = (1, 3, 11, 45)): + def getHostByName(self, name, timeout=None): if name in dnscache: return defer.succeed(dnscache[name]) + if not timeout: + timeout = self.timeout d = super(CachingThreadedResolver, self).getHostByName(name, timeout) d.addCallback(self._cache_result, name) return d diff --git a/scrapy/settings/default_settings.py b/scrapy/settings/default_settings.py index 5b8dc4eaa..22005eb5d 100644 --- a/scrapy/settings/default_settings.py +++ b/scrapy/settings/default_settings.py @@ -52,6 +52,8 @@ DEPTH_STATS = True DEPTH_PRIORITY = 0 DNSCACHE_ENABLED = True +DNSCACHE_SIZE = 10000 +DNS_TIMEOUT = 60 DOWNLOAD_DELAY = 0 From 42f7674619a779b029a3a4a9db8242d90f140453 Mon Sep 17 00:00:00 2001 From: "Victoria Terenina (torymur)" Date: Thu, 2 Apr 2015 17:48:38 +0300 Subject: [PATCH 0166/2974] fixed unhandled error in deferred (RobotsTxtMiddleware) --- scrapy/contrib/downloadermiddleware/robotstxt.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scrapy/contrib/downloadermiddleware/robotstxt.py b/scrapy/contrib/downloadermiddleware/robotstxt.py index 0ea402719..a58ecca8e 100644 --- a/scrapy/contrib/downloadermiddleware/robotstxt.py +++ b/scrapy/contrib/downloadermiddleware/robotstxt.py @@ -49,8 +49,14 @@ class RobotsTxtMiddleware(object): ) dfd = self.crawler.engine.download(robotsreq, spider) dfd.addCallback(self._parse_robots) + dfd.addErrback(self._logerror, robotsreq, spider) return self._parsers[netloc] + def _logerror(self, failure, request, spider): + if failure.type is not IgnoreRequest: + log.msg(format="Error downloading %%(request)s: %s" % failure.value, + level=log.ERROR, request=request, spider=spider) + def _parse_robots(self, response): rp = robotparser.RobotFileParser(response.url) rp.parse(response.body.splitlines()) From 9d0ea5a396481e4240adeaa8d71fbc4977143fd9 Mon Sep 17 00:00:00 2001 From: dinesh Date: Sun, 2 Nov 2014 22:24:00 +0530 Subject: [PATCH 0167/2974] Added zsh completion for the Scrapy command-line tool --- debian/scrapy.install | 1 + extras/_scrapy_zsh_completion | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 extras/_scrapy_zsh_completion diff --git a/debian/scrapy.install b/debian/scrapy.install index 5977d5f43..17a5861c4 100644 --- a/debian/scrapy.install +++ b/debian/scrapy.install @@ -1 +1,2 @@ extras/scrapy_bash_completion etc/bash_completion.d/ +extras/_scrapy_zsh_completion /usr/local/share/zsh/site-functions/ diff --git a/extras/_scrapy_zsh_completion b/extras/_scrapy_zsh_completion new file mode 100644 index 000000000..3d1ae0f82 --- /dev/null +++ b/extras/_scrapy_zsh_completion @@ -0,0 +1,25 @@ +#compdef scrapy + +# zsh completion for the Scrapy command-line tool + +_scrapy() { + local curcontext="$curcontext" cmd spiders + typeset -A opt_args + cmd=$words[2] + + case "$cmd" in + crawl|edit|check) + spiders=$(scrapy list 2>/dev/null) || spiders="" + if [[ -n "$spiders" ]]; then + compadd `echo $spiders` + fi + ;; + *) + if [[ CURRENT -eq 2 ]]; then + _arguments '*: :(check crawl deploy edit fetch genspider list parse runspider server settings shell startproject version view)' + fi + ;; + esac +} + +_scrapy \ No newline at end of file From e2b4fabbf5ab118ea9bd5ad43b54c5d6e5d52aaf Mon Sep 17 00:00:00 2001 From: dinesh Date: Fri, 3 Apr 2015 08:34:16 +0530 Subject: [PATCH 0168/2974] Fix zsh completion path --- debian/scrapy.install | 2 +- extras/_scrapy_zsh_completion => scrapy_zsh_completion | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename extras/_scrapy_zsh_completion => scrapy_zsh_completion (100%) diff --git a/debian/scrapy.install b/debian/scrapy.install index 17a5861c4..c288ebed3 100644 --- a/debian/scrapy.install +++ b/debian/scrapy.install @@ -1,2 +1,2 @@ extras/scrapy_bash_completion etc/bash_completion.d/ -extras/_scrapy_zsh_completion /usr/local/share/zsh/site-functions/ +extras/scrapy_zsh_completion /usr/share/zsh/vendor-completions/_scrapy diff --git a/extras/_scrapy_zsh_completion b/scrapy_zsh_completion similarity index 100% rename from extras/_scrapy_zsh_completion rename to scrapy_zsh_completion From 7129dc36d151807e389ccbf81918f0a77a8819cd Mon Sep 17 00:00:00 2001 From: dinesh Date: Fri, 3 Apr 2015 21:20:06 +0530 Subject: [PATCH 0169/2974] Move scrapy_zsh_completion inside extras directory --- scrapy_zsh_completion => extras/scrapy_zsh_completion | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scrapy_zsh_completion => extras/scrapy_zsh_completion (100%) diff --git a/scrapy_zsh_completion b/extras/scrapy_zsh_completion similarity index 100% rename from scrapy_zsh_completion rename to extras/scrapy_zsh_completion From 843a9cdbb59a35a3340f6af07db4aa61eb2b799a Mon Sep 17 00:00:00 2001 From: dinesh Date: Fri, 3 Apr 2015 21:24:31 +0530 Subject: [PATCH 0170/2974] Remove deploy command from arguments As scrapy deploy is being removed --- extras/scrapy_zsh_completion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/scrapy_zsh_completion b/extras/scrapy_zsh_completion index 3d1ae0f82..17db1b18d 100644 --- a/extras/scrapy_zsh_completion +++ b/extras/scrapy_zsh_completion @@ -16,7 +16,7 @@ _scrapy() { ;; *) if [[ CURRENT -eq 2 ]]; then - _arguments '*: :(check crawl deploy edit fetch genspider list parse runspider server settings shell startproject version view)' + _arguments '*: :(check crawl edit fetch genspider list parse runspider server settings shell startproject version view)' fi ;; esac From b1247355dc0236fb30ac0d9d08213819d70bd062 Mon Sep 17 00:00:00 2001 From: dinesh Date: Sun, 5 Apr 2015 17:48:54 +0530 Subject: [PATCH 0171/2974] Remove server command --- extras/scrapy_zsh_completion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/scrapy_zsh_completion b/extras/scrapy_zsh_completion index 17db1b18d..564991aa8 100644 --- a/extras/scrapy_zsh_completion +++ b/extras/scrapy_zsh_completion @@ -16,7 +16,7 @@ _scrapy() { ;; *) if [[ CURRENT -eq 2 ]]; then - _arguments '*: :(check crawl edit fetch genspider list parse runspider server settings shell startproject version view)' + _arguments '*: :(check crawl edit fetch genspider list parse runspider settings shell startproject version view)' fi ;; esac From d55ae535131a411c66fe387d965b7ac0a4a375b8 Mon Sep 17 00:00:00 2001 From: dinesh Date: Sun, 5 Apr 2015 17:50:12 +0530 Subject: [PATCH 0172/2974] Remove deploy and server commands from bash completion --- extras/scrapy_bash_completion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/scrapy_bash_completion b/extras/scrapy_bash_completion index 19fd81c87..07a7d88c5 100644 --- a/extras/scrapy_bash_completion +++ b/extras/scrapy_bash_completion @@ -11,7 +11,7 @@ _scrapy_completion() { ;; *) if [ $COMP_CWORD -eq 1 ]; then - commands="check crawl deploy edit fetch genspider list parse runspider server settings shell startproject version view" + commands="check crawl edit fetch genspider list parse runspider settings shell startproject version view" COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W "$commands" -- "$cmd")) fi ;; From 5a96a16914ad6e4e54a7bd313802891f82a8dc08 Mon Sep 17 00:00:00 2001 From: Dharmesh Pandav Date: Mon, 6 Apr 2015 14:44:47 +0530 Subject: [PATCH 0173/2974] Update form.py to improve existing capability Add capability to search HTML Form using formid when using `FormRequest.from_response()` refrenced issue :https://github.com/scrapy/scrapy/issues/1136 --- scrapy/http/request/form.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scrapy/http/request/form.py b/scrapy/http/request/form.py index a4695f1a2..cae56f229 100644 --- a/scrapy/http/request/form.py +++ b/scrapy/http/request/form.py @@ -31,10 +31,10 @@ class FormRequest(Request): self._set_url(self.url + ('&' if '?' in self.url else '?') + querystr) @classmethod - def from_response(cls, response, formname=None, formnumber=0, formdata=None, + def from_response(cls, response, formname=None, formid=None, formnumber=0, formdata=None, clickdata=None, dont_click=False, formxpath=None, **kwargs): kwargs.setdefault('encoding', response.encoding) - form = _get_form(response, formname, formnumber, formxpath) + 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) @@ -67,6 +67,11 @@ def _get_form(response, formname, formnumber, formxpath): if f: return f[0] + if formid is not None: + f = root.xpath('//form[@id="%s"]' % formid) + if f: + return f[0] + # Get form element from xpath, if not found, go up if formxpath is not None: nodes = root.xpath(formxpath) From 6d48c19acd2eb9d5e9e7e8808d349b14c290bda4 Mon Sep 17 00:00:00 2001 From: nyov Date: Fri, 3 Apr 2015 13:02:56 +0000 Subject: [PATCH 0174/2974] dissolve `scrapy.contrib_exp` --- .../downloadermiddleware/decompression.py | 86 ++++++++++++++++++ scrapy/contrib_exp/__init__.py | 9 -- scrapy/contrib_exp/djangoitem.py | 6 -- .../downloadermiddleware/decompression.py | 91 ++----------------- scrapy/contrib_exp/iterators.py | 46 +--------- scrapy/utils/iterators.py | 40 ++++++++ ...test_downloadermiddleware_decompression.py | 2 +- tests/test_utils_iterators.py | 3 +- 8 files changed, 139 insertions(+), 144 deletions(-) create mode 100644 scrapy/contrib/downloadermiddleware/decompression.py delete mode 100644 scrapy/contrib_exp/djangoitem.py diff --git a/scrapy/contrib/downloadermiddleware/decompression.py b/scrapy/contrib/downloadermiddleware/decompression.py new file mode 100644 index 000000000..c08f50b5f --- /dev/null +++ b/scrapy/contrib/downloadermiddleware/decompression.py @@ -0,0 +1,86 @@ +""" This module implements the DecompressionMiddleware which tries to recognise +and extract the potentially compressed responses that may arrive. +""" + +import bz2 +import gzip +import zipfile +import tarfile +from tempfile import mktemp + +import six + +try: + from cStringIO import StringIO as BytesIO +except ImportError: + from io import BytesIO + +from scrapy import log +from scrapy.responsetypes import responsetypes + + +class DecompressionMiddleware(object): + """ This middleware tries to recognise and extract the possibly compressed + responses that may arrive. """ + + def __init__(self): + self._formats = { + 'tar': self._is_tar, + 'zip': self._is_zip, + 'gz': self._is_gzip, + 'bz2': self._is_bzip2 + } + + def _is_tar(self, response): + archive = BytesIO(response.body) + try: + tar_file = tarfile.open(name=mktemp(), fileobj=archive) + except tarfile.ReadError: + return + + body = tar_file.extractfile(tar_file.members[0]).read() + respcls = responsetypes.from_args(filename=tar_file.members[0].name, body=body) + return response.replace(body=body, cls=respcls) + + def _is_zip(self, response): + archive = BytesIO(response.body) + try: + zip_file = zipfile.ZipFile(archive) + except zipfile.BadZipfile: + return + + namelist = zip_file.namelist() + body = zip_file.read(namelist[0]) + respcls = responsetypes.from_args(filename=namelist[0], body=body) + return response.replace(body=body, cls=respcls) + + def _is_gzip(self, response): + archive = BytesIO(response.body) + try: + body = gzip.GzipFile(fileobj=archive).read() + except IOError: + return + + respcls = responsetypes.from_args(body=body) + return response.replace(body=body, cls=respcls) + + def _is_bzip2(self, response): + try: + body = bz2.decompress(response.body) + except IOError: + return + + respcls = responsetypes.from_args(body=body) + return response.replace(body=body, cls=respcls) + + def process_response(self, request, response, spider): + if not response.body: + return response + + for fmt, func in six.iteritems(self._formats): + new_response = func(response) + if new_response: + log.msg(format='Decompressed response with format: %(responsefmt)s', + level=log.DEBUG, spider=spider, responsefmt=fmt) + return new_response + return response diff --git a/scrapy/contrib_exp/__init__.py b/scrapy/contrib_exp/__init__.py index 406c7c74d..e69de29bb 100644 --- a/scrapy/contrib_exp/__init__.py +++ b/scrapy/contrib_exp/__init__.py @@ -1,9 +0,0 @@ -""" -This module contains experimental code that may go into scrapy.contrib in the -future, but it's not yet stable enough to go there (either API stable or -functionality stable). - -Subscribe to Scrapy developers mailing list or join the IRC channel if you want -to discuss about this code. - -""" diff --git a/scrapy/contrib_exp/djangoitem.py b/scrapy/contrib_exp/djangoitem.py deleted file mode 100644 index 1e855b404..000000000 --- a/scrapy/contrib_exp/djangoitem.py +++ /dev/null @@ -1,6 +0,0 @@ -import warnings -from scrapy.exceptions import ScrapyDeprecationWarning -warnings.warn("Module `scrapy.contrib_exp.djangoitem` is deprecated, use `scrapy.contrib.djangoitem` instead", - ScrapyDeprecationWarning, stacklevel=2) - -from scrapy.contrib.djangoitem import DjangoItem diff --git a/scrapy/contrib_exp/downloadermiddleware/decompression.py b/scrapy/contrib_exp/downloadermiddleware/decompression.py index c08f50b5f..8b1d61b3d 100644 --- a/scrapy/contrib_exp/downloadermiddleware/decompression.py +++ b/scrapy/contrib_exp/downloadermiddleware/decompression.py @@ -1,86 +1,7 @@ -""" This module implements the DecompressionMiddleware which tries to recognise -and extract the potentially compressed responses that may arrive. -""" +import warnings +from scrapy.exceptions import ScrapyDeprecationWarning +warnings.warn("Module `scrapy.contrib_exp.downloadermiddleware.decompression` is deprecated, " + "use `scrapy.contrib.downloadermiddleware.decompression` instead", + ScrapyDeprecationWarning, stacklevel=2) -import bz2 -import gzip -import zipfile -import tarfile -from tempfile import mktemp - -import six - -try: - from cStringIO import StringIO as BytesIO -except ImportError: - from io import BytesIO - -from scrapy import log -from scrapy.responsetypes import responsetypes - - -class DecompressionMiddleware(object): - """ This middleware tries to recognise and extract the possibly compressed - responses that may arrive. """ - - def __init__(self): - self._formats = { - 'tar': self._is_tar, - 'zip': self._is_zip, - 'gz': self._is_gzip, - 'bz2': self._is_bzip2 - } - - def _is_tar(self, response): - archive = BytesIO(response.body) - try: - tar_file = tarfile.open(name=mktemp(), fileobj=archive) - except tarfile.ReadError: - return - - body = tar_file.extractfile(tar_file.members[0]).read() - respcls = responsetypes.from_args(filename=tar_file.members[0].name, body=body) - return response.replace(body=body, cls=respcls) - - def _is_zip(self, response): - archive = BytesIO(response.body) - try: - zip_file = zipfile.ZipFile(archive) - except zipfile.BadZipfile: - return - - namelist = zip_file.namelist() - body = zip_file.read(namelist[0]) - respcls = responsetypes.from_args(filename=namelist[0], body=body) - return response.replace(body=body, cls=respcls) - - def _is_gzip(self, response): - archive = BytesIO(response.body) - try: - body = gzip.GzipFile(fileobj=archive).read() - except IOError: - return - - respcls = responsetypes.from_args(body=body) - return response.replace(body=body, cls=respcls) - - def _is_bzip2(self, response): - try: - body = bz2.decompress(response.body) - except IOError: - return - - respcls = responsetypes.from_args(body=body) - return response.replace(body=body, cls=respcls) - - def process_response(self, request, response, spider): - if not response.body: - return response - - for fmt, func in six.iteritems(self._formats): - new_response = func(response) - if new_response: - log.msg(format='Decompressed response with format: %(responsefmt)s', - level=log.DEBUG, spider=spider, responsefmt=fmt) - return new_response - return response +from scrapy.contrib.downloadermiddleware.decompression import DecompressionMiddleware diff --git a/scrapy/contrib_exp/iterators.py b/scrapy/contrib_exp/iterators.py index d96105fb9..c59f47bcc 100644 --- a/scrapy/contrib_exp/iterators.py +++ b/scrapy/contrib_exp/iterators.py @@ -1,42 +1,6 @@ -from scrapy.http import Response -from scrapy.selector import Selector +import warnings +from scrapy.exceptions import ScrapyDeprecationWarning +warnings.warn("Module `scrapy.contrib_exp.iterators` is deprecated, use `scrapy.utils.iterators` instead", + ScrapyDeprecationWarning, stacklevel=2) - -def xmliter_lxml(obj, nodename, namespace=None, prefix='x'): - from lxml import etree - reader = _StreamReader(obj) - tag = '{%s}%s' % (namespace, nodename) if namespace else nodename - iterable = etree.iterparse(reader, tag=tag, encoding=reader.encoding) - selxpath = '//' + ('%s:%s' % (prefix, nodename) if namespace else nodename) - for _, node in iterable: - nodetext = etree.tostring(node) - node.clear() - xs = Selector(text=nodetext, type='xml') - if namespace: - xs.register_namespace(prefix, namespace) - yield xs.xpath(selxpath)[0] - - -class _StreamReader(object): - - def __init__(self, obj): - self._ptr = 0 - if isinstance(obj, Response): - self._text, self.encoding = obj.body, obj.encoding - else: - self._text, self.encoding = obj, 'utf-8' - self._is_unicode = isinstance(self._text, unicode) - - def read(self, n=65535): - self.read = self._read_unicode if self._is_unicode else self._read_string - return self.read(n).lstrip() - - def _read_string(self, n=65535): - s, e = self._ptr, self._ptr + n - self._ptr = e - return self._text[s:e] - - def _read_unicode(self, n=65535): - s, e = self._ptr, self._ptr + n - self._ptr = e - return self._text[s:e].encode('utf-8') +from scrapy.utils.iterators import xmliter_lxml diff --git a/scrapy/utils/iterators.py b/scrapy/utils/iterators.py index 78ea7114e..a889114d5 100644 --- a/scrapy/utils/iterators.py +++ b/scrapy/utils/iterators.py @@ -35,6 +35,46 @@ def xmliter(obj, nodename): yield Selector(text=nodetext, type='xml').xpath('//' + nodename)[0] +def xmliter_lxml(obj, nodename, namespace=None, prefix='x'): + from lxml import etree + reader = _StreamReader(obj) + tag = '{%s}%s' % (namespace, nodename) if namespace else nodename + iterable = etree.iterparse(reader, tag=tag, encoding=reader.encoding) + selxpath = '//' + ('%s:%s' % (prefix, nodename) if namespace else nodename) + for _, node in iterable: + nodetext = etree.tostring(node) + node.clear() + xs = Selector(text=nodetext, type='xml') + if namespace: + xs.register_namespace(prefix, namespace) + yield xs.xpath(selxpath)[0] + + +class _StreamReader(object): + + def __init__(self, obj): + self._ptr = 0 + if isinstance(obj, Response): + self._text, self.encoding = obj.body, obj.encoding + else: + self._text, self.encoding = obj, 'utf-8' + self._is_unicode = isinstance(self._text, unicode) + + def read(self, n=65535): + self.read = self._read_unicode if self._is_unicode else self._read_string + return self.read(n).lstrip() + + def _read_string(self, n=65535): + s, e = self._ptr, self._ptr + n + self._ptr = e + return self._text[s:e] + + def _read_unicode(self, n=65535): + s, e = self._ptr, self._ptr + n + self._ptr = e + return self._text[s:e].encode('utf-8') + + def csviter(obj, delimiter=None, headers=None, encoding=None, quotechar=None): """ Returns an iterator of dictionaries from the given csv object diff --git a/tests/test_downloadermiddleware_decompression.py b/tests/test_downloadermiddleware_decompression.py index 81e12b4f9..f3f862604 100644 --- a/tests/test_downloadermiddleware_decompression.py +++ b/tests/test_downloadermiddleware_decompression.py @@ -1,6 +1,6 @@ from unittest import TestCase, main from scrapy.http import Response, XmlResponse -from scrapy.contrib_exp.downloadermiddleware.decompression import DecompressionMiddleware +from scrapy.contrib.downloadermiddleware.decompression import DecompressionMiddleware from scrapy.spider import Spider from tests import get_testdata from scrapy.utils.test import assert_samelines diff --git a/tests/test_utils_iterators.py b/tests/test_utils_iterators.py index d8faa810c..a7042a6cf 100644 --- a/tests/test_utils_iterators.py +++ b/tests/test_utils_iterators.py @@ -1,8 +1,7 @@ import os from twisted.trial import unittest -from scrapy.utils.iterators import csviter, xmliter, _body_or_str -from scrapy.contrib_exp.iterators import xmliter_lxml +from scrapy.utils.iterators import csviter, xmliter, _body_or_str, xmliter_lxml from scrapy.http import XmlResponse, TextResponse, Response from tests import get_testdata From 9ea309c354711e5e33864624a16b4f15b8ddd242 Mon Sep 17 00:00:00 2001 From: Pablo Hoffman Date: Thu, 9 Apr 2015 16:56:35 -0300 Subject: [PATCH 0175/2974] rename deployment.rst -> deploy.rst (consist with others like debug.rst) --- docs/topics/{deployment.rst => deploy.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/topics/{deployment.rst => deploy.rst} (100%) diff --git a/docs/topics/deployment.rst b/docs/topics/deploy.rst similarity index 100% rename from docs/topics/deployment.rst rename to docs/topics/deploy.rst From 1a12922b77fcabaea66b118980318a0f7117d2c0 Mon Sep 17 00:00:00 2001 From: Pablo Hoffman Date: Thu, 9 Apr 2015 17:39:01 -0300 Subject: [PATCH 0176/2974] improve scrapy deploy documentation --- docs/faq.rst | 2 +- docs/index.rst | 6 ++-- docs/topics/deploy.rst | 77 ++++++++++++++++++++++++++++++++---------- 3 files changed, 64 insertions(+), 21 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 71d9e4c4e..b92d173dd 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -154,7 +154,7 @@ For more info see: What is the recommended way to deploy a Scrapy crawler in production? --------------------------------------------------------------------- -See :ref:`topics-scrapyd`. +See :ref:`topics-deploy`. Can I use JSON for large exports? --------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 4097aba31..b442df4ba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -150,7 +150,7 @@ Solving specific problems topics/leaks topics/images topics/ubuntu - topics/deployment + topics/deploy topics/autothrottle topics/benchmarking topics/jobs @@ -186,8 +186,8 @@ Solving specific problems :doc:`topics/ubuntu` Install latest Scrapy packages easily on Ubuntu -:doc:`topics/deployment` - Deploying your Scrapy project in production. +:doc:`topics/deploy` + Deploying your Scrapy spiders and run them in a remote server. :doc:`topics/autothrottle` Adjust crawl rate dynamically based on load. diff --git a/docs/topics/deploy.rst b/docs/topics/deploy.rst index 8f8c55c2f..f57069921 100644 --- a/docs/topics/deploy.rst +++ b/docs/topics/deploy.rst @@ -1,29 +1,72 @@ -.. _topics-deployment: +.. _topics-deploy: -========== -Deployment -========== +================= +Deploying Spiders +================= -The recommended way to deploy Scrapy projects to a server is through `Scrapyd`_. +This section describes the different options you have for deploying your Scrapy +spiders to run them on a regular basis. Running Scrapy spiders in your local +machine is very convenient for the (early) development stage, but not so much +when you need to execute long-running spiders or move spiders to run in +production continously. This is where the solutions for deploying Scrapy +spiders come in. -.. _Scrapyd: https://github.com/scrapy/scrapyd +The most popular choices, for deploying Scrapy spiders, are: + +* :ref:`Scrapy Cloud ` (open source, easier to setup) +* :ref:`Scrapyd ` (open source, harder to setup) + +.. _deploy-scrapy-cloud: + +Deploying to Scrapy Cloud +========================= + +`Scrapy Cloud`_ is a hosted, cloud-based service by `Scrapinghub`_, the company +behind Scrapy. + +Advantages: + +- easy to setup (no need to setup or manage servers) +- well-designed UI to manage spiders and review scraped items, logs and stats +- cheap pricing (cheaper than renting a server, for small workloads) + +Disadvantages: + +- it's not open source + +To deploy spiders to Scrapy Cloud you can use the `shub`_ command line tool. +Please refer to the `Scrapy Cloud documentation`_ for more information. + +The configuration is read from the ``scrapy.cfg`` file just like +``scrapyd-deploy``. + +.. _deploy-scrapyd: Deploying to a Scrapyd Server ============================= -You can deploy to a Scrapyd server using the `Scrapyd client `_. You can add targets to your ``scrapy.cfg`` file which can be deployed to using the ``scrapyd-deploy`` command. +`Scrapyd`_ is an open source application to run Scrapy spiders. It is +maintained by some of the Scrapy developers. -The basic syntax is as follows: +Advantages: - scrapyd-deploy -p +- it's open source, so it can be installed and run anywhere -For more information please refer to the `Deploying your project`_ section. +Disadvantages: +- simple UI (no analytics, graphs or rich log/items browsing) +- requires setting up servers, installing and configuring scrapyd on them. An + APT repo with Ubuntu packages is provided by the Scrapyd team + +To deploy spiders to Scrapyd, you can use the scrapyd-deploy tool provided by +the `scrapyd-client`_ package. Please refer to the `scrapyd-deploy +documentation`_ for more information. + +.. _Scrapyd: https://github.com/scrapy/scrapyd .. _Deploying your project: https://scrapyd.readthedocs.org/en/latest/deploy.html - -Deploying to Scrapinghub -======================== - -You can deploy to Scrapinghub using Scrapinghub's command line client, `shub`_. The configuration is read from the ``scrapy.cfg`` file just like ``scrapyd-deploy``. - -.. _shub: https://github.com/scrapinghub/shub +.. _Scrapy Cloud: http://scrapinghub.com/scrapy-cloud/ +.. _scrapyd-client: https://github.com/scrapy/scrapyd-client +.. _shub: http://doc.scrapinghub.com/shub.html +.. _scrapyd-deploy documentation: http://scrapyd.readthedocs.org/en/latest/deploy.html +.. _Scrapy Cloud documentation: http://doc.scrapinghub.com/scrapy-cloud.html +.. _Scrapinghub: http://scrapinghub.com/ From 39fd2e62ef8f67170edcd6d9b3da68ee0ba2c4e5 Mon Sep 17 00:00:00 2001 From: Pablo Hoffman Date: Thu, 9 Apr 2015 18:01:11 -0300 Subject: [PATCH 0177/2974] fix typo in deploy.rst --- docs/topics/deploy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/deploy.rst b/docs/topics/deploy.rst index f57069921..e2e796cf7 100644 --- a/docs/topics/deploy.rst +++ b/docs/topics/deploy.rst @@ -13,7 +13,7 @@ spiders come in. The most popular choices, for deploying Scrapy spiders, are: -* :ref:`Scrapy Cloud ` (open source, easier to setup) +* :ref:`Scrapy Cloud ` (cloud-based, easier to setup) * :ref:`Scrapyd ` (open source, harder to setup) .. _deploy-scrapy-cloud: From 543d02bc64a76d7e508b05bff31cb3a6515a2461 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Thu, 9 Apr 2015 21:36:24 -0300 Subject: [PATCH 0178/2974] Avoid name clashing in deploy.py, fixes #1143 --- scrapy/commands/deploy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scrapy/commands/deploy.py b/scrapy/commands/deploy.py index b7d989f69..bd8ab3f15 100644 --- a/scrapy/commands/deploy.py +++ b/scrapy/commands/deploy.py @@ -194,21 +194,21 @@ def _upload_egg(target, eggpath, project, version): _log('Deploying to project "%s" in %s' % (project, url)) return _http_post(req) -def _add_auth_header(request, target): +def _add_auth_header(req, target): if 'username' in target: u, p = target.get('username'), target.get('password', '') - request.add_header('Authorization', basic_auth_header(u, p)) + req.add_header('Authorization', basic_auth_header(u, p)) else: # try netrc try: host = urlparse(target['url']).hostname a = netrc.netrc().authenticators(host) - request.add_header('Authorization', basic_auth_header(a[0], a[2])) + req.add_header('Authorization', basic_auth_header(a[0], a[2])) except (netrc.NetrcParseError, IOError, TypeError): pass -def _http_post(request): +def _http_post(req): try: - f = request.urlopen(request) + f = request.urlopen(req) _log("Server response (%s):" % f.code) print(f.read()) return True From 6b4439eacc5612ce584de94053e725610f2e15a5 Mon Sep 17 00:00:00 2001 From: bosnj Date: Fri, 10 Apr 2015 15:32:32 +0200 Subject: [PATCH 0179/2974] default return value for extract_first --- scrapy/selector/unified.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapy/selector/unified.py b/scrapy/selector/unified.py index c0eefb85e..db8b0bc2d 100644 --- a/scrapy/selector/unified.py +++ b/scrapy/selector/unified.py @@ -182,9 +182,9 @@ class SelectorList(list): def extract(self): return [x.extract() for x in self] - def extract_first(self): + def extract_first(self, default=None): for x in self: - return x.extract() + return x.extract() or default @deprecated(use_instead='.extract()') def extract_unquoted(self): From bc705843444472dbcb8eb583b8d87e055a1b9798 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Fri, 10 Apr 2015 11:52:57 -0300 Subject: [PATCH 0180/2974] Support crawlers as first positional arg in CrawlerRunner.crawl() --- scrapy/crawler.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 692a896be..615303528 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -82,9 +82,12 @@ class CrawlerRunner(object): self.crawlers = set() self._active = set() - def crawl(self, spidercls, *args, **kwargs): - crawler = self._create_crawler(spidercls) - self._setup_crawler_logging(crawler) + def crawl(self, crawler_or_spidercls, *args, **kwargs): + crawler = crawler_or_spidercls + if not isinstance(crawler_or_spidercls, Crawler): + crawler = self._create_crawler(crawler_or_spidercls) + self._setup_crawler_logging(crawler) + self.crawlers.add(crawler) d = crawler.crawl(*args, **kwargs) self._active.add(d) From 86b095132770f8d1ba84f6502a5929c6ae102e04 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Fri, 10 Apr 2015 12:06:27 -0300 Subject: [PATCH 0181/2974] Delete `crawler_deferreds` doc in CrawlerRunner This attribute is now an internal one since it's main use-case was covered by CrawlerRunner.stop(). --- docs/topics/api.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 57b8ee0cf..70b75a868 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -116,11 +116,6 @@ how you :ref:`configure the downloader middlewares Set of :class:`crawlers ` created by the :meth:`crawl` method. - .. attribute:: crawl_deferreds - - Set of the `deferreds`_ return by the :meth:`crawl` method. This - collection it's useful for keeping track of current crawling state. - .. method:: crawl(spidercls, \*args, \**kwargs) This method sets up the crawling of the given `spidercls` with the From 3dabde6706b6c2f1015167f959fefec836bab9b1 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Fri, 10 Apr 2015 12:09:07 -0300 Subject: [PATCH 0182/2974] Update docs for CrawlerRunner.crawl() new usage --- docs/topics/api.rst | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 70b75a868..2055682dc 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -101,7 +101,7 @@ how you :ref:`configure the downloader middlewares .. class:: CrawlerRunner(settings) - This is a convenient helper class that creates, configures and runs + This is a convenient helper class that keeps track of, manages and runs crawlers inside an already setup Twisted `reactor`_. The CrawlerRunner object must be instantiated with a @@ -116,18 +116,23 @@ how you :ref:`configure the downloader middlewares Set of :class:`crawlers ` created by the :meth:`crawl` method. - .. method:: crawl(spidercls, \*args, \**kwargs) + .. method:: crawl(crawler_or_spidercls, \*args, \**kwargs) - This method sets up the crawling of the given `spidercls` with the - provided arguments. + This method runs a crawler with the provided arguments. - It takes care of loading the spider class while configuring and starting - a crawler for it. + It will keep track of the given crawler so it can be stopped later, + while calling its :meth:`Crawler.crawl` method. + + If `crawler_or_spidercls` isn't a :class:`~scrapy.crawler.Crawler` + instance, it will try to create one using this parameter as the spider + class given to it. Returns a deferred that is fired when the crawl is finished. - :param spidercls: spider class or spider's name inside the project - :type spidercls: :class:`~scrapy.spider.Spider` subclass or str + :param crawler_or_spidercls: already created crawler, or a spider class + or spider's name inside the project to create it + :type crawler_or_spidercls: :class:`~scrapy.crawler.Crawler` instance, + :class:`~scrapy.spider.Spider` subclass or string :param args: arguments to initializate the spider :type args: list From 24a07fd892c1bc70b284f4e41780681888516336 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Fri, 10 Apr 2015 13:43:33 -0300 Subject: [PATCH 0183/2974] Verify SPIDER_MANAGER_CLASS interface in CrawlerRunner --- scrapy/crawler.py | 3 +++ tests/test_crawler.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 692a896be..82d94144d 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -3,9 +3,11 @@ import signal import warnings from twisted.internet import reactor, defer +from zope.interface.verify import verifyClass from scrapy.core.engine import ExecutionEngine from scrapy.resolver import CachingThreadedResolver +from scrapy.interfaces import ISpiderManager from scrapy.extension import ExtensionManager from scrapy.signalmanager import SignalManager from scrapy.exceptions import ScrapyDeprecationWarning @@ -78,6 +80,7 @@ class CrawlerRunner(object): def __init__(self, settings): self.settings = settings smcls = load_object(settings['SPIDER_MANAGER_CLASS']) + verifyClass(ISpiderManager, smcls) self.spiders = smcls.from_settings(settings.frozencopy()) self.crawlers = set() self._active = set() diff --git a/tests/test_crawler.py b/tests/test_crawler.py index 34fc1a367..2cf860593 100644 --- a/tests/test_crawler.py +++ b/tests/test_crawler.py @@ -2,6 +2,7 @@ import warnings import unittest from twisted.internet import defer +from zope.interface.verify import DoesNotImplement from scrapy.crawler import Crawler, CrawlerRunner from scrapy.settings import Settings @@ -42,3 +43,19 @@ class CrawlerTestCase(unittest.TestCase): self.assertFalse(settings.frozen) self.assertTrue(crawler.settings.frozen) + + +def SpiderManagerWithWrongInterface(object): + + def unneeded_method(self): + pass + + +class CrawlerRunnerTestCase(unittest.TestCase): + + def test_spider_manager_verify_interface(self): + settings = Settings({ + 'SPIDER_MANAGER_CLASS': 'tests.test_crawler.SpiderManagerWithWrongInterface' + }) + with self.assertRaises(DoesNotImplement): + CrawlerRunner(settings) From bf301b73284153f8f6de0be5eb47dbaa197a9d3e Mon Sep 17 00:00:00 2001 From: Nikolaos-Digenis Karagiannis Date: Mon, 17 Nov 2014 19:30:08 +0200 Subject: [PATCH 0184/2974] encode invalid xpath with unicode_escape under PY2 The exception quotes an xpath string which may be unicode. --- scrapy/selector/unified.py | 4 +++- tests/test_selector.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/scrapy/selector/unified.py b/scrapy/selector/unified.py index c0eefb85e..90dcfd686 100644 --- a/scrapy/selector/unified.py +++ b/scrapy/selector/unified.py @@ -3,6 +3,7 @@ XPath selectors based on lxml """ from lxml import etree +import six from scrapy.utils.misc import extract_regex from scrapy.utils.trackref import object_ref @@ -95,7 +96,8 @@ class Selector(object_ref): result = xpathev(query, namespaces=self.namespaces, smart_strings=self._lxml_smart_strings) except etree.XPathError: - raise ValueError("Invalid XPath: %s" % query) + msg = u"Invalid XPath: %s" % query + raise ValueError(msg if six.PY3 else msg.encode("unicode_escape")) if type(result) is not list: result = [result] diff --git a/tests/test_selector.py b/tests/test_selector.py index 9b8613319..a46f7635c 100644 --- a/tests/test_selector.py +++ b/tests/test_selector.py @@ -1,6 +1,7 @@ import re import warnings import weakref +import six from twisted.trial import unittest from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.http import TextResponse, HtmlResponse, XmlResponse @@ -242,17 +243,19 @@ class SelectorTestCase(unittest.TestCase): self.assertEqual(xs.xpath('.').extract(), [u'lala']) def test_invalid_xpath(self): + "Test invalid xpath raises ValueError with the invalid xpath" response = XmlResponse(url="http://example.com", body="") x = self.sscls(response) xpath = "//test[@foo='bar]" - try: - x.xpath(xpath) - except ValueError as e: - assert xpath in str(e), "Exception message does not contain invalid xpath" - except Exception: - raise AssertionError("A invalid XPath does not raise ValueError") - else: - raise AssertionError("A invalid XPath does not raise an exception") + self.assertRaisesRegexp(ValueError, re.escape(xpath), x.xpath, xpath) + + def test_invalid_xpath_unicode(self): + "Test *Unicode* invalid xpath raises ValueError with the invalid xpath" + response = XmlResponse(url="http://example.com", body="") + x = self.sscls(response) + xpath = u"//test[@foo='\u0431ar]" + encoded = xpath if six.PY3 else xpath.encode('unicode_escape') + self.assertRaisesRegexp(ValueError, re.escape(encoded), x.xpath, xpath) def test_http_header_encoding_precedence(self): # u'\xa3' = pound symbol in unicode From dce48b8654977d3f7a003addd507b0aa5e71a76c Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Sat, 11 Apr 2015 13:57:55 -0300 Subject: [PATCH 0185/2974] documenting Files Pipeline together with Images Pipeline --- docs/index.rst | 2 +- docs/topics/images.rst | 250 ++++++++++++++++++++++++++--------------- 2 files changed, 159 insertions(+), 93 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index de3e015d5..f61e7f348 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -181,7 +181,7 @@ Solving specific problems Learn how to find and get rid of memory leaks in your crawler. :doc:`topics/images` - Download static images associated with your scraped items. + Download files and/or images associated with your scraped items. :doc:`topics/ubuntu` Install latest Scrapy packages easily on Ubuntu diff --git a/docs/topics/images.rst b/docs/topics/images.rst index 890c7fd4a..ab4a98ebd 100644 --- a/docs/topics/images.rst +++ b/docs/topics/images.rst @@ -1,76 +1,94 @@ .. _topics-images: -======================= -Downloading Item Images -======================= +=========================================== +Downloading and processing files and images +=========================================== .. currentmodule:: scrapy.contrib.pipeline.images -Scrapy provides an :doc:`item pipeline ` for downloading -images attached to a particular item, for example, when you scrape products and -also want to download their images locally. +Scrapy provides reusable :doc:`item pipelines ` for +downloading fies attached to a particular item (for example, when you scrape +products and also want to download their images locally). These pipelines share +a bit of functionality and structure (we refer to them as media pipelines), but +typically you'll either use the Files Pipeline or the Images Pipeline. -This pipeline, called the Images Pipeline and implemented in the -:class:`ImagesPipeline` class, provides a convenient way for -downloading and storing images locally with some additional features: +Both pipelines implement these features: + +* Avoid re-downloading media that was downloaded recently +* Specifying where to store the files (filesystem directory, Amazon S3 bucket) + +The Images Pipeline has a few extra functions for processing images: * Convert all downloaded images to a common format (JPG) and mode (RGB) -* Avoid re-downloading images which were downloaded recently * Thumbnail generation * Check images width/height to make sure they meet a minimum constraint -This pipeline also keeps an internal queue of those images which are currently -being scheduled for download, and connects those items that arrive containing -the same image, to that queue. This avoids downloading the same image more than +The pipelines also keep an internal queue of those images which are currently +being scheduled for download, and connect those items that arrive containing +the same image to that queue. This avoids downloading the same media more than once when it's shared by several items. -`Pillow`_ is used for thumbnailing and normalizing images to JPEG/RGB format, -so you need to install this library in order to use the images pipeline. -`Python Imaging Library`_ (PIL) should also work in most cases, but it -is known to cause troubles in some setups, so we recommend to use `Pillow`_ -instead of `PIL `_. +The Images Pipeline uses `Pillow`_ for thumbnailing and normalizing images to +JPEG/RGB format, so you need to install this library in order to use it. +`Python Imaging Library`_ (PIL) should also work in most cases, but it is known +to cause troubles in some setups, so we recommend to use `Pillow`_ instead of +`PIL `_. .. _Pillow: https://github.com/python-pillow/Pillow .. _Python Imaging Library: http://www.pythonware.com/products/pil/ -Using the Images Pipeline +Using the Files Pipeline ========================= -The typical workflow, when using the :class:`ImagesPipeline` goes like +The typical workflow, when using the :class:`FilesPipeline` goes like this: -1. In a Spider, you scrape an item and put the URLs of its images into a - ``image_urls`` field. +1. In a Spider, you scrape an item and put the URLs of the desired into a + ``file_urls`` field. 2. The item is returned from the spider and goes to the item pipeline. -3. When the item reaches the :class:`ImagesPipeline`, the URLs in the - ``image_urls`` field are scheduled for download using the standard +3. When the item reaches the :class:`FilesPipeline`, the URLs in the + ``file_urls`` field are scheduled for download using the standard Scrapy scheduler and downloader (which means the scheduler and downloader middlewares are reused), but with a higher priority, processing them before other pages are scraped. The item remains "locked" at that particular pipeline stage - until the images have finish downloading (or fail for some reason). + until the files have finish downloading (or fail for some reason). -4. When the images are downloaded another field (``images``) will be populated +4. When the files are downloaded, another field (``files``) will be populated with the results. This field will contain a list of dicts with information - about the images downloaded, such as the downloaded path, the original - scraped url (taken from the ``image_urls`` field) , and the image checksum. - The images in the list of the ``images`` field will retain the same order of - the original ``image_urls`` field. If some image failed downloading, an - error will be logged and the image won't be present in the ``images`` field. + about the downloaded files, such as the downloaded path, the original + scraped url (taken from the ``file_urls`` field) , and the file checksum. + The files in the list of the ``files`` field will retain the same order of + the original ``file_urls`` field. If some file failed downloading, an + error will be logged and the file won't be present in the ``files`` field. + + +Using the Images Pipeline +========================= + +Using the :class:`ImagesPipeline` is a lot like using the :class:`FilesPipeline`, +except the default field names used are different: you use ``image_urls`` for +the image URLs of an item and it will populate an ``images`` field for the information +about the downloaded images. + +The advantage of using the :class:`ImagesPipeline` for image files is that you +can configure some extra functions like generating thumbnails and filtering +the images based on their size. Usage example ============= -In order to use the image pipeline first -:ref:`enable it `. +In order to use a media pipeline first, :ref:`enable it +`. -Then, if a spider returns a dict with 'image_urls' key, -the pipeline will put the results under 'images' key. +Then, if a spider returns a dict with the URLs key ('file_urls' or +'image_urls', for the Files or Images Pipeline respectively), the pipeline will +put the results under respective key ('files' or images'). -If you prefer to use :class:`~.Item` then define a custom -item with the ``image_urls`` and ``images`` fields:: +If you prefer to use :class:`~.Item`, then define a custom item with the +necessary fields, like in this example for Images Pipeline:: import scrapy @@ -80,42 +98,57 @@ item with the ``image_urls`` and ``images`` fields:: image_urls = scrapy.Field() images = scrapy.Field() -If you need something more complex and want to override the custom images -pipeline behaviour, see :ref:`topics-images-override`. +If you need something more complex and want to override the custom pipeline +behaviour, see :ref:`topics-media-pipeline-override`. -.. _topics-images-enabling: +.. _topics-media-pipeline-enabling: -Enabling your Images Pipeline +Enabling your Media Pipeline ============================= .. setting:: IMAGES_STORE +.. setting:: FILES_STORE -To enable your images pipeline you must first add it to your project -:setting:`ITEM_PIPELINES` setting:: +To enable your media pipeline you must first add it to your project +:setting:`ITEM_PIPELINES` setting. + +For Images Pipeline, use:: ITEM_PIPELINES = {'scrapy.contrib.pipeline.images.ImagesPipeline': 1} -And set the :setting:`IMAGES_STORE` setting to a valid directory that will be -used for storing the downloaded images. Otherwise the pipeline will remain -disabled, even if you include it in the :setting:`ITEM_PIPELINES` setting. +For Files Pipeline, use:: -For example:: + ITEM_PIPELINES = {'scrapy.contrib.pipeline.files.FilesPipeline': 1} + + +.. note:: + You can also use both the Files and Images Pipeline at the same time. + + +Then, configure the target storage setting to a valid value that will be used +for storing the downloaded images. Otherwise the pipeline will remain disabled, +even if you include it in the :setting:`ITEM_PIPELINES` setting. + +For the Files Pipeline, set the :setting:`FILES_STORE` setting:: + + FILES_STORE = '/path/to/valid/dir' + +For the Images Pipeline, set the :setting:`IMAGES_STORE` setting:: IMAGES_STORE = '/path/to/valid/dir' -Images Storage -============== +Supported Storage +================= File system is currently the only officially supported storage, but there is -also (undocumented) support for `Amazon S3`_. +also (undocumented) support for storing files in `Amazon S3`_. .. _Amazon S3: http://aws.amazon.com/s3/ File system storage ------------------- -The images are stored in files (one per image), using a `SHA1 hash`_ of their -URLs for the file names. +The files are stored using a `SHA1 hash`_ of their URLs for the file names. For example, the following image URL:: @@ -132,29 +165,36 @@ Will be downloaded and stored in the following file:: Where: * ```` is the directory defined in :setting:`IMAGES_STORE` setting +for the Images Pipeline. * ``full`` is a sub-directory to separate full images from thumbnails (if used). For more info see :ref:`topics-images-thumbnails`. + Additional features =================== -Image expiration +File expiration ---------------- .. setting:: IMAGES_EXPIRES +.. setting:: FILES_EXPIRES -The Image Pipeline avoids downloading images that were downloaded recently. To -adjust this retention delay use the :setting:`IMAGES_EXPIRES` setting, which +The Image Pipeline avoids downloading files that were downloaded recently. To +adjust this retention delay use the :setting:`FILES_EXPIRES` setting (or +:setting:`IMAGES_EXPIRES`, in case of Images Pipeline), which specifies the delay in number of days:: - # 90 days of delay for image expiration - IMAGES_EXPIRES = 90 + # 90 days of delay for files expiration + FILES_EXPIRES = 90 + + # 30 days of delay for images expiration + IMAGES_EXPIRES = 30 .. _topics-images-thumbnails: -Thumbnail generation --------------------- +Thumbnail generation for images +------------------------------- The Images Pipeline can automatically create thumbnails of the downloaded images. @@ -200,8 +240,9 @@ Filtering out small images .. setting:: IMAGES_MIN_WIDTH -You can drop images which are too small, by specifying the minimum allowed size -in the :setting:`IMAGES_MIN_HEIGHT` and :setting:`IMAGES_MIN_WIDTH` settings. +When using the Images Pipeline, you can drop images which are too small, by +specifying the minimum allowed size in the :setting:`IMAGES_MIN_HEIGHT` and +:setting:`IMAGES_MIN_WIDTH` settings. For example:: @@ -212,45 +253,45 @@ Note: these size constraints don't affect thumbnail generation at all. By default, there are no size constraints, so all images are processed. -.. _topics-images-override: +.. _topics-media-pipeline-override: -Implementing your custom Images Pipeline -======================================== +Extending the Media Pipelines +============================= -.. module:: scrapy.contrib.pipeline.images - :synopsis: Images Pipeline +.. module:: scrapy.contrib.pipeline.files + :synopsis: Files Pipeline -Here are the methods that you should override in your custom Images Pipeline: +See here the methods that you can override in your custom Files Pipeline: -.. class:: ImagesPipeline +.. class:: FilesPipeline - .. method:: get_media_requests(item, info) + .. method:: FilesPipeline.get_media_requests(item, info) As seen on the workflow, the pipeline will get the URLs of the images to - download from the item. In order to do this, you must override the + download from the item. In order to do this, you can override the :meth:`~get_media_requests` method and return a Request for each - image URL:: + file URL:: def get_media_requests(self, item, info): - for image_url in item['image_urls']: - yield scrapy.Request(image_url) + for file_url in item['file_urls']: + yield scrapy.Request(file_url) Those requests will be processed by the pipeline and, when they have finished downloading, the results will be sent to the :meth:`~item_completed` method, as a list of 2-element tuples. - Each tuple will contain ``(success, image_info_or_failure)`` where: + Each tuple will contain ``(success, file_info_or_error)`` where: * ``success`` is a boolean which is ``True`` if the image was downloaded successfully or ``False`` if it failed for some reason - * ``image_info_or_error`` is a dict containing the following keys (if success + * ``file_info_or_error`` is a dict containing the following keys (if success is ``True``) or a `Twisted Failure`_ if there was a problem. - * ``url`` - the url where the image was downloaded from. This is the url of + * ``url`` - the url where the file was downloaded from. This is the url of the request returned from the :meth:`~get_media_requests` method. - * ``path`` - the path (relative to :setting:`IMAGES_STORE`) where the image + * ``path`` - the path (relative to :setting:`FILES_STORE`) where the file was stored * ``checksum`` - a `MD5 hash`_ of the image contents @@ -263,21 +304,17 @@ Here are the methods that you should override in your custom Images Pipeline: [(True, {'checksum': '2b00042f7481c7b056c4b410d28f33cf', - 'path': 'full/7d97e98f8af710c7e7fe703abc8f639e0ee507c4.jpg', - 'url': 'http://www.example.com/images/product1.jpg'}), - (True, - {'checksum': 'b9628c4ab9b595f72f280b90c4fd093d', - 'path': 'full/1ca5879492b8fd606df1964ea3c1e2f4520f076f.jpg', - 'url': 'http://www.example.com/images/product2.jpg'}), + 'path': 'full/0a79c461a4062ac383dc4fade7bc09f1384a3910.jpg', + 'url': 'http://www.example.com/files/product1.pdf'}), (False, Failure(...))] By default the :meth:`get_media_requests` method returns ``None`` which - means there are no images to download for the item. + means there are no files to download for the item. - .. method:: item_completed(results, items, info) + .. method:: FilesPipeline.item_completed(results, items, info) - The :meth:`ImagesPipeline.item_completed` method called when all image + The :meth:`FilesPipeline.item_completed` method called when all file requests for a single item have completed (either finished downloading, or failed for some reason). @@ -286,21 +323,50 @@ Here are the methods that you should override in your custom Images Pipeline: return (or drop) the item, as you would in any pipeline. Here is an example of the :meth:`~item_completed` method where we - store the downloaded image paths (passed in results) in the ``image_paths`` - item field, and we drop the item if it doesn't contain any images:: + store the downloaded file paths (passed in results) in the ``file_paths`` + item field, and we drop the item if it doesn't contain any files:: from scrapy.exceptions import DropItem def item_completed(self, results, item, info): - image_paths = [x['path'] for ok, x in results if ok] - if not image_paths: - raise DropItem("Item contains no images") - item['image_paths'] = image_paths + file_paths = [x['path'] for ok, x in results if ok] + if not file_paths: + raise DropItem("Item contains no files") + item['file_paths'] = file_paths return item By default, the :meth:`item_completed` method returns the item. +.. module:: scrapy.contrib.pipeline.images + :synopsis: Images Pipeline + +See here the methods that you can override in your custom Images Pipeline: + +.. class:: ImagesPipeline + + The :class:`ImagesPipeline` is an extension of the :class:`FilesPipeline`, + customizing the field names and adding custom behavior for images. + + .. method:: ImagesPipeline.get_media_requests(item, info) + + Works the same way as :meth:`FilesPipeline.get_media_requests` method, + but using a different field name for image urls. + + Must return a Request for each image URL. + + .. method:: ImagesPipeline.item_completed(results, items, info) + + The :meth:`ImagesPipeline.item_completed` method is called when all image + requests for a single item have completed (either finished downloading, or + failed for some reason). + + Works the same way as :meth:`FilesPipeline.item_completed` method, + but using a different field names for storing image downloading results. + + By default, the :meth:`item_completed` method returns the item. + + Custom Images pipeline example ============================== From 1eccd34a7032c3e50c56f61da34ef39d1aaf43ec Mon Sep 17 00:00:00 2001 From: mrpandav Date: Sun, 12 Apr 2015 11:11:28 +0530 Subject: [PATCH 0186/2974] adding feature improvement for selecting form using form-id, in addition to formname , formnumer before we go for xpath.. making it more idiomatic in nature --- scrapy/http/request/form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/http/request/form.py b/scrapy/http/request/form.py index cae56f229..82cb5f5d4 100644 --- a/scrapy/http/request/form.py +++ b/scrapy/http/request/form.py @@ -54,7 +54,7 @@ def _urlencode(seq, enc): return urlencode(values, doseq=1) -def _get_form(response, formname, formnumber, formxpath): +def _get_form(response, formname, formid, formnumber, formxpath): """Find the form element """ from scrapy.selector.lxmldocument import LxmlDocument root = LxmlDocument(response, lxml.html.HTMLParser) From cf9d848f0f8169f82a455977c2df6390a50a1e59 Mon Sep 17 00:00:00 2001 From: Jakob de Maeyer Date: Sun, 12 Apr 2015 14:18:47 +0200 Subject: [PATCH 0187/2974] Fixed deprecated settings API in cmdline test --- tests/test_cmdline/settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_cmdline/settings.py b/tests/test_cmdline/settings.py index 9aceffb0d..8a719ddf2 100644 --- a/tests/test_cmdline/settings.py +++ b/tests/test_cmdline/settings.py @@ -1,5 +1,5 @@ -EXTENSIONS = [ - 'tests.test_cmdline.extensions.TestExtension' -] +EXTENSIONS = { + 'tests.test_cmdline.extensions.TestExtension': 0, +} TEST1 = 'default' From 91a60d9f3d4107df334edbbc42f7ef52c4e57706 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Sun, 12 Apr 2015 13:44:32 -0400 Subject: [PATCH 0188/2974] Remove references to the `sel` object in shell.rst The current documentation has references to the deprecated `sel` when interacting with the shell. I've removed them and replaced uses of `sel.xpath` with `response.xpath` instead. --- docs/topics/shell.rst | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/docs/topics/shell.rst b/docs/topics/shell.rst index 5c1cfbd47..8fce0cea1 100644 --- a/docs/topics/shell.rst +++ b/docs/topics/shell.rst @@ -85,9 +85,6 @@ Those objects are: * ``response`` - a :class:`~scrapy.http.Response` object containing the last fetched page - * ``sel`` - a :class:`~scrapy.selector.Selector` object constructed - with the last response fetched - * ``settings`` - the current :ref:`Scrapy settings ` Example of shell session @@ -117,7 +114,6 @@ all start with the ``[s]`` prefix):: [s] item {} [s] request [s] response <200 http://scrapy.org> - [s] sel \n \n >> response.xpath("//h1/text()").extract()[0] + u'Meet Scrapy' >>> fetch("http://slashdot.org") [s] Available Scrapy objects: @@ -138,7 +134,6 @@ After that, we can start playing with the objects:: [s] item {} [s] request [s] response <200 http://slashdot.org> - [s] sel \n\n\n\n\n\n', re.IGNORECASE | re.DOTALL) _metaref_cache = weakref.WeakKeyDictionary() @@ -40,10 +41,11 @@ def get_meta_refresh(response): text = response.body_as_unicode()[0:4096] text = _noscript_re.sub(u'', text) text = _script_re.sub(u'', text) - _metaref_cache[response] = html.get_meta_refresh(text, response.url, \ + _metaref_cache[response] = html.get_meta_refresh(text, response.url, response.encoding) return _metaref_cache[response] + def response_status_message(status): """Return status code plus status text descriptive message @@ -55,19 +57,21 @@ def response_status_message(status): """ return '%s %s' % (status, http.responses.get(int(status))) + def response_httprepr(response): - """Return raw HTTP representation (as string) of the given response. This + """Return raw HTTP representation (as bytes) of the given response. This is provided only for reference, since it's not the exact stream of bytes that was received (that's not exposed by Twisted). """ - - s = "HTTP/1.1 %d %s\r\n" % (response.status, RESPONSES.get(response.status, '')) + s = b"HTTP/1.1 " + to_bytes(str(response.status)) + b" " + \ + to_bytes(RESPONSES.get(response.status, b'')) + b"\r\n" if response.headers: - s += response.headers.to_string() + "\r\n" - s += "\r\n" + s += response.headers.to_string() + b"\r\n" + s += b"\r\n" s += response.body return s + def open_in_browser(response, _openfunc=webbrowser.open): """Open the given response in a local web browser, populating the tag for external links to work @@ -76,14 +80,15 @@ def open_in_browser(response, _openfunc=webbrowser.open): # XXX: this implementation is a bit dirty and could be improved body = response.body if isinstance(response, HtmlResponse): - if '', '' % response.url) + if b'', to_bytes(repl)) ext = '.html' elif isinstance(response, TextResponse): ext = '.txt' else: - raise TypeError("Unsupported response type: %s" % \ - response.__class__.__name__) + raise TypeError("Unsupported response type: %s" % + response.__class__.__name__) fd, fname = tempfile.mkstemp(ext) os.write(fd, body) os.close(fd) diff --git a/tests/py3-ignores.txt b/tests/py3-ignores.txt index c51bc5981..432b58cac 100644 --- a/tests/py3-ignores.txt +++ b/tests/py3-ignores.txt @@ -42,7 +42,6 @@ tests/test_stats.py tests/test_utils_iterators.py tests/test_utils_log.py tests/test_utils_reqser.py -tests/test_utils_response.py tests/test_utils_template.py tests/test_webclient.py diff --git a/tests/test_utils_response.py b/tests/test_utils_response.py index 92c92c057..1d1638e06 100644 --- a/tests/test_utils_response.py +++ b/tests/test_utils_response.py @@ -3,53 +3,59 @@ import unittest from six.moves.urllib.parse import urlparse from scrapy.http import Response, TextResponse, HtmlResponse -from scrapy.utils.response import response_httprepr, open_in_browser, get_meta_refresh +from scrapy.utils.python import to_bytes +from scrapy.utils.response import (response_httprepr, open_in_browser, + get_meta_refresh, get_base_url) __doctests__ = ['scrapy.utils.response'] + class ResponseUtilsTest(unittest.TestCase): - dummy_response = TextResponse(url='http://example.org/', body='dummy_response') + dummy_response = TextResponse(url='http://example.org/', body=b'dummy_response') def test_response_httprepr(self): r1 = Response("http://www.example.com") - self.assertEqual(response_httprepr(r1), 'HTTP/1.1 200 OK\r\n\r\n') + self.assertEqual(response_httprepr(r1), b'HTTP/1.1 200 OK\r\n\r\n') - r1 = Response("http://www.example.com", status=404, headers={"Content-type": "text/html"}, body="Some body") - self.assertEqual(response_httprepr(r1), 'HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\nSome body') + r1 = Response("http://www.example.com", status=404, headers={"Content-type": "text/html"}, body=b"Some body") + self.assertEqual(response_httprepr(r1), b'HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\nSome body') - r1 = Response("http://www.example.com", status=6666, headers={"Content-type": "text/html"}, body="Some body") - self.assertEqual(response_httprepr(r1), 'HTTP/1.1 6666 \r\nContent-Type: text/html\r\n\r\nSome body') + r1 = Response("http://www.example.com", status=6666, headers={"Content-type": "text/html"}, body=b"Some body") + self.assertEqual(response_httprepr(r1), b'HTTP/1.1 6666 \r\nContent-Type: text/html\r\n\r\nSome body') def test_open_in_browser(self): url = "http:///www.example.com/some/page.html" - body = " test page test body " + body = b" test page test body " + def browser_open(burl): path = urlparse(burl).path if not os.path.exists(path): path = burl.replace('file://', '') - bbody = open(path).read() - assert '' % url in bbody, " tag not added" + with open(path, "rb") as f: + bbody = f.read() + self.assertIn(b'', bbody) return True response = HtmlResponse(url, body=body) assert open_in_browser(response, _openfunc=browser_open), \ "Browser not called" - self.assertRaises(TypeError, open_in_browser, Response(url, body=body), \ - debug=True) + + resp = Response(url, body=body) + self.assertRaises(TypeError, open_in_browser, resp, debug=True) def test_get_meta_refresh(self): - r1 = HtmlResponse("http://www.example.com", body=""" + r1 = HtmlResponse("http://www.example.com", body=b""" Dummy blahablsdfsal& """) - r2 = HtmlResponse("http://www.example.com", body=""" + r2 = HtmlResponse("http://www.example.com", body=b""" Dummy blahablsdfsal& """) - r3 = HtmlResponse("http://www.example.com", body=""" + r3 = HtmlResponse("http://www.example.com", body=b"""