Как сделать параллельные асинхронные HTTP-запросы с использованием httpx (по сравнению с aiohttp) в Python?

Это было основано на опечатке и простой ошибке.

Не удалять, так как у него есть пример кода для httpx.

Я пытаюсь использовать asyncio для распараллеливания нескольких длинных веб-запросов. Поскольку я перехожу с библиотеки requests, я хотел бы использовать библиотеку httpx из-за похожего API. Моя среда — это дистрибутив Python 3.7.7 Anaconda со всеми установленными необходимыми пакетами (Windows 10).

Однако, несмотря на возможность использовать httpx для синхронных веб-запросов (или для последовательного выполнения асинхронных запросов, которые выполняются один за другим), мне не удалось выполнить более одного асинхронного запроса за раз, несмотря на то, что это легко сделать с помощью aiohttp библиотека.

Вот пример кода, который работает чисто в aiohttp: (Обратите внимание, что я работаю в Jupyter, поэтому у меня уже есть цикл обработки событий, поэтому отсутствует asyncio.run().

import aiohttp
import asyncio
import time
import httpx

async def call_url(session):
    url = "https://services.cancerimagingarchive.net/services/v3/TCIA/query/getCollectionValues"        
    response = await session.request(method='GET', url=url)
    #response.raise_for_status() 
    return response

for i in range(1,5):
    start = time.time() # start time for timing event
    async with aiohttp.ClientSession() as session: #use aiohttp
    #async with httpx.AsyncClient as session:  #use httpx
        await asyncio.gather(*[call_url(session) for x in range(i)])
    print(f'{i} call(s) in {time.time() - start} seconds')

Это приводит к ожидаемому профилю времени отклика:

1 call(s) in 7.9129478931427 seconds
2 call(s) in 8.876991510391235 seconds
3 call(s) in 9.730034589767456 seconds
4 call(s) in 10.630006313323975 seconds

Однако, если я раскомментирую async with httpx.AsyncClient as session: #use httpx и закомментирую async with aiohttp.ClientSession() as session: #use aiohttp (чтобы заменить httpx на aiohttp), я получу следующую ошибку:

AttributeError                            Traceback (most recent call last)
<ipython-input-108-25244245165a> in async-def-wrapper()
     17         await asyncio.gather(*[call_url(session) for x in range(i)])
     18     print(f'{i} call(s) in {time.time() - start} seconds')

AttributeError: __aexit__

В своих исследованиях в Интернете я смог найти только одну статью Саймона Хоу на Medium, в которой показано, как использовать httpx для параллельного запроса. См. https://medium.com/swlh/how-to-boost-your-python-apps-using-httpx-and-asynchronous-calls-9cfe6f63d6ad

Однако пример асинхронного кода даже не использует объект асинхронного сеанса, поэтому я был немного подозрительным, чтобы начать. Код не выполняется ни в среде Python 3.7.7, ни в Jupyter. (Код находится здесь: https://gist.githubusercontent.com/Shawe82/a218066975f4b325e026337806f8c781/raw/3cb492e971c13e76a07d1a1e77b48de94aa7229c/concurrent_download.py)

Это приводит к этой ошибке:

Traceback (most recent call last):
  File ".\async_http_test.py", line 24, in <module>
    asyncio.run(download_all_photos('100_photos'))
  File "C:\Users\stborg\AppData\Local\Continuum\anaconda3\envs\fastai2\lib\asyncio\runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "C:\Users\stborg\AppData\Local\Continuum\anaconda3\envs\fastai2\lib\asyncio\base_events.py", line 587, in run_until_complete
    return future.result()
  File ".\async_http_test.py", line 16, in download_all_photos
    resp = await httpx.get("https://jsonplaceholder.typicode.com/photos")
TypeError: object Response can't be used in 'await' expression

Я явно делаю что-то не так, так как httpx создан для асинхронности. Я просто не уверен, что это такое!


person Steven Borg    schedule 01.05.2020    source источник


Ответы (2)


ХОРОШО. Это откровенно смущает. Нет необходимости в обходном пути. В постановке задачи я совершенно забыл вызвать конструктор AsyncClient... Не могу поверить, что так долго пропустил это. О, мой...

Чтобы исправить это, просто добавьте недостающую скобку в конструктор AsyncClient:

    async with httpx.AsyncClient() as session:  #use httpx
        await asyncio.gather(*[call_url(session) for x in range(i)])
person Steven Borg    schedule 01.05.2020

Продолжая экспериментировать при написании этого вопроса, я обнаружил тонкую разницу в том, как httpx и aiohttp относятся к менеджерам контекста.

В коде, который вводит вопрос, следующий код работал с aiohttp:

    async with aiohttp.ClientSession() as session: #use aiohttp
        await asyncio.gather(*[call_url(session) for x in range(i)])

Этот код передает контекст ClientSession в качестве параметра методу call_url. Я предполагаю, что после завершения asyncio.gather() ресурсы очищаются в соответствии с обычным оператором with.

Однако тот же подход с httpx не работает, как описано выше. Однако это можно легко исправить, просто избегая оператора with и вручную закрывая файл AsyncClient.

Другими словами, заменить

    async with httpx.AsyncClient as session:  #use httpx
        await asyncio.gather(*[call_url(session) for x in range(i)])

с

    session = httpx.AsyncClient() #use httpx
    await asyncio.gather(*[call_url(session) for x in range(i)])
    await session.aclose()

чтобы решить проблему.

Вот рабочий код целиком:

import aiohttp
import asyncio
import time
import httpx

async def call_url(session):
    url = "https://services.cancerimagingarchive.net/services/v3/TCIA/query/getCollectionValues"
    response = await session.request(method='GET', url=url)
    return response

for i in range(1,5):
    start = time.time() # start time for timing event
    #async with aiohttp.ClientSession() as session: #use aiohttp
    session = httpx.AsyncClient() #use httpx
    await asyncio.gather(*[call_url(session) for x in range(i)])
    await session.aclose()
    print(f'{i} call(s) in {time.time() - start} seconds')
person Steven Borg    schedule 01.05.2020
comment
Почему вы импортируете aiohttp в последнем примере кода? - person Dominux; 09.10.2020
comment
Ваш окончательный код возвращает SyntaxError. Это должно быть так: ` for i in range(1,5): start = time.time() # время начала для события time session = httpx.AsyncClient() #use httpx asyncio.gather(*[call_url(session) для x в диапазоне (i)]) print(f'{i} вызовов в {time.time() - start} секунд') ` - person Ruben García Tutor; 18.11.2020