Диагностика6 мин чтения

Scrapy: start_requests() не вызывается, Crawled 0 pages

В Scrapy 2.16 удалён Spider.start_requests(): паук пишет Crawled 0 pages и закрывается как finished без предупреждений. Решение: async def start(). Проверено на 2.19.

На этой странице

После обновления до Scrapy 2.16 или новее паук, в котором определён только start_requests(), открывается, не отправляет ни одного запроса и закрывается так, будто отработал:

code
2026-10-09 12:33:33 [scrapy.core.engine] INFO: Spider opened
2026-10-09 12:33:33 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2026-10-09 12:33:33 [scrapy.core.engine] INFO: Closing spider (finished)
2026-10-09 12:33:33 [scrapy.core.engine] INFO: Spider closed (finished)

Это строки движка и logstats из запуска на Scrapy 2.19.0. В журнале не было ни одной строки WARNING или ERROR, код выхода был 0, а parse() не вызывался. В итоговой статистике было 'finish_reason': 'finished' и не было ключа downloader/request_count.

Причина: в Scrapy 2.16.0 (19 мая 2026 года) удалён метод Spider.start_requests(), устаревший с версии 2.13.0. В примечаниях к версии сказано, что он «is removed and no longer called» (pull request #7490). Решение: переименовать метод в async def start():

python
# Before: Scrapy 2.16 and later never call this
def start_requests(self):
    yield scrapy.Request("https://example.com/", callback=self.parse)
python
# After
async def start(self):
    yield scrapy.Request("https://example.com/", callback=self.parse)

После одной этой правки тот же паук обошёл свою страницу на Scrapy 2.16.0 и 2.19.0.

Что вы видите на 2.16+ПричинаРешение
Crawled 0 pages, затем Spider closed (finished), предупреждений нетВ пауке определён только start_requests()Переименуйте его в async def start()
Обход идёт, но middleware больше не меняет стартовые запросыВ spider middleware определён только process_start_requests()Добавьте async def process_start()
TypeError: Middleware ... doesn't support asynchronous spider output, код выхода 1В spider middleware синхронный process_spider_output()Сделайте его асинхронным генератором

Всё, что описано ниже, записано 9 октября 2026 года на Scrapy 2.19.0, Twisted 26.4.0 и Python 3.14.4 и повторено на Scrapy 2.16.0, 2.15.2 и 2.12.0. Паука, который только задаёт start_urls и не определяет ни одного из двух методов, изменение не затрагивает: он обошёл страницу на всех четырёх версиях.

Замените start_requests() на async def start()

Spider.start() — асинхронный генератор: async def с yield. Сами запросы остаются прежними. Две оплошности дают другой симптом: строку ERROR и по-прежнему ни одного запроса:

code
TypeError: e1_start_not_async.LabSpider.start() must be an asynchronous generator, i.e. an async def method with yield statements, got <class 'generator'>

Это def start() без async. Если async def start() возвращает список, сообщение заканчивается на got <class 'coroutine'>. В обоих случаях Scrapy 2.19.0 закрыл паука с 'finish_reason': 'start_error', а код выхода остался 0.

Если паук переопределял start_requests(), чтобы добавить заголовки или meta к запросам из start_urls, сохраните цикл:

python
async def start(self):
    for url in self.start_urls:
        yield scrapy.Request(
            url,
            headers={"Accept-Language": "en-AU"},
            meta={"page": 1},
            dont_filter=True,
        )

Стандартный start() — тот же цикл с dont_filter=True и больше ничем, поэтому оставьте этот аргумент. В лаборатории callback прочитал заголовок из response.request.headers, а page из response.meta.

Прокси по-прежнему задаётся в meta:

python
async def start(self):
    yield scrapy.Request(
        "https://example.com/",
        meta={"proxy": "http://user:pass@proxy.example.com:8080"},
    )

В лаборатории запрос к адресу http:// пришёл на локальный прокси с Basic-авторизацией с верными учётными данными. Руководство Прокси в Scrapy 2.19 описывает ротацию и переменные окружения прокси, а Ошибка прокси 407 разбирает случай, когда прокси отклоняет учётные данные.

Поддержка Scrapy ниже 2.13 и 2.16+: оба метода

Код, который должен работать по обе стороны от этого изменения, определяет оба метода. В примечаниях к версии сказано так: «Use start() instead, or both to maintain support for lower Scrapy versions.»

python
async def start(self):
    for request in self.start_requests():
        yield request

def start_requests(self):
    yield scrapy.Request("https://example.com/", callback=self.parse)

Scrapy 2.12.0 вызвал start_requests(), а 2.15.2, 2.16.0 и 2.19.0 вызвали start(). Каждая версия обошла страницу, и 2.15.2 не вывел предупреждения об устаревании. Обратная ошибка так же незаметна: паук только с async def start() не отправил ни одного запроса на 2.12.0 и закрылся как finished.

Spider middleware: process_start_requests() пропускается без предупреждения

В том же выпуске удалена поддержка process_start_requests() в spider middleware. Middleware из лаборатории выставлял meta["tagged"] = True на каждом стартовом запросе. На 2.19.0 с уже переведённым пауком страница была обойдена, callback вывел tagged=None, а предупреждений в журнале не было: метод ни разу не вызывался. Scrapy 2.15.2 останавливал ту же пару с ValueError, так что обновление превратило ошибку в тишину.

Замените его на process_start():

python
class TagStartMiddleware:
    async def process_start(self, start):
        async for item_or_request in start:
            if isinstance(item_or_request, scrapy.Request):
                item_or_request.meta["tagged"] = True
            yield item_or_request

С ним callback вывел tagged=True на 2.15.2, 2.16.0 и 2.19.0. Чтобы поддерживать и Scrapy ниже 2.13, оставьте рядом process_start_requests(). Когда оба метода были и в middleware, и в пауке, запрос был помечен на всех четырёх версиях.

TypeError: Middleware doesn't support asynchronous spider output

Синхронный process_spider_output() отказывает громко. Обход останавливается до строки Spider opened, код выхода 1:

code
TypeError: Middleware d2_mw_sync_output.StampMiddleware doesn't support asynchronous spider output. Its process_spider_output() method should be an async generator function or it should additionally define a process_spider_output_async() method.

d2_mw_sync_output.StampMiddleware — это middleware из лаборатории, на его месте будет имя вашего. Scrapy 2.15.2 запускал тот же middleware с предупреждением, что он «will stop working in a future version of Scrapy». Сделайте метод асинхронным генератором:

python
class StampMiddleware:
    async def process_spider_output(self, response, result):
        async for item_or_request in result:
            if isinstance(item_or_request, dict):
                item_or_request["stamped"] = True
            yield item_or_request

В сигнатуре нет параметра spider. С ним 2.19.0 всё ещё запускал middleware и выводил ScrapyDeprecationWarning: StampMiddleware.process_spider_output() requires a spider argument. Middleware, в котором уже есть и синхронный process_spider_output(), и process_spider_output_async(), загрузился без ошибки, и 2.19.0 выполнил асинхронный метод. Если класс из сообщения относится к установленному пакету, поищите выпуск этого пакета, который поддерживает вашу версию Scrapy.

Как узнать, какая версия Scrapy у вас запущена

sh
scrapy version
python -m pip show scrapy

В лаборатории первая команда вывела Scrapy 2.19.0, а вторая Version: 2.19.0. Версию называет и первая строка журнала обхода: Scrapy 2.19.0 started (bot: scrapybot). Выполняйте команды в том окружении, где идёт обход: в виртуальном окружении, образе контейнера или задаче CI.

Строка requirements.txt, в которой написано только scrapy, при следующей сборке установит самый новый выпуск. Если мигрировать сегодня нельзя, закрепите последнюю ветку выпусков, которая ещё вызывает start_requests():

code
scrapy>=2.15,<2.16

В лаборатории это дало 2.15.2: он обошёл страницу неизменённым пауком и вывел ScrapyDeprecationWarning, в котором назван класс паука и сказано, что метод «will stop being called in a future version of Scrapy». Считайте такое закрепление временным: в примечаниях к версии 2.17.0 перечислены исправления уязвимостей. После миграции закрепите ту версию, которую вы проверили. На Python 3.15 без компилятора C команда pip install --only-binary brotli scrapy молча установила Scrapy 2.17.0 вместо 2.19.0; как это заметить, показано в статье Python 3.15 для веб-скрапинга.

Как поймать обход без запросов в CI

При коде выхода 0 и finish_reason, равном finished, задача, которая проверяет только успешность обхода, проходит. Проверяйте число запросов:

python
from scrapy.crawler import CrawlerProcess

from myproject.spiders.quotes import QuotesSpider


def test_spider_sends_requests():
    process = CrawlerProcess(settings={"LOG_LEVEL": "WARNING"})
    crawler = process.create_crawler(QuotesSpider)
    process.crawl(crawler)
    process.start()
    assert crawler.stats.get_value("downloader/request_count", 0) > 0

На 2.16.0 и 2.19.0 тест упал для паука со start_requests() с сообщением AssertionError: assert 0 > 0 и прошёл после переименования. На 2.15.2 он прошёл для обоих. В лаборатории каждый процесс pytest выполнял один обход локального python -m http.server, поэтому направьте свой тест на тестовый стенд или на страницу, которую вам разрешено запрашивать.

Для задачи по расписанию возьмите то же число из статистики в конце журнала. В запуске, который отправлял запросы, есть запись 'downloader/request_count', а в неудачном её не было.

Как это проверялось

Ubuntu 26.04.1, Python 3.14.4, всё на 127.0.0.1: python -m http.server в роли цели и прямой прокси с Basic-авторизацией. В четырёх виртуальных окружениях стояли Scrapy 2.19.0 и 2.16.0 с Twisted 26.4.0, 2.15.2 с Twisted 25.5.0 и 2.12.0 с Twisted 24.11.0. Каждый из 17 пауков запускался на каждой версии командой scrapy runspider в новом интерпретаторе с чистым окружением: 68 запусков и ещё 8 запусков pytest. Аккаунт прокси не использовался.

Архив лаборатории содержит скрипт запуска, пауков, проверку для pytest, README с таблицей всех результатов и записанные результаты.

Не проверялось: macOS и Windows, Scrapy 2.13, 2.14, 2.17 и 2.18, правила CrawlSpider и сторонние middleware.

Когда паук снова отправляет запросы, каждый из них расходует трафик прокси-тарифа. В статье Сколько ГБ прокси вам нужно измерено, сколько тратит загрузка одного HTML.

ipvolt — прокси-сервис для разработчиков, который пока не открыт; запишитесь в список раннего доступа, и вы получите одно письмо, когда он откроется.

Источники и дополнительное чтение

Технические материалы, использованные при подготовке руководства. Сверяйтесь с документацией вашей версии и с поддерживаемой конфигурацией вашего провайдера.