全部文章

Python爬虫-结合 asyncio 和 aiohttp 的性能优化方案

阅读本文之前请确保已经掌握:Python异步编程 基础

在 I/O 密集型任务(如高频次网络请求)中,使用 asyncio(异步 I/O 框架)和 aiohttp(异步 HTTP 客户端)可以显著提升性能,其核心优势在于 单线程内通过事件循环实现高并发,避免了多进程或多线程的上下文切换和资源开销。以下是详细方案及示例代码:


一、同步 vs 多进程 vs 异步的性能对比

方案特点适用场景
同步阻塞 单线程顺序执行,性能最低 简单脚本或低频请求
多进程/多线程 并行处理,但存在进程/线程切换开销 CPU 密集型或中等并发
异步非阻塞 单线程高并发,资源占用极低,性能最优 高并发 I/O 密集型任务

二、异步优化实现步骤

    1. 定义异步请求函数
      使用 aiohttp 发起非阻塞 HTTP 请求,配置超时和重试机制。

    2. 控制并发量
      通过 asyncio.Semaphore 限制最大并发数,避免服务器拒绝或资源耗尽。

    3. 批量提交任务
      利用 asyncio.gather 或 asyncio.create_task 批量提交异步任务。

    4. 异步处理结果
      在请求完成后解析数据、存储结果或处理异常。

    5. 性能监控
      统计总耗时、请求成功率等指标。 


三、完整示例代码

import asyncio
import aiohttp
from aiohttp import ClientSession, ClientTimeout
import time
from urllib.parse import urlparse

# 异步请求函数(支持超时、重试和异常处理)
async def fetch_url(session: ClientSession, url: str, semaphore: asyncio.Semaphore, retries=3):
    try:
        async with semaphore:  # 控制并发量
            for attempt in range(retries):
                try:
                    async with session.get(url, timeout=ClientTimeout(total=10)) as response:
                        if response.status == 200:
                            html = await response.text()
                            print(f"成功获取 {url}")
                            return html
                        else:
                            print(f"请求失败: {url}, 状态码 {response.status}")
                except (aiohttp.ClientError, asyncio.TimeoutError) as e:
                    if attempt < retries - 1:
                        print(f"第 {attempt + 1} 次重试 {url}: {str(e)}")
                        await asyncio.sleep(1)
                    else:
                        print(f"请求失败(最终): {url}, 错误: {str(e)}")
                        return None
    except Exception as e:
        print(f"未知错误: {url}, {str(e)}")
        return None

# 解析 HTML(模拟数据处理)
async def parse_html(html: str):
    # 使用 BeautifulSoup 或其他库解析
    await asyncio.sleep(0.001)  # 模拟轻量处理
    return {"data": "parsed_result"}

# 主异步函数
async def main(urls: list, max_concurrency=100):
    start_time = time.time()
    timeout = ClientTimeout(total=10)
    semaphore = asyncio.Semaphore(max_concurrency)  # 限制并发量
    
    async with ClientSession(timeout=timeout) as session:
        tasks = [fetch_url(session, url, semaphore) for url in urls]
        results = await asyncio.gather(*tasks)  # 批量提交任务
        
        # 异步处理结果(如解析、存储)
        parsed_tasks = [parse_html(html) for html in results if html]
        parsed_results = await asyncio.gather(*parsed_tasks)
        
    # 性能统计
    total_time = time.time() - start_time
    success_count = sum(1 for res in results if res is not None)
    print(f"\n总耗时: {total_time:.2f} 秒")
    print(f"总请求数: {len(urls)}, 成功数: {success_count}")
    print(f"QPS: {len(urls) / total_time:.2f} (次/秒)")

if __name__ == "__main__":
    # 测试 URL 列表(示例)
    urls = [
        "https://www.example.com",
        "https://www.google.com",
        "https://www.github.com"
    ] * 100  # 重复 100 次模拟高并发
    
    # 运行事件循环
    asyncio.run(main(urls, max_concurrency=200))

四、关键优化点说明

优化点作用
异步非阻塞 单线程处理数千并发请求,避免进程/线程切换开销
信号量控制并发 防止瞬时高并发导致服务器拒绝或本地端口耗尽
超时与重试 提升鲁棒性,避免单个请求阻塞整体任务
批量任务提交 使用 asyncio.gather 高效管理大量任务
结果异步处理 解析和存储操作也异步执行,最大化利用 I/O 等待时间

五、性能对比

方案1000 次请求耗时内存占用QPS
同步阻塞 ~200s 50MB ~5
多进程 (16 workers) ~30s 800MB ~33
异步 (单线程) ~5s 100MB ~200

六、进阶优化技巧

    1. 连接池复用
      aiohttp.ClientSession 默认复用 TCP 连接,减少握手开销。

    2. DNS 缓存
      使用 aiohttp.resolver.AsyncResolver 缓存 DNS 查询结果。

    3. 动态调整并发
      根据服务器响应时间动态调整信号量值。

    4. 结合 UVLoop
      替换默认事件循环为 uvloop,提升性能 20-30%:

import uvloop
uvloop.install()
  • uvloop 是一个基于 libuv 的高性能异步事件循环库,其底层依赖的 C 扩展模块在 Windows 平台上存在兼容性问题。

  • 官方明确声明不支持 Windows(GitHub 仓库说明),主要支持 Linux 和 macOS。

 

七、注意事项

  1. 避免阻塞操作
    异步函数内不要调用同步阻塞代码(如 time.sleep()),需替换为 await asyncio.sleep()

  2. 异常隔离
    单个任务异常不会影响整体事件循环,但需合理记录日志。

  3. 资源限制
    根据系统最大文件描述符数调整并发量(Linux 默认约 1024)。


通过上述方案,可轻松实现每秒数百至数千次的高并发请求,尤其适合网络爬虫、API 轮询等高 I/O 负载场景。

posted @ 2025-03-27 18:09  指尖下的世界  阅读(153)  评论(0)    收藏  举报