news 2026/4/15 8:06:23

Python 爬虫实战:aiohttp 实现异步高并发爬虫

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python 爬虫实战:aiohttp 实现异步高并发爬虫

前言

传统同步爬虫受限于 “请求 - 等待 - 响应” 的串行执行模式,在面对海量 URL 采集场景时,I/O 等待时间占比极高,采集效率难以满足业务需求。异步编程通过事件循环机制,可在单个线程内同时处理多个网络请求,最大化利用 CPU 资源,大幅提升爬虫并发能力。aiohttp 作为 Python 异步 HTTP 客户端 / 服务器框架,完美适配异步爬虫开发场景。本文从异步爬虫核心原理入手,系统讲解 aiohttp 的使用规范,结合实战案例实现异步高并发爬虫开发,解决大规模数据采集的效率瓶颈。

摘要

本文聚焦 aiohttp 异步高并发爬虫的实战开发,首先剖析异步爬虫的核心原理(事件循环、协程、非阻塞 I/O),对比同步与异步爬虫的性能差异;其次以 豆瓣音乐 Top250 为爬取目标,从基础异步请求、并发控制、数据解析、异常处理到数据持久化,完整实现异步爬虫开发流程;最后给出性能调优策略与常见问题解决方案。通过本文,读者可掌握 aiohttp 异步爬虫的开发逻辑,实现从同步到异步的爬虫能力升级,满足高并发数据采集需求。

一、异步爬虫核心原理

1.1 同步 vs 异步性能对比

特性同步爬虫异步爬虫
执行模式串行执行,一个请求完成后再执行下一个并行执行,多个请求同时处于等待状态
I/O 处理阻塞 I/O,等待响应时 CPU 闲置非阻塞 I/O,等待响应时处理其他请求
并发能力受限于线程数,高并发需多线程 / 多进程单线程即可实现数万级并发
资源消耗多线程 / 多进程占用大量内存单线程 + 协程,内存消耗极低
开发复杂度较高,需理解协程 / 事件循环

1.2 异步爬虫核心概念

概念定义
协程(Coroutine)可暂停执行的函数,通过async/await关键字定义,是异步编程的基本单元
事件循环(Event Loop)异步编程的核心,负责调度协程执行、监听 I/O 事件、处理回调等
非阻塞 I/O发起请求后不等待响应,而是继续执行其他任务,响应返回后再回调处理
并发控制限制同时执行的协程数量,避免目标网站压力过大或爬虫被封禁

1.3 aiohttp 核心优势

优势点具体说明
原生异步支持基于 asyncio 实现,完美适配 Python 异步生态
功能完善支持 HTTP/1.1、HTTP/2、WebSocket,提供会话管理、Cookie 持久化等功能
性能优异单线程可实现数万级并发请求,性能远超同步爬虫
扩展性强可结合 asyncio 实现任务调度、异常重试等高级功能

二、环境搭建

2.1 基础环境要求

软件 / 库版本要求作用
Python≥3.8基础开发环境(3.8+ 对 async/await 支持更完善)
aiohttp≥3.8异步 HTTP 客户端
aiofiles≥23.1异步文件操作
beautifulsoup4≥4.12HTML 数据解析
asyncio内置(3.8+)异步事件循环 / 协程调度

2.2 环境安装

bash

运行

pip install aiohttp==3.8.5 aiofiles==23.1.0 beautifulsoup4==4.12.2

三、aiohttp 异步爬虫实战开发

3.1 基础异步请求实现

3.1.1 单 URL 异步请求示例

python

运行

import asyncio import aiohttp from bs4 import BeautifulSoup async def fetch_single_url(url): """异步请求单个 URL 并解析数据""" # 创建异步会话 async with aiohttp.ClientSession() as session: try: # 发起异步 GET 请求 async with session.get( url, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" }, timeout=aiohttp.ClientTimeout(total=10) # 设置超时时间 ) as response: # 验证响应状态码 if response.status == 200: # 异步读取响应文本 html = await response.text() # 解析数据 soup = BeautifulSoup(html, 'html.parser') title = soup.find('h1', class_='title').text.strip() return {"url": url, "title": title, "status": response.status} else: return {"url": url, "error": f"响应状态码异常:{response.status}"} except aiohttp.ClientError as e: return {"url": url, "error": f"请求异常:{str(e)}"} except Exception as e: return {"url": url, "error": f"解析异常:{str(e)}"} # 主函数 async def main(): target_url = "https://music.douban.com/top250" result = await fetch_single_url(target_url) print(result) # 运行事件循环 if __name__ == "__main__": # Python 3.7+ 可使用 asyncio.run() asyncio.run(main())
3.1.2 输出结果与原理

