如何在Python中使用HTTPX和asyncio的异步HTTP请求

546 阅读6分钟

你好,感谢你的阅读!这篇博文是用HTTPX和asyncio翻译的Python中的异步HTTP请求。在我们改进翻译流程的过程中,如果您发现有任何翻译错误的地方,我们将感谢您的反馈,help@twilio.com。感谢你对Twilio Swag的有益贡献 :)

异步代码已经逐渐成为Python开发的主流。随着asyncio成为标准库的一部分,以及许多第三方软件包提供与其兼容的功能,这种模式不会很快消失。

让我们来看看如何使用HTTPX库来为异步HTTP请求做这件事。这是非阻塞性代码最常见的使用情况之一。

什么是非阻塞式代码?

你可能会听到 "异步"、"非阻塞 "或 "并发 "等术语,并对它们的含义感到有些困惑。根据这个更详细的教程,有两个主要属性:

  • 异步例程可以在等待其最终结果时暂停,以便其他例程可以在此期间执行。
  • 异步代码通过上述机制促进了并发执行。换句话说,异步代码给人以并行的感觉。

因此,异步代码是可以在等待结果时挂起的代码,以便在此期间可以执行其他代码。它不会 "阻塞 "其他代码,所以我们可以称它为 "非阻塞 "代码。

asyncio库为Python开发者提供了各种工具来实现这一点,而aiohttp为HTTP请求提供了更具体的功能。HTTP请求是一个典型的例子,它可以很好地适用于异步,因为它们涉及到等待服务器的响应。在这段时间里,执行其他代码是很方便和有效的。

设置

在开始之前,请确保你的Python环境已经设置好了。如果你需要帮助,请按照本指南中的virtualenv部分。当在同一台计算机上运行多个项目时,一切正常工作是很重要的,特别是在虚拟环境下,可以隔离你的依赖关系。你至少需要Python 3.7或更高版本来运行本帖的代码。

在你的环境设置好之后,你需要安装HTTPX库,用于发出异步和同步请求,我们将对其进行比较。在激活你的虚拟环境后,用下面的命令安装该库:

pip install httpx==0.18.2

你应该准备好继续写代码了。

用HTTPX做一个HTTP请求

让我们从一个使用HTTPX的单一GET请求开始,以演示关键词asyncawait 的工作原理。我们将使用Pokemon API作为一个例子。让我们首先尝试获取传说中的第151只小精灵Mew 的数据

运行下面的Python代码,终端应该显示 "mew "这个名字:

import asyncio
import httpx


async def main():
    pokemon_url = 'https://pokeapi.co/api/v2/pokemon/151'

    async with httpx.AsyncClient() as client:

        resp = await client.get(pokemon_url)

        pokemon = resp.json()
        print(pokemon['name'])

asyncio.run(main())

在这段代码中,我们创建了一个名为 "main "的循环程序,我们用asyncio事件循环来运行它。在这里,我们向Pokemon API发出请求,然后等待响应。

这个关键字async 基本上告诉 Python 解释器用一个事件循环异步运行我们定义的 coroutine。关键字await ,将控制权返回给事件循环,中断周围循环程序的执行,让事件循环做其他事情,直到返回 "等待 "的结果。

提出大量的请求

提出一个单一的异步HTTP请求是很好的,因为它允许事件循环在其他任务上执行,而不是在等待响应时阻塞整个线程。如果你想提出更多的请求,这个功能真的很出色。让我们通过运行与之前相同的请求来证明这一点,不过是针对所有150种原始神奇宝贝

让我们把前面的请求代码放到一个循环中。这将更新请求和使用的小精灵数据,并为每个请求使用await

import asyncio
import httpx
import time

start_time = time.time()


async def main():

    async with httpx.AsyncClient() as client:

        for number in range(1, 151):
            pokemon_url = f'https://pokeapi.co/api/v2/pokemon/{number}'

            resp = await client.get(pokemon_url)
            pokemon = resp.json()
            print(pokemon['name'])

asyncio.run(main())
print("--- %s seconds ---" % (time.time() - start_time))

这次我们还测量了整个过程需要多少时间。如果你在你的Python shell中运行这段代码,你应该在终端上看到这样的情况:

Konsolenausgabe von 150 asynchronen Anfragen

8.6秒对于150个请求来说似乎相当不错,但我们没有什么可以比较的。让我们试着同步地实现同样的事情。

与同步请求的速度比较

运行下面的代码,像以前一样打印前150个小精灵,但没有async/await:

import httpx
import time

start_time = time.time()
client = httpx.Client()

for number in range(1, 151):
    url = f'https://pokeapi.co/api/v2/pokemon/{number}'
    resp = client.get(url)
    pokemon = resp.json()
    print(pokemon['name'])

print("--- %s seconds ---" % (time.time() - start_time))

你应该看到相同的输出,但运行时间不同:

Konsolenausgabe von 150 synchronen Anfragen mit einer Zeit von ~10 Sekunden

然而,速度似乎并不比以前慢多少。这可能是因为HTTPX的-Clients 连接池做了大部分的工作。然而,我们可以使用更多的asyncio 函数来获得更好的性能。

使用asyncio来提高性能

asyncio 提供其他工具,可以显著提高我们的整体性能。在原来的例子中,我们在每一个HTTP请求之后都使用await,这不是很理想。相反,我们可以作为 任务 "同时 "运行所有这些请求,然后在最后使用 和 检查结果。asyncio asyncio.ensure_future asyncio.gather

如果实际发出请求的代码被分割成自己的coroutine函数,我们可以每个请求创建一个由期货组成的任务列表。然后,我们可以将这个列表解包为一个聚集调用,将它们全部执行。如果我们再使用awaitasyncio.gather ,我们就会得到一个所有传入的期货的可迭代文件,保持列表中的顺序。这样,我们只使用一次await

运行下面的代码,看看当我们实现这一点时会发生什么:

import asyncio
import httpx
import time


start_time = time.time()


async def get_pokemon(client, url):
        resp = await client.get(url)
        pokemon = resp.json()

        return pokemon['name']


async def main():

    async with httpx.AsyncClient() as client:

        tasks = []
        for number in range(1, 151):
            url = f'https://pokeapi.co/api/v2/pokemon/{number}'
            tasks.append(asyncio.ensure_future(get_pokemon(client, url)))

        original_pokemon = await asyncio.gather(*tasks)
        for pokemon in original_pokemon:
            print(pokemon)

asyncio.run(main())
print("--- %s seconds ---" % (time.time() - start_time))

Konsolenausgabe von 150 asynchronen Anfragen, jedoch mit einer deutlich schnelleren Laufzeit von 1,54 Sekunden

这使我们的时间减少到150个HTTP请求只有1.54秒!与之前的例子相比,这是一个巨大的进步。这是完全无阻塞的,所以执行所有150个请求的总时间与执行最长请求的时间大致相同。确切的数字将根据你的互联网连接而有所不同。

最后的想法

正如你所看到的,使用像HTTPX这样的库来重新思考你进行HTTP请求的方式可以给你的代码带来巨大的性能提升,并在进行大量的请求时节省大量的时间。