mirror of https://github.com/scrapy/scrapy.git
203 lines
7.4 KiB
Python
203 lines
7.4 KiB
Python
"""
|
|
An extension to retry failed requests that are potentially caused by temporary
|
|
problems such as a connection timeout or HTTP 500 error.
|
|
|
|
You can change the behaviour of this middleware by modifying the scraping settings:
|
|
RETRY_TIMES - how many times to retry a failed page
|
|
RETRY_HTTP_CODES - which HTTP response codes to retry
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from logging import Logger, getLevelName, getLogger
|
|
from typing import TYPE_CHECKING
|
|
|
|
from scrapy.exceptions import NotConfigured
|
|
from scrapy.utils.decorators import _warn_spider_arg
|
|
from scrapy.utils.misc import _load_objects
|
|
from scrapy.utils.python import global_object_name
|
|
from scrapy.utils.response import response_status_message
|
|
|
|
if TYPE_CHECKING:
|
|
# typing.Self requires Python 3.11
|
|
from typing_extensions import Self
|
|
|
|
import scrapy
|
|
from scrapy.crawler import Crawler
|
|
from scrapy.http import Response
|
|
from scrapy.http.request import Request
|
|
from scrapy.settings import BaseSettings
|
|
|
|
|
|
retry_logger = getLogger(__name__)
|
|
|
|
|
|
def get_retry_request(
|
|
request: Request,
|
|
*,
|
|
spider: scrapy.Spider,
|
|
reason: str | Exception | type[Exception] = "unspecified",
|
|
max_retry_times: int | None = None,
|
|
priority_adjust: int | None = None,
|
|
logger: Logger = retry_logger,
|
|
give_up_log_level: int | str | None = None,
|
|
stats_base_key: str = "retry",
|
|
) -> Request | None:
|
|
"""
|
|
Returns a new :class:`~scrapy.Request` object to retry the specified
|
|
request, or ``None`` if retries of the specified request have been
|
|
exhausted.
|
|
|
|
For example, in a :class:`~scrapy.Spider` callback, you could use it as
|
|
follows:
|
|
|
|
.. code-block:: python
|
|
|
|
def parse(self, response):
|
|
if not response.text:
|
|
new_request_or_none = get_retry_request(
|
|
response.request,
|
|
spider=self,
|
|
reason="empty",
|
|
)
|
|
return new_request_or_none
|
|
|
|
*spider* is the :class:`~scrapy.Spider` instance which is asking for the
|
|
retry request. It is used to access the :ref:`settings <topics-settings>`
|
|
and :ref:`stats <topics-stats>`, and to provide extra logging context (see
|
|
:func:`logging.debug`).
|
|
|
|
*reason* is a string, an :class:`Exception` subclass or an
|
|
:class:`Exception` object that indicates the reason why the request needs
|
|
to be retried. It is used to name retry stats.
|
|
|
|
*max_retry_times* is a number that determines the maximum number of times
|
|
that *request* can be retried. If not specified or ``None``, the number is
|
|
read from the :reqmeta:`max_retry_times` meta key of the request. If the
|
|
:reqmeta:`max_retry_times` meta key is not defined or ``None``, the number
|
|
is read from the :setting:`RETRY_TIMES` setting.
|
|
|
|
*priority_adjust* is a number that determines how the priority of the new
|
|
request changes in relation to *request*. If not specified, the number is
|
|
read from the :setting:`RETRY_PRIORITY_ADJUST` setting.
|
|
|
|
*logger* is the logging.Logger object to be used when logging messages
|
|
|
|
*give_up_log_level* is the :ref:`logging level <levels>` used for the
|
|
message logged when a request exceeds its retries. See
|
|
:setting:`RETRY_GIVE_UP_LOG_LEVEL` for details.
|
|
|
|
.. versionadded:: 2.17.0
|
|
The *give_up_log_level* parameter.
|
|
|
|
*stats_base_key* is a string to be used as the base key for the
|
|
retry-related job stats
|
|
"""
|
|
settings = spider.crawler.settings
|
|
stats = spider.crawler.stats
|
|
retry_times = request.meta.get("retry_times", 0) + 1
|
|
if max_retry_times is None:
|
|
max_retry_times = request.meta.get("max_retry_times")
|
|
if max_retry_times is None:
|
|
max_retry_times = settings.getint("RETRY_TIMES")
|
|
if retry_times <= max_retry_times:
|
|
logger.debug(
|
|
"Retrying %(request)s (failed %(retry_times)d times): %(reason)s",
|
|
{"request": request, "retry_times": retry_times, "reason": reason},
|
|
extra={"spider": spider},
|
|
)
|
|
new_request: Request = request.copy()
|
|
new_request.meta["retry_times"] = retry_times
|
|
new_request.dont_filter = True
|
|
if priority_adjust is None:
|
|
priority_adjust = settings.getint("RETRY_PRIORITY_ADJUST")
|
|
new_request.priority = request.priority + priority_adjust
|
|
|
|
if callable(reason):
|
|
reason = reason()
|
|
if isinstance(reason, Exception):
|
|
reason = global_object_name(reason.__class__)
|
|
|
|
stats.inc_value(f"{stats_base_key}/count")
|
|
stats.inc_value(f"{stats_base_key}/reason_count/{reason}")
|
|
return new_request
|
|
if give_up_log_level is None:
|
|
give_up_log_level = settings["RETRY_GIVE_UP_LOG_LEVEL"]
|
|
if isinstance(give_up_log_level, str):
|
|
level = getLevelName(give_up_log_level)
|
|
if not isinstance(level, int):
|
|
raise ValueError(f"Invalid give-up log level: {give_up_log_level!r}")
|
|
give_up_log_level = level
|
|
stats.inc_value(f"{stats_base_key}/max_reached")
|
|
logger.log(
|
|
give_up_log_level,
|
|
"Gave up retrying %(request)s (failed %(retry_times)d times): %(reason)s",
|
|
{"request": request, "retry_times": retry_times, "reason": reason},
|
|
extra={"spider": spider},
|
|
)
|
|
return None
|
|
|
|
|
|
class RetryMiddleware:
|
|
crawler: Crawler
|
|
|
|
def __init__(self, settings: BaseSettings):
|
|
if not settings.getbool("RETRY_ENABLED"):
|
|
raise NotConfigured
|
|
self.max_retry_times = settings.getint("RETRY_TIMES")
|
|
self.retry_http_codes = {int(x) for x in settings.getlist("RETRY_HTTP_CODES")}
|
|
self.priority_adjust = settings.getint("RETRY_PRIORITY_ADJUST")
|
|
self.give_up_log_level = settings["RETRY_GIVE_UP_LOG_LEVEL"]
|
|
self.exceptions_to_retry = _load_objects(settings.getlist("RETRY_EXCEPTIONS"))
|
|
|
|
@classmethod
|
|
def from_crawler(cls, crawler: Crawler) -> Self:
|
|
o = cls(crawler.settings)
|
|
o.crawler = crawler
|
|
return o
|
|
|
|
@_warn_spider_arg
|
|
def process_response(
|
|
self,
|
|
request: Request,
|
|
response: Response,
|
|
spider: scrapy.Spider | None = None,
|
|
) -> Request | Response:
|
|
if request.meta.get("dont_retry", False):
|
|
return response
|
|
if response.status in self.retry_http_codes:
|
|
reason = response_status_message(response.status)
|
|
return self._retry(request, reason) or response
|
|
return response
|
|
|
|
@_warn_spider_arg
|
|
def process_exception(
|
|
self,
|
|
request: Request,
|
|
exception: Exception,
|
|
spider: scrapy.Spider | None = None,
|
|
) -> Request | Response | None:
|
|
if isinstance(exception, self.exceptions_to_retry) and not request.meta.get(
|
|
"dont_retry", False
|
|
):
|
|
return self._retry(request, exception)
|
|
return None
|
|
|
|
def _retry(
|
|
self, request: Request, reason: str | Exception | type[Exception]
|
|
) -> Request | None:
|
|
max_retry_times = request.meta.get("max_retry_times", self.max_retry_times)
|
|
priority_adjust = request.meta.get("priority_adjust", self.priority_adjust)
|
|
give_up_log_level = request.meta.get(
|
|
"give_up_log_level", self.give_up_log_level
|
|
)
|
|
assert self.crawler.spider
|
|
return get_retry_request(
|
|
request,
|
|
reason=reason,
|
|
spider=self.crawler.spider,
|
|
max_retry_times=max_retry_times,
|
|
priority_adjust=priority_adjust,
|
|
give_up_log_level=give_up_log_level,
|
|
)
|