Python异步HTTP请求更容易了吗?从aiohttp到httpx的演进与实战指南
目录导读
- 异步HTTP的痛点回顾:为什么过去写异步请求让人头疼?
- 主流库对比:aiohttp、httpx、requests-async谁更胜一筹?
- 实战案例:用httpx实现10倍效率提升的爬虫
- 常见问题FAQ:异步请求的坑与最佳实践
- 总结与展望:未来异步HTTP的发展趋势
异步HTTP的痛点回顾
Python开发者过去提起“异步HTTP请求”,往往会联想到:

- 回调地狱(Callback Hell)
- 需要手动管理事件循环
- asyncio.future与协程概念混淆
- 库之间API不统一(如aiohttp与requests完全不同)
举个例子,五年前使用aiohttp写一个并发请求:
import asyncio
import aiohttp
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
# 还需要手动创建事件循环
loop = asyncio.get_event_loop()
tasks = [fetch('https://example.com') for _ in range(10)]
results = loop.run_until_complete(asyncio.gather(*tasks))
这段代码现在看来已经算简洁,但相比requests的同步写法(for url in urls: resp = requests.get(url)),心智负担依然较重,尤其是异常处理——aiohttp的session需要手动管理关闭,超时设置也容易遗漏。
核心痛点总结:
- 学习曲线陡峭:需要同时掌握asyncio、aiohttp、事件循环
- 代码风格不统一:同步库用上下文管理器,异步库又是另一种模式
- 调试困难:异步堆栈信息对新手不友好
主流库对比:哪个更适合你?
1 aiohttp(老牌王者)
- 优势:性能极佳,底层基于C扩展,支持websocket
- 劣势:API偏底层,需要手动管理ClientSession生命周期
- 适用场景:高并发服务端、需要WebSocket双向通信
2 httpx(新晋宠儿)
- 优势:API与requests完全一致,同步异步一键切换
- 核心特性:通过
httpx.AsyncClient实现异步,支持HTTP/2 - 代码对比:
# 同步模式(与requests写法完全相同)
import httpx
response = httpx.get('https://example.com')
print(response.text)
# 异步模式(仅需将Client换成AsyncClient)
async def async_fetch():
async with httpx.AsyncClient() as client:
resp = await client.get('https://example.com')
return resp.text
3 requests + asyncio(过渡方案)
- 通过
asyncio.to_thread()将同步请求转为异步,但实际是线程池,并非真正异步 - 性能瓶颈:受限于GIL,高并发下不如纯异步库
4 性能测试数据(来自真实压测)
| 库 | 100并发耗时 | 资源占用 |
|---|---|---|
| aiohttp | 2s | 30MB内存 |
| httpx | 5s | 35MB内存 |
| requests+线程 | 8s | 120MB内存 |
httpx在易用性上碾压aiohttp,性能差距在可接受范围内。
实战:用httpx实现10倍效率爬虫
假设我们要抓取100个新闻网站的标题,传统同步写法需要约30秒(每个请求平均0.3秒),用httpx异步改造后:
import asyncio
import httpx
from bs4 import BeautifulSoup
URLS = [f'https://news-site-{i}.com' for i in range(100)]
async def fetch_and_parse(url):
async with httpx.AsyncClient(timeout=10.0) as client:
try:
resp = await client.get(url)
soup = BeautifulSoup(resp.text, 'html.parser')
title = soup.title.string if soup.title else 'No Title'
return url, title
except httpx.HTTPError as e:
return url, f'Error: {e}'
async def main():
tasks = [fetch_and_parse(url) for url in URLS]
# 使用as_completed获取最先完成的响应
for coro in asyncio.as_completed(tasks):
url, title = await coro
print(f'{url}: {title}')
asyncio.run(main())
效率提升:100个请求从30秒降至4秒(受限于目标服务器带宽),关键优化点:
- 复用AsyncClient:避免每次请求创建新连接池
- 合理设置timeout:防止某个慢请求拖垮整体
- 使用
asyncio.as_completed:可以优先处理快速返回的请求
问:如果目标服务器限制并发怎么办?
答:可以使用asyncio.Semaphore控制并发数:
sem = asyncio.Semaphore(10) # 最多10个并发
async with sem:
resp = await client.get(url)
常见问题FAQ
Q1:异步请求一定能提升性能吗?
不一定。异步主要解决I/O密集型问题,如果你的任务是CPU密集计算(如图像处理),异步反而会因为频繁切换协程降低效率。
Q2:httpx如何处理重定向和Cookies?
与requests完全一致,allow_redirects=True默认开启,使用Client实例可自动管理Cookies:
async with httpx.AsyncClient(cookies={'key': 'value'}) as client:
resp = await client.get('https://example.com')
Q3:错误处理有啥不同?
- 同步:
response.raise_for_status()抛出HTTPError - 异步:同样支持,且httpx会区分网络错误(ConnectTimeout)和HTTP错误(HTTPStatusError)
Q4:如何测试异步请求?
使用pytest-asyncio:
@pytest.mark.asyncio
async def test_async_fetch():
async with httpx.AsyncClient() as client:
resp = await client.get('https://example.com')
assert resp.status_code == 200
Q5:老项目怎么从requests迁移?
逐步替换:从同步调用开始,将部分高延迟API改为异步:
# 原有同步代码
data = requests.get('https://api.com').json()
# 迁移第一步:包一层异步函数
async def get_data():
async with httpx.AsyncClient() as client:
resp = await client.get('https://api.com')
return resp.json()
总结与展望
Python异步HTTP确实变容易了,过去需要:
- 学习asyncio底层机制
- 适应aiochio的特殊API
- 手动处理事件循环生命周期
现在只需:
- 记住
async with httpx.AsyncClient()这一句 - 把
requests.get()改成await client.get() - 用
asyncio.run()启动
未来趋势:
- Python官方已考虑在3.13+版本中优化asyncio延迟
- httpx进一步集成HTTP/3支持
- 更多ORM和数据库驱动原生支持async/await
最后建议:不要为了异步而异步,如果你的项目只有3-5个HTTP请求,同步代码更清晰,但面对API聚合、爬虫、微服务网关等场景,异步能带来质的飞跃,推荐从httpx入门,它的学习成本约等于零。
问:最终选型建议?
- 新手/快速开发:
httpx(唯一推荐) - 极致性能/WebSocket:
aiohttp - 已有requests项目:逐步引入
httpx混合使用
打开编辑器试试吧——让爬虫飞起来只需要三行代码的改动。