diff --git a/scrapy/link.py b/scrapy/link.py index 8bdcce761..2c8301680 100644 --- a/scrapy/link.py +++ b/scrapy/link.py @@ -4,20 +4,26 @@ This module defines the Link object used in Link extractors. For actual link extractors implementation see scrapy.linkextractors, or its documentation in: docs/topics/link-extractors.rst """ - +import warnings import six +from scrapy.utils.python import to_bytes + + class Link(object): """Link objects represent an extracted link by the LinkExtractor.""" __slots__ = ['url', 'text', 'fragment', 'nofollow'] def __init__(self, url, text='', fragment='', nofollow=False): - if isinstance(url, six.text_type): - import warnings - warnings.warn("Do not instantiate Link objects with unicode urls. " - "Assuming utf-8 encoding (which could be wrong)") - url = url.encode('utf-8') + if not isinstance(url, str): + if six.PY2: + warnings.warn("Link urls must be str objects. " + "Assuming utf-8 encoding (which could be wrong)") + url = to_bytes(url, encoding='utf8') + else: + got = url.__class__.__name__ + raise TypeError("Link urls must be str objects, got %s" % got) self.url = url self.text = text self.fragment = fragment diff --git a/scrapy/linkextractors/__init__.py b/scrapy/linkextractors/__init__.py index bb799e572..64efa0c55 100644 --- a/scrapy/linkextractors/__init__.py +++ b/scrapy/linkextractors/__init__.py @@ -39,7 +39,7 @@ IGNORED_EXTENSIONS = [ _re_type = type(re.compile("", 0)) _matches = lambda url, regexs: any((r.search(url) for r in regexs)) -_is_valid_url = lambda url: url.split('://', 1)[0] in set(['http', 'https', 'file']) +_is_valid_url = lambda url: url.split('://', 1)[0] in {'http', 'https', 'file'} class FilteringLinkExtractor(object): @@ -51,8 +51,10 @@ class FilteringLinkExtractor(object): self.link_extractor = link_extractor - self.allow_res = [x if isinstance(x, _re_type) else re.compile(x) for x in arg_to_iter(allow)] - self.deny_res = [x if isinstance(x, _re_type) else re.compile(x) for x in arg_to_iter(deny)] + self.allow_res = [x if isinstance(x, _re_type) else re.compile(x) + for x in arg_to_iter(allow)] + self.deny_res = [x if isinstance(x, _re_type) else re.compile(x) + for x in arg_to_iter(deny)] self.allow_domains = set(arg_to_iter(allow_domains)) self.deny_domains = set(arg_to_iter(deny_domains)) @@ -64,7 +66,7 @@ class FilteringLinkExtractor(object): self.canonicalize = canonicalize if deny_extensions is None: deny_extensions = IGNORED_EXTENSIONS - self.deny_extensions = set(['.' + e for e in arg_to_iter(deny_extensions)]) + self.deny_extensions = {'.' + e for e in arg_to_iter(deny_extensions)} def _link_allowed(self, link): if not _is_valid_url(link.url): @@ -104,5 +106,6 @@ class FilteringLinkExtractor(object): def _extract_links(self, *args, **kwargs): return self.link_extractor._extract_links(*args, **kwargs) + # Top-level imports from .lxmlhtml import LxmlLinkExtractor as LinkExtractor diff --git a/scrapy/linkextractors/lxmlhtml.py b/scrapy/linkextractors/lxmlhtml.py index 7064e886d..71d57b392 100644 --- a/scrapy/linkextractors/lxmlhtml.py +++ b/scrapy/linkextractors/lxmlhtml.py @@ -1,16 +1,14 @@ """ Link extractor based on lxml.html """ - -import re +import six from six.moves.urllib.parse import urlparse, urljoin import lxml.etree as etree -from scrapy.selector import Selector from scrapy.link import Link from scrapy.utils.misc import arg_to_iter, rel_has_nofollow -from scrapy.utils.python import unique as unique_list +from scrapy.utils.python import unique as unique_list, to_native_str from scrapy.linkextractors import FilteringLinkExtractor from scrapy.utils.response import get_base_url @@ -20,8 +18,9 @@ XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml" _collect_string_content = etree.XPath("string()") + def _nons(tag): - if isinstance(tag, basestring): + if isinstance(tag, six.string_types): if tag[0] == '{' and tag[1:len(XHTML_NAMESPACE)+1] == XHTML_NAMESPACE: return tag.split('}')[-1] return tag @@ -57,16 +56,13 @@ class LxmlParserLinkExtractor(object): url = self.process_attr(attr_val) if url is None: continue - if isinstance(url, unicode): - url = url.encode(response_encoding) + url = to_native_str(url, encoding=response_encoding) # to fix relative links after process_value url = urljoin(response_url, url) link = Link(url, _collect_string_content(el) or u'', nofollow=rel_has_nofollow(el.get('rel'))) links.append(link) - - return unique_list(links, key=lambda link: link.url) \ - if self.unique else links + return self._deduplicate_if_needed(links) def extract_links(self, response): base_url = get_base_url(response) @@ -77,7 +73,11 @@ class LxmlParserLinkExtractor(object): The subclass should override it if neccessary """ - links = unique_list(links, key=lambda link: link.url) if self.unique else links + return self._deduplicate_if_needed(links) + + def _deduplicate_if_needed(self, links): + if self.unique: + return unique_list(links, key=lambda link: link.url) return links @@ -110,4 +110,3 @@ class LxmlLinkExtractor(FilteringLinkExtractor): links = self._extract_links(doc, response.url, response.encoding, base_url) all_links.extend(self._process_links(links)) return unique_list(all_links) - diff --git a/scrapy/spidermiddlewares/depth.py b/scrapy/spidermiddlewares/depth.py index 795b60eb4..e2f039146 100644 --- a/scrapy/spidermiddlewares/depth.py +++ b/scrapy/spidermiddlewares/depth.py @@ -35,14 +35,18 @@ class DepthMiddleware(object): if self.prio: request.priority -= depth * self.prio if self.maxdepth and depth > self.maxdepth: - logger.debug("Ignoring link (depth > %(maxdepth)d): %(requrl)s ", - {'maxdepth': self.maxdepth, 'requrl': request.url}, - extra={'spider': spider}) + logger.debug( + "Ignoring link (depth > %(maxdepth)d): %(requrl)s ", + {'maxdepth': self.maxdepth, 'requrl': request.url}, + extra={'spider': spider} + ) return False elif self.stats: if self.verbose_stats: - self.stats.inc_value('request_depth_count/%s' % depth, spider=spider) - self.stats.max_value('request_depth_max', depth, spider=spider) + self.stats.inc_value('request_depth_count/%s' % depth, + spider=spider) + self.stats.max_value('request_depth_max', depth, + spider=spider) return True # base case (depth=0) diff --git a/scrapy/spidermiddlewares/offsite.py b/scrapy/spidermiddlewares/offsite.py index a90f9f1e0..ea1c9270f 100644 --- a/scrapy/spidermiddlewares/offsite.py +++ b/scrapy/spidermiddlewares/offsite.py @@ -13,6 +13,7 @@ from scrapy.utils.httpobj import urlparse_cached logger = logging.getLogger(__name__) + class OffsiteMiddleware(object): def __init__(self, stats): diff --git a/scrapy/spiders/crawl.py b/scrapy/spiders/crawl.py index 77551753e..031f649d6 100644 --- a/scrapy/spiders/crawl.py +++ b/scrapy/spiders/crawl.py @@ -6,14 +6,17 @@ See documentation in docs/topics/spiders.rst """ import copy +import six from scrapy.http import Request, HtmlResponse from scrapy.utils.spider import iterate_spider_output from scrapy.spiders import Spider + def identity(x): return x + class Rule(object): def __init__(self, link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=identity): @@ -27,6 +30,7 @@ class Rule(object): else: self.follow = follow + class CrawlSpider(Spider): rules = () @@ -49,7 +53,8 @@ class CrawlSpider(Spider): return seen = set() for n, rule in enumerate(self._rules): - links = [l for l in rule.link_extractor.extract_links(response) if l not in seen] + links = [lnk for lnk in rule.link_extractor.extract_links(response) + if lnk not in seen] if links and rule.process_links: links = rule.process_links(links) for link in links: @@ -77,7 +82,7 @@ class CrawlSpider(Spider): def get_method(method): if callable(method): return method - elif isinstance(method, basestring): + elif isinstance(method, six.string_types): return getattr(self, method, None) self._rules = [copy.copy(r) for r in self.rules] diff --git a/scrapy/spiders/init.py b/scrapy/spiders/init.py index 7717c8819..2efb1a869 100644 --- a/scrapy/spiders/init.py +++ b/scrapy/spiders/init.py @@ -1,6 +1,7 @@ from scrapy.spiders import Spider from scrapy.utils.spider import iterate_spider_output + class InitSpider(Spider): """Base Spider with initialization facilities""" diff --git a/scrapy/spiders/sitemap.py b/scrapy/spiders/sitemap.py index 5aa0b944d..eede467a8 100644 --- a/scrapy/spiders/sitemap.py +++ b/scrapy/spiders/sitemap.py @@ -1,5 +1,6 @@ import re import logging +import six from scrapy.spiders import Spider from scrapy.http import Request, XmlResponse @@ -20,13 +21,14 @@ class SitemapSpider(Spider): super(SitemapSpider, self).__init__(*a, **kw) self._cbs = [] for r, c in self.sitemap_rules: - if isinstance(c, basestring): + if isinstance(c, six.string_types): c = getattr(self, c) self._cbs.append((regex(r), c)) self._follow = [regex(x) for x in self.sitemap_follow] def start_requests(self): - return (Request(x, callback=self._parse_sitemap) for x in self.sitemap_urls) + for url in self.sitemap_urls: + yield Request(url, self._parse_sitemap) def _parse_sitemap(self, response): if response.url.endswith('/robots.txt'): @@ -52,8 +54,8 @@ class SitemapSpider(Spider): break def _get_sitemap_body(self, response): - """Return the sitemap body contained in the given response, or None if the - response is not a sitemap. + """Return the sitemap body contained in the given response, + or None if the response is not a sitemap. """ if isinstance(response, XmlResponse): return response.body @@ -64,11 +66,13 @@ class SitemapSpider(Spider): elif response.url.endswith('.xml.gz'): return gunzip(response.body) + def regex(x): - if isinstance(x, basestring): + if isinstance(x, six.string_types): return re.compile(x) return x + def iterloc(it, alt=False): for d in it: yield d['loc'] diff --git a/scrapy/utils/gz.py b/scrapy/utils/gz.py index 741948359..7fa4bba57 100644 --- a/scrapy/utils/gz.py +++ b/scrapy/utils/gz.py @@ -7,6 +7,7 @@ except ImportError: from gzip import GzipFile + def gunzip(data): """Gunzip the given data and return as much data as possible. @@ -31,7 +32,8 @@ def gunzip(data): raise return output + def is_gzipped(response): """Return True if the response is gzipped, or False otherwise""" - ctype = response.headers.get('Content-Type', '') - return ctype in ('application/x-gzip', 'application/gzip') + ctype = response.headers.get('Content-Type', b'') + return ctype in (b'application/x-gzip', b'application/gzip') diff --git a/tests/py3-ignores.txt b/tests/py3-ignores.txt index f380e6679..759eeffff 100644 --- a/tests/py3-ignores.txt +++ b/tests/py3-ignores.txt @@ -3,9 +3,8 @@ tests/test_command_fetch.py tests/test_command_shell.py tests/test_commands.py tests/test_exporters.py -tests/test_linkextractors.py +tests/test_linkextractors_deprecated.py tests/test_crawl.py -tests/test_crawler.py tests/test_downloader_handlers.py tests/test_downloadermiddleware_ajaxcrawlable.py tests/test_downloadermiddleware_defaultheaders.py @@ -24,11 +23,7 @@ tests/test_mail.py tests/test_pipeline_files.py tests/test_pipeline_images.py tests/test_proxy_connect.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_utils_iterators.py tests/test_utils_template.py tests/test_webclient.py diff --git a/tests/test_link.py b/tests/test_link.py index 0b79e47cd..955430b37 100644 --- a/tests/test_link.py +++ b/tests/test_link.py @@ -1,8 +1,10 @@ import unittest import warnings +import six from scrapy.link import Link + class LinkTest(unittest.TestCase): def _assert_same_links(self, link1, link2): @@ -14,38 +16,42 @@ class LinkTest(unittest.TestCase): self.assertNotEqual(hash(link1), hash(link2)) def test_eq_and_hash(self): - l1 = Link(b"http://www.example.com") - l2 = Link(b"http://www.example.com/other") - l3 = Link(b"http://www.example.com") + l1 = Link("http://www.example.com") + l2 = Link("http://www.example.com/other") + l3 = Link("http://www.example.com") self._assert_same_links(l1, l1) self._assert_different_links(l1, l2) self._assert_same_links(l1, l3) - l4 = Link(b"http://www.example.com", text="test") - l5 = Link(b"http://www.example.com", text="test2") - l6 = Link(b"http://www.example.com", text="test") + l4 = Link("http://www.example.com", text="test") + l5 = Link("http://www.example.com", text="test2") + l6 = Link("http://www.example.com", text="test") self._assert_same_links(l4, l4) self._assert_different_links(l4, l5) self._assert_same_links(l4, l6) - l7 = Link(b"http://www.example.com", text="test", fragment='something', nofollow=False) - l8 = Link(b"http://www.example.com", text="test", fragment='something', nofollow=False) - l9 = Link(b"http://www.example.com", text="test", fragment='something', nofollow=True) - l10 = Link(b"http://www.example.com", text="test", fragment='other', nofollow=False) + l7 = Link("http://www.example.com", text="test", fragment='something', nofollow=False) + l8 = Link("http://www.example.com", text="test", fragment='something', nofollow=False) + l9 = Link("http://www.example.com", text="test", fragment='something', nofollow=True) + l10 = Link("http://www.example.com", text="test", fragment='other', nofollow=False) self._assert_same_links(l7, l8) self._assert_different_links(l7, l9) self._assert_different_links(l7, l10) def test_repr(self): - l1 = Link(b"http://www.example.com", text="test", fragment='something', nofollow=True) + l1 = Link("http://www.example.com", text="test", fragment='something', nofollow=True) l2 = eval(repr(l1)) self._assert_same_links(l1, l2) - def test_unicode_url(self): - with warnings.catch_warnings(record=True) as w: - link = Link(u"http://www.example.com/\xa3") - self.assertIsInstance(link.url, bytes) - self.assertEqual(link.url, b'http://www.example.com/\xc2\xa3') + def test_non_str_url_py2(self): + if six.PY2: + with warnings.catch_warnings(record=True) as w: + link = Link(u"http://www.example.com/\xa3") + self.assertIsInstance(link.url, str) + self.assertEqual(link.url, b'http://www.example.com/\xc2\xa3') assert len(w) == 1, "warning not issued" + else: + with self.assertRaises(TypeError): + Link(b"http://www.example.com/\xc2\xa3") diff --git a/tests/test_linkextractors.py b/tests/test_linkextractors.py index d32ff2d55..129336d14 100644 --- a/tests/test_linkextractors.py +++ b/tests/test_linkextractors.py @@ -1,200 +1,361 @@ import re import unittest -from scrapy.linkextractors.regex import RegexLinkExtractor + +import pytest + from scrapy.http import HtmlResponse, XmlResponse from scrapy.link import Link -from scrapy.linkextractors.htmlparser import HtmlParserLinkExtractor -from scrapy.linkextractors.sgml import SgmlLinkExtractor, BaseSgmlLinkExtractor from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor from tests import get_testdata -class LinkExtractorTestCase(unittest.TestCase): - def test_basic(self): - html = """
-
-
-
- """
- response = HtmlResponse("http://example.org/somepage/index.html", body=html)
+# a hack to skip base class tests in pytest
+class Base:
+ class LinkExtractorTestCase(unittest.TestCase):
+ extractor_cls = None
- lx = BaseSgmlLinkExtractor() # default: tag=a, attr=href
- self.assertEqual(lx.extract_links(response),
- [Link(url='http://example.org/somepage/item/12.html', text='Item 12'),
- Link(url='http://example.org/about.html', text='About us'),
- Link(url='http://example.org/othercat.html', text='Other category'),
- Link(url='http://example.org/', text='>>'),
- Link(url='http://example.org/', text='')])
+ def setUp(self):
+ body = get_testdata('link_extractor', 'sgml_linkextractor.html')
+ self.response = HtmlResponse(url='http://example.com/index', body=body)
- def test_base_url(self):
- html = """
"""
+ response = HtmlResponse("http://example.com/index.html", body=html)
+
+ lx = self.extractor_cls(tags=None)
+ self.assertEqual(lx.extract_links(response), [])
+
+ lx = self.extractor_cls()
+ self.assertEqual(lx.extract_links(response), [
+ Link(url='http://example.com/sample1.html', text=u''),
+ Link(url='http://example.com/sample2.html', text=u'sample 2'),
+ ])
+
+ lx = self.extractor_cls(tags="area")
+ self.assertEqual(lx.extract_links(response), [
+ Link(url='http://example.com/sample1.html', text=u''),
+ ])
+
+ lx = self.extractor_cls(tags="a")
+ self.assertEqual(lx.extract_links(response), [
+ Link(url='http://example.com/sample2.html', text=u'sample 2'),
+ ])
+
+ lx = self.extractor_cls(tags=("a","img"), attrs=("href", "src"), deny_extensions=())
+ self.assertEqual(lx.extract_links(response), [
+ Link(url='http://example.com/sample2.html', text=u'sample 2'),
+ Link(url='http://example.com/sample2.jpg', text=u''),
+ ])
+
+ def test_tags_attrs(self):
+ html = b"""
+
+
+
+
+ """
+ response = HtmlResponse("http://example.com/index.html", body=html)
+
+ lx = self.extractor_cls(tags='div', attrs='data-url')
+ self.assertEqual(lx.extract_links(response), [
+ Link(url='http://example.com/get?id=1', text=u'Item 1', fragment='', nofollow=False),
+ Link(url='http://example.com/get?id=2', text=u'Item 2', fragment='', nofollow=False)
+ ])
+
+ lx = self.extractor_cls(tags=('div',), attrs=('data-url',))
+ self.assertEqual(lx.extract_links(response), [
+ Link(url='http://example.com/get?id=1', text=u'Item 1', fragment='', nofollow=False),
+ Link(url='http://example.com/get?id=2', text=u'Item 2', fragment='', nofollow=False)
+ ])
+
+ def test_xhtml(self):
+ xhtml = b"""
+
+
+
+
+
"""
- response = HtmlResponse("http://example.com/index.html", body=html)
-
- lx = self.extractor_cls(tags=None)
- self.assertEqual(lx.extract_links(response), [])
-
- lx = self.extractor_cls()
- self.assertEqual(lx.extract_links(response), [
- Link(url='http://example.com/sample1.html', text=u''),
- Link(url='http://example.com/sample2.html', text=u'sample 2'),
- ])
-
- lx = self.extractor_cls(tags="area")
- self.assertEqual(lx.extract_links(response), [
- Link(url='http://example.com/sample1.html', text=u''),
- ])
-
- lx = self.extractor_cls(tags="a")
- self.assertEqual(lx.extract_links(response), [
- Link(url='http://example.com/sample2.html', text=u'sample 2'),
- ])
-
- lx = self.extractor_cls(tags=("a","img"), attrs=("href", "src"), deny_extensions=())
- self.assertEqual(lx.extract_links(response), [
- Link(url='http://example.com/sample2.html', text=u'sample 2'),
- Link(url='http://example.com/sample2.jpg', text=u''),
- ])
-
- def test_tags_attrs(self):
- html = """
-
-
-
-
- """
- response = HtmlResponse("http://example.com/index.html", body=html)
-
- lx = self.extractor_cls(tags='div', attrs='data-url')
- self.assertEqual(lx.extract_links(response), [
- Link(url='http://example.com/get?id=1', text=u'Item 1', fragment='', nofollow=False),
- Link(url='http://example.com/get?id=2', text=u'Item 2', fragment='', nofollow=False)
- ])
-
- lx = self.extractor_cls(tags=('div',), attrs=('data-url',))
- self.assertEqual(lx.extract_links(response), [
- Link(url='http://example.com/get?id=1', text=u'Item 1', fragment='', nofollow=False),
- Link(url='http://example.com/get?id=2', text=u'Item 2', fragment='', nofollow=False)
- ])
-
- def test_xhtml(self):
- xhtml = """
-
-
-
-
-
+
+
+
+ """
+ response = HtmlResponse("http://example.org/somepage/index.html", body=html)
+
+ lx = BaseSgmlLinkExtractor() # default: tag=a, attr=href
+ self.assertEqual(lx.extract_links(response),
+ [Link(url='http://example.org/somepage/item/12.html', text='Item 12'),
+ Link(url='http://example.org/about.html', text='About us'),
+ Link(url='http://example.org/othercat.html', text='Other category'),
+ Link(url='http://example.org/', text='>>'),
+ Link(url='http://example.org/', text='')])
+
+ def test_base_url(self):
+ html = """