`Python`的`HTTP`客户端:`httpx`的`异步`用法与`requests`的`对比`。

Python HTTP 客户端:httpx 异步用法与 requests 的对比

大家好,今天我们来聊聊 Python 中 HTTP 客户端,重点对比 httpxrequests 在异步场景下的使用。requests 库以其简洁易用而闻名,长期以来一直是 Python HTTP 请求的首选方案。然而,在需要高并发和异步处理的场景下,requests 的同步阻塞特性就显得力不从心。httpx 库的出现,填补了这一空白,它提供了与 requests 类似的 API,同时支持同步和异步操作,更好地满足了现代 Web 应用的需求。

1. 两种库的基础概念

在深入异步用法之前,我们先简单回顾一下 requestshttpx 的基本概念。

  • requests: 一个优雅而简洁的 Python HTTP 库,基于 urllib3 构建,以人类友好的 API 著称。它默认是同步阻塞的。

  • httpx: 一个完全兼容 requests 的 HTTP 客户端,但增加了对 HTTP/2 的支持,并且支持同步和异步操作。它构建于 asyncio 之上,能够充分利用异步编程的优势。

2. requests 的同步请求

首先,我们来看 requests 的基本用法。以下是一个使用 requests 发送 GET 请求的例子:

import requests

url = "https://www.example.com"

try:
    response = requests.get(url)
    response.raise_for_status()  # 检查请求是否成功
    print(f"Status code: {response.status_code}")
    print(f"Content: {response.text[:100]}...") # 打印前100个字符
except requests.exceptions.RequestException as e:
    print(f"An error occurred: {e}")

这段代码非常简单易懂。requests.get() 函数发送一个 GET 请求,并返回一个 Response 对象。我们可以通过 response.status_code 访问状态码,通过 response.text 访问响应内容。response.raise_for_status() 方法会在状态码表示错误时抛出异常,方便我们进行错误处理。

然而,requests 的同步特性意味着,当程序执行 requests.get() 时,它会阻塞,直到服务器返回响应。在高并发的场景下,这种阻塞会导致程序性能下降。

3. httpx 的同步请求

httpx 也支持同步请求,API 与 requests 非常相似。

import httpx

url = "https://www.example.com"

try:
    with httpx.Client() as client:
        response = client.get(url)
        response.raise_for_status()
        print(f"Status code: {response.status_code}")
        print(f"Content: {response.text[:100]}...")
except httpx.RequestError as e:
    print(f"An error occurred: {e}")
except httpx.HTTPStatusError as e:
    print(f"An HTTP error occurred: {e}")

这里我们使用 httpx.Client() 创建一个客户端实例,并使用 client.get() 发送请求。需要注意的是,httpx.Client() 最好用 with 语句管理,确保连接在使用完毕后正确关闭。 错误处理也需要考虑httpx.RequestErrorhttpx.HTTPStatusError 两种异常。

尽管 httpx 提供了同步接口,但它的真正优势在于异步支持。

4. httpx 的异步请求

httpx 的异步支持基于 asyncio 库。要使用 httpx 进行异步请求,我们需要使用 asyncawait 关键字。

import asyncio
import httpx

async def fetch_url(url):
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url)
            response.raise_for_status()
            return response.text[:100]
        except httpx.RequestError as e:
            print(f"An error occurred: {e} while requesting {url}")
            return None
        except httpx.HTTPStatusError as e:
            print(f"An HTTP error occurred: {e} while requesting {url}")
            return None

async def main():
    urls = ["https://www.example.com", "https://www.python.org", "https://www.google.com"]
    tasks = [fetch_url(url) for url in urls]
    results = await asyncio.gather(*tasks) # 并发执行所有任务
    for url, result in zip(urls, results):
        if result:
            print(f"Content from {url}: {result}...")

if __name__ == "__main__":
    asyncio.run(main())

