任何使用 Python 超过几分钟的人都一定接触过 Requests 库。它如此普及,以至于有些人甚至误以为它是 Python 标准库的一部分。Requests 的设计极其直观,以至于 r = requests.get(...) 这样的写法几乎成了肌肉记忆。相比之下,如果用 Python 内置的 urllib 编写脚本,往往第一步就是打开官方文档查阅用法。
但 Python 在不断演进,如今再一味默认使用 Requests 已经不再是最优选择。虽然对于简短的同步脚本,Requests 依然是可靠之选,但在现代 Python 开发中------尤其是涉及异步编程的场景下------像 HTTPX 和 AIOHTTP 这样的新库往往更加合适。
本文将对 Python 中三个主流的 HTTP 客户端库------Requests、HTTPX 和 AIOHTTP------进行详细对比,分析它们各自的优缺点和适用场景,帮助你在下一个项目中做出更明智的技术选型。
同步请求 vs 异步请求:核心概念
同步请求:在单进程、单线程的代码中,发起一个请求后,必须等待其返回结果,才能执行下一个请求。
异步请求:同样在单进程、单线程中,发起请求后,在等待响应的"空闲时间"里,可以继续发起其他请求,从而实现并发。
✅ 说明:异步并非多线程,而是通过事件循环(event loop)在 I/O 等待期间切换任务,提升 CPU 利用率。
一切始于 urllib:Guido 的"原初"设计
在深入探讨现代 HTTP 库之前,我们不妨先回溯一下历史:Python 内置的 urllib 模块。
自 Python 早期版本起,urllib 就作为标准库的一部分存在。它的初衷是提供一套完整的 URL 处理与网络操作工具集。然而,其 API 以复杂难用著称,即便是执行一个简单的 HTTP 请求,也常常需要多步操作。
下面是一个使用 urllib 发起 GET 请求的基本示例:
python
# urllib_basic.py
from urllib.request import urlopen
with urlopen('https://api.github.com') as response:
body = response.read()
print(body)
对于简单 GET 请求,这段代码看起来尚可接受。但一旦涉及设置请求头、发送 POST 请求或进行身份认证,代码就会迅速变得冗长繁琐。例如,下面是使用 urllib 实现带基本认证(Basic Auth)的请求:
python
# urllib_example.py
import urllib.request
import json
url = 'http://httpbin.org/basic-auth/user/passwd'
username = 'user'
password = 'passwd'
# 创建带认证处理器的 opener
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
password_mgr.add_password(None, url, username, password)
auth_handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
opener = urllib.request.build_opener(auth_handler)
# 发起请求
with opener.open(url) as response:
raw_data = response.read()
encoding = response.info().get_content_charset('utf-8')
data = json.loads(raw_data.decode(encoding))
print(data)
在这个例子中,我们需要手动构建认证处理器、创建 opener、读取响应、解码内容,再解析 JSON。这种冗余和复杂性正是推动第三方 HTTP 库诞生的主要原因。
Requests:为人类设计的 HTTP 库™️
2011 年情人节(没错,就是那天),Kenneth Reitz 发布了 Requests 库,目标是让 HTTP 请求变得对开发者极度友好。仅两年后(2013 年 7 月),Requests 的下载量就突破了 330 万次;截至 2024 年 8 月,它每天的下载量已高达 1200 万次。
事实证明,开发者体验(Developer Experience, DevEx)真的很重要!
安装 Requests 非常简单:
python
pip install requests
让我们把前面 urllib 的例子用 Requests 重写一遍:
python
# requests_example.py
import requests
# GET 请求
response = requests.get('https://api.github.com')
print(response.text)
# 带认证的请求
url = 'http://httpbin.org/basic-auth/user/passwd'
username = 'user'
password = 'passwd'
response = requests.get(url, auth=(username, password))
data = response.json()
print(data)
对比之下,Requests 的简洁性和可读性一目了然。它自动处理了认证头、JSON 解析等繁琐细节,大大降低了使用门槛。
Requests 成为事实标准的关键特性包括:
- 自动内容解码:根据 Content-Type 头自动解码响应体。
- 会话持久化:通过 Session 对象在多个请求间复用连接和参数(如 cookies、headers)。
- 优雅的错误处理:对网络错误和 HTTP 状态码异常提供清晰的异常类型。
- 自动解压缩:能自动解压 gzip 等编码的响应内容。
然而,随着 Python 生态的发展,特别是 异步编程(asynchronous programming) 的兴起(Python 3.4 引入了 asyncio),Requests 的局限性逐渐显现------它完全不支持异步操作。
AIOHTTP:专为 asyncio 而生
AIOHTTP 最早发布于 2014 年 10 月,是最早全面拥抱 Python asyncio 框架的 HTTP 库之一。它从底层设计就围绕异步操作展开,非常适合需要高并发、高性能的网络应用。截至 2024 年 5 月,AIOHTTP 日均下载量约为 600 万次。
AIOHTTP 的核心优势包括:
- 纯异步架构:所有操作均为 async/await 风格,可高效处理成百上千个并发连接。
- 客户端 + 服务器双支持:不仅能发起 HTTP 请求,还能构建异步 Web 服务。
- 原生 WebSocket 支持:完整支持 WebSocket 协议。
安装方式:
pip
pip install aiohttp
下面是一个基本用法示例:
python
# aiohttp_basic.py
import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
html = await fetch(session, 'https://api.github.com')
print(html)
asyncio.run(main())
要真正体现 AIOHTTP 的并发优势,我们需要同时发起多个请求。以下示例并发获取多个带延迟的 URL:
python
# aiohttp_multiple.py
import asyncio
import aiohttp
import time
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
]
async def fetch(session, url, i):
try:
start_time = time.perf_counter()
async with session.get(url) as response:
await response.text()
elapsed = time.perf_counter() - start_time
print(f"请求 {i} 耗时 {elapsed:.2f} 秒")
except asyncio.TimeoutError:
print(f"请求 {i} 超时")
async def async_requests():
start_time = time.perf_counter()
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
tasks = [fetch(session, url, i) for i, url in enumerate(urls, 1)]
await asyncio.gather(*tasks)
total_time = time.perf_counter() - start_time
print(f"\n总耗时:{total_time:.2f} 秒")
if __name__ == "__main__":
asyncio.run(async_requests())
输出结果类似如下(注意:由于并发执行,总时间接近最慢请求的延迟):
请求 1 耗时 2.22 秒
请求 4 耗时 2.22 秒
请求 5 耗时 3.20 秒
请求 2 耗时 3.20 秒
请求 3 耗时 4.30 秒
总耗时:4.31 秒
作为对比,用 Requests 实现相同功能(同步顺序执行):
python
# requests_multiple.py
import requests
import time
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
]
def sync_requests():
start_time = time.time()
with requests.Session() as session:
# 注意:requests.Session 没有 timeout 属性,需在 get 中指定
for i, url in enumerate(urls, 1):
try:
response = session.get(url, timeout=10)
print(f"请求 {i} 耗时 {response.elapsed.total_seconds():.2f} 秒")
except requests.Timeout:
print(f"请求 {i} 超时")
total_time = time.time() - start_time
print(f"\n总耗时:{total_time:.2f} 秒")
if __name__ == "__main__":
sync_requests()
其输出总耗时显著更长(约 12 秒以上),因为每个请求必须等前一个完成后才能开始。
HTTPX:融合两者之长
HTTPX 由 Django REST Framework 的作者 Tom Christie 于 2019 年 8 月发布,旨在融合 Requests 的易用性和 AIOHTTP 的异步能力。它既提供类似 Requests 的同步 API,也原生支持异步操作。
HTTPX 的主要特性包括:
- 熟悉的 Requests 风格 API:迁移成本极低。
- 同步与异步双模支持:同一库内无缝切换。
- 原生 HTTP/2 支持:与现代 Web 服务器通信更高效。
- 完整的类型注解(Type Annotations):提升 IDE 智能提示和静态检查能力。
安装命令:
pip
pip install httpx
同步用法示例(几乎与 Requests 一致):
python
# httpx_sync.py
import httpx
response = httpx.get('https://api.github.com')
print(response.status_code)
print(response.json())
异步用法示例:
python
# httpx_async.py
import asyncio
import httpx
import time
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
]
async def fetch(client, url, i):
response = await client.get(url)
print(f"请求 {i} 耗时 {response.elapsed.total_seconds():.2f} 秒")
async def async_requests():
start_time = time.time()
async with httpx.AsyncClient(timeout=10.0) as client:
tasks = [fetch(client, url, i) for i, url in enumerate(urls, 1)]
await asyncio.gather(*tasks)
total_time = time.time() - start_time
print(f"\n总耗时:{total_time:.2f} 秒")
if __name__ == "__main__":
asyncio.run(async_requests())
输出结果与 AIOHTTP 类似,总耗时约 4--5 秒。
HTTPX 的最大优势在于:你可以在同一个项目中自由混合同步与异步代码,而无需引入多个 HTTP 库。
如何为你的项目选择合适的 HTTP 客户端?
下表总结了三者的功能对比:
| 特性 / 能力 | Requests | AIOHTTP | HTTPX |
|---|---|---|---|
| 同步操作 | ✅ | ❌ | ✅ |
| 异步操作 | ❌ | ✅ | ✅ |
| 原生 HTTP/2 支持 | ❌ | ❌ | ✅ |
| WebSocket 支持 | ❌ | ✅ | 通过插件支持 |
| 类型注解(Type Hints) | 部分支持 | ✅ | ✅ |
| 带退避策略的自动重试 | 通过插件 | ✅ | ✅ |
| SOCKS 代理支持 | 通过插件 | 通过插件 | ✅ |
| 事件钩子(Event Hooks) | ✅ | ❌ | ✅ |
| Brotli 压缩支持 | 通过插件 | ✅ | ✅ |
| 异步 DNS 查询 | ❌ | ✅ | ✅ |
推荐使用场景
- Requests:适合简单脚本、教学示例或不需要异步的中小型项目。其简洁性和社区支持无可替代。
- AIOHTTP:适用于高并发异步服务(如 Web 爬虫、实时数据处理、微服务网关),尤其当你需要 WebSocket 或自建异步服务器时。
- HTTPX:如果你希望兼顾同步与异步,或想为未来升级到 HTTP/2 做准备,HTTPX 是最佳选择。它也是从 Requests 平滑过渡到异步世界的理想桥梁。
FQA
- 异步有哪些使用场景
- I/O 密集型任务:比如同时请求 100 个 API、下载多个文件。
- 高并发服务:如 FastAPI、Quart 等异步 Web 框架处理成千上万的并发连接。
- 所有请求都用aiohttp,可以吗?
技术上完全可以,但仍然不建议。
有很多场景并不是高并发,如果简单的请求都用aiohttp,就会带来几个问题:
- 简单请求代码也很复杂
python
import asyncio
import aiohttp
async def fetch_once():
async with aiohttp.ClientSession() as session:
async with session.get("https://api.example.com/data") as resp:
return await resp.json()
# 调用
result = asyncio.run(fetch_once())
- 所有调用点都必须变成 async/await
一旦底层用了 aiohttp,上层函数也必须是 async,否则无法获取结果。
python
# 如果你有一个同步函数想调用它?
def old_function():
data = fetch_once() # ❌ 报错!不能在同步函数中直接调用协程
你只能:
改成 async def old_function() → 连锁反应,整个调用链变异步
或用 asyncio.run(fetch_once()) → 但不能在已有事件循环中使用(比如在 Jupyter、FastAPI 里会报错)
-
调试和测试更复杂
(1)异步代码在 IDE 中调试体验较差(断点、堆栈)
(2)单元测试需用 pytest-asyncio 或手动管理事件循环
(3)初学者容易写出"看似并发实则串行"的 bug(比如忘记 await)
-
在某些环境无法运行
(1)Jupyter Notebook:默认已有事件循环,asyncio.run() 会报错
(2)Django/Flask 同步视图:在同步 Web 框架中强行跑异步代码很别扭
(3)简单脚本/REPL:每次都要写 asyncio.run(),破坏交互流畅性
-
性能未必更好(甚至更差)
如果你只发 1~5 个请求,aiohttp 的事件循环开销可能超过收益
(1)100 并发:aiohttp 快
(2)1 次请求:requests 更快(无协程调度开销)