Python异步编程实战:用asyncio将爬虫效率提升10倍的完整指南(附代码)
在日常开发中,我们经常遇到这样的场景:需要同时请求多个API、爬取多个页面、或者并行处理大量IO任务。如果你还在用同步代码一个一个地执行,那效率可能低得离谱。
今天船长带你从0到1搞懂Python的asyncio,用真实案例演示如何把一个耗时60秒的同步爬虫,优化到只需6秒。
一、先看效果:同步 vs 异步的性能对比
我们先写一个简单的对比脚本:
import asyncio
import time
import aiohttp
# 模拟一个需要1秒的IO请求
async def fetch(session, url):
async with session.get(url) as resp:
return await resp.text()
async def async_main(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
return await asyncio.gather(*tasks)
def sync_main(urls):
import requests
results = []
for url in urls:
resp = requests.get(url)
results.append(resp.text)
return results
if __name__ == "__main__":
urls = ["https://httpbin.org/delay/1"] * 10 # 模拟10个耗时请求
# 同步方式
start = time.time()
sync_main(urls)
print(f"同步耗时: {time.time() - start:.2f}秒")
# 异步方式
start = time.time()
asyncio.run(async_main(urls))
print(f"异步耗时: {time.time() - start:.2f}秒")
输出结果:
同步耗时: 10.23秒
异步耗时: 1.12秒
同样是10个请求,同步要10秒,异步只要1秒。10倍的效率提升,这就是asyncio的威力。
二、asyncio核心概念速通
很多人一看到async和await就头大,其实核心概念就三个:
① 协程(Coroutine)
用async def定义的函数就是协程。它看起来像普通函数,但可以"暂停"执行,把控制权交还给事件循环。
async def my_task():
# 这是一个协程
result = await some_io_operation() # 遇到IO就暂停
return result
② 事件循环(Event Loop)
事件循环是asyncio的大脑,它负责调度所有协程的执行。当一个协程在等待IO时,事件循环会自动切换到另一个协程执行。
③ await关键字
await告诉Python:"这里有个IO操作,我先暂停,等结果回来了再继续。"在等待期间,事件循环可以去执行其他协程。
三、实战案例:异步批量请求API
下面是一个完整的实战案例——批量查询股票数据:
import asyncio
import aiohttp
import json
from datetime import datetime
# 模拟股票API接口(实际替换为你的接口)
API_URL = "https://httpbin.org/json"
STOCK_CODES = ["000001", "000002", "600519", "601318", "000858",
"002714", "300750", "603259", "002415", "600036"]
async def fetch_stock_data(session, code):
"""异步获取单只股票数据"""
try:
async with session.get(f"{API_URL}?code={code}", timeout=10) as resp:
if resp.status == 200:
data = await resp.json()
return {"code": code, "data": data, "status": "ok"}
return {"code": code, "status": f"error_{resp.status}"}
except asyncio.TimeoutError:
return {"code": code, "status": "timeout"}
except Exception as e:
return {"code": code, "status": f"error: {str(e)[:50]}"}
async def batch_fetch_stocks(codes, concurrency=5):
"""批量获取股票数据(控制并发数)"""
semaphore = asyncio.Semaphore(concurrency) # 限制并发数
async def limited_fetch(session, code):
async with semaphore:
return await fetch_stock_data(session, code)
async with aiohttp.ClientSession() as session:
tasks = [limited_fetch(session, code) for code in codes]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
def main():
start = datetime.now()
print(f"开始时间: {start.strftime('%H:%M:%S')}")
print(f"查询{len(STOCK_CODES)}只股票...\n")
results = asyncio.run(batch_fetch_stocks(STOCK_CODES, concurrency=5))
success = sum(1 for r in results if isinstance(r, dict) and r.get("status") == "ok")
failed = len(results) - success
print(f"成功: {success}, 失败: {failed}")
print(f"总耗时: {(datetime.now() - start).total_seconds():.2f}秒")
# 输出失败详情
for r in results:
if isinstance(r, dict) and r.get("status") != "ok":
print(f" 失败: {r['code']} - {r['status']}")
if __name__ == "__main__":
main()
代码要点解析:
-
asyncio.Semaphore(5)—— 控制并发数为5,避免瞬间发送太多请求被封IP -
asyncio.gather(*tasks)—— 并发执行所有任务 -
return_exceptions=True—— 即使某个任务报错也不影响其他任务 -
aiohttp.ClientSession()—— 异步HTTP客户端,替代同步的requests
四、asyncio常见踩坑指南
坑1:在async函数里调用同步IO
# 错误示范:在协程中调用同步requests
async def bad_example():
import requests # 这是同步库!会阻塞整个事件循环
resp = requests.get("https://example.com")
# 正确做法:用aiohttp替代requests
async def good_example():
async with aiohttp.ClientSession() as session:
async with session.get("https://example.com") as resp:
return await resp.text()
坑2:忘记await
# 错误:忘记await,协程不会被实际执行
async def wrong():
result = fetch_data() # 只是创建了协程对象,没有执行
# 正确:必须用await
async def right():
result = await fetch_data() # 实际执行并等待结果
坑3:并发数不控制导致被封
# 危险:1000个请求同时发出
tasks = [fetch(url) for url in 1000_urls] # 容易被封IP
# 安全:用Semaphore控制并发
sem = asyncio.Semaphore(10) # 最多同时10个请求
async def safe_fetch(session, url):
async with sem:
return await fetch(session, url)
坑4:Windows上event loop策略问题
# Windows上可能需要手动设置事件循环策略
import sys
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
五、什么时候用asyncio,什么时候不用?
适合用asyncio的场景:
-
网络请求(API调用、爬虫、WebSocket)
-
文件IO(大量小文件读写)
-
数据库查询(异步数据库驱动如asyncpg、aiomysql)
-
任何"等待"类型的操作
不适合用asyncio的场景:
-
CPU密集型任务(数据分析、机器学习训练)
-
需要顺序执行的逻辑
-
简单脚本(异步的复杂度不值得)
**CPU密集型任务怎么办?**用concurrent.futures.ProcessPoolExecutor多进程:
from concurrent.futures import ProcessPoolExecutor
import numpy as np
def heavy_computation(n):
return np.sum(np.random.rand(n))
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(heavy_computation, [1000000] * 8))
六、总结
asyncio的核心价值就一句话:在等待IO的时候,去做别的事情。
掌握asyncio后,你的Python代码可以轻松处理:
-
批量API请求(从串行变并行)
-
高并发爬虫(效率提升5-10倍)
-
异步Web服务(FastAPI就是基于asyncio的)
-
实时数据处理(WebSocket、消息队列)
代码在 GitHub 上,需要的朋友自取。
有问题评论区见,船长在线答疑。