aiohttp+asyncio 实现高并发异步爬虫

在网络爬虫开发中,高并发 是提升爬取效率的核心需求 ------ 传统同步爬虫因 "请求发送后等待响应" 的阻塞特性,在面对大量页面爬取时效率极低。而 Python 的asyncio(异步 IO 框架)结合aiohttp(异步 HTTP 客户端),能让爬虫在等待网络响应的间隙处理其他请求,实现非阻塞的高并发爬取,轻松应对海量页面的爬取需求。本文将从核心原理、环境准备、完整实现到优化技巧,手把手教你打造高性能异步爬虫。

一、核心原理:为什么 aiohttp+asyncio 能实现高并发?

要理解异步爬虫的高并发本质,需先理清两个核心组件的作用及异步编程的核心逻辑:

  1. asyncio :Python 内置的异步 IO 框架,核心是事件循环(Event Loop)------ 作为异步任务的 "调度中心",它会不断监听任务状态,当某个任务因网络请求、IO 操作进入 "等待状态" 时,立即切换到其他就绪任务执行,避免 CPU 空等,最大化利用资源。
  2. aiohttp :专为asyncio设计的异步 HTTP 客户端库,替代了同步的requests库,支持异步发送 HTTP 请求 ,与asyncio的事件循环完美兼容,不会因单个请求的网络延迟阻塞整个程序。
  3. 异步核心优势:同步爬虫是 "单线程串行执行"(一个请求完成再发下一个),而异步爬虫是 "单线程多任务并发"(同时发起多个请求,等待响应时处理其他任务),在网络 IO 密集型的爬虫场景中,效率能提升数倍甚至数十倍。

同步与异步的核心区别

  • 同步:请求1发送 → 等待响应 → 处理数据 → 请求2发送(全程阻塞)
  • 异步:请求1发送 → 立即发送请求2 → ... → 某个请求响应完成 → 处理该请求数据(无空闲等待)

二、环境准备

仅需安装异步 HTTP 客户端aiohttpasyncio是 Python3.4 + 内置模块,无需额外安装:

bash

运行

复制代码
pip install aiohttp -i https://pypi.tuna.tsinghua.edu.cn/simple

推荐 Python 版本:3.7+(对异步语法的支持更完善,避免低版本的兼容性问题)

三、完整实现:高并发异步爬虫开发

以爬取通用测试站点(http://httpbin.org/get)为例,实现一个可配置、高并发的异步爬虫,包含请求发送、响应处理、异常捕获、并发控制等核心功能,直接可运行。

3.1 完整代码

python

运行

复制代码
import asyncio
import aiohttp
from typing import List, Dict
import logging

# 配置日志,方便查看爬取过程和异常信息
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)

# 爬虫核心配置(可根据需求修改)
CONFIG = {
    "TARGET_URLS": [f"http://httpbin.org/get?page={i}" for i in range(1, 101)],  # 待爬取100个测试链接
    "CONCURRENT_LIMIT": 20,  # 最大并发数(关键:避免请求过多被封IP)
    "TIMEOUT": 10,  # 单个请求超时时间(秒)
    "HEADERS": {  # 请求头:模拟浏览器,避免被反爬
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    }
}

async def fetch(session: aiohttp.ClientSession, url: str) -> Dict:
    """
    单个URL的异步请求函数:发送请求、处理响应、捕获异常
    :param session: 共享的aiohttp客户端会话(关键:减少资源开销)
    :param url: 待爬取的URL
    :return: 爬取的结果(字典格式,包含URL和响应数据)
    """
    try:
        # 异步发送GET请求,设置超时和请求头
        async with session.get(
            url=url,
            headers=CONFIG["HEADERS"],
            timeout=aiohttp.ClientTimeout(total=CONFIG["TIMEOUT"])
        ) as response:
            # 异步解析JSON响应(aiohttp的方法均为async,需加await)
            data = await response.json()
            logger.info(f"成功爬取:{url},状态码:{response.status}")
            return {"url": url, "status": response.status, "data": data}
    except aiohttp.ClientError as e:
        # 捕获HTTP请求相关异常(连接失败、超时、状态码错误等)
        logger.error(f"请求异常:{url},错误信息:{str(e)}")
    except Exception as e:
        # 捕获其他未知异常
        logger.error(f"未知异常:{url},错误信息:{str(e)}")
    return {"url": url, "status": -1, "data": None, "error": str(e)}

