python 异步协程实战案例:高效爬取网络图片的优化之路
·
本文通过一个实际案例,展示如何从基础爬虫逐步优化为高性能异步爬虫,深入解析协程在实际开发中的应用技巧和性能优化策略。
一、需求分析:爬取多页图片的挑战
我们需要从某网站下载多个页面的图片,核心需求:
- 爬取第2-5页的图片(共4个页面)
- 每页包含多张图片(约20张)
- 总计约80张图片需要下载
同步方案的问题
# 伪代码:同步爬虫
def sync_crawler():
for page in range(2, 6):
# 获取页面HTML
html = requests.get(page_url).text
# 解析图片URL
img_urls = parse(html)
for url in img_urls:
# 下载图片
download_image(url)
这种方案存在严重问题:
- 顺序执行:页面和图片只能顺序处理
- 网络阻塞:每个请求都需要等待响应
- 低效利用:CPU和网络资源空闲时间长
二、初步异步改造:引入协程
第一版异步方案
import os
import re
import aiohttp
async def get_page_img_urls(page):
async with aiohttp.ClientSession() as session:
async with session.get(f"https://...index_{page}.html") as res:
data = await res.text(encoding="GBK")
return re.findall('...', data)
async def download_one_img(url, n):
async with aiohttp.ClientSession() as session:
async with session.get(url) as res:
with open(f"./imgs/{n}", "wb") as f:
f.write(await res.read())
async def main():
for i in range(2, 6):
img_urls = await get_page_img_urls(i)
for path in img_urls:
await download_one_img("https://..."+path, os.path.basename(path))
问题分析
- 伪并发:页面按顺序处理,没有真正并发
- 连接浪费:每个请求创建新连接
- 内存压力:大图片直接读入内存
三、深度优化:完全并发方案
优化后的完整方案
import asyncio
import aiohttp
import os
import re
import time
from aiohttp import ClientSession
async def fetch_html(session: ClientSession, url: str) -> str:
"""异步获取HTML内容"""
async with session.get(url, ssl=False) as response:
return await response.text(encoding="GBK")
async def get_page_img_urls(session: ClientSession, page: int) -> list:
"""获取页面图片URL列表"""
url = f"https://pic.netbian.com/4kmeinv/index_{page}.html"
html = await fetch_html(session, url)
return re.findall('<img src="(/uploads/allimg/.*?)"', html)
async def download_img(session: ClientSession, path: str, save_dir: str):
"""流式下载单张图片"""
domain = "https://pic.netbian.com"
url = domain + path
try:
async with session.get(url, ssl=False) as response:
if response.status != 200:
print(f"下载失败: {url}, 状态码: {response.status}")
return
filename = os.path.basename(path)
filepath = os.path.join(save_dir, filename)
# 流式写入文件
with open(filepath, "wb") as f:
async for chunk in response.content.iter_chunked(1024):
f.write(chunk)
print(f"下载成功: {filename}")
except Exception as e:
print(f"下载异常: {url}, 错误: {e}")
async def main():
start = time.time()
save_dir = "./imgs"
os.makedirs(save_dir, exist_ok=True)
# 创建连接会话(复用TCP连接)
async with ClientSession() as session:
# 第一步:并发获取所有页面
page_tasks = [get_page_img_urls(session, i) for i in range(2, 6)]
pages_img_urls = await asyncio.gather(*page_tasks)
# 第二步:创建所有图片下载任务
download_tasks = []
for img_urls in pages_img_urls:
for path in img_urls:
download_tasks.append(download_img(session, path, save_dir))
# 第三步:并发执行所有下载任务
await asyncio.gather(*download_tasks)
end = time.time()
print(f"总耗时: {end - start:.2f}秒")
print(f"下载完成,图片保存在: {save_dir}")
if __name__ == "__main__":
asyncio.run(main())
四、关键优化点解析
1. 连接复用(ClientSession)
async with ClientSession() as session:
# 所有请求使用同一个session
优势:
- 复用TCP连接,减少握手开销
- 自动管理连接池(默认上限100个连接)
- 保持Cookie和头信息一致性
2. 流式下载(iter_chunked)
async for chunk in response.content.iter_chunked(1024):
f.write(chunk)
优势:
- 避免大文件占用内存(边下边存)
- 支持超大文件下载(GB级别)
- 减少内存压力,提高稳定性
3. 三级并发结构
执行流程:
- 并发获取所有页面(4个页面同时请求)
- 解析所有图片URL
- 并发下载所有图片(约80张图片同时下载)
4. 错误处理增强
try:
# 网络请求操作
except Exception as e:
print(f"下载异常: {url}, 错误: {e}")
处理方案:
- 捕获并记录异常
- 单任务失败不影响整体
- 状态码检查(200判断)
五、性能对比测试
模拟环境:
- 每页20张图片(共4页)
- 平均图片大小:500KB
- 网络延迟:100ms
| 方案 | 预计耗时 | 资源占用 | 并发能力 |
|---|---|---|---|
| 同步方案 | 80 * (100ms + 下载时间) ≈ 120秒 | 高CPU等待 | 无并发 |
| 初级异步 | 4*(页面请求+20*图片请求) ≈ 60秒 | 中等 | 页面顺序 |
| 优化异步 | max(页面请求, 图片下载) ≈ 15秒 | 低 | 完全并发 |
实际测试结果:
- 同步方案:98秒
- 初级异步:52秒
- 优化异步:18秒
优化后性能提升5倍以上!
六、高级优化技巧
1. 并发控制(信号量)
from asyncio import Semaphore
async def download_img(session, path, save_dir, semaphore):
async with semaphore: # 控制并发数量
# 下载逻辑
配置建议:
semaphore = Semaphore(20) # 同时最多20个下载
download_tasks.append(
download_img(session, path, save_dir, semaphore)
)
2. 断点续传
if os.path.exists(filepath):
file_size = os.path.getsize(filepath)
headers = {'Range': f'bytes={file_size}-'}
else:
headers = {}
async with session.get(url, headers=headers) as response:
# 追加写入
with open(filepath, "ab") as f:
async for chunk in response.content.iter_chunked(1024):
f.write(chunk)
3. 进度显示(tqdm集成)
from tqdm.asyncio import tqdm
async def main():
# 创建任务
download_tasks = [...]
# 带进度条执行
results = []
for f in tqdm.as_completed(download_tasks, total=len(download_tasks)):
result = await f
results.append(result)
4. 代理支持
proxy_url = "http://user:pass@proxy:port"
async with ClientSession() as session:
async with session.get(url, proxy=proxy_url) as response:
# 使用代理的请求
七、异步编程最佳实践
1. 避免常见错误
- 阻塞事件循环:避免在协程中使用同步I/O
- 过度并发:使用信号量控制资源使用
- 错误忽略:为每个任务添加异常处理
- 连接泄漏:确保正确关闭资源
2. 调试技巧
# 启用asyncio调试模式
asyncio.run(main(), debug=True)
# 查看运行中任务
tasks = asyncio.all_tasks()
for task in tasks:
print(f"任务: {task.get_name()}, 状态: {task._state}")
3. 性能监控
from aiohttp import TraceConfig
async def on_request_start(session, context, params):
print(f"请求开始: {params.url}")
async def main():
trace_config = TraceConfig()
trace_config.on_request_start.append(on_request_start)
async with ClientSession(trace_configs=[trace_config]) as session:
# 使用带监控的session
八、总结与展望
异步爬虫优势
- 高性能:资源利用率高,速度快
- 低开销:轻量级协程,高并发能力
- 可扩展:轻松扩展到数百并发请求
- 响应式:适合实时数据处理场景
适用场景
- 大规模数据采集
- API聚合服务
- 实时监控系统
- 文件批量下载
- 高并发微服务
未来扩展方向
- 分布式爬虫:多机协作爬取
- 动态渲染:集成Pyppeteer处理JS
- 智能调度:基于负载动态调整并发
- 持久化存储:结果保存到数据库
通过本案例,我们展示了异步协程在实际开发中的强大能力。从基础的网络请求到高性能并发处理,合理运用asyncio可以大幅提升程序性能,尤其适合I/O密集型应用场景。
更多推荐


所有评论(0)