mirror of https://github.com/scrapy/scrapy.git
532 lines
15 KiB
Python
532 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
|
|
from scrapy.commands import parse
|
|
from scrapy.settings import Settings
|
|
from tests.test_commands import TestProjectBase
|
|
from tests.utils.cmdline import call, proc
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
from tests.mockserver.http import MockServer
|
|
|
|
|
|
class TestParseCommand(TestProjectBase):
|
|
spider_name = "parse_spider"
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def create_files(self, proj_path: Path) -> None:
|
|
proj_mod_path = proj_path / self.project_name
|
|
(proj_mod_path / "spiders" / "myspider.py").write_text(
|
|
f"""
|
|
import scrapy
|
|
from scrapy.linkextractors import LinkExtractor
|
|
from scrapy.spiders import CrawlSpider, Rule
|
|
from scrapy.utils.test import get_from_asyncio_queue
|
|
import asyncio
|
|
|
|
|
|
class BaseSpider(scrapy.Spider):
|
|
custom_settings = {{
|
|
"DOWNLOAD_DELAY": 0,
|
|
}}
|
|
|
|
|
|
class AsyncDefAsyncioReturnSpider(BaseSpider):
|
|
name = "asyncdef_asyncio_return"
|
|
|
|
async def parse(self, response):
|
|
await asyncio.sleep(0.2)
|
|
status = await get_from_asyncio_queue(response.status)
|
|
self.logger.info(f"Got response {{status}}")
|
|
return [{{'id': 1}}, {{'id': 2}}]
|
|
|
|
class AsyncDefAsyncioReturnSingleElementSpider(BaseSpider):
|
|
name = "asyncdef_asyncio_return_single_element"
|
|
|
|
async def parse(self, response):
|
|
await asyncio.sleep(0.1)
|
|
status = await get_from_asyncio_queue(response.status)
|
|
self.logger.info(f"Got response {{status}}")
|
|
return {{'foo': 42}}
|
|
|
|
class AsyncDefAsyncioGenLoopSpider(BaseSpider):
|
|
name = "asyncdef_asyncio_gen_loop"
|
|
|
|
async def parse(self, response):
|
|
for i in range(10):
|
|
await asyncio.sleep(0.1)
|
|
yield {{'foo': i}}
|
|
self.logger.info(f"Got response {{response.status}}")
|
|
|
|
class AsyncDefAsyncioSpider(BaseSpider):
|
|
name = "asyncdef_asyncio"
|
|
|
|
async def parse(self, response):
|
|
await asyncio.sleep(0.2)
|
|
status = await get_from_asyncio_queue(response.status)
|
|
self.logger.debug(f"Got response {{status}}")
|
|
|
|
class AsyncDefAsyncioGenExcSpider(BaseSpider):
|
|
name = "asyncdef_asyncio_gen_exc"
|
|
|
|
async def parse(self, response):
|
|
for i in range(10):
|
|
await asyncio.sleep(0.1)
|
|
yield {{'foo': i}}
|
|
if i > 5:
|
|
raise ValueError("Stopping the processing")
|
|
|
|
class CallbackSignatureDownloaderMiddleware:
|
|
def process_request(self, request, spider):
|
|
from inspect import signature
|
|
spider.logger.debug(f"request.callback signature: {{signature(request.callback)}}")
|
|
|
|
|
|
class MySpider(scrapy.Spider):
|
|
name = '{self.spider_name}'
|
|
|
|
custom_settings = {{
|
|
"DOWNLOADER_MIDDLEWARES": {{
|
|
CallbackSignatureDownloaderMiddleware: 0,
|
|
}},
|
|
"DOWNLOAD_DELAY": 0,
|
|
}}
|
|
|
|
def parse(self, response):
|
|
if getattr(self, 'test_arg', None):
|
|
self.logger.debug('It Works!')
|
|
return [scrapy.Item(), dict(foo='bar')]
|
|
|
|
def parse_request_with_meta(self, response):
|
|
foo = response.meta.get('foo', 'bar')
|
|
|
|
if foo == 'bar':
|
|
self.logger.debug('It Does Not Work :(')
|
|
else:
|
|
self.logger.debug('It Works!')
|
|
|
|
def parse_request_with_cb_kwargs(self, response, foo=None, key=None):
|
|
if foo == 'bar' and key == 'value':
|
|
self.logger.debug('It Works!')
|
|
else:
|
|
self.logger.debug('It Does Not Work :(')
|
|
|
|
def parse_request_without_meta(self, response):
|
|
foo = response.meta.get('foo', 'bar')
|
|
|
|
if foo == 'bar':
|
|
self.logger.debug('It Works!')
|
|
else:
|
|
self.logger.debug('It Does Not Work :(')
|
|
|
|
class MyGoodCrawlSpider(CrawlSpider):
|
|
name = 'goodcrawl{self.spider_name}'
|
|
|
|
custom_settings = {{
|
|
"DOWNLOAD_DELAY": 0,
|
|
}}
|
|
|
|
rules = (
|
|
Rule(LinkExtractor(allow=r'/html'), callback='parse_item', follow=True),
|
|
Rule(LinkExtractor(allow=r'/text'), follow=True),
|
|
)
|
|
|
|
def parse_item(self, response):
|
|
return [scrapy.Item(), dict(foo='bar')]
|
|
|
|
def parse(self, response):
|
|
return [scrapy.Item(), dict(nomatch='default')]
|
|
|
|
|
|
class MyBadCrawlSpider(CrawlSpider):
|
|
'''Spider which doesn't define a parse_item callback while using it in a rule.'''
|
|
name = 'badcrawl{self.spider_name}'
|
|
|
|
custom_settings = {{
|
|
"DOWNLOAD_DELAY": 0,
|
|
}}
|
|
|
|
rules = (
|
|
Rule(LinkExtractor(allow=r'/html'), callback='parse_item', follow=True),
|
|
)
|
|
|
|
def parse(self, response):
|
|
return [scrapy.Item(), dict(foo='bar')]
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
(proj_mod_path / "pipelines.py").write_text(
|
|
"""
|
|
import logging
|
|
|
|
class MyPipeline:
|
|
component_name = 'my_pipeline'
|
|
|
|
def process_item(self, item):
|
|
logging.info('It Works!')
|
|
return item
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with (proj_mod_path / "settings.py").open("a", encoding="utf-8") as f:
|
|
f.write(
|
|
f"""
|
|
ITEM_PIPELINES = {{'{self.project_name}.pipelines.MyPipeline': 1}}
|
|
"""
|
|
)
|
|
|
|
def test_spider_arguments(self, proj_path: Path, mockserver: MockServer) -> None:
|
|
_, _, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
"-a",
|
|
"test_arg=1",
|
|
"-c",
|
|
"parse",
|
|
"--verbose",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "DEBUG: It Works!" in stderr
|
|
|
|
def test_request_with_meta(self, proj_path: Path, mockserver: MockServer) -> None:
|
|
raw_json_string = '{"foo" : "baz"}'
|
|
_, _, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
"--meta",
|
|
raw_json_string,
|
|
"-c",
|
|
"parse_request_with_meta",
|
|
"--verbose",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "DEBUG: It Works!" in stderr
|
|
|
|
_, _, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
"-m",
|
|
raw_json_string,
|
|
"-c",
|
|
"parse_request_with_meta",
|
|
"--verbose",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "DEBUG: It Works!" in stderr
|
|
|
|
def test_request_with_cb_kwargs(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
raw_json_string = '{"foo" : "bar", "key": "value"}'
|
|
_, _, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
"--cbkwargs",
|
|
raw_json_string,
|
|
"-c",
|
|
"parse_request_with_cb_kwargs",
|
|
"--verbose",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "DEBUG: It Works!" in stderr
|
|
assert (
|
|
"DEBUG: request.callback signature: (response, foo=None, key=None)"
|
|
in stderr
|
|
)
|
|
|
|
def test_request_without_meta(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
_, _, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
"-c",
|
|
"parse_request_without_meta",
|
|
"--nolinks",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "DEBUG: It Works!" in stderr
|
|
|
|
def test_pipelines(self, proj_path: Path, mockserver: MockServer) -> None:
|
|
_, _, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
"--pipelines",
|
|
"-c",
|
|
"parse",
|
|
"--verbose",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "INFO: It Works!" in stderr
|
|
|
|
def test_async_def_asyncio_parse_items_list(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
_, out, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
"asyncdef_asyncio_return",
|
|
"-c",
|
|
"parse",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "INFO: Got response 200" in stderr
|
|
assert "{'id': 1}" in out
|
|
assert "{'id': 2}" in out
|
|
|
|
def test_async_def_asyncio_parse_items_single_element(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
_, out, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
"asyncdef_asyncio_return_single_element",
|
|
"-c",
|
|
"parse",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "INFO: Got response 200" in stderr
|
|
assert "{'foo': 42}" in out
|
|
|
|
def test_async_def_asyncgen_parse_loop(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
_, out, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
"asyncdef_asyncio_gen_loop",
|
|
"-c",
|
|
"parse",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "INFO: Got response 200" in stderr
|
|
for i in range(10):
|
|
assert f"{{'foo': {i}}}" in out
|
|
|
|
def test_async_def_asyncgen_parse_exc(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
_, out, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
"asyncdef_asyncio_gen_exc",
|
|
"-c",
|
|
"parse",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "ValueError" in stderr
|
|
for i in range(7):
|
|
assert f"{{'foo': {i}}}" in out
|
|
|
|
def test_async_def_asyncio_parse(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
_, _, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
"asyncdef_asyncio",
|
|
"-c",
|
|
"parse",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "DEBUG: Got response 200" in stderr
|
|
|
|
def test_parse_items(self, proj_path: Path, mockserver: MockServer) -> None:
|
|
_, out, _ = proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
"-c",
|
|
"parse",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "[{}, {'foo': 'bar'}]" in out
|
|
|
|
def test_parse_items_no_callback_passed(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
_, out, _ = proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "[{}, {'foo': 'bar'}]" in out
|
|
|
|
def test_wrong_callback_passed(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
_, out, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
"-c",
|
|
"dummy",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert re.search(r"# Scraped Items -+\r?\n\[\]", out)
|
|
assert "Cannot find callback" in stderr
|
|
|
|
def test_crawlspider_matching_rule_callback_set(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
"""If a rule matches the URL, use it's defined callback."""
|
|
_, out, _ = proc(
|
|
"parse",
|
|
"--spider",
|
|
"goodcrawl" + self.spider_name,
|
|
"-r",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "[{}, {'foo': 'bar'}]" in out
|
|
|
|
def test_crawlspider_matching_rule_default_callback(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
"""If a rule match but it has no callback set, use the 'parse' callback."""
|
|
_, out, _ = proc(
|
|
"parse",
|
|
"--spider",
|
|
"goodcrawl" + self.spider_name,
|
|
"-r",
|
|
mockserver.url("/text"),
|
|
cwd=proj_path,
|
|
)
|
|
assert "[{}, {'nomatch': 'default'}]" in out
|
|
|
|
def test_spider_with_no_rules_attribute(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
"""Using -r with a spider with no rule should not produce items."""
|
|
_, out, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
"-r",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert re.search(r"# Scraped Items -+\r?\n\[\]", out)
|
|
assert "No CrawlSpider rules found" in stderr
|
|
|
|
def test_crawlspider_missing_callback(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
_, out, _ = proc(
|
|
"parse",
|
|
"--spider",
|
|
"badcrawl" + self.spider_name,
|
|
"-r",
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
assert re.search(r"# Scraped Items -+\r?\n\[\]", out)
|
|
|
|
def test_crawlspider_no_matching_rule(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
"""The requested URL has no matching rule, so no items should be scraped"""
|
|
_, out, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
"badcrawl" + self.spider_name,
|
|
"-r",
|
|
mockserver.url("/enc-gb18030"),
|
|
cwd=proj_path,
|
|
)
|
|
assert re.search(r"# Scraped Items -+\r?\n\[\]", out)
|
|
assert "Cannot find a rule that matches" in stderr
|
|
|
|
def test_crawlspider_not_exists_with_not_matched_url(
|
|
self, proj_path: Path, mockserver: MockServer
|
|
) -> None:
|
|
assert call("parse", mockserver.url("/invalid_url"), cwd=proj_path) == 0
|
|
|
|
def test_output_flag(self, proj_path: Path, mockserver: MockServer) -> None:
|
|
"""Checks if a file was created successfully having
|
|
correct format containing correct data in it.
|
|
"""
|
|
file_name = "data.json"
|
|
file_path = proj_path / file_name
|
|
proc(
|
|
"parse",
|
|
"--spider",
|
|
self.spider_name,
|
|
"-c",
|
|
"parse",
|
|
"-o",
|
|
file_name,
|
|
mockserver.url("/html"),
|
|
cwd=proj_path,
|
|
)
|
|
|
|
assert file_path.exists()
|
|
assert file_path.is_file()
|
|
|
|
content = '[\n{},\n{"foo": "bar"}\n]'
|
|
assert file_path.read_text(encoding="utf-8") == content
|
|
|
|
def test_parse_add_options(self):
|
|
command = parse.Command()
|
|
command.settings = Settings()
|
|
parser = argparse.ArgumentParser(
|
|
prog="scrapy",
|
|
formatter_class=argparse.HelpFormatter,
|
|
conflict_handler="resolve",
|
|
prefix_chars="-",
|
|
)
|
|
command.add_options(parser)
|
|
namespace = parser.parse_args(
|
|
["--verbose", "--nolinks", "-d", "2", "--spider", self.spider_name]
|
|
)
|
|
assert namespace.nolinks
|
|
assert namespace.depth == 2
|
|
assert namespace.spider == self.spider_name
|
|
assert namespace.verbose
|
|
|
|
def test_no_reactor(self, proj_path: Path, mockserver: MockServer) -> None:
|
|
_, out, stderr = proc(
|
|
"parse",
|
|
"--spider",
|
|
"asyncdef_asyncio_return",
|
|
"-c",
|
|
"parse",
|
|
mockserver.url("/html"),
|
|
"-s",
|
|
"TWISTED_REACTOR_ENABLED=False",
|
|
cwd=proj_path,
|
|
)
|
|
assert "INFO: Got response 200" in stderr
|
|
assert "{'id': 1}" in out
|
|
assert "{'id': 2}" in out
|