diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index e90cb0223..64693f25b 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -247,6 +247,18 @@ HttpCompressionMiddleware This middleware allows compressed (gzip, deflate) traffic to be sent/received from web sites. +HttpProxyMiddleware +------------------- + +.. module:: scrapy.contrib.downloadermiddleware.httpproxy + :synopsis: Http Proxy Middleware + +.. class:: HttpProxyMiddleware + + This middleware sets proxy to use for requests, it obeys enviroment + variables 'http_proxy', 'https_proxy', and 'no_proxy' + + RedirectMiddleware ------------------- diff --git a/docs/topics/settings.rst b/docs/topics/settings.rst index 79b679fad..5cd6ff5b2 100644 --- a/docs/topics/settings.rst +++ b/docs/topics/settings.rst @@ -725,6 +725,33 @@ Default: ``+2`` Adjust redirect request priority relative to original request. A negative priority adjust means more priority. +.. setting:: REQUEST_HANDLERS + +REQUEST_HANDLERS +---------------- + +Default: ``{}`` + +A dict containing the request downloader handlers enabled in your project. +See `REQUEST_HANDLERS_BASE` for example format. + +.. setting:: REQUEST_HANDLERS_BASE + +REQUEST_HANDLERS_BASE +--------------------- + +Default:: + + { + 'file': 'scrapy.core.downloader.handlers.file.download_file', + 'http': 'scrapy.core.downloader.handlers.http.download_http', + 'https': 'scrapy.core.downloader.handlers.http.download_http', + } + +A dict containing the request download handlers enabled by default in Scrapy. +You should never modify this setting in your project, modify +:setting:`REQUEST_HANDLERS` instead. + .. setting:: REQUESTS_PER_DOMAIN REQUESTS_PER_DOMAIN diff --git a/extras/build_release.sh b/extras/build_release.sh index 57873b12e..2230685e8 100755 --- a/extras/build_release.sh +++ b/extras/build_release.sh @@ -6,8 +6,8 @@ hg purge --all # build packages -version=$(python -c "import scrapy; print scrapy.__version__") -python setup.py sdist +#version=$(python -c "import scrapy; print scrapy.__version__") +#python setup.py sdist # FIXME: bdist_wininst doesn't work on Unix (it doesn't include the data_files) # To build the win32 release you need to use Windows for now. #python setup.py bdist_wininst -t "Scrapy $version" -p "win32" diff --git a/scrapy/__init__.py b/scrapy/__init__.py index bee059b0b..5e206de2f 100644 --- a/scrapy/__init__.py +++ b/scrapy/__init__.py @@ -3,8 +3,8 @@ Scrapy - a screen scraping framework written in Python """ # IMPORTANT: remember to also update the version in docs/conf.py -version_info = (0, 7, 0, 'final', 0) -__version__ = "0.7" +version_info = (0, 8, 0, '', 0) +__version__ = "0.8-dev" import sys, os diff --git a/scrapy/conf/default_settings.py b/scrapy/conf/default_settings.py index 17b4f7db0..a4d92faa9 100644 --- a/scrapy/conf/default_settings.py +++ b/scrapy/conf/default_settings.py @@ -60,6 +60,7 @@ DOWNLOADER_MIDDLEWARES_BASE = { 'scrapy.contrib.downloadermiddleware.defaultheaders.DefaultHeadersMiddleware': 550, 'scrapy.contrib.downloadermiddleware.redirect.RedirectMiddleware': 600, 'scrapy.contrib.downloadermiddleware.cookies.CookiesMiddleware': 700, + 'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': 750, 'scrapy.contrib.downloadermiddleware.httpcompression.HttpCompressionMiddleware': 800, 'scrapy.contrib.downloadermiddleware.stats.DownloaderStats': 850, 'scrapy.contrib.downloadermiddleware.httpcache.HttpCacheMiddleware': 900, @@ -100,6 +101,7 @@ ITEM_PROCESSOR = 'scrapy.contrib.pipeline.ItemPipelineManager' ITEM_PIPELINES = [] LOG_ENABLED = True +LOG_FORMATTER_CRAWLED = 'scrapy.contrib.logformatter.crawled_logline' LOG_STDOUT = False LOG_LEVEL = 'DEBUG' LOG_FILE = None @@ -124,6 +126,13 @@ REDIRECT_MAX_METAREFRESH_DELAY = 100 REDIRECT_MAX_TIMES = 20 # uses Firefox default setting REDIRECT_PRIORITY_ADJUST = +2 +REQUEST_HANDLERS = {} +REQUEST_HANDLERS_BASE = { + 'file': 'scrapy.core.downloader.handlers.file.download_file', + 'http': 'scrapy.core.downloader.handlers.http.download_http', + 'https': 'scrapy.core.downloader.handlers.http.download_http', +} + REQUESTS_QUEUE_SIZE = 0 REQUESTS_PER_DOMAIN = 8 # max simultaneous requests per domain diff --git a/scrapy/contrib/downloadermiddleware/httpproxy.py b/scrapy/contrib/downloadermiddleware/httpproxy.py new file mode 100644 index 000000000..f2b531d28 --- /dev/null +++ b/scrapy/contrib/downloadermiddleware/httpproxy.py @@ -0,0 +1,51 @@ +import base64 +from urllib import getproxies, unquote, proxy_bypass +from urllib2 import _parse_proxy +from urlparse import urlunparse + +from scrapy.utils.httpobj import urlparse_cached +from scrapy.core.exceptions import NotConfigured + + +class HttpProxyMiddleware(object): + + def __init__(self): + self.proxies = {} + for type, url in getproxies().items(): + self.proxies[type] = self._get_proxy(url, type) + + if not self.proxies: + raise NotConfigured + + def _get_proxy(self, url, orig_type): + proxy_type, user, password, hostport = _parse_proxy(url) + proxy_url = urlunparse((proxy_type or orig_type, hostport, '', '', '', '')) + + if user and password: + user_pass = '%s:%s' % (unquote(user), unquote(password)) + creds = base64.b64encode(user_pass).strip() + else: + creds = None + + return creds, proxy_url + + def process_request(self, request, spider): + # ignore if proxy is already seted + if 'proxy' in request.meta: + return + + parsed = urlparse_cached(request) + scheme = parsed.scheme + + # 'no_proxy' is only supported by http schemes + if scheme in ('http', 'https') and proxy_bypass(parsed.hostname): + return + + if scheme in self.proxies: + self._set_proxy(request, scheme) + + def _set_proxy(self, request, scheme): + creds, proxy = self.proxies[scheme] + request.meta['proxy'] = proxy + if creds: + request.headers['Proxy-Authorization'] = 'Basic ' + creds diff --git a/scrapy/contrib/logformatter.py b/scrapy/contrib/logformatter.py new file mode 100644 index 000000000..55995f6e8 --- /dev/null +++ b/scrapy/contrib/logformatter.py @@ -0,0 +1,7 @@ +"""Functions for logging diferent actions""" + +def crawled_logline(request, response): + referer = request.headers.get('Referer') + flags = ' %s' % str(response.flags) if response.flags else '' + return "Crawled (%d) %s (referer: %s)%s" % (response.status, \ + request, referer, flags) diff --git a/scrapy/contrib_exp/loader/__init__.py b/scrapy/contrib_exp/loader/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scrapy/contrib_exp/loader/lxmlloader.py b/scrapy/contrib_exp/loader/lxmlloader.py new file mode 100644 index 000000000..724e7c43c --- /dev/null +++ b/scrapy/contrib_exp/loader/lxmlloader.py @@ -0,0 +1,34 @@ +from lxml import html, etree + +from scrapy.contrib.loader import ItemLoader + + +class LxmlItemLoader(ItemLoader): + + def __init__(self, response, item=None, **context): + self.tree = html.fromstring(response.body_as_unicode()) + context.update(response=response) + super(LxmlItemLoader, self).__init__(item, **context) + + def add_xpath(self, field_name, xpath): + self.add_value(field_name, self._get_xpath(xpath)) + + def replace_xpath(self, field_name, xpath): + self.replace_value(field_name, self._get_xpath(xpath)) + + def _get_xpath(self, xpath): + return self._get_values(self.tree.xpath(xpath)) + + def add_css(self, field_name, css): + self.add_value(field_name, self._get_css(css)) + + def replace_css(self, field_name, css): + self.replace_value(field_name, self._get_css(css)) + + def _get_css(self, css): + return self._get_values(self.tree.cssselect(css)) + + def _get_values(self, elems): + for e in elems: + yield etree.tostring(e) if isinstance(e, etree.ElementBase) else e + diff --git a/scrapy/core/downloader/handlers.py b/scrapy/core/downloader/handlers.py deleted file mode 100644 index 016e6677d..000000000 --- a/scrapy/core/downloader/handlers.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Download handlers for different schemes -""" -from __future__ import with_statement - -import urlparse - -from twisted.internet import reactor -try: - from twisted.internet import ssl -except ImportError: - pass - -from scrapy import optional_features -from scrapy.core import signals -from scrapy.http import Headers -from scrapy.core.exceptions import NotSupported -from scrapy.utils.defer import defer_succeed -from scrapy.utils.httpobj import urlparse_cached -from scrapy.utils.signal import send_catch_log -from scrapy.utils.misc import load_object -from scrapy.core.downloader.responsetypes import responsetypes -from scrapy.conf import settings - - -HTTPClientFactory = load_object(settings['DOWNLOADER_HTTPCLIENTFACTORY']) -default_timeout = settings.getint('DOWNLOAD_TIMEOUT') -ssl_supported = 'ssl' in optional_features - -def download_any(request, spider): - scheme = urlparse_cached(request).scheme - if scheme == 'http': - return download_http(request, spider) - elif scheme == 'https': - if ssl_supported: - return download_https(request, spider) - else: - raise NotSupported("HTTPS not supported: install pyopenssl library") - elif scheme == 'file': - return download_file(request, spider) - else: - raise NotSupported("Unsupported URL scheme '%s' in: <%s>" % (scheme, request.url)) - -def create_factory(request, spider): - """Return HTTPClientFactory for the given Request""" - url = urlparse.urldefrag(request.url)[0] - timeout = getattr(spider, "download_timeout", None) or default_timeout - factory = HTTPClientFactory.from_request(request, timeout) - - def _create_response(body): - body = body or '' - status = int(factory.status) - headers = Headers(factory.response_headers) - respcls = responsetypes.from_args(headers=headers, url=url) - r = respcls(url=request.url, status=status, headers=headers, body=body) - send_catch_log(signal=signals.request_uploaded, sender='download_http', \ - request=request, spider=spider) - send_catch_log(signal=signals.response_downloaded, sender='download_http', \ - response=r, spider=spider) - return r - - factory.deferred.addCallbacks(_create_response) - return factory - -def download_http(request, spider): - """Return a deferred for the HTTP download""" - factory = create_factory(request, spider) - url = urlparse_cached(request) - port = url.port - reactor.connectTCP(url.hostname, port or 80, factory) - return factory.deferred - -def download_https(request, spider): - """Return a deferred for the HTTPS download""" - factory = create_factory(request, spider) - url = urlparse_cached(request) - port = url.port - contextFactory = ssl.ClientContextFactory() - reactor.connectSSL(url.hostname, port or 443, factory, contextFactory) - return factory.deferred - -def download_file(request, spider) : - """Return a deferred for a file download.""" - filepath = request.url.split("file://")[1] - with open(filepath) as f: - body = f.read() - respcls = responsetypes.from_args(filename=filepath, body=body) - response = respcls(url=request.url, body=body) - - return defer_succeed(response) diff --git a/scrapy/core/downloader/handlers/__init__.py b/scrapy/core/downloader/handlers/__init__.py new file mode 100644 index 000000000..94d619202 --- /dev/null +++ b/scrapy/core/downloader/handlers/__init__.py @@ -0,0 +1,27 @@ +"""Download handlers for different schemes""" + +from scrapy.core.exceptions import NotSupported +from scrapy.utils.httpobj import urlparse_cached +from scrapy.conf import settings +from scrapy.utils.misc import load_object + + +class RequestHandlers(object): + + def __init__(self): + self._handlers = {} + handlers = settings.get('REQUEST_HANDLERS_BASE') + handlers.update(settings.get('REQUEST_HANDLERS', {})) + for scheme, cls in handlers.iteritems(): + self._handlers[scheme] = load_object(cls) + + def download_request(self, request, spider): + scheme = urlparse_cached(request).scheme + try: + handler = self._handlers[scheme] + except KeyError: + raise NotSupported("Unsupported URL scheme '%s' in: <%s>" % (scheme, request.url)) + return handler(request, spider) + + +download_any = RequestHandlers().download_request diff --git a/scrapy/core/downloader/handlers/file.py b/scrapy/core/downloader/handlers/file.py new file mode 100644 index 000000000..8a02d02d6 --- /dev/null +++ b/scrapy/core/downloader/handlers/file.py @@ -0,0 +1,18 @@ +"""Download handler for file:// scheme""" +from __future__ import with_statement + +from twisted.internet import defer +from scrapy.core.downloader.responsetypes import responsetypes + + +def download_file(request, spider): + """Return a deferred for a file download.""" + return defer.maybeDeferred(_all_in_one_read_download_file, request, spider) + +def _all_in_one_read_download_file(request, spider): + filepath = request.url.split("file://")[1] + with open(filepath) as f: + body = f.read() + respcls = responsetypes.from_args(filename=filepath, body=body) + return respcls(url=request.url, body=body) + diff --git a/scrapy/core/downloader/handlers/http.py b/scrapy/core/downloader/handlers/http.py new file mode 100644 index 000000000..756bde6e3 --- /dev/null +++ b/scrapy/core/downloader/handlers/http.py @@ -0,0 +1,50 @@ +"""Download handlers for http and https schemes""" + +from twisted.internet import reactor + +from scrapy.core import signals +from scrapy.core.exceptions import NotSupported +from scrapy.utils.signal import send_catch_log +from scrapy.utils.misc import load_object +from scrapy.conf import settings +from scrapy import optional_features + +ssl_supported = 'ssl' in optional_features +if ssl_supported: + from twisted.internet.ssl import ClientContextFactory + + +HTTPClientFactory = load_object(settings['DOWNLOADER_HTTPCLIENTFACTORY']) +default_timeout = settings.getint('DOWNLOAD_TIMEOUT') + +def _create_factory(request, spider): + def _download_signals(response): + send_catch_log(signal=signals.request_uploaded, \ + sender='download_http', request=request, spider=spider) + send_catch_log(signal=signals.response_downloaded, \ + sender='download_http', response=response, spider=spider) + return response + + timeout = getattr(spider, "download_timeout", None) or default_timeout + factory = HTTPClientFactory(request, timeout) + factory.deferred.addCallbacks(_download_signals) + return factory + + +def _connect(factory): + host, port = factory.host, factory.port + if factory.scheme == 'https': + if ssl_supported: + return reactor.connectSSL(host, port, factory, ClientContextFactory()) + raise NotSupported("HTTPS not supported: install pyopenssl library") + else: + return reactor.connectTCP(host, port, factory) + + +def download_http(request, spider): + """Return a deferred for the HTTP download""" + factory = _create_factory(request, spider) + _connect(factory) + return factory.deferred + + diff --git a/scrapy/core/downloader/webclient.py b/scrapy/core/downloader/webclient.py index ead74ae79..552fc6597 100644 --- a/scrapy/core/downloader/webclient.py +++ b/scrapy/core/downloader/webclient.py @@ -1,12 +1,14 @@ -from urlparse import urlparse, urlunparse +from urlparse import urlparse, urlunparse, urldefrag from twisted.python import failure -from twisted.web.client import HTTPClientFactory, PartialDownloadError +from twisted.web.client import PartialDownloadError, HTTPClientFactory from twisted.web.http import HTTPClient from twisted.internet import defer from scrapy.http import Headers from scrapy.utils.httpobj import urlparse_cached +from scrapy.core.downloader.responsetypes import responsetypes + def _parsed_url_args(parsed): path = urlunparse(('', '', parsed.path or '/', parsed.params, parsed.query, '')) @@ -18,11 +20,13 @@ def _parsed_url_args(parsed): port = 443 if scheme == 'https' else 80 return scheme, netloc, host, port, path + def _parse(url): url = url.strip() parsed = urlparse(url) return _parsed_url_args(parsed) + class ScrapyHTTPPageGetter(HTTPClient): def connectionMade(self): @@ -64,7 +68,9 @@ class ScrapyHTTPPageGetter(HTTPClient): def timeout(self): self.transport.loseConnection() - self.factory.noPage(defer.TimeoutError("Getting %s took longer than %s seconds." % (self.factory.url, self.factory.timeout))) + self.factory.noPage(\ + defer.TimeoutError("Getting %s took longer than %s seconds." % \ + (self.factory.url, self.factory.timeout))) class ScrapyHTTPClientFactory(HTTPClientFactory): @@ -74,22 +80,19 @@ class ScrapyHTTPClientFactory(HTTPClientFactory): """ protocol = ScrapyHTTPPageGetter - response_headers = None waiting = 1 noisy = False - def __init__(self, url, method='GET', body=None, headers=None, timeout=0, parsedurl=None): - self.url = url - self.method = method - self.body = body or None - if parsedurl: - self.scheme, self.netloc, self.host, self.port, self.path = _parsed_url_args(parsedurl) - else: - self.scheme, self.netloc, self.host, self.port, self.path = _parse(url) - + def __init__(self, request, timeout=0): + self.url = urldefrag(request.url)[0] + self.method = request.method + self.body = request.body or None + self.headers = request.headers + self.response_headers = None self.timeout = timeout - self.headers = Headers(headers or {}) - self.deferred = defer.Deferred() + self.deferred = defer.Deferred().addCallback(self._build_response) + + self._set_connection_attributes(request) # set Host header based on url self.headers.setdefault('Host', self.netloc) @@ -100,33 +103,19 @@ class ScrapyHTTPClientFactory(HTTPClientFactory): # just in case a broken http/1.1 decides to keep connection alive self.headers.setdefault("Connection", "close") - @classmethod - def from_request(cls, request, timeout): - return cls(request.url, - method=request.method, - body=request.body or None, # see http://dev.scrapy.org/ticket/60 - headers=Headers(request.headers or {}), - timeout=timeout, - parsedurl=urlparse_cached(request), - ) + def _build_response(self, body): + status = int(self.status) + headers = Headers(self.response_headers) + respcls = responsetypes.from_args(headers=headers, url=self.url) + return respcls(url=self.url, status=status, headers=headers, body=body) + + def _set_connection_attributes(self, request): + parsed = urlparse_cached(request) + self.scheme, self.netloc, self.host, self.port, self.path = _parsed_url_args(parsed) + proxy = request.meta.get('proxy') + if proxy: + self.scheme, _, self.host, self.port, _ = _parse(proxy) + self.path = self.url def gotHeaders(self, headers): self.response_headers = headers - - - -def getPage(url, contextFactory=None, *args, **kwargs): - """ - Download a web page as a string. - - Download a page. Return a deferred, which will callback with a - page (as a string) or errback with a description of the error. - - See HTTPClientFactory to see what extra args can be passed. - """ - from twisted.web.client import _makeGetterFactory - return _makeGetterFactory( - url, - ScrapyHTTPClientFactory, - contextFactory=contextFactory, - *args, **kwargs).deferred diff --git a/scrapy/core/engine.py b/scrapy/core/engine.py index 5b03d4231..aaaed7fd9 100644 --- a/scrapy/core/engine.py +++ b/scrapy/core/engine.py @@ -34,6 +34,7 @@ class ExecutionEngine(object): self.paused = False self._next_request_pending = set() self._mainloop_task = task.LoopingCall(self._mainloop) + self._crawled_logline = load_object(settings['LOG_FORMATTER_CRAWLED']) def configure(self): """ @@ -198,15 +199,14 @@ class ExecutionEngine(object): def download(self, request, spider): domain = spider.domain_name - referer = request.headers.get('Referer') def _on_success(response): """handle the result of a page download""" assert isinstance(response, (Response, Request)) if isinstance(response, Response): response.request = request # tie request to response received - log.msg("Crawled %s (referer: <%s>)" % (request, referer), \ - level=log.DEBUG, domain=domain) + log.msg(self._crawled_logline(request, response), \ + level=log.DEBUG, domain=spider.domain_name) return response elif isinstance(response, Request): newrequest = response @@ -224,14 +224,17 @@ class ExecutionEngine(object): errmsg = str(_failure) level = log.ERROR if errmsg: - log.msg("Downloading <%s> (referer: <%s>): %s" % (request.url, \ - referer, errmsg), level=level, domain=domain) + log.msg("Crawling <%s>: %s" % (request.url, errmsg), \ + level=level, domain=domain) return Failure(IgnoreRequest(str(exc))) def _on_complete(_): self.next_request(spider) return _ + if spider not in self.downloader.sites: + return defer.fail(Failure(IgnoreRequest())).addBoth(_on_complete) + dwld = mustbe_deferred(self.downloader.fetch, request, spider) dwld.addCallbacks(_on_success, _on_error) dwld.addBoth(_on_complete) diff --git a/scrapy/http/request/__init__.py b/scrapy/http/request/__init__.py index 8cd55fe99..65355b03d 100644 --- a/scrapy/http/request/__init__.py +++ b/scrapy/http/request/__init__.py @@ -85,10 +85,7 @@ class Request(object_ref): return self._encoding def __str__(self): - if self.method == 'GET': - return "<%s>" % self.url - else: - return "<%s %s>" % (self.method, self.url) + return "<%s %s>" % (self.method, self.url) def __repr__(self): attrs = ['url', 'method', 'body', 'headers', 'cookies', 'meta'] diff --git a/scrapy/http/response/__init__.py b/scrapy/http/response/__init__.py index 11a9bfcde..f632ea117 100644 --- a/scrapy/http/response/__init__.py +++ b/scrapy/http/response/__init__.py @@ -65,9 +65,7 @@ class Response(object_ref): return "%s(%s)" % (self.__class__.__name__, args) def __str__(self): - flags = "(%s) " % ",".join(self.flags) if self.flags else "" - status = "%d " % self.status + " " if self.status != 200 else "" - return "%s<%s%s>" % (flags, status, self.url) + return "<%d %s>" % (self.status, self.url) def copy(self): """Return a copy of this Response""" diff --git a/scrapy/tests/test_contrib_exp_loader_lxmlloader.py b/scrapy/tests/test_contrib_exp_loader_lxmlloader.py new file mode 100644 index 000000000..1980648c1 --- /dev/null +++ b/scrapy/tests/test_contrib_exp_loader_lxmlloader.py @@ -0,0 +1,67 @@ +from twisted.trial import unittest + +from scrapy.contrib.loader.processor import MapCompose +from scrapy.item import Item, Field +from scrapy.http import HtmlResponse + +try: + import lxml +except ImportError: + lxml = False + + +class TestItem(Item): + name = Field() + + +if lxml: + from scrapy.contrib_exp.loader.lxmlloader import LxmlItemLoader + + class TestLxmlItemLoader(LxmlItemLoader): + default_item_class = TestItem + + +class LxmlItemLoaderTest(unittest.TestCase): + response = HtmlResponse(url="", body='
paragraph
') + + def setUp(self): + if not lxml: + raise unittest.SkipTest("lxml is not available") + + def test_constructor_with_response(self): + l = TestLxmlItemLoader(response=self.response) + self.assert_(l.tree) + + def test_add_xpath(self): + l = TestLxmlItemLoader(response=self.response) + l.add_xpath('name', '//div') + self.assertEqual(l.get_output_value('name'), [u'paragraph
']) + + +if __name__ == "__main__": + unittest.main() + diff --git a/scrapy/tests/test_contrib_logformatter.py b/scrapy/tests/test_contrib_logformatter.py new file mode 100644 index 000000000..e096f7359 --- /dev/null +++ b/scrapy/tests/test_contrib_logformatter.py @@ -0,0 +1,22 @@ +import unittest + +from scrapy.http import Request, Response +from scrapy.contrib.logformatter import crawled_logline + + +class LoggingContribTest(unittest.TestCase): + + def test_crawled_logline(self): + req = Request("http://www.example.com") + res = Response("http://www.example.com") + self.assertEqual(crawled_logline(req, res), + "Crawled (200)