让我们逐步分析这段代码:

  • async def fetch_url(url): 定义一个异步函数,用于获取指定 URL 的内容。注意 async 关键字。
  • async with httpx.AsyncClient() as client:: 创建一个异步客户端实例。AsyncClient 必须用 async with 语句管理。
  • response = await client.get(url): 发送异步 GET 请求。await 关键字表示等待请求完成。
  • async def main():: 定义主函数,用于组织异步任务。
  • tasks = [fetch_url(url) for url in urls]: 创建一个任务列表,每个任务对应一个 URL。
  • *`results = await asyncio.gather(tasks):** 使用asyncio.gather()并发执行所有任务。asyncio.gather()` 接受一个任务列表,并返回一个包含所有任务结果的列表。
  • asyncio.run(main()): 运行异步主函数。

requests 的同步请求相比,httpx 的异步请求不会阻塞程序。当一个请求正在等待服务器响应时,程序可以继续执行其他任务。这使得程序能够更有效地利用 CPU 资源,提高并发性能。

5. 异步任务并发数控制

虽然异步可以提高并发,但如果不加以控制,过多的并发请求可能会压垮服务器或客户端自身。我们需要限制并发数量。可以使用 asyncio.Semaphore 来控制并发数。

import asyncio
import httpx

CONCURRENCY = 5  # 设置最大并发数为 5
semaphore = asyncio.Semaphore(CONCURRENCY)

async def fetch_url(url):
    async with semaphore: # 获取一个信号量
        async with httpx.AsyncClient() as client:
            try:
                response = await client.get(url)
                response.raise_for_status()
                return response.text[:100]
            except httpx.RequestError as e:
                print(f"An error occurred: {e} while requesting {url}")
                return None
            except httpx.HTTPStatusError as e:
                print(f"An HTTP error occurred: {e} while requesting {url}")
                return None

async def main():
    urls = ["https://www.example.com"] * 20  # 模拟20个请求
    tasks = [fetch_url(url) for url in urls]
    results = await asyncio.gather(*tasks)
    for url, result in zip(urls, results):
        if result:
            print(f"Content from {url}: {result}...")

if __name__ == "__main__":
    asyncio.run(main())

在这个例子中,我们创建了一个 asyncio.Semaphore 对象,设置最大并发数为 5。在 fetch_url() 函数中,我们使用 async with semaphore: 获取一个信号量。只有当信号量可用时,才能执行请求。当请求完成后,信号量会自动释放。这样,我们就可以有效地控制并发数量,避免压垮服务器或客户端。

6. 超时设置

在进行网络请求时,设置超时时间非常重要,以避免程序长时间阻塞。requestshttpx 都提供了超时设置。

requests:

import requests

url = "https://www.example.com"

try:
    response = requests.get(url, timeout=5)  # 设置超时时间为 5 秒
    response.raise_for_status()
    print(f"Status code: {response.status_code}")
    print(f"Content: {response.text[:100]}...")
except requests.exceptions.RequestException as e:
    print(f"An error occurred: {e}")

httpx (同步):

import httpx

url = "https://www.example.com"

try:
    with httpx.Client(timeout=5) as client:  # 设置全局超时时间
        response = client.get(url)
        response.raise_for_status()
        print(f"Status code: {response.status_code}")
        print(f"Content: {response.text[:100]}...")
except httpx.RequestError as e:
    print(f"An error occurred: {e}")
except httpx.HTTPStatusError as e:
    print(f"An HTTP error occurred: {e}")
except httpx.TimeoutException as e: #处理超时异常
    print(f"Timeout occurred: {e}")

httpx (异步):

import asyncio
import httpx

async def main():
    url = "https://www.example.com"
    async with httpx.AsyncClient(timeout=5) as client: #设置全局超时
        try:
            response = await client.get(url)
            response.raise_for_status()
            print(f"Status code: {response.status_code}")
            print(f"Content: {response.text[:100]}...")
        except httpx.RequestError as e:
            print(f"An error occurred: {e}")
        except httpx.HTTPStatusError as e:
            print(f"An HTTP error occurred: {e}")
        except httpx.TimeoutException as e: #处理超时异常
            print(f"Timeout occurred: {e}")

