本文通过一个实际案例,展示如何从基础爬虫逐步优化为高性能异步爬虫,深入解析协程在实际开发中的应用技巧和性能优化策略。

一、需求分析:爬取多页图片的挑战

我们需要从某网站下载多个页面的图片,核心需求:

  1. 爬取第2-5页的图片(共4个页面)
  2. 每页包含多张图片(约20张)
  3. 总计约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)

这种方案存在严重问题:

  1. 顺序执行:页面和图片只能顺序处理
  2. 网络阻塞:每个请求都需要等待响应
  3. 低效利用: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))

问题分析

  1. 伪并发:页面按顺序处理,没有真正并发
  2. 连接浪费:每个请求创建新连接
  3. 内存压力:大图片直接读入内存

三、深度优化:完全并发方案

优化后的完整方案

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. 三级并发结构

主任务
页面获取任务
图片下载任务
页面1获取
页面2获取
页面3获取
页面4获取
图片1下载
图片2下载
...
图片80下载

执行流程

  1. 并发获取所有页面(4个页面同时请求)
  2. 解析所有图片URL
  3. 并发下载所有图片(约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

八、总结与展望

异步爬虫优势

  1. 高性能:资源利用率高,速度快
  2. 低开销:轻量级协程,高并发能力
  3. 可扩展:轻松扩展到数百并发请求
  4. 响应式:适合实时数据处理场景

适用场景

  1. 大规模数据采集
  2. API聚合服务
  3. 实时监控系统
  4. 文件批量下载
  5. 高并发微服务

未来扩展方向

  1. 分布式爬虫:多机协作爬取
  2. 动态渲染:集成Pyppeteer处理JS
  3. 智能调度:基于负载动态调整并发
  4. 持久化存储:结果保存到数据库

通过本案例,我们展示了异步协程在实际开发中的强大能力。从基础的网络请求到高性能并发处理,合理运用asyncio可以大幅提升程序性能,尤其适合I/O密集型应用场景。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