introduce CLOSESPIDER_TIMEOUT_NO_ITEM in CloseSpider

This commit is contained in:
Kevin Lloyd Bernal 2023-07-18 18:30:21 +08:00
parent 8055a948dc
commit 3f5bbe3a8f
3 changed files with 65 additions and 1 deletions

View File

@ -258,6 +258,7 @@ The conditions for closing a spider can be configured through the following
settings:
* :setting:`CLOSESPIDER_TIMEOUT`
* :setting:`CLOSESPIDER_TIMEOUT_NO_ITEM`
* :setting:`CLOSESPIDER_ITEMCOUNT`
* :setting:`CLOSESPIDER_PAGECOUNT`
* :setting:`CLOSESPIDER_ERRORCOUNT`
@ -280,6 +281,18 @@ more than that number of second, it will be automatically closed with the
reason ``closespider_timeout``. If zero (or non set), spiders won't be closed by
timeout.
.. setting:: CLOSESPIDER_TIMEOUT_NO_ITEM
CLOSESPIDER_TIMEOUT_NO_ITEM
"""""""""""""""""""""""""""
Default: ``0``
An integer which specifies a number of seconds. If the spider has not produced
any items in the last number of seconds, it will be closed with the reason
``closespider_timeout_no_item``. If zero (or non set), spiders won't be closed
regardless if it hasn't produced any items.
.. setting:: CLOSESPIDER_ITEMCOUNT
CLOSESPIDER_ITEMCOUNT

View File

@ -4,11 +4,14 @@ conditions are met.
See documentation in docs/topics/extensions.rst
"""
import logging
from collections import defaultdict
from scrapy import signals
from scrapy.exceptions import NotConfigured
logger = logging.getLogger(__name__)
class CloseSpider:
def __init__(self, crawler):
@ -19,6 +22,7 @@ class CloseSpider:
"itemcount": crawler.settings.getint("CLOSESPIDER_ITEMCOUNT"),
"pagecount": crawler.settings.getint("CLOSESPIDER_PAGECOUNT"),
"errorcount": crawler.settings.getint("CLOSESPIDER_ERRORCOUNT"),
"timeout_no_item": crawler.settings.getint("CLOSESPIDER_TIMEOUT_NO_ITEM"),
}
if not any(self.close_on.values()):
@ -34,6 +38,15 @@ class CloseSpider:
crawler.signals.connect(self.spider_opened, signal=signals.spider_opened)
if self.close_on.get("itemcount"):
crawler.signals.connect(self.item_scraped, signal=signals.item_scraped)
if self.close_on.get("timeout_no_item"):
self.timeout_no_item = self.close_on["timeout_no_item"]
self.items_in_period = 0
crawler.signals.connect(
self.spider_opened_no_item, signal=signals.spider_opened
)
crawler.signals.connect(
self.item_scraped_no_item, signal=signals.item_scraped
)
crawler.signals.connect(self.spider_closed, signal=signals.spider_closed)
@classmethod
@ -69,3 +82,31 @@ class CloseSpider:
task = getattr(self, "task", False)
if task and task.active():
task.cancel()
task_no_item = getattr(self, "task_no_item", False)
if task_no_item.running:
task_no_item.stop()
def spider_opened_no_item(self, spider):
from twisted.internet import task
self.task_no_item = task.LoopingCall(self._count_items_produced, spider)
self.task_no_item.start(self.timeout_no_item, now=False)
logger.info(
f"Spider will stop when no items are produced after "
f"{self.timeout_no_item} seconds."
)
def item_scraped_no_item(self, item, spider):
self.items_in_period += 1
def _count_items_produced(self, spider):
if self.items_in_period >= 1:
self.items_in_period = 0
else:
logger.info(
f"Closing spider since no items were produced in the last "
f"{self.timeout_no_item} seconds."
)
self.crawler.engine.close_spider(spider, "closespider_timeout_no_item")

View File

@ -3,7 +3,7 @@ from twisted.trial.unittest import TestCase
from scrapy.utils.test import get_crawler
from tests.mockserver import MockServer
from tests.spiders import ErrorSpider, FollowAllSpider, ItemSpider
from tests.spiders import DelaySpider, ErrorSpider, FollowAllSpider, ItemSpider
class TestCloseSpider(TestCase):
@ -54,3 +54,13 @@ class TestCloseSpider(TestCase):
self.assertEqual(reason, "closespider_timeout")
total_seconds = crawler.stats.get_value("elapsed_time_seconds")
self.assertTrue(total_seconds >= close_on)
@defer.inlineCallbacks
def test_closespider_timeout_no_item(self):
timeout = 1
crawler = get_crawler(DelaySpider, {"CLOSESPIDER_TIMEOUT_NO_ITEM": timeout})
yield crawler.crawl(n=3, total=10, mockserver=self.mockserver)
reason = crawler.spider.meta["close_reason"]
self.assertEqual(reason, "closespider_timeout_no_item")
total_seconds = crawler.stats.get_value("elapsed_time_seconds")
self.assertTrue(total_seconds >= timeout)