async def main():
    """
    主异步函数:创建会话、创建任务、控制并发、执行任务、收集结果
    """
    # 1. 创建aiohttp.ClientSession(全局共享,整个爬虫仅创建一次)
    # 作用:复用TCP连接,减少网络资源开销,提升并发效率
    async with aiohttp.ClientSession() as session:
        # 2. 创建信号量(Semaphore):控制最大并发数
        # 原理:维护一个计数器,获取信号量时-1,释放时+1,计数器为0时阻塞新任务
        semaphore = asyncio.Semaphore(CONFIG["CONCURRENT_LIMIT"])
        
        # 3. 定义带并发控制的任务包装函数
        async def bounded_fetch(url: str):
            # async with semaphore:自动获取和释放信号量,无需手动操作
            async with semaphore:
                return await fetch(session, url)
        
        # 4. 创建所有异步任务(列表推导式)
        # 注意:asyncio.create_task()将协程包装为任务,立即加入事件循环等待执行
        tasks = [asyncio.create_task(bounded_fetch(url)) for url in CONFIG["TARGET_URLS"]]
        
        # 5. 等待所有任务执行完成,收集结果(gather:并发执行多个任务)
        # return_exceptions=True:某个任务异常时,不终止整个爬虫,仅返回异常对象
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        # 6. 处理爬取结果(过滤异常、统计成功/失败数)
        success_count = sum(1 for res in results if isinstance(res, dict) and res["status"] == 200)
        fail_count = len(CONFIG["TARGET_URLS"]) - success_count
        logger.info(f"爬取完成!总任务数:{len(CONFIG['TARGET_URLS'])},成功:{success_count},失败:{fail_count}")
        
        # 可选:保存结果到本地(如JSON文件)
        # import json
        # with open("crawl_results.json", "w", encoding="utf-8") as f:
        #     json.dump(results, f, ensure_ascii=False, indent=2)

if __name__ == "__main__":
    # 核心:启动asyncio事件循环,执行主异步函数
    # Python3.7+简化语法:asyncio.run(),自动创建和关闭事件循环
    asyncio.run(main())

3.2 核心组件详解

(1)aiohttp.ClientSession:共享会话的重要性

ClientSession是 aiohttp 的核心对象,整个爬虫仅需创建一次,所有请求共享该会话:

  • 底层复用 TCP 连接,避免为每个请求创建新连接的资源开销;
  • 统一管理 Cookie、请求头、超时配置,简化代码;
  • 支持连接池,提升高并发下的请求效率。

注意:必须使用async with管理会话,确保任务完成后自动释放资源。

(2)asyncio.Semaphore:并发数的关键控制

异步爬虫的最大坑 是:无限制发起并发请求,会导致目标服务器拒绝连接(429/503)、本地端口耗尽,甚至 IP 被封。asyncio.Semaphore(N)的作用是限制同时执行的任务数为N,通过async with semaphore实现自动加锁 / 释放 ,无需手动调用acquire()release(),简洁且安全。并发数建议:根据目标网站的反爬策略调整,一般建议 10-50,反爬严格的网站可设置为 5-10。

(3)asyncio.gather:批量任务的并发执行

asyncio.gather(*tasks, return_exceptions=True)是批量执行异步任务的核心方法:

  • *tasks:解包任务列表,将多个任务传入;
  • return_exceptions=True:核心参数,某个任务抛出异常时,不会终止整个爬虫,而是将异常对象作为结果返回,保证其他任务正常执行;
  • 返回值:与任务列表顺序一致的结果列表,方便后续统一处理。

四、关键注意事项:避免异步爬虫踩坑

4.1 禁用同步 IO 操作

异步编程的核心原则事件循环中不能有同步阻塞的 IO 操作 (如requests.get()time.sleep()open()的同步读取、数据库的同步连接)。这些操作会阻塞整个事件循环,导致异步爬虫退化为 "同步爬虫",完全丧失高并发优势。替代方案

  • 网络请求:用aiohttp替代requests
  • 延时操作:用asyncio.sleep()替代time.sleep()
  • 文件操作:用aiofiles替代原生open()
  • 数据库操作:用asyncpg(PostgreSQL)、aiomysql(MySQL)替代同步数据库驱动。

4.2 全局共享 ClientSession,禁止重复创建

错误做法 :在fetch函数内部每次创建ClientSession

python

运行

复制代码
# 错误!每次请求创建新会话,资源开销大,无连接复用
async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            pass

正确做法 :在主函数main中创建一次ClientSession,通过参数传递给fetch函数,所有请求共享该会话(如示例代码所示)。

4.3 所有异步操作必须加 await

aiohttp 的所有 IO 相关方法(session.get()response.json()response.text())、asyncio 的方法(asyncio.sleep()asyncio.gather())都是协程对象 ,必须在前面加await才能执行,否则仅会创建协程对象,不会实际运行。典型错误

python

运行

复制代码
data = response.json()  # 错误!未加await,仅返回协程对象,无实际数据
data = await response.json()  # 正确!异步解析响应

五、进阶优化:让爬虫更稳定、更高效

5.1 增加请求重试机制

网络波动、临时的服务器错误(500/502)会导致请求失败,增加重试机制可提升爬取成功率:

python

运行

复制代码
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

# 为fetch函数增加重试:最多3次,指数退避等待(1s→2s→4s),仅对ClientError重试
@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=4),
    retry=retry_if_exception_type(aiohttp.ClientError),
    before_sleep=lambda retry_state: logger.info(f"重试:{retry_state.fn.__name__},第{retry_state.attempt_number}次")
)
async def fetch(session: aiohttp.ClientSession, url: str) -> Dict:
    # 原有fetch逻辑不变
    pass

需安装依赖:pip install tenacity

5.2 随机延时与请求头轮换