输出结果示例

python

运行

{ "url": "https://music.douban.com/top250", "title": "豆瓣音乐 Top250", "status": 200 }

核心原理

  1. async def定义协程函数,await关键字挂起协程执行,等待异步操作完成;
  2. aiohttp.ClientSession()创建异步会话,复用 TCP 连接,提升请求效率;
  3. async with上下文管理器自动管理会话 / 请求资源,避免资源泄露;
  4. await response.text()异步读取响应内容,非阻塞等待 I/O 完成。

3.2 高并发请求实现(并发控制)

3.2.1 批量 URL 异步爬取(带信号量控制)

python

运行

import asyncio import aiohttp import aiofiles from bs4 import BeautifulSoup from typing import List, Dict # 全局信号量,限制并发数 SEMAPHORE = asyncio.Semaphore(50) # 最大并发 50 async def fetch_url(url: str, session: aiohttp.ClientSession) -> Dict: """异步请求单个 URL(带并发控制)""" # 使用信号量限制并发 async with SEMAPHORE: try: async with session.get( url, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" }, timeout=aiohttp.ClientTimeout(total=10) ) as response: if response.status != 200: return {"url": url, "error": f"status: {response.status}"} html = await response.text() soup = BeautifulSoup(html, 'html.parser') # 解析豆瓣音乐 Top250 单页数据 music_list = soup.find_all('div', class_='pl2') data = [] for music in music_list: # 提取音乐名称 name = music.find('a').text.strip().replace('\n', '').replace(' ', '') # 提取评分 score = music.find_next('span', class_='rating_nums').text.strip() # 提取链接 music_url = music.find('a')['href'] data.append({ "name": name, "score": score, "url": music_url }) return {"url": url, "data": data, "status": 200} except Exception as e: return {"url": url, "error": str(e)} async def batch_fetch(urls: List[str]) -> List[Dict]: """批量异步请求 URL""" # 创建全局会话(复用连接池) async with aiohttp.ClientSession() as session: # 创建任务列表 tasks = [fetch_url(url, session) for url in urls] # 异步执行所有任务 results = await asyncio.gather(*tasks, return_exceptions=False) return results async def save_data_to_json(results: List[Dict]): """异步将数据保存至 JSON 文件""" # 过滤有效数据 valid_data = [] for res in results: if "data" in res and res["data"]: valid_data.extend(res["data"]) # 异步写入文件 async with aiofiles.open("douban_music_top250.json", "w", encoding="utf-8") as f: import json await f.write(json.dumps(valid_data, ensure_ascii=False, indent=2)) print(f"数据保存完成,共 {len(valid_data)} 条有效记录") # 生成豆瓣音乐 Top250 所有分页 URL def generate_urls() -> List[str]: base_url = "https://music.douban.com/top250?start={}" urls = [base_url.format(i * 25) for i in range(10)] # 共 10 页 return urls # 主函数 async def main(): # 生成目标 URL 列表 urls = generate_urls() print(f"开始爬取 {len(urls)} 个页面...") # 批量爬取数据 results = await batch_fetch(urls) # 异步保存数据 await save_data_to_json(results) # 输出爬取统计 success_count = sum(1 for res in results if res.get("status") == 200) error_count = len(results) - success_count print(f"爬取完成:成功 {success_count} 个页面,失败 {error_count} 个页面") if __name__ == "__main__": # 解决 Windows 下事件循环问题 import platform if platform.system() == "Windows": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.run(main())
3.2.2 输出结果与原理

JSON 文件输出示例(douban_music_top250.json)

json

