При асинхронном парсинге вы получаете не только высокую скорость сбора данных, но и повышенную вероятность получить бан по IP. Как работать с прокси, мы подробно разобрали в прошлом модуле. Также нередки случаи, когда сервер закрывает одно из ваших асинхронных подключений, это чаще всего происходит из-за большого количества запросов. Это еще не бан, но запрос сервером обработан не был. Для повторного запроса в случае неудачи существует отличный клиент, который работает совместно с aiohttp и называется aiohttp-retry.
Установите этот модуль себе в окружающую среду вашего проекта, он нам понадобится для запуска кода.
Установка:
pip install aiohttp-retry
Импорт:
from aiohttp_retry import RetryClient
Самый простой пример из документации. Но у такого примера есть один недостаток: по умолчанию будет производиться всего один повторный запрос. В случае, если и второй запрос окажется неудачным , вы не получите требуемые данные. Мне пришлось немного модифицировать пример из документации, мы ведь любим пользоваться менеджером контекста with/as. В этом случае нам не нужно закрывать вручную нашу сессию командой => await client_session.close()
import aiohttp from aiohttp_retry import RetryClient async def main(): async with aiohttp.ClientSession() as client_session: retry_client = RetryClient(client_session=client_session) async with retry_client.get('https ://ya.ru') as response: print(response.status)
Для того, чтобы у нас была возможность управлять количеством повторных подключений, нам потребуется использовать класс ExponentialRetry из модуля aiohttp_retry. Импортируем этот класс из модуля и немного модифицируем первый пример.
Пример, демонстрирующий использование RetryClient() для асинхронного опроса нескольких html страниц.
import time import asyncio import aiohttp from aiohttp_retry import RetryClient, ExponentialRetry # Последние 2 ссылки — 404-е, добавлены в демонстрационных целях links = ['https ://mob25.com/watch/1/1_1.html', 'https ://mob25.com/watch/1/1_2.html', 'https ://mob25.com/watch/1/1_3.html', 'https ://mob25.com/watch/8/1_3.html', 'https ://mob25.com/watch/8/2_3.html'] # Корутина для вывода сообщения вида link:response.status async def get_data(retry_client, link): async with retry_client.get(link) as response: print(f'{link}:{response.status}') # Базовая корутина async def main(): async with aiohttp.ClientSession() as client_session: # statuses=[404] выбран для демонстрации, на практике # повторное обращение к несуществующей странице скорее всего бессмысленно retry_options = ExponentialRetry(attempts=4, statuses={404}) async with RetryClient( raise_for_status=False, retry_options=retry_options, client_session=client_session) as retry_client: await asyncio.gather(*[get_data(retry_client, link) for link in links]) if __name__ == '__main__': start = time.time() asyncio.run(main()) print(f'время: {time.time() - start}')
Если изменять количество попыток, то можно увидеть закономерность изменения времени, затраченного на работу скрипта:
При attempts=4 время: 1.6515371799468994
При attempts=5 время: 3.2961649894714355
При attempts=6 время: 6.614838123321533
Что косвенно демонстрирует экспоненциальное нарастание паузы между запросами.
Давайте разбираться, что тут написано и как с этим работать.
В базовой асинхронной функции main() c помощью контекстного менеджера и библиотеки aiohttp создаем объект клиентской сессии client_session.
retry_options = ExponentialRetry(attempts=4, statuses={404}) — создаем экземпляр класса ExponentialRetry (для изменения части политик повторного запроса принятых по умолчанию) и передаем атрибут attempts=, этот атрибут принимает целое число, которое указывает на количество повторных попыток подключения. В данном примере будет произведено четыре попытки повторных подключений.
statuses= — принимает множество (set) статусов на которые нужно вызвать повторный запрос. Последними в списке links нашего примера стоят несуществующие страницы (404), на них и будут продемонстрированы повторные попытки подключения.
Ниже указаны все доступные атрибуты.
ExponentialRetry(attempts=10) — количество повторных подключений (int);
ExponentialRetry(start_timeout=0.5) — базовое время ожидания, с каждым новым запросом значение возрастает экспоненциально (float);
ExponentialRetry(max_timeout=30.0) — максимально возможное время ожидания (float);
ExponentialRetry(factor=3.0) — на переданное значение будет увеличиваться timeout ожидания для следующего запроса (float);
ExponentialRetry(statuses={400,403}) — на каких статусах нужно повторить запрос. set(int, int) ;
ExponentialRetry(exceptions=[Exсeption, TimeoutError]) — при каких исключениях повторять запрос, по умолчанию None. list(type_Exception, type_Exception));
ExponentialRetry(retry_all_server_errors=True) — повторить подключение при любых ошибках сервера, явно указывать не нужно, т.к. по умолчанию True.
С помощью контекстного менеджера создаем объект RetryClient() — retry_client, а в атрибутах передаём следующие параметры:
RetryClient(client_session=client_session) — передаёт объект сессии которую мы получили от aiohttp;
RetryClient(retry_options=retry_options) — передает экземпляр класса ExponentialRetry(), в котором мы настраивали параметры повторного запроса.
RetryClient(raise_for_status=True) — Если True, использует время ответа сервера в качестве параметра для расчета следующего timeout, лучше использовать параметр False, который установлен по умолчанию. Если установить True, вы можете столкнуться с проблемой, когда сервер не ответил и следующий timeout получит значение None, тогда расчёт времени для следующего запроса, рассчитан не будет , потому что из None невозможно ничего рассчитать.
Через await asyncio.gather() создаем из get_data() и запускаем асинхронное выполнение ряда задач для тестирования статусов страниц из списка links.
Создаем асинхронную функцию get_data(retry_client, link) для вывода сообщения {link}:{response.status}.
Передаем в нее объект RetryClient() — retry_client и ссылку на проверяемую страницу — link.
Внутри функции выполняется асинхронный GET-запрос к указанному в link URL.
Если запрос не удаётся (например, из-за временной ошибки соединения или серверной ошибки, реакции на заданный статус), RetryClient автоматически попытается повторить запрос согласно заданной политике повторных попыток.
В примере мы специально провоцируем появление в ответе статуса 404 для двух последних страниц.
Запуск скрипта происходит по команде: asyncio.run(main()), что приводит к созданию цикла событий и автоматическому созданию и запуску в нем задачи из базовой корутины main().
RetryClient(logger=[type_logger]) устанавливает логирование запросов.