
前言在海量多页面公开资讯采集场景中传统多线程爬虫受线程调度开销、系统线程数量上限等因素制约面对上百甚至上千个资讯页面并发抓取时性能提升逐渐遇到瓶颈。协程作为 Python 高并发编程的主流技术依托单线程实现多路 IO 复用无需承担线程创建、切换的系统开销在网络 IO 密集型的爬虫业务中具备得天独厚的优势。asyncio 是 Python 官方提供的异步 IO 标准库也是实现协程爬虫的核心基础结合异步请求组件可构建出吞吐量更高、资源占用更低的资讯采集程序。本文系统讲解基于 asyncio 协程实现多页面公开资讯异步抓取的全流程从底层原理、环境部署、代码实战、调优方案到故障排查逐层展开。文中使用的核心开发库均附上官方访问链接便于开发者查阅文档、版本适配与功能拓展asyncioPython 内置异步 IO 库官方文档aiohttp异步 HTTP 请求库官方文档aiolimiter异步限流库官方文档lxmlHTML/XML 解析库官方文档jsonPython 内置 JSON 处理库官方文档资讯类页面具备链接数量多、单页面请求耗时短、页面结构统一的特征与协程异步模型高度适配。本文结合真实资讯采集业务场景落地可直接运行的工程代码同时拆解每一段代码的运行逻辑与底层机制帮助开发者理解协程爬虫与传统线程爬虫的本质区别掌握异步爬虫的开发、调优与落地能力。一、协程异步爬虫核心理论基础1.1 并发模型横向对比网络爬虫属于典型的 IO 密集型任务程序绝大部分运行时间都处于等待网络响应、数据读写的状态不同并发模型在该场景下的表现差异显著。结合资讯抓取的业务特征将串行、多线程、协程三种主流模型进行全方位对比如下表所示表格对比维度串行单线程多线程线程池asyncio 协程执行载体单个线程多个线程单线程内多个协程切换开销无切换操作操作系统级线程切换开销较高用户态协程切换开销极低资源占用最低仅单线程资源中等线程数量越多占用越高极低单线程承载高并发并发上限受串行逻辑限制并发为 1受系统线程数上限约束理论上限极高仅受网络与目标站点限制IO 利用率极低空闲等待时间长较高多线程并行利用 IO 空闲极高IO 阻塞时自动切换其他协程代码复杂度简单逻辑直观中等需处理线程安全、并发数中等需遵循异步语法规范适用场景少量页面、低时效采集中量级页面采集、混合业务大批量页面、高时效资讯抓取从表格可以明确看出当采集页面数量达到数十页、上百页的多页面资讯场景时协程模型的优势会全面显现。单线程承载数百个协程并发执行不会触发系统线程资源瓶颈同时极小的切换开销能够进一步压缩整体采集耗时。1.2 asyncio 协程核心运行原理协程并非操作系统层面的线程或进程而是由 Python 解释器调度的用户态轻量级执行单元asyncio 库则承担了协程调度、事件循环管理、异步任务编排的核心工作。其核心运行机制可分为四个核心部分第一事件循环。事件循环是 asyncio 的调度核心相当于协程的 “总指挥”。程序启动后会创建唯一的事件循环对象所有协程任务都会注册到事件循环中。事件循环持续轮询所有任务一旦某个协程触发 IO 阻塞如等待网络请求响应事件循环会立刻暂停当前协程切换至其他处于就绪状态的协程继续执行全程无需操作系统介入。第二协程函数与 await 关键字。使用async def定义的函数即为协程函数调用协程函数不会直接执行函数内部逻辑而是返回一个协程对象。await是异步编程的阻塞标记仅能在协程函数内部使用。当代码执行到await语句时当前协程主动让出执行权事件循环调度其他任务直到await对应的异步操作完成再恢复当前协程继续运行。这也是协程实现 IO 多路复用的关键。第三异步任务封装。单纯的协程对象无法被事件循环直接调度需要通过asyncio.create_task()将协程对象封装为异步任务。任务对象会被纳入事件循环的管理队列支持任务状态监听、取消、回调等拓展功能是批量执行多页面抓取任务的基础。第四单线程异步特性。Python 的 GIL 全局解释器锁在协程场景下依然存在但由于协程始终运行在同一个线程中不存在多线程争抢 GIL 的问题。同时网络 IO 属于外部阻塞操作阻塞期间 GIL 会主动释放因此协程可以在单线程下实现真正意义上的高并发。1.3 异步爬虫语法规范与禁忌基于 asyncio 开发爬虫必须严格遵循异步语法规则混用同步代码会直接破坏异步执行逻辑导致性能退化至串行水平。核心规范如下所有网络请求、文件读写、耗时解析等操作必须使用异步版本库禁止在协程内部调用requests、同步文件读写等同步接口协程函数内部的阻塞操作必须使用await修饰未使用await的异步函数会变成 “悬空协程”无法正常执行同步耗时代码如大规模数据计算不建议放入协程中会阻塞整个事件循环造成所有任务停滞批量任务优先使用asyncio.gather()统一编排该方法可并行执行多个异步任务并统一收集所有任务执行结果。1.4 环境依赖安装本文案例所使用的库中asyncio、json为 Python 标准内置库无需额外安装。其余第三方异步库可通过 pip 命令一键安装适配 Python3.7 及以上版本bash运行# 安装异步HTTP请求核心库 pip install aiohttp # 安装异步限流工具库控制并发频率 pip install aiolimiter # 安装HTML解析库 pip install lxml二、多页面资讯异步抓取整体方案设计2.1 业务场景与需求定义本次实战聚焦公开综合资讯多页面采集模拟主流资讯站点业务逻辑具体需求如下站点结构包含资讯列表分页共 20 个分页每个分页包含 20 条资讯摘要单站点总计 400 条资讯采集目标抓取每一条资讯详情页的标题、发布时间、作者、正文内容、标签、原始链接六大字段非功能性要求控制异步并发数量避免高频请求触发站点反爬机制自动捕获异常任务保证整体程序不中断采集完成后统一存储结构化数据。2.2 整体架构分层结合异步编程思想与爬虫工程化设计将整个资讯爬虫划分为五层结构各层级职责独立、解耦性强全局配置层统一管理请求头、并发数量、超时时间、分页范围、基础域名等固定参数便于后期维护与参数调优异步请求层基于 aiohttp 创建异步会话封装通用异步 GET 请求方法统一处理请求超时、连接异常、状态码校验数据解析层接收异步请求返回的页面源码使用 lxml 结合 XPath 完成页面数据提取做数据清洗与格式标准化任务调度层基于 asyncio 事件循环、异步任务、限流组件批量生成分页任务与详情页抓取任务管控整体并发节奏数据持久化层汇总所有异步任务的采集结果完成数据去重、校验最终将结构化数据落地存储。2.3 并发控制策略协程单线程可支持上千级别的并发请求无节制的高并发会直接触发目标站点的 IP 封禁、验证码拦截、请求限流等反爬策略。本次方案采用双层限流机制保障爬虫稳定性全局并发限制使用 aiolimiter 设置全局最大并发数限制同一时间内活跃的异步请求数量资讯类站点推荐并发数设置为 20~50任务间隔控制对分页请求、高频详情页请求添加随机短间隔模拟自然人浏览行为进一步降低被识别为爬虫的概率。三、asyncio 协程资讯爬虫完整代码实战3.1 全量可运行代码python运行import asyncio import aiohttp import random from aiolimiter import AsyncLimiter from lxml import etree import json # 全局配置区 # 模拟浏览器请求头 REQUEST_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept: text/html,text/plain,*/*, Accept-Encoding: gzip, deflate, Accept-Language: zh-CN,zh;q0.9 } # 异步请求超时时间单位秒 REQUEST_TIMEOUT aiohttp.ClientTimeout(total15) # 全局异步限流最大并发40个请求 GLOBAL_LIMITER AsyncLimiter(max_rate40, time_period1) # 资讯站点基础域名 BASE_DOMAIN https://news.example.com # 分页范围第1页至第20页 PAGE_START 1 PAGE_END 20 # 存储最终采集结果 NEWS_RESULT [] # 异步请求封装 async def async_fetch(session: aiohttp.ClientSession, url: str) - str | None: 通用异步请求函数 :param session: aiohttp异步会话对象 :param url: 目标请求链接 :return: 页面HTML源码请求失败返回None async with GLOBAL_LIMITER: try: async with session.get(url, headersREQUEST_HEADERS, timeoutREQUEST_TIMEOUT) as resp: if resp.status ! 200: print(f请求异常链接{url}状态码{resp.status}) return None # 读取页面文本内容指定编码 html await resp.text(encodingutf-8) # 随机休眠0.1~0.3秒模拟人为浏览间隔 await asyncio.sleep(random.uniform(0.1, 0.3)) return html except aiohttp.ClientError as e: print(f网络连接异常 {url}{str(e)}) return None except asyncio.TimeoutError: print(f请求超时 {url}) return None except Exception as e: print(f未知异常 {url}{str(e)}) return None # 页面数据解析 def parse_list_page(html: str) - list: 解析资讯列表分页提取所有详情页链接 :param html: 列表页HTML源码 :return: 详情页URL列表 detail_url_list [] if not html: return detail_url_list tree etree.HTML(html) # XPath匹配资讯条目链接根据实际页面调整表达式 raw_links tree.xpath(//div[classnews-item]/a/href) for link in raw_links: if link.startswith(/): full_link BASE_DOMAIN link else: full_link link detail_url_list.append(full_link) return detail_url_list def parse_detail_page(html: str, url: str) - dict | None: 解析资讯详情页提取核心资讯字段 :param html: 详情页HTML源码 :param url: 详情页链接 :return: 结构化资讯字典解析失败返回None if not html: return None try: tree etree.HTML(html) # 提取各字段设置默认值防止数据缺失报错 news_title tree.xpath(//h1[classnews-title]/text())[0].strip() if tree.xpath(//h1[classnews-title]/text()) else 无标题 news_author tree.xpath(//span[classauthor]/text())[0].strip() if tree.xpath(//span[classauthor]/text()) else 匿名作者 news_time tree.xpath(//span[classpublish-time]/text())[0].strip() if tree.xpath(//span[classpublish-time]/text()) else 未知时间 news_tag ,.join(tree.xpath(//div[classnews-tag]/span/text())).strip() if tree.xpath(//div[classnews-tag]/span/text()) else 无标签 news_content .join(tree.xpath(//div[classnews-content]//text())).strip() if tree.xpath(//div[classnews-content]//text()) else 无正文 news_data { news_url: url, news_title: news_title, news_author: news_author, news_time: news_time, news_tag: news_tag, news_content: news_content } return news_data except Exception as e: print(f详情页解析失败 {url}{str(e)}) return None # 异步任务函数 async def crawl_detail_news(session: aiohttp.ClientSession, detail_url: str): 单条资讯详情页抓取任务 html await async_fetch(session, detail_url) news_data parse_detail_page(html, detail_url) if news_data: NEWS_RESULT.append(news_data) print(f资讯采集完成{news_data[news_title]}) async def crawl_list_page(session: aiohttp.ClientSession, page_num: int) - list: 单个资讯列表分页抓取任务 list_url f{BASE_DOMAIN}/list?page{page_num} print(f开始抓取列表分页第{page_num}页) html await async_fetch(session, list_url) detail_urls parse_list_page(html) print(f第{page_num}页获取到{len(detail_urls)}条资讯链接) return detail_urls async def main(): 程序主入口协程总调度函数 # 创建异步会话全局复用TCP连接提升请求效率 async with aiohttp.ClientSession() as session: # 1. 批量创建列表页任务抓取所有分页的资讯链接 list_tasks [] for page in range(PAGE_START, PAGE_END 1): task asyncio.create_task(crawl_list_page(session, page)) list_tasks.append(task) # 并行执行所有列表页任务汇总全部详情页链接 all_detail_urls await asyncio.gather(*list_tasks) # 扁平化二维列表得到一维链接集合 total_urls [] for url_list in all_detail_urls: total_urls.extend(url_list) print(f全部分页解析完成总计获取{len(total_urls)}条资讯链接) # 2. 批量创建详情页抓取任务 detail_tasks [] for url in total_urls: task asyncio.create_task(crawl_detail_news(session, url)) detail_tasks.append(task) # 并行执行所有详情页抓取任务 await asyncio.gather(*detail_tasks) # 3. 数据持久化保存为JSON文件 with open(资讯采集结果.json, w, encodingutf-8) as f: json.dump(NEWS_RESULT, f, ensure_asciiFalse, indent2) print(f采集任务全部结束共采集有效资讯{len(NEWS_RESULT)}条数据已保存至本地文件) # 程序启动 if __name__ __main__: # 启动事件循环执行主协程 asyncio.run(main())3.2 代码模块逐段原理剖析3.2.1 全局配置模块原理全局配置模块集中管理爬虫运行的核心参数实现配置与业务逻辑解耦是工程化爬虫的基础。REQUEST_HEADERS模拟标准浏览器请求标识规避基础反爬检测aiohttp.ClientTimeout统一设置异步请求超时阈值防止单个请求无限阻塞事件循环AsyncLimiter实例GLOBAL_LIMITER作为全局限流控制器作用于每一次网络请求严格约束单位时间内的并发数量分页参数划定采集范围适配分页类资讯站点的通用结构。所有参数集中定义后期调整并发、超时、采集范围时无需修改业务代码。3.2.2 异步请求函数 async_fetch 原理该函数是整个爬虫的网络请求底层完全基于 aiohttp 异步语法实现。首先通过async with GLOBAL_LIMITER接入限流组件每一次请求都需要先获取限流令牌超出并发上限的请求会自动排队等待从根源上控制请求频率。aiohttp.ClientSession是异步会话对象区别于单次请求全局复用会话可以保留 Cookie、连接池信息复用 TCP 连接大幅减少三次握手的网络开销这也是异步爬虫性能优化的关键点。async with session.get()发起异步 GET 请求await resp.text()异步读取响应内容所有 IO 操作均使用await标记确保阻塞时事件循环可以切换其他协程。函数内部做了分层异常捕获分别处理连接错误、请求超时、未知异常单个链接请求失败不会影响整体任务执行。末尾添加随机短休眠模拟自然人浏览节奏降低爬虫特征。3.2.3 页面解析函数原理parse_list_page与parse_detail_page属于同步解析函数这里需要重点说明lxml 解析 HTML 属于 CPU 轻量级运算耗时极短不会阻塞事件循环因此无需改造为异步函数。函数接收请求返回的 HTML 文本通过etree.HTML构建文档树结合 XPath 语法定位页面元素。代码中对所有 XPath 取值做了判空处理当页面字段缺失、元素结构变更时会赋予默认值避免因索引报错导致程序终止。列表解析负责提取分页内所有资讯详情链接并将相对路径拼接为完整可访问 URL详情页解析完成多字段提取最终封装为字典格式的结构化数据统一存入结果列表。3.2.4 分层异步任务调度原理本案例采用两层任务调度先抓列表页、再抓详情页符合分页资讯站点的抓取逻辑。 第一层任务crawl_list_page遍历所有分页编号通过asyncio.create_task()将每一个分页抓取逻辑封装为独立异步任务存入任务列表。最终通过asyncio.gather(*list_tasks)并行执行全部列表任务gather会等待所有子任务执行完毕并按任务顺序返回结果汇总得到全量资讯详情链接。第二层任务crawl_detail_news基于上一步获取的所有详情链接再次批量创建异步任务并行抓取每一条资讯内容。分层调度的优势在于逻辑清晰先完成链接采集再执行内容抓取便于中间环节的数据校验与异常排查。3.2.5 主函数与事件循环启动原理main函数使用async def定义是整个程序的根协程。在主协程内部创建全局ClientSession保证所有请求复用同一个会话。完成两层异步任务调度后执行数据落地逻辑。程序入口处asyncio.run(main())是 Python3.7 推荐的事件循环启动方式该方法会自动创建、运行、关闭事件循环简化了底层循环操作同时保证资源正常释放。采集完成后使用内置json库将结构化数据写入本地文件完成数据持久化。四、核心技术要点深度解析4.1 aiohttp 会话复用的性能优势在异步爬虫中aiohttp.ClientSession绝对不建议频繁创建和销毁这是新手最容易出现的错误。每一个ClientSession内部维护了异步连接池默认会缓存 TCP 连接。HTTP 请求基于 TCP 协议建立连接需要经过三次握手断开连接需要四次挥手频繁创建会话会反复执行连接创建与销毁消耗大量网络资源。全局仅创建一个ClientSession并贯穿整个爬虫生命周期所有异步请求共用连接池复用已建立的 TCP 通道省去重复握手的耗时。在大批量页面抓取场景下会话复用可将整体采集耗时降低 20%~40%是异步爬虫必须遵循的基础优化规则。4.2 asyncio.create_task 与 asyncio.gather 工作机制asyncio.create_task()的作用是将协程对象转换为可被事件循环调度的任务对象任务创建后会立即加入事件循环队列进入就绪状态等待执行。批量创建任务并不会立刻执行仅完成注册操作。asyncio.gather()接收多个任务对象作为参数核心特性为并行执行、统一等待、有序返回。调用该方法后事件循环会并发执行所有传入的任务直到全部任务执行结束后gather才会返回结果。返回结果的顺序与传入任务的顺序完全一致不会因为任务执行快慢打乱顺序非常适合批量任务的结果汇总。与之对比若使用await串行执行任务协程会退化为串行逻辑彻底丧失并发能力这也是批量任务必须使用gather的原因。4.3 协程爬虫的限流实现方案对比针对资讯站点的反爬防护本文使用aiolimiter实现异步限流除此之外行业内还有两种常用限流方案三种方案对比如下表格限流方案实现方式优点缺点适用场景aiolimiter 组件限流基于异步信号量全局统一管控并发精度高、使用简单、不阻塞事件循环需额外安装第三方库绝大多数异步爬虫、高并发场景手动信号量 asyncio.SemaphorePython 内置异步信号量手动控制并发无需额外依赖、底层可控代码编写略繁琐极简环境、禁止安装第三方库场景固定休眠 time.sleep同步休眠函数强制间隔请求实现最简单阻塞事件循环、并发效率大幅下降极低并发、测试场景在正式生产环境中优先选择aiolimiter兼顾并发效率与限流精度受限环境下可使用asyncio.Semaphore替代同步休眠仅用于临时测试禁止在正式异步爬虫中使用。4.4 同步代码与异步代码混用风险协程的核心是非阻塞 IO 切换一旦在协程内部调用requests、同步文件读写、time.sleep()等同步阻塞代码整个事件循环会被卡住。因为同步阻塞操作不会主动让出执行权事件循环无法切换其他协程所有并发任务都会停滞异步爬虫直接退化为串行执行。针对资讯爬虫场景若需要执行数据统计、复杂解析等同步耗时操作有两种解决方案第一将同步代码放到所有异步任务执行完成后统一运行第二使用asyncio.to_thread()将同步代码放入子线程执行隔离阻塞逻辑保护主线程的事件循环。五、常见问题排查与解决方案5.1 问题一大量请求连接失败、连接被重置现象程序运行后出现大量ClientError异常链接无法正常访问。原因分析目标站点开启连接限制短时间内连接数过多被拒绝网络不稳定请求头缺失关键字段。解决方案下调AsyncLimiter的最大并发数将并发数从 40 降至 20 以内补充完整请求头增加Referer字段模拟页面跳转开启连接池保活配置在创建ClientSession时设置连接超时参数。5.2 问题二程序运行卡死无日志输出也不退出现象爬虫执行到某一阶段后静止既不报错也不继续执行。原因分析存在未设置超时的异步请求单个请求永久阻塞事件循环存在悬空协程未被等待死循环逻辑。解决方案强制为所有异步请求配置ClientTimeout超时时间检查所有协程任务确保全部被await或gather接收排查代码中无限循环逻辑。5.3 问题三采集数据大量缺失解析结果为空现象链接请求成功但解析后字段全部为默认值。原因分析目标页面编码非 UTF-8XPath 表达式失效站点页面结构更新页面为动态渲染内容纯静态 HTML 无数据。解决方案尝试gbk、gb2312等编码格式读取页面通过浏览器开发者工具重新校验并修改 XPath 路径若为动态渲染页面将 aiohttp 替换为 Playwright 等异步无头浏览器组件。5.4 问题四并发数设置越高采集速度反而越慢现象提升协程并发数量后整体耗时不降反升。原因分析并发超出目标站点承载上限站点主动降速、延迟响应本地网络带宽不足高并发造成网络拥堵。解决方案逐步下调并发数测试站点最优并发阈值拆分任务批次分批次执行抓取任务避免一次性发起海量请求。六、协程爬虫性能优化进阶方案6.1 连接池精细化配置默认的aiohttp.ClientSession连接池参数较为保守针对大批量资讯抓取场景可以手动配置连接池参数提升并发承载能力python运行# 自定义连接池参数 connector aiohttp.TCPConnector(limit50, limit_per_host20) async with aiohttp.ClientSession(connectorconnector) as session: # 执行业务逻辑 passlimit设置全局最大连接数limit_per_host设置单个域名最大连接数根据目标站点特性合理配置避免连接池耗尽。6.2 任务分批执行当资讯链接数量超过 1000 条时一次性创建数千个异步任务会造成事件循环队列臃肿。可将链接列表拆分为多个子列表分批次执行gather任务每批次执行完成后再启动下一批降低内存与调度压力。6.3 结果去重与数据校验资讯站点常会出现重复推送、同源资讯的情况在数据落地前增加 URL 去重逻辑基于 URL 做集合去重剔除重复数据。同时增加字段长度校验过滤空标题、空正文等无效数据提升采集数据质量。6.4 日志系统替代打印输出生产环境中使用 Python 内置logging模块替代print语句分级记录信息日志、警告日志、错误日志同时记录请求链接、异常类型、时间戳便于线上问题追溯与运维监控。七、协程爬虫与线程池爬虫选型建议结合前序线程池爬虫与本文协程爬虫的技术特性针对资讯、榜单、商品等不同爬虫业务给出明确的选型标准小规模采集页面数量50线程池与协程性能差距极小优先选择代码更易上手的线程池维护成本更低中大规模采集50页面数量500优先使用 asyncio 协程爬虫更低的资源开销、更快的采集速度优势明显超大规模采集页面数量500协程为基础结合任务分批、分布式架构、代理 IP 池组合使用混合业务IO 任务 大量计算任务选择多线程池计算任务与 IO 任务拆分至不同线程规避协程被同步代码阻塞的问题服务器资源受限场景低配云服务器、嵌入式设备强制使用协程单线程高并发特性可最大化利用有限资源。八、总结asyncio 协程异步模型凭借轻量级、高并发、低开销的核心优势成为多页面公开资讯批量抓取的最优技术方案。本文从理论原理、架构设计、代码实战、问题排查、性能优化多个维度完整落地了一套工程化协程爬虫。协程爬虫的核心精髓在于合理使用异步语法、复用异步会话、精准控制并发频率。在实际开发中开发者需要牢记异步 IO 的运行规则杜绝同步阻塞代码混用结合限流、分批、连接池优化等手段在采集效率与站点反爬规则之间找到平衡。相较于线程池爬虫协程爬虫更适合资讯、新闻、分页列表这类页面数量多、IO 密集的业务场景。掌握 asyncio 协程爬虫技术不仅能够大幅提升数据采集效率同时也为后续学习 aiohttp 专项异步请求、异步队列、分布式异步爬虫等高阶技术打下扎实基础。本套代码可直接适配绝大多数静态资讯站点仅需修改请求 URL 与 XPath 解析规则即可快速完成业务迁移与二次开发。