针对有反爬的网站,在请求之间增加随机异步延时 ,并轮换User-AgentReferer等请求头,模拟真实浏览器行为:

python

运行

复制代码
# 1. 定义请求头池
USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2) Firefox/121.0 Safari/537.36",
    "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) Safari/605.1.15"
]

# 2. 带随机延时的bounded_fetch
async def bounded_fetch(url: str):
    async with semaphore:
        # 随机延时0.5-2秒(异步延时,不阻塞事件循环)
        await asyncio.sleep(random.uniform(0.5, 2))
        # 随机选择User-Agent
        headers = CONFIG["HEADERS"].copy()
        headers["User-Agent"] = random.choice(USER_AGENTS)
        return await fetch(session, url, headers)  # 传递新的请求头

5.3 分布式异步爬虫

当爬取量达到百万级甚至千万级时,单台机器的性能有限,可结合Redis实现分布式异步爬虫

  1. 用 Redis 的列表(List)作为任务队列,存储所有待爬取的 URL;
  2. 多台机器(或多个进程)运行相同的异步爬虫代码,从 Redis 队列中取 URL 爬取;
  3. 用 Redis 的集合(Set)做去重,避免重复爬取;
  4. 每台机器内部仍通过Semaphore控制单进程并发数。核心依赖:aioredis(异步 Redis 客户端),替代同步的redis-py

5.4 数据持久化的异步优化

避免用原生open()同步写入数据(阻塞事件循环),使用aiofiles实现异步文件操作,或用异步数据库驱动将数据写入数据库:

python

运行

复制代码
import aiofiles

# 异步保存结果到JSON文件
async def save_results(results: List[Dict]):
    async with aiofiles.open("crawl_results.json", "w", encoding="utf-8") as f:
        import json
        await f.write(json.dumps(results, ensure_ascii=False, indent=2))

# 在main函数中调用
await save_results(results)

六、同步爬虫 vs 异步爬虫:效率对比

以爬取 100 个网络链接为例,对比 ** 同步爬虫(requests)异步爬虫(aiohttp+asyncio)** 的耗时(测试环境:网络延迟约 100ms,单进程):

爬虫类型 并发数 总耗时 核心问题
同步爬虫(requests) 1(串行) 约 10 秒 全程阻塞,CPU 空等
异步爬虫(aiohttp) 20 约 0.8 秒 无阻塞,资源利用率最大化

结论:在 IO 密集型的爬虫场景中,异步爬虫的效率远高于同步爬虫,且并发数越高,效率提升越明显。

七、总结

本文详细讲解了基于aiohttp+asyncio的高并发异步爬虫的实现原理和完整流程,核心要点可总结为 3 个核心、4 个注意、5 个优化:

核心要点

  1. 异步核心:asyncio的事件循环实现任务调度,aiohttp实现异步 HTTP 请求,两者结合突破同步阻塞的效率瓶颈;
  2. 资源复用:全局共享aiohttp.ClientSession,复用 TCP 连接,减少资源开销;
  3. 并发控制:通过asyncio.Semaphore严格限制最大并发数,避免被反爬或资源耗尽。

核心注意

  1. 禁用所有同步 IO 操作,替换为对应的异步库;
  2. 仅创建一次ClientSession,全局共享;
  3. 所有异步协程方法必须加await才能执行;
  4. asyncio.gather(return_exceptions=True)保证爬虫的健壮性。

进阶优化

  1. 增加重试机制,提升爬取成功率;
  2. 随机延时 + 请求头轮换,规避反爬;
  3. aiofiles/ 异步数据库实现异步数据持久化;
  4. 结合 Redis 实现分布式异步爬虫,处理海量任务;
  5. 增加日志和监控,方便问题排查。

aiohttp+asyncio是 Python 实现高并发网络爬虫的最优组合之一,掌握其核心思想和使用技巧,不仅能打造高性能爬虫,还能将异步编程思维应用到其他 IO 密集型场景(如异步接口、消息队列消费等),大幅提升程序的运行效率。

相关推荐
2301_790300968 小时前
Python单元测试(unittest)实战指南
jvm·数据库·python
Data_Journal8 小时前
Scrapy vs. Crawlee —— 哪个更好?!
运维·人工智能·爬虫·媒体·社媒营销
VCR__8 小时前
python第三次作业
开发语言·python
韩立学长8 小时前
【开题答辩实录分享】以《助农信息发布系统设计与实现》为例进行选题答辩实录分享
python·web
深蓝电商API9 小时前
async/await与多进程结合的混合爬虫架构
爬虫·架构
2401_838472519 小时前
使用Scikit-learn构建你的第一个机器学习模型
jvm·数据库·python
u0109272719 小时前
使用Python进行网络设备自动配置
jvm·数据库·python
工程师老罗9 小时前
优化器、反向传播、损失函数之间是什么关系,Pytorch中如何使用和设置?
人工智能·pytorch·python
Fleshy数模9 小时前
我的第一只Python爬虫:从Requests库到爬取整站新书
开发语言·爬虫·python
CoLiuRs9 小时前
Image-to-3D — 让 2D 图片跃然立体*
python·3d·flask