if __name__ == "__main__":
    asyncio.run(main())

requests 使用 timeout 参数设置超时时间,单位为秒。 httpx 也可以使用 timeout 参数,但需要注意,httpxtimeout 参数可以设置全局超时时间(在 ClientAsyncClient 初始化时设置)或单个请求的超时时间(在 get()post() 等方法中设置)。

7. 会话管理 (Session)

会话管理允许我们跨多个请求保持状态,例如 cookie。requestshttpx 都提供了会话管理功能。

requests:

import requests

session = requests.Session()

# 设置 cookie
session.cookies.set("my_cookie", "my_value", domain="www.example.com")

# 发送请求
response = session.get("https://www.example.com")
print(response.status_code)

# 关闭会话
session.close()

httpx (同步):

import httpx

with httpx.Client() as client:
    # 设置 cookie
    client.cookies.set("my_cookie", "my_value", domain="www.example.com")

    # 发送请求
    response = client.get("https://www.example.com")
    print(response.status_code)

httpx (异步):

import asyncio
import httpx

async def main():
    async with httpx.AsyncClient() as client:
        # 设置 cookie
        client.cookies.set("my_cookie", "my_value", domain="www.example.com")

        # 发送请求
        response = await client.get("https://www.example.com")
        print(response.status_code)

if __name__ == "__main__":
    asyncio.run(main())

requests 使用 requests.Session() 创建会话对象。 httpx 使用 httpx.Client()httpx.AsyncClient() 创建客户端实例,该实例也充当会话管理器。cookie 可以使用 cookies.set() 方法设置。

8. 数据提交 (POST)

发送 POST 请求并提交数据是常见的 Web 开发任务。 requestshttpx 都提供了方便的 API。

requests:

import requests

url = "https://www.example.com/post"
data = {"key1": "value1", "key2": "value2"}

response = requests.post(url, data=data)
print(response.status_code)

httpx (同步):

import httpx

url = "https://www.example.com/post"
data = {"key1": "value1", "key2": "value2"}

with httpx.Client() as client:
    response = client.post(url, data=data)
    print(response.status_code)

httpx (异步):

import asyncio
import httpx

async def main():
    url = "https://www.example.com/post"
    data = {"key1": "value1", "key2": "value2"}

    async with httpx.AsyncClient() as client:
        response = await client.post(url, data=data)
        print(response.status_code)

if __name__ == "__main__":
    asyncio.run(main())

requestshttpx 都使用 post() 方法发送 POST 请求。数据可以通过 data 参数传递。 如果需要发送 JSON 数据,可以使用 json 参数。

9. 区别总结

下表对 requestshttpx 在异步方面的使用进行了总结:

Feature requests httpx
异步支持 不支持,同步阻塞 支持,基于 asyncio
异步客户端 httpx.AsyncClient
异步方法 async with client.get()async with client.post()
并发控制 需要使用线程或进程实现 asyncio.Semaphore
异常处理 requests.exceptions.RequestException httpx.RequestError, httpx.HTTPStatusError, httpx.TimeoutException
是否需要事件循环 不需要 需要,使用 asyncio.run()

10. 应该选择哪个库?

  • requests: 如果你的应用是同步的,并且不需要处理高并发,那么 requests 仍然是一个不错的选择,它简单易用。
  • httpx: 如果你需要处理高并发,或者你的应用是异步的,那么 httpx 是更好的选择。它提供了与 requests 类似的 API,同时支持异步操作,能够更好地满足现代 Web 应用的需求。

11. 异步编程是关键

httpx 的异步功能依赖于 asyncio 库。理解异步编程的概念,例如事件循环、协程、asyncawait 关键字,对于使用 httpx 进行异步开发至关重要。

12. httpx 异步请求优势明显

总而言之,httpx 在异步场景下相对于 requests 具有明显的优势。它能够更好地利用 CPU 资源,提高并发性能,并且提供了方便的 API 和丰富的功能。如果你正在开发需要处理高并发的 Web 应用,那么 httpx 绝对值得尝试。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注