import gzip import inspect import warnings from io import BytesIO from unittest import mock from testfixtures import LogCapture from twisted.trial import unittest from w3lib.url import safe_url_string from scrapy import signals from scrapy.settings import Settings from scrapy.http import Request, Response, TextResponse, XmlResponse, HtmlResponse from scrapy.spiders.init import InitSpider from scrapy.spiders import ( CSVFeedSpider, CrawlSpider, Rule, SitemapSpider, Spider, XMLFeedSpider, ) from scrapy.linkextractors import LinkExtractor from scrapy.utils.test import get_crawler from tests import get_testdata class SpiderTest(unittest.TestCase): spider_class = Spider def setUp(self): warnings.simplefilter("always") def tearDown(self): warnings.resetwarnings() def test_base_spider(self): spider = self.spider_class("example.com") self.assertEqual(spider.name, 'example.com') self.assertEqual(spider.start_urls, []) def test_start_requests(self): spider = self.spider_class('example.com') start_requests = spider.start_requests() self.assertTrue(inspect.isgenerator(start_requests)) self.assertEqual(list(start_requests), []) def test_spider_args(self): """``__init__`` method arguments are assigned to spider attributes""" spider = self.spider_class('example.com', foo='bar') self.assertEqual(spider.foo, 'bar') def test_spider_without_name(self): """``__init__`` method arguments are assigned to spider attributes""" self.assertRaises(ValueError, self.spider_class) self.assertRaises(ValueError, self.spider_class, somearg='foo') 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') 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) 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') def test_logger(self): spider = self.spider_class('example.com') with LogCapture() as lc: spider.logger.info('test log msg') lc.check(('example.com', 'INFO', 'test log msg')) record = lc.records[0] self.assertIn('spider', record.__dict__) self.assertIs(record.spider, spider) def test_log(self): spider = self.spider_class('example.com') with mock.patch('scrapy.spiders.Spider.logger') as mock_logger: spider.log('test log msg', 'INFO') mock_logger.log.assert_called_once_with('INFO', 'test log msg') class InitSpiderTest(SpiderTest): spider_class = InitSpider class XMLFeedSpiderTest(SpiderTest): spider_class = XMLFeedSpider def test_register_namespace(self): body = b""" http://www.example.com/Special-Offers.html2009-08-16 http://www.example.com/2009-08-16 """ response = XmlResponse(url='http://example.com/sitemap.xml', body=body) class _XMLSpider(self.spider_class): itertag = 'url' namespaces = ( ('a', 'http://www.google.com/schemas/sitemap/0.84'), ('b', 'http://www.example.com/schemas/extras/1.0'), ) def parse_node(self, response, selector): yield { 'loc': selector.xpath('a:loc/text()').getall(), 'updated': selector.xpath('b:updated/text()').getall(), 'other': selector.xpath('other/@value').getall(), 'custom': selector.xpath('other/@b:custom').getall(), } for iterator in ('iternodes', 'xml'): spider = _XMLSpider('example', iterator=iterator) output = list(spider._parse(response)) self.assertEqual(len(output), 2, iterator) self.assertEqual(output, [ {'loc': ['http://www.example.com/Special-Offers.html'], 'updated': ['2009-08-16'], 'custom': ['fuu'], 'other': ['bar']}, {'loc': [], 'updated': ['2009-08-16'], 'other': ['foo'], 'custom': []}, ], iterator) class CSVFeedSpiderTest(SpiderTest): spider_class = CSVFeedSpider def test_parse_rows(self): body = get_testdata('feeds', 'feed-sample6.csv') response = Response("http://example.org/dummy.csv", body=body) class _CrawlSpider(self.spider_class): name = "test" delimiter = "," quotechar = "'" def parse_row(self, response, row): return row spider = _CrawlSpider() rows = list(spider.parse_rows(response)) assert rows[0] == {'id': '1', 'name': 'alpha', 'value': 'foobar'} assert len(rows) == 4 class CrawlSpiderTest(SpiderTest): test_body = b"""Page title<title> <body> <p><a href="item/12.html">Item 12</a></p> <div class='links'> <p><a href="/about.html">About us</a></p> </div> <div> <p><a href="/nofollow.html">This shouldn't be followed</a></p> </div> </body></html>""" spider_class = CrawlSpider def test_rule_without_link_extractor(self): response = HtmlResponse("http://example.org/somepage/index.html", body=self.test_body) class _CrawlSpider(self.spider_class): name = "test" allowed_domains = ['example.org'] rules = ( Rule(), ) spider = _CrawlSpider() output = list(spider._requests_to_follow(response)) self.assertEqual(len(output), 3) self.assertTrue(all(map(lambda r: isinstance(r, Request), output))) self.assertEqual([r.url for r in output], ['http://example.org/somepage/item/12.html', 'http://example.org/about.html', 'http://example.org/nofollow.html']) def test_process_links(self): response = HtmlResponse("http://example.org/somepage/index.html", body=self.test_body) class _CrawlSpider(self.spider_class): name = "test" allowed_domains = ['example.org'] rules = ( Rule(LinkExtractor(), process_links="dummy_process_links"), ) def dummy_process_links(self, links): return links spider = _CrawlSpider() output = list(spider._requests_to_follow(response)) self.assertEqual(len(output), 3) self.assertTrue(all(map(lambda r: isinstance(r, Request), output))) self.assertEqual([r.url for r in output], ['http://example.org/somepage/item/12.html', 'http://example.org/about.html', 'http://example.org/nofollow.html']) def test_process_links_filter(self): response = HtmlResponse("http://example.org/somepage/index.html", body=self.test_body) class _CrawlSpider(self.spider_class): import re name = "test" allowed_domains = ['example.org'] rules = ( Rule(LinkExtractor(), process_links="filter_process_links"), ) _test_regex = re.compile('nofollow') def filter_process_links(self, links): return [link for link in links if not self._test_regex.search(link.url)] spider = _CrawlSpider() output = list(spider._requests_to_follow(response)) self.assertEqual(len(output), 2) self.assertTrue(all(map(lambda r: isinstance(r, Request), output))) self.assertEqual([r.url for r in output], ['http://example.org/somepage/item/12.html', 'http://example.org/about.html']) def test_process_links_generator(self): response = HtmlResponse("http://example.org/somepage/index.html", body=self.test_body) class _CrawlSpider(self.spider_class): name = "test" allowed_domains = ['example.org'] rules = ( Rule(LinkExtractor(), process_links="dummy_process_links"), ) def dummy_process_links(self, links): for link in links: yield link spider = _CrawlSpider() output = list(spider._requests_to_follow(response)) self.assertEqual(len(output), 3) self.assertTrue(all(map(lambda r: isinstance(r, Request), output))) self.assertEqual([r.url for r in output], ['http://example.org/somepage/item/12.html', 'http://example.org/about.html', 'http://example.org/nofollow.html']) def test_process_request(self): response = HtmlResponse("http://example.org/somepage/index.html", body=self.test_body) def process_request_change_domain(request, response): return request.replace(url=request.url.replace('.org', '.com')) class _CrawlSpider(self.spider_class): name = "test" allowed_domains = ['example.org'] rules = ( Rule(LinkExtractor(), process_request=process_request_change_domain), ) spider = _CrawlSpider() output = list(spider._requests_to_follow(response)) self.assertEqual(len(output), 3) self.assertTrue(all(map(lambda r: isinstance(r, Request), output))) self.assertEqual([r.url for r in output], ['http://example.com/somepage/item/12.html', 'http://example.com/about.html', 'http://example.com/nofollow.html']) def test_process_request_with_response(self): response = HtmlResponse("http://example.org/somepage/index.html", body=self.test_body) def process_request_meta_response_class(request, response): request.meta['response_class'] = response.__class__.__name__ return request class _CrawlSpider(self.spider_class): name = "test" allowed_domains = ['example.org'] rules = ( Rule(LinkExtractor(), process_request=process_request_meta_response_class), ) spider = _CrawlSpider() output = list(spider._requests_to_follow(response)) self.assertEqual(len(output), 3) self.assertTrue(all(map(lambda r: isinstance(r, Request), output))) self.assertEqual([r.url for r in output], ['http://example.org/somepage/item/12.html', 'http://example.org/about.html', 'http://example.org/nofollow.html']) self.assertEqual([r.meta['response_class'] for r in output], ['HtmlResponse', 'HtmlResponse', 'HtmlResponse']) def test_process_request_instance_method(self): response = HtmlResponse("http://example.org/somepage/index.html", body=self.test_body) class _CrawlSpider(self.spider_class): name = "test" allowed_domains = ['example.org'] rules = ( Rule(LinkExtractor(), process_request='process_request_upper'), ) def process_request_upper(self, request, response): return request.replace(url=request.url.upper()) spider = _CrawlSpider() output = list(spider._requests_to_follow(response)) self.assertEqual(len(output), 3) self.assertTrue(all(map(lambda r: isinstance(r, Request), output))) self.assertEqual([r.url for r in output], [safe_url_string('http://EXAMPLE.ORG/SOMEPAGE/ITEM/12.HTML'), safe_url_string('http://EXAMPLE.ORG/ABOUT.HTML'), safe_url_string('http://EXAMPLE.ORG/NOFOLLOW.HTML')]) def test_process_request_instance_method_with_response(self): response = HtmlResponse("http://example.org/somepage/index.html", body=self.test_body) class _CrawlSpider(self.spider_class): name = "test" allowed_domains = ['example.org'] rules = ( Rule(LinkExtractor(), process_request='process_request_meta_response_class'), ) def process_request_meta_response_class(self, request, response): request.meta['response_class'] = response.__class__.__name__ return request spider = _CrawlSpider() output = list(spider._requests_to_follow(response)) self.assertEqual(len(output), 3) self.assertTrue(all(map(lambda r: isinstance(r, Request), output))) self.assertEqual([r.url for r in output], ['http://example.org/somepage/item/12.html', 'http://example.org/about.html', 'http://example.org/nofollow.html']) self.assertEqual([r.meta['response_class'] for r in output], ['HtmlResponse', 'HtmlResponse', 'HtmlResponse']) 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) 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) def test_start_url(self): spider = self.spider_class("example.com") spider.start_url = 'https://www.example.com' with self.assertRaisesRegex(AttributeError, r'^Crawling could not start.*$'): list(spider.start_requests()) class SitemapSpiderTest(SpiderTest): spider_class = SitemapSpider BODY = b"SITEMAP" f = BytesIO() g = gzip.GzipFile(fileobj=f, mode='w+b') g.write(BODY) g.close() GZBODY = f.getvalue() def assertSitemapBody(self, response, body): spider = self.spider_class("example.com") self.assertEqual(spider._get_sitemap_body(response), body) def test_get_sitemap_body(self): r = XmlResponse(url="http://www.example.com/", body=self.BODY) self.assertSitemapBody(r, self.BODY) r = HtmlResponse(url="http://www.example.com/", body=self.BODY) self.assertSitemapBody(r, None) r = Response(url="http://www.example.com/favicon.ico", body=self.BODY) self.assertSitemapBody(r, None) def test_get_sitemap_body_gzip_headers(self): r = Response(url="http://www.example.com/sitemap", body=self.GZBODY, headers={"content-type": "application/gzip"}) self.assertSitemapBody(r, self.BODY) def test_get_sitemap_body_xml_url(self): r = TextResponse(url="http://www.example.com/sitemap.xml", body=self.BODY) self.assertSitemapBody(r, self.BODY) def test_get_sitemap_body_xml_url_compressed(self): r = Response(url="http://www.example.com/sitemap.xml.gz", body=self.GZBODY) self.assertSitemapBody(r, self.BODY) # .xml.gz but body decoded by HttpCompression middleware already r = Response(url="http://www.example.com/sitemap.xml.gz", body=self.BODY) self.assertSitemapBody(r, self.BODY) def test_get_sitemap_urls_from_robotstxt(self): robots = b"""# Sitemap files Sitemap: http://example.com/sitemap.xml Sitemap: http://example.com/sitemap-product-index.xml Sitemap: HTTP://example.com/sitemap-uppercase.xml Sitemap: /sitemap-relative-url.xml """ r = TextResponse(url="http://www.example.com/robots.txt", body=robots) spider = self.spider_class("example.com") self.assertEqual([req.url for req in spider._parse_sitemap(r)], ['http://example.com/sitemap.xml', 'http://example.com/sitemap-product-index.xml', 'http://example.com/sitemap-uppercase.xml', 'http://www.example.com/sitemap-relative-url.xml']) def test_alternate_url_locs(self): sitemap = b"""<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> <url> <loc>http://www.example.com/english/</loc> <xhtml:link rel="alternate" hreflang="de" href="http://www.example.com/deutsch/"/> <xhtml:link rel="alternate" hreflang="de-ch" href="http://www.example.com/schweiz-deutsch/"/> <xhtml:link rel="alternate" hreflang="it" href="http://www.example.com/italiano/"/> <xhtml:link rel="alternate" hreflang="it"/><!-- wrong tag without href --> </url> </urlset>""" r = TextResponse(url="http://www.example.com/sitemap.xml", body=sitemap) spider = self.spider_class("example.com") self.assertEqual([req.url for req in spider._parse_sitemap(r)], ['http://www.example.com/english/']) spider.sitemap_alternate_links = True self.assertEqual([req.url for req in spider._parse_sitemap(r)], ['http://www.example.com/english/', 'http://www.example.com/deutsch/', 'http://www.example.com/schweiz-deutsch/', 'http://www.example.com/italiano/']) def test_sitemap_filter(self): sitemap = b"""<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> <url> <loc>http://www.example.com/english/</loc> <lastmod>2010-01-01</lastmod> </url> <url> <loc>http://www.example.com/portuguese/</loc> <lastmod>2005-01-01</lastmod> </url> </urlset>""" class FilteredSitemapSpider(self.spider_class): def sitemap_filter(self, entries): from datetime import datetime for entry in entries: date_time = datetime.strptime(entry['lastmod'], '%Y-%m-%d') if date_time.year > 2008: yield entry r = TextResponse(url="http://www.example.com/sitemap.xml", body=sitemap) spider = self.spider_class("example.com") self.assertEqual([req.url for req in spider._parse_sitemap(r)], ['http://www.example.com/english/', 'http://www.example.com/portuguese/']) spider = FilteredSitemapSpider("example.com") self.assertEqual([req.url for req in spider._parse_sitemap(r)], ['http://www.example.com/english/']) def test_sitemap_filter_with_alternate_links(self): sitemap = b"""<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> <url> <loc>http://www.example.com/english/article_1/</loc> <lastmod>2010-01-01</lastmod> <xhtml:link rel="alternate" hreflang="de" href="http://www.example.com/deutsch/article_1/"/> </url> <url> <loc>http://www.example.com/english/article_2/</loc> <lastmod>2015-01-01</lastmod> </url> </urlset>""" class FilteredSitemapSpider(self.spider_class): def sitemap_filter(self, entries): for entry in entries: alternate_links = entry.get('alternate', tuple()) for link in alternate_links: if '/deutsch/' in link: entry['loc'] = link yield entry r = TextResponse(url="http://www.example.com/sitemap.xml", body=sitemap) spider = self.spider_class("example.com") self.assertEqual([req.url for req in spider._parse_sitemap(r)], ['http://www.example.com/english/article_1/', 'http://www.example.com/english/article_2/']) spider = FilteredSitemapSpider("example.com") self.assertEqual([req.url for req in spider._parse_sitemap(r)], ['http://www.example.com/deutsch/article_1/']) def test_sitemapindex_filter(self): sitemap = b"""<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>http://www.example.com/sitemap1.xml</loc> <lastmod>2004-01-01T20:00:00+00:00</lastmod> </sitemap> <sitemap> <loc>http://www.example.com/sitemap2.xml</loc> <lastmod>2005-01-01</lastmod> </sitemap> </sitemapindex>""" class FilteredSitemapSpider(self.spider_class): def sitemap_filter(self, entries): from datetime import datetime for entry in entries: date_time = datetime.strptime(entry['lastmod'].split('T')[0], '%Y-%m-%d') if date_time.year > 2004: yield entry r = TextResponse(url="http://www.example.com/sitemap.xml", body=sitemap) spider = self.spider_class("example.com") self.assertEqual([req.url for req in spider._parse_sitemap(r)], ['http://www.example.com/sitemap1.xml', 'http://www.example.com/sitemap2.xml']) spider = FilteredSitemapSpider("example.com") self.assertEqual([req.url for req in spider._parse_sitemap(r)], ['http://www.example.com/sitemap2.xml']) class DeprecationTest(unittest.TestCase): def test_crawl_spider(self): assert issubclass(CrawlSpider, Spider) assert isinstance(CrawlSpider(name='foo'), Spider) class NoParseMethodSpiderTest(unittest.TestCase): spider_class = Spider def test_undefined_parse_method(self): spider = self.spider_class('example.com') text = b'Random text' resp = TextResponse(url="http://www.example.com/random_url", body=text) exc_msg = 'Spider.parse callback is not defined' with self.assertRaisesRegex(NotImplementedError, exc_msg): spider.parse(resp)