使用HTTPX和asyncio的Python异步HTTP请求

4,113 阅读6分钟

异步代码已经日益成为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 工作。我们将使用口袋妖怪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 关键字将控制权传回给事件循环,暂停周围的 coroutine 的执行,让事件循环运行其他的东西,直到被 "等待 "的结果被返回。

提出大量的请求

提出一个单一的异步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中运行这段代码,你应该看到类似下面的东西打印到你的终端。

Console output from 150 asynchronous requests

对于150个请求来说,8.6秒似乎很不错,但我们并没有什么可以比较的。让我们试着用同步方式完成同样的事情。

比较同步请求的速度

要像以前一样打印前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))

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

Console output from 150 synchronous requests, displaying a time of ~10 seconds

尽管它看起来并没有比以前慢多少。这很可能是因为HTTPXClient 所做的连接池正在做大部分的重活。然而,我们可以利用更多的asyncio 功能来获得比这更好的性能。

利用asyncio来提高性能

asyncio 提供了更多的工具,可以大大改善我们的整体性能。在原来的例子中,我们在每个单独的HTTP请求后使用await,这不是很理想。我们可以将所有这些请求作为asyncio 任务 "并发 "运行,然后在最后使用asyncio.ensure_futureasyncio.gather 检查结果。

如果实际发出请求的代码被分解成自己的coroutine函数,我们可以创建一个任务列表,由每个请求的 期货组成。然后,我们可以将这个列表解压到一个 集合调用,将它们全部运行起来。当我们await 这个调用到asyncio.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))

Console output from 150 asynchronous requests, but with a much faster runtime of 1.54 seconds

这使我们的时间降到了150个HTTP请求的1.54秒!这比以前有很大的进步。这比之前的例子有了很大的进步。这是完全无阻塞的,所以运行所有150个请求的总时间将大致等于最长请求的运行时间。确切的数字会因你的互联网连接而有所不同。

结论性的想法

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

在这个教程中,我们只是触及了用asyncio可以做什么的表面,但我希望这能使你开始进入异步Python世界的旅程更容易一些。如果你对另一个类似的异步 HTTP 请求库感兴趣,可以看看我写的关于 aiohttp 的另一篇博文。

我期待着看到你所建立的东西。请随时联系我,分享你的经验或提出任何问题。