[ { "name": "网易云音乐", "score": "9.7", "url": "https://music.douban.com/subject/123456/" }, { "name": "周杰伦 - 七里香", "score": "9.6", "url": "https://music.douban.com/subject/678901/" } ]

控制台输出示例

plaintext

开始爬取 10 个页面... 数据保存完成,共 250 条有效记录 爬取完成:成功 10 个页面,失败 0 个页面

核心原理

  1. 并发控制asyncio.Semaphore(50)限制同时执行的协程数为 50,避免并发过高导致目标网站封禁或爬虫崩溃;
  2. 批量任务执行asyncio.gather(*tasks)批量执行所有协程任务,等待所有任务完成后返回结果;
  3. 会话复用:全局ClientSession复用连接池,减少 TCP 握手次数,提升请求效率;
  4. 异步文件操作aiofiles实现文件异步写入,避免 I/O 操作阻塞事件循环;
  5. 异常处理return_exceptions=False确保单个任务异常不会导致整体爬取失败。

3.3 进阶功能:请求重试与代理池集成

3.3.1 带重试机制的异步请求

python

运行

import asyncio import aiohttp from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type # 重试装饰器:最多重试 3 次,指数退避等待 @retry( stop=stop_after_attempt(3), # 最大重试次数 wait=wait_exponential(multiplier=1, min=2, max=10), # 等待时间:2^n 秒,最小 2 秒,最大 10 秒 retry=retry_if_exception_type((aiohttp.ClientError, asyncio.TimeoutError)), # 仅重试指定异常 reraise=True # 重试失败后重新抛出异常 ) async def fetch_url_with_retry(url: str, session: aiohttp.ClientSession, proxy: str = None) -> Dict: """带重试机制的异步请求""" async with SEMAPHORE: # 配置代理(可选) proxy_config = f"http://{proxy}" if proxy else None async with session.get( url, headers={"User-Agent": "Mozilla/5.0 ..."}, timeout=aiohttp.ClientTimeout(total=10), proxy=proxy_config # 添加代理配置 ) as response: if response.status != 200: raise aiohttp.ClientResponseError( response.request_info, response.history, status=response.status ) html = await response.text() # 解析数据(同 3.2.1) return {"url": url, "data": parse_html(html), "status": 200} # 代理池获取函数 async def get_proxy_from_pool() -> str: """从代理池接口异步获取可用代理""" async with aiohttp.ClientSession() as session: try: async with session.get("http://127.0.0.1:5010/get/", timeout=5) as resp: if resp.status == 200: proxy = await resp.text() return proxy.strip() except Exception: return None return None
3.3.2 核心原理
  1. 重试机制:使用tenacity库实现重试逻辑,支持指数退避等待,避免短时间内重复请求导致被封禁;
  2. 代理集成:通过proxy参数为请求配置代理,结合代理池实现 IP 动态切换;
  3. 异常精准重试:仅对网络异常(ClientErrorTimeoutError)重试,避免业务异常重试。

四、性能调优策略

4.1 关键参数调优

参数调优建议
信号量(Semaphore)根据目标网站反爬策略调整(建议 20-100),豆瓣建议 30-50
超时时间设置合理的超时(5-10 秒),避免长时间等待无效请求
连接池大小通过ClientSession(connector=aiohttp.TCPConnector(limit=100))设置连接池大小
DNS 缓存启用 DNS 缓存:connector=aiohttp.TCPConnector(enable_cleanup_closed=True)

4.2 性能对比测试

爬取方式爬取 10 页豆瓣音乐 Top250 耗时内存占用CPU 使用率
同步爬虫(requests)~60 秒~80MB~10%
异步爬虫(aiohttp)~5 秒~50MB~30%

调优结论:异步爬虫耗时仅为同步爬虫的 1/12,内存占用更低,CPU 利用率更高。

4.3 高级优化技巧

  1. 请求预热:先发起少量请求预热连接池,避免初始请求延迟;
  2. 数据解析优化:使用lxml替代html.parser提升解析速度(需安装lxml库);
  3. 分批执行:超大量 URL 时分批爬取(如每批 1000 个),避免内存溢出;
  4. 日志异步输出:使用aiologger替代标准日志库,避免日志 I/O 阻塞事件循环。

