在做接口调用或批量数据抓取时,同步库requests会阻塞等待每个响应,当需要请求几十上百个URL时,串行耗时极长。Python的异步IO(asyncio)配合aiohttp可以并发发起HTTP请求,等待网络IO时自动切换协程,大幅提升效率。本文从零开始,讲解asyncio与aiohttp的核心用法,包含并发控制、限流、超时配置等进阶技巧,最后提供一个可直接用于爬虫的健壮模板。
运行环境:Python 3.7及以上(支持async/await关键字)。
一、核心基础概念
1. 两个关键字
- async def:定义协程函数。调用协程函数不会立即执行代码,而是返回一个协程对象。
- await:挂起当前协程,让出CPU给事件循环,等待IO完成后再恢复执行。所有读取响应(text()、json()、read())都是IO操作,前面必须加await。
2. 事件循环(EventLoop)
事件循环是异步程序的调度中枢,负责管理多个协程。新版Python中统一使用asyncio.run()来启动事件循环,无需手动创建。
3. 异步请求库:aiohttp
aiohttp完全适配asyncio协程模型,自带连接池,是目前异步HTTP请求的主流方案。安装命令:pip install aiohttp
二、第一个异步请求:单条GET请求
同步写法(requests):- import requests
- def sync_request():
- resp = requests.get("https://httpbin.org/get")
- print(resp.status_code)
- print(resp.text)
- sync_request()
复制代码
异步写法(aiohttp):- import asyncio
- import aiohttp
- async def async_request():
- async with aiohttp.ClientSession() as session:
- async with session.get("https://httpbin.org/get") as resp:
- print("状态码:", resp.status)
- text = await resp.text()
- print(text[:200])
- if __name__ == "__main__":
- asyncio.run(async_request())
复制代码
新手踩坑要点:
- 协程函数不能直接调用,必须通过asyncio.run()执行。
- 读取响应的text()、json()、read()前面必须加await。
- 使用async with上下文管理器自动关闭会话和连接,避免泄漏。
三、真正的并发:一次性请求多个URL
单条请求看不出优势,批量请求才是异步的主战场。使用asyncio.create_task()创建任务,再用asyncio.gather()收集所有结果。
- import asyncio
- import aiohttp
- url_list = [
- "https://httpbin.org/get",
- "https://httpbin.org/ip",
- "https://httpbin.org/headers"
- ]
- async def fetch(session, url):
- async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
- return await resp.json()
- async def main():
- tasks = []
- async with aiohttp.ClientSession() as session:
- for url in url_list:
- task = asyncio.create_task(fetch(session, url))
- tasks.append(task)
- results = await asyncio.gather(*tasks)
- for res in results:
- print(res)
- if __name__ == "__main__":
- asyncio.run(main())
复制代码
运行效果:多个请求几乎同时发出,总耗时只等于最慢的那一次网络请求,而串行请求总耗时是每次请求时间之和。
四、常用进阶用法
1. POST请求(传递JSON数据)- async def post_json(session):
- data = {"username": "test", "password": "123456"}
- async with session.post(
- url="https://httpbin.org/post",
- json=data
- ) as resp:
- return await resp.json()
复制代码
2. 限制并发数量(防止请求过猛被封IP)
无限并发容易触发反爬或打爆服务器。用asyncio.Semaphore控制最大并发数:- sem = asyncio.Semaphore(5)
- async def limited_fetch(session, url):
- async with sem:
- async with session.get(url) as resp:
- return await resp.status
复制代码
3. 配置请求头、超时、连接池- connector = aiohttp.TCPConnector(limit=50) # 连接池最大连接数
- timeout = aiohttp.ClientTimeout(total=15) # 全局超时15秒
- headers = {"User-Agent": "Mozilla/5.0"}
- async def main():
- async with aiohttp.ClientSession(
- connector=connector,
- timeout=timeout,
- headers=headers
- ) as session:
- pass
复制代码
五、异步 vs 同步:性能对比测试
串行同步代码(requests):- import time
- import requests
- url = "https://httpbin.org/get"
- start = time.time()
- for _ in range(10):
- requests.get(url)
- print(f"同步串行耗时:{time.time()-start:.2f}s")
复制代码
异步并发代码(aiohttp):- import time
- import asyncio
- import aiohttp
- url = "https://httpbin.org/get"
- async def fetch(session):
- async with session.get(url) as resp:
- await resp.text()
- async def main():
- tasks = []
- async with aiohttp.ClientSession() as session:
- for _ in range(10):
- tasks.append(asyncio.create_task(fetch(session)))
- await asyncio.gather(*tasks)
- start = time.time()
- asyncio.run(main())
- print(f"异步并发耗时:{time.time()-start:.2f}s")
复制代码
测试结论:同步10次请求总耗时约5~8秒(受网络延迟影响),异步并发仅需0.5~1.5秒。注意,异步只优化IO等待,不影响CPU密集型任务,后者需用多进程。
六、新手常见问题总结
- RuntimeWarning: coroutine was never awaited:协程没有被调度执行,必须通过create_task或await调用,不能直接调用协程函数。
- 事件循环嵌套报错:在Jupyter Notebook等环境中可能遇到,本地脚本直接用asyncio.run()即可。
- 不要反复新建ClientSession:全局只创建一个Session以复用TCP连接池,性能更高。
- 必须捕获异常:网络请求可能超时或失败,建议在fetch内部加try-except,防止整个程序崩溃。
七、完整健壮版模板(可直接用于爬虫)- import asyncio
- import aiohttp
- MAX_CONCURRENT = 10
- TIMEOUT = aiohttp.ClientTimeout(total=10)
- URLS = [f"https://httpbin.org/get?id={i}" for i in range(20)]
- sem = asyncio.Semaphore(MAX_CONCURRENT)
- async def request_one(session, url):
- async with sem:
- try:
- async with session.get(url, timeout=TIMEOUT) as resp:
- if resp.status == 200:
- return await resp.json()
- else:
- return f"异常状态码:{resp.status}"
- except Exception as e:
- return f"请求失败:{url} | {str(e)}"
- async def run_all():
- tasks = []
- connector = aiohttp.TCPConnector(limit=MAX_CONCURRENT)
- async with aiohttp.ClientSession(connector=connector) as session:
- for url in URLS:
- task = asyncio.create_task(request_one(session, url))
- tasks.append(task)
- results = await asyncio.gather(*tasks)
- for item in results:
- print(item)
- if __name__ == "__main__":
- asyncio.run(run_all())
复制代码
结语
异步请求是Python爬虫和批量接口调用的必备技能。入门记住三点:
1. 用async def定义协程,IO操作加await。
2. 使用aiohttp.ClientSession管理连接。
3. 通过gather + task实现并发,利用信号量控制请求频率。
掌握本文内容后,你可以轻松写出高并发网络请求程序,避免同步阻塞的低效问题。后续可继续学习异步MySQL、Redis,搭建完整的异步服务。 |