Upstream scrapy/scrapy state of the touched files before c9f94dc564
This commit is contained in:
commit
6675a8b884
2 files changed
+458
No files matched your search
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
This is a middleware to respect robots.txt policies. To activate it you must
|
||||
enable this middleware and enable the ROBOTSTXT_OBEY setting.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
|
||||
from scrapy import signals
|
||||
from scrapy.exceptions import IgnoreRequest, NotConfigured
|
||||
from scrapy.http import Request, Response
|
||||
from scrapy.http.request import NO_CALLBACK
|
||||
from scrapy.utils.decorators import _warn_spider_arg
|
||||
from scrapy.utils.defer import maybe_deferred_to_future
|
||||
from scrapy.utils.httpobj import urlparse_cached
|
||||
from scrapy.utils.misc import build_from_crawler, load_object
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scrapy import Spider
|
||||
from scrapy.crawler import Crawler
|
||||
from scrapy.robotstxt import RobotParser
|
||||
from scrapy.statscollectors import StatsCollector
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RobotsTxtMiddleware:
|
||||
DOWNLOAD_PRIORITY: int = 1000
|
||||
|
||||
def __init__(self, crawler: Crawler):
|
||||
if not crawler.settings.getbool("ROBOTSTXT_OBEY"):
|
||||
raise NotConfigured
|
||||
self._default_useragent: str = crawler.settings["USER_AGENT"]
|
||||
self._robotstxt_useragent: str | None = crawler.settings["ROBOTSTXT_USER_AGENT"]
|
||||
self.crawler: Crawler = crawler
|
||||
self._stats: StatsCollector = crawler.stats
|
||||
self._parsers: dict[str, RobotParser | Deferred[RobotParser | None] | None] = {}
|
||||
self._parserimpl: type[RobotParser] = load_object(
|
||||
crawler.settings.get("ROBOTSTXT_PARSER")
|
||||
)
|
||||
|
||||
# check if parser dependencies are met, this should throw an error otherwise.
|
||||
build_from_crawler(self._parserimpl, self.crawler, b"")
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler: Crawler) -> Self:
|
||||
return cls(crawler)
|
||||
|
||||
@_warn_spider_arg
|
||||
async def process_request(
|
||||
self, request: Request, spider: Spider | None = None
|
||||
) -> None:
|
||||
if request.meta.get("dont_obey_robotstxt"):
|
||||
return
|
||||
if request.url.startswith("data:") or request.url.startswith("file:"):
|
||||
return
|
||||
rp = await self.robot_parser(request)
|
||||
self.process_request_2(rp, request)
|
||||
|
||||
def process_request_2(self, rp: RobotParser | None, request: Request) -> None:
|
||||
if rp is None:
|
||||
return
|
||||
|
||||
useragent: str | bytes | None = self._robotstxt_useragent
|
||||
if not useragent:
|
||||
useragent = request.headers.get(b"User-Agent", self._default_useragent)
|
||||
assert useragent is not None
|
||||
if not rp.allowed(request.url, useragent):
|
||||
logger.debug(
|
||||
"Forbidden by robots.txt: %(request)s",
|
||||
{"request": request},
|
||||
extra={"spider": self.crawler.spider},
|
||||
)
|
||||
self._stats.inc_value("robotstxt/forbidden")
|
||||
raise IgnoreRequest("Forbidden by robots.txt")
|
||||
|
||||
async def robot_parser(self, request: Request) -> RobotParser | None:
|
||||
url = urlparse_cached(request)
|
||||
netloc = url.netloc
|
||||
|
||||
if netloc not in self._parsers:
|
||||
self._parsers[netloc] = Deferred()
|
||||
robotsurl = f"{url.scheme}://{url.netloc}/robots.txt"
|
||||
meta: dict[str, Any] = {
|
||||
"dont_obey_robotstxt": True,
|
||||
"is_robotstxt_request": True,
|
||||
}
|
||||
if "download_handler" in request.meta:
|
||||
meta["download_handler"] = request.meta["download_handler"]
|
||||
robotsreq = Request(
|
||||
robotsurl,
|
||||
priority=self.DOWNLOAD_PRIORITY,
|
||||
meta=meta,
|
||||
callback=NO_CALLBACK,
|
||||
)
|
||||
try:
|
||||
resp = await self.crawler.engine.download_async(robotsreq)
|
||||
await self._parse_robots(resp, netloc, request)
|
||||
except Exception as e:
|
||||
if not isinstance(e, IgnoreRequest):
|
||||
logger.exception(
|
||||
f"Error downloading {request}",
|
||||
extra={"spider": self.crawler.spider},
|
||||
)
|
||||
self._robots_error(e, netloc)
|
||||
self._stats.inc_value("robotstxt/request_count")
|
||||
|
||||
parser = self._parsers[netloc]
|
||||
if isinstance(parser, Deferred):
|
||||
return await maybe_deferred_to_future(parser)
|
||||
return parser
|
||||
|
||||
async def _parse_robots(
|
||||
self, response: Response, netloc: str, request: Request
|
||||
) -> None:
|
||||
self._stats.inc_value("robotstxt/response_count")
|
||||
self._stats.inc_value(f"robotstxt/response_status_count/{response.status}")
|
||||
rp = build_from_crawler(self._parserimpl, self.crawler, response.body)
|
||||
await self.crawler.signals.send_catch_log_async(
|
||||
signal=signals.robots_parsed,
|
||||
robotparser=rp,
|
||||
request=request,
|
||||
)
|
||||
rp_dfd = self._parsers[netloc]
|
||||
assert isinstance(rp_dfd, Deferred)
|
||||
self._parsers[netloc] = rp
|
||||
rp_dfd.callback(rp)
|
||||
|
||||
def _robots_error(self, exc: Exception, netloc: str) -> None:
|
||||
if not isinstance(exc, IgnoreRequest):
|
||||
key = f"robotstxt/exception_count/{type(exc)}"
|
||||
self._stats.inc_value(key)
|
||||
rp_dfd = self._parsers[netloc]
|
||||
assert isinstance(rp_dfd, Deferred)
|
||||
self._parsers[netloc] = None
|
||||
rp_dfd.callback(None)
|
||||
Reference in new issue
Block a user