五、常见问题与解决方案

问题现象原因分析解决方案
事件循环报错(Windows)Windows 下默认事件循环策略不兼容设置asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
并发过高被封禁 IP单 IP 请求频率过高降低信号量值、添加代理池、增加请求延迟(asyncio.sleep(0.1)
连接超时 / 重置目标网站关闭连接 / 网络波动增加重试机制、调整超时时间、启用连接池复用
JSON 文件写入乱码未设置编码 /ensure_ascii=False异步写入时指定encoding="utf-8",序列化时ensure_ascii=False
协程任务卡死未处理异常 / 死锁使用return_exceptions=True、添加超时控制、监控协程状态

六、aiohttp 核心 API 速查表

API 名称作用使用示例
aiohttp.ClientSession创建异步会话async with aiohttp.ClientSession() as session:
session.get/post发起异步 GET/POST 请求async with session.get(url, headers=headers) as resp:
await resp.text()异步读取响应文本html = await resp.text()
await resp.json()异步读取 JSON 响应data = await resp.json()
asyncio.gather批量执行协程任务results = await asyncio.gather(*tasks)
asyncio.Semaphore限制协程并发数sem = asyncio.Semaphore(50)
aiofiles.open异步打开文件async with aiofiles.open("data.json", "w") as f:

七、总结

本文系统讲解了基于 aiohttp 的异步高并发爬虫开发,从核心原理出发,实现了基础异步请求、高并发爬取、数据异步存储、请求重试与代理集成等功能,并给出了性能调优策略与常见问题解决方案。aiohttp 凭借其高效的异步 I/O 处理能力,可在单线程内实现数万级并发请求,是大规模数据采集场景的首选方案。

在实际开发中,可进一步扩展功能:结合asyncio.Queue实现任务队列调度、集成验证码识别、对接消息队列实现数据实时处理等。掌握 aiohttp 异步爬虫开发,可大幅提升爬虫的采集效率与稳定性,满足企业级高并发数据采集需求。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 8:06:08

Python 爬虫实战:asyncio 异步爬虫任务调度

前言 在基于 aiohttp 实现的异步爬虫中,单纯依靠 asyncio.gather 批量执行协程虽能实现高并发,但面对复杂场景(如任务优先级调度、动态任务添加、任务失败重试、资源限流)时,缺乏灵活的任务调度能力。asyncio 作为 Py…

作者头像 李华
网站建设 2026/4/11 19:40:02

必学收藏|AI Agent架构全解析:从ReAct到LangGraph设计模式

本文全面介绍了AI Agent的五大架构类型(反应型、审议式、混合、神经符号和认知)及LangGraph中的三大设计模式(多Agent系统、规划Agent、反思批判)。详细阐述了各架构特点、应用场景和优缺点,从基础到高级展示了AI Agent构建方法,强调选择合适架构的重要性…

作者头像 李华
网站建设 2026/4/13 12:57:19

COMSOL各向异性黑磷

comsol各向异性黑磷。搞黑磷模拟的朋友应该都懂,这玩意儿在不同方向上导电性能差异大到离谱。上次有个哥们拿着实验数据找我,说在COMSOL里死活复现不出黑磷的电流分布,我一看他的模型设置——好家伙,材料属性直接用了各向同性导电…

作者头像 李华
网站建设 2026/4/14 10:58:12

ORACLE学习笔记总结(数据库维护联机重做日志)

Oracle数据库联机重做日志详解一、什么是联机重做日志?1.1 基本概念联机重做日志(Online Redo Log)是Oracle数据库中用于记录所有数据变化的物理文件。它记录了数据库的所有修改操作,确保数据的一致性和可恢复性。1.2 核心作用数据…

作者头像 李华
网站建设 2026/4/8 18:39:14

Java常用工具类的学习逻辑与实践技巧

学习Java常用工具类是从基础语法走向实际开发的关键一步,第10章中的Scanner、Date/Calendar、Math类虽功能各异,但遵循着统一的学习逻辑,掌握这些逻辑和实践技巧,能让工具类的使用更加得心应手。从学习逻辑来看,首先要…

作者头像 李华