Python: Асинхронные HTTP запросы на N url`s
Мне необходимо получить данные с 8 url
Как я это делаю синхронно:
urls = [
'example.com',
'example.com',
'example.com',
...
]
for url in urls:
req += json.loads(requests.get(url).text)
req - содержит данные с сайта (то, что мне и необходимо), все работает как часы, НО скорость меня не устраивает, так как таких запросов у меня большое количество будет
Я вспомнил про то что есть асинхронность, попробовал ее реализовать:
async def fetch_pages(links):
async with aiohttp.ClientSession() as session:
results = []
for link in links:
async with session.get(link) as resp:
results += json.loads(await resp.text())
return results
К сожалению, эта функцию не работает шустрее, чем синхронный способ (понимаю, что это я сделал что то не так)
Как по моему мнению должен работать мой скрипт:
Даю функции массив с ссылками, он отправляет запросы по этим ссылкам, получает со всех ссылок ответы, и отдает массив с данными обратно
Сама проблема в том, что я не знаю как сделать так, чтобы скрипт сначала отправил запросы, а после ждал того, как получит ответ от всех
Пояснение: синхронный код работал так: отправлял запрос на сайт, ждал получения данных, после отправлял второй запрос, ждал и так далее..
Ответы (1 шт):
Идея такая: нужно не дожидаться завершения каждого запроса, а собирать их в список, потом ожидать завершения всех их через asyncio.gather.
Рабочий прототип такой:
import asyncio
import aiohttp
import requests
from time import time
async def fetch(url, session):
async with session.get(url) as response:
return await response.text()
async def fetch_pages_sync(links):
"""
Последовательное скачивание с каждой ссылки
(дожидаемся скачивания по ссылке, потом переходим к следующей)
"""
async with aiohttp.ClientSession() as session:
results = []
for link in links:
results.append(await fetch(link, session))
return results
async def fetch_pages_parallel(links):
"""
Параллельное скачивание с каждой ссылки
(собираем awitable запросы в список, потом параллельно собираем результат через asyncio.gather)
"""
async with aiohttp.ClientSession() as session:
results = []
for link in links:
results.append(fetch(link, session))
return await asyncio.gather(*results)
async def main():
urls = [
'https://google.com',
] * 40
t = time()
# print([text[:100] for text in await fetch_pages_sync(urls)])
result = await fetch_pages_sync(urls)
print(time() - t)
t = time()
# print([text[:100] for text in await fetch_pages_parallel(urls)])
result = await fetch_pages_parallel(urls)
print(time() - t)
t = time()
with requests.Session() as session:
for url in urls:
result = session.get(url).text
print(time() - t)
asyncio.run(main())
Тут fetch_pages_sync ожидает завершения каждого запроса, потом переходит к следующему, fetch_pages_parallel - ожидает завершения всех их одновременно, для контроля еще добавил гарантированно синхронных запрос через requests в цикле.
Замеры по времени:
6.519821405410767
0.6476857662200928
7.366809368133545
Видим, что второй вариант намного лучше по времени, а первый и третий примерно одинаковы.
Запускал на Python 3.10, Linux Mint 21.1. Код сначала писал с нуля, потом нашел вот эту статью, взял некоторые идеи оттуда: Making 1 million requests with python-aiohttp. Обратите внимание на раздел Testing the limits: там тестируются 1000 запросов, и все начинает упираться в количество открытых одновременно сокетов.
В ensure_future/create_task не стал оборачивать, т.к. насколько я помню, gather с какой-то версии python сам оборачивает корутины в таски.