Python爬虫终极提速异步IOasyncioaiohttp优化比多线程还快4倍在上一篇博文中我们通过多线程ThreadPoolExecutor对单线程小说爬虫进行了优化将爬取速度提升了5~10倍解决了单线程串行执行导致的效率瓶颈。但不少小伙伴在评论区留言多线程已经很快了还有更高效的优化方式吗答案是肯定的——今天这篇我们将引入异步IOasyncio aiohttp实现爬虫的终极提速比多线程再快2~4倍同时降低资源占用让爬取体验更流畅。本文将完全衔接上一篇多线程优化内容从多线程的局限性、异步IO核心原理、异步脚本实战实现、三者速度对比、注意事项五个方面展开全程实战导向每一步都有详细解析保留原有爬取功能和排版格式新手也能直接复制运行。同时我们会对比单线程、多线程、异步IO三种方式的耗时差异让大家直观感受到异步IO的优势真正掌握Python爬虫的进阶优化技巧。一、回顾与思考多线程的局限性在上一篇的实战中多线程确实解决了单线程串行执行的效率问题通过并行发送多个网络请求充分利用了网络等待时间让爬取速度大幅提升。但多线程并非完美它依然存在一定的局限性尤其是在爬取大量章节、高并发场景下这些问题会更加明显。多线程的核心局限性主要有两点线程切换开销虽然多线程能并行执行任务但Python的GIL全局解释器锁导致同一时刻只有一个线程执行Python字节码线程之间的切换需要消耗一定的系统资源。当线程数量过多时切换开销会急剧增加反而会降低爬取效率甚至出现卡顿。资源占用较高每个线程都需要占用一定的内存和CPU资源线程数量越多资源占用越高。如果爬取章节数量极多上千章创建大量线程会导致电脑内存飙升影响其他程序的正常运行。而我们今天要讲的异步IO恰好能解决这两个问题。异步IO不需要创建多个线程而是通过“事件循环”机制在一个线程内同时处理多个网络请求无需线程切换资源占用极低在IO密集型任务如爬虫中效率比多线程更高。举个直观的例子爬取10章小说单线程耗时815秒多线程耗时13秒而异步IO耗时仅需0.3~0.8秒速度提升效果非常显著。接下来我们就详细讲解异步IO的核心原理以及如何将多线程脚本优化为异步版本。二、异步IO核心原理新手易懂版提到异步IO很多新手会觉得“比多线程更复杂”其实核心逻辑和多线程类似都是为了充分利用网络等待时间但实现方式完全不同。我们依然用一个生活化的例子帮大家快速理解假设你是一名厨师需要做10碗面每碗面的流程是“煮面5分钟无需看管 调味1分钟需要看管”。单线程模式煮第一碗面等5分钟→ 调味1分钟→ 煮第二碗面等5分钟→ 调味1分钟… 总耗时60分钟多线程模式找5个助手每个助手负责2碗面同时煮面、同时调味总耗时12分钟异步IO模式你一个人先把10碗面同时放进锅里煮无需看管在煮面的5分钟里你可以同时给已经煮好的面调味煮面和调味并行进行总耗时6分钟。对应到我们的小说爬虫中单线程依次发送请求等待响应再处理内容多线程多个线程同时发送请求等待响应线程切换有开销异步IO一个线程内同时发送多个请求在等待响应的过程中处理其他已响应的请求无需线程切换开销极低。在Python中实现异步IO的核心工具是两个模块asyncioPython内置的异步核心模块负责管理事件循环、任务调度是异步编程的基础aiohttp异步HTTP请求库用于发送异步网络请求替代了多线程中的requests库requests库是同步的无法在异步环境中使用。这里需要特别说明异步IO和多线程一样更适合处理“IO密集型任务”如网络请求、文件读写而不是“CPU密集型任务”。我们的小说爬虫是典型的IO密集型任务因此用异步IO优化能达到比多线程更优的提速效果。三、异步IO脚本优化实现衔接多线程无缝改写本次优化完全基于上一篇的多线程脚本不改变原有核心功能爬取内容、排版格式、文件保存方式仅将同步请求改为异步请求添加异步事件循环逻辑完善异常处理确保大家能无缝衔接直接替换使用。下面我们分步骤讲解优化过程最后给出完整可运行脚本。3.1 新增依赖与导入多线程脚本中我们导入了requests、lxml、os、time、ThreadPoolExecutor五个模块异步IO版本需要替换同步请求库新增异步相关模块具体如下移除requests库同步请求无法用于异步环境、ThreadPoolExecutor多线程相关新增aiohttp异步HTTP请求库、asyncio异步核心模块保留lxml解析HTML、os文件操作、time计时。新增导入代码importaiohttpimportasyncio需要注意aiohttp不是Python内置模块需要手动安装安装命令如下CMD终端执行pipinstallaiohttp lxml-ihttps://pypi.doubanio.com/simple/3.2 重构核心逻辑同步转异步多线程脚本中我们将单个章节的爬取任务拆分为crawl_chapter函数用线程池分配任务异步IO版本则需要将该函数改为异步函数用async/await关键字标记同时将同步的requests.get请求改为异步的aiohttp.ClientSession.get请求。核心优化点函数定义前添加async关键字标记为异步函数用aiohttp.ClientSession替代requests发送异步请求异步请求、获取响应文本时添加await关键字等待任务完成保留原有排版逻辑、文件写入逻辑确保功能一致。异步爬取函数代码# 异步爬取单个章节函数核心优化部分asyncdefcrawl_chapter(session,i):try:# 提取章节链接拼接完整URL与多线程、单线程逻辑一致ai.xpath(./a/href)[0]urlurl_parta# 异步发送请求替换同步的requests.getasyncwithsession.get(url,headersheaders)asresp:# 异步获取响应文本指定编码格式避免乱码htmlawaitresp.text(encodingutf-8)# 解析HTML与多线程、单线程逻辑一致treeetree.HTML(html)# 提取章节标题与原有逻辑一致titletree.xpath(//*[idneirong]/h1/text())chapter_titletitle[0].strip()iftitleelse无标题章节# 提取并优化正文排版与原有逻辑一致text_listtree.xpath(//*[idtxt]//text())# 过滤空文本清理多余空格lines[t.strip()fortintext_listift.strip()]# 段落之间空一行保持小说排版content\n\n.join(lines)full_contentf\n{chapter_title}\n\n{content}\n\n# 写入文件与原有逻辑一致文件写入是同步操作无需异步withopen(fnovel/{chapter_title}.txt,w,encodingutf-8)asf:f.write(full_content)# 打印爬取进度print(f已写入{chapter_title})exceptExceptionase:# 异常处理单个章节失败不影响其他章节print(f爬取失败章节异常{e})3.3 异步主函数事件循环与任务调度异步函数定义完成后需要创建异步主函数负责创建事件循环、管理异步会话、调度异步任务。核心步骤如下定义异步主函数main添加async关键字创建aiohttp.ClientSession对象异步会话用于发送所有异步请求生成所有异步任务将每个章节的爬取任务封装为asyncio任务用asyncio.gather()方法并发执行所有异步任务运行事件循环执行异步主函数。异步主函数代码# 异步主函数管理事件循环和任务asyncdefmain():# 创建异步会话复用会话提升效率asyncwithaiohttp.ClientSession()assession:# 生成所有异步任务将每个章节列表项传递给crawl_chapter函数tasks[crawl_chapter(session,i)foriinchapter_list]# 并发执行所有异步任务等待所有任务完成awaitasyncio.gather(*tasks)这里需要注意aiohttp.ClientSession建议复用不要在每个异步任务中单独创建否则会增加连接开销影响爬取效率。asyncio.gather(*tasks)会并发执行所有任务直到所有任务都完成才会继续执行后续代码。3.4 保留计时功能对比三者耗时为了直观看到异步IO的提速效果我们保留上两篇中的计时逻辑在脚本开始和结束时分别记录时间最后输出总耗时与单线程、多线程的耗时进行对比让大家清晰看到异步IO的优势。计时代码位置与原有脚本一致# 开始计时start_timetime.time()# 中间为核心爬取逻辑异步主函数定义、任务调度等# 运行异步主函数结束计时asyncio.run(main())end_timetime.time()print(f\n【异步IO版】总耗时{end_time-start_time:.2f}秒)3.5 章节列表获取同步即可无需异步需要特别说明获取小说章节列表的操作只需执行一次耗时极短无需异步处理我们依然用同步的方式获取这里可以继续使用requests库也可以用aiohttp异步获取差异不大。本次实战中我们沿用同步方式保持代码简洁。章节列表获取代码与多线程、单线程一致# 同步获取章节列表只需一次无需异步responserequests.get(urlurl_html,headersheaders)response.encodingutf-8treeetree.HTML(response.text)litree.xpath(/html/body/div[1]/div[4]/ul/li)max_chapter10chapter_listli[:max_chapter]# 只取前10章四、完整可运行脚本异步IO版 三者对比为了方便大家直接测试、对比效果下面同时给出“单线程版、多线程版、异步IO版”三个完整脚本大家可以分别运行观察三者的耗时差异直观感受异步IO的终极提速效果。4.1 单线程版衔接前文无修改importrequestsfromlxmlimportetreeimportosimporttime# 开始计时start_timetime.time()# 确保 novel 文件夹存在ifnotos.path.exists(novel):os.mkdir(novel)url_parthttps://www.biquge365.neturl_htmlhttps://www.biquge365.net/newbook/83621/headers{User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/138.0.0.0}responserequests.get(urlurl_html,headersheaders)response.encodingutf-8treeetree.HTML(response.text)litree.xpath(/html/body/div[1]/div[4]/ul/li)chapter_count0max_chapter10foriinli:ifchapter_countmax_chapter:breakai.xpath(./a/href)[0]urlurl_parta responserequests.get(urlurl,headersheaders)response.encodingutf-8treeetree.HTML(response.text)titletree.xpath(//*[idneirong]/h1/text())chapter_titletitle[0].strip()iftitleelsef第{chapter_count1}章text_listtree.xpath(//*[idtxt]//text())lines[t.strip()fortintext_listift.strip()]content\n\n.join(lines)full_contentf\n{chapter_title}\n\n{content}\n\nwithopen(fnovel/{chapter_title}.txt,w,encodingutf-8)asf:f.write(full_content)print(f已写入{chapter_title})chapter_count1# 结束计时end_timetime.time()print(f\n【单线程】总耗时{end_time-start_time:.2f}秒)4.2 多线程版衔接前文无修改importrequestsfromlxmlimportetreeimportosimporttimefromconcurrent.futuresimportThreadPoolExecutor# 开始计时start_timetime.time()# 确保 novel 文件夹存在ifnotos.path.exists(novel):os.mkdir(novel)url_parthttps://www.biquge365.neturl_htmlhttps://www.biquge365.net/newbook/83621/headers{User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/138.0.0.0}# 获取章节列表responserequests.get(urlurl_html,headersheaders)response.encodingutf-8treeetree.HTML(response.text)litree.xpath(/html/body/div[1]/div[4]/ul/li)max_chapter10chapter_listli[:max_chapter]# 定义单个章节的爬取函数defcrawl_chapter(i):try:ai.xpath(./a/href)[0]urlurl_parta responserequests.get(urlurl,headersheaders)response.encodingutf-8treeetree.HTML(response.text)titletree.xpath(//*[idneirong]/h1/text())chapter_titletitle[0].strip()iftitleelse无标题章节text_listtree.xpath(//*[idtxt]//text())lines[t.strip()fortintext_listift.strip()]content\n\n.join(lines)full_contentf\n{chapter_title}\n\n{content}\n\nwithopen(fnovel/{chapter_title}.txt,w,encodingutf-8)asf:f.write(full_content)print(f已写入{chapter_title})exceptExceptionase:print(f爬取失败章节异常{e})# 多线程执行withThreadPoolExecutor(max_workers5)asexecutor:executor.map(crawl_chapter,chapter_list)# 结束计时end_timetime.time()print(f\n【多线程】总耗时{end_time-start_time:.2f}秒)4.3 异步IO版优化后可直接运行importaiohttpimportasynciofromlxmlimportetreeimportosimporttimeimportrequests# 仅用于获取章节列表同步无需异步# 开始计时start_timetime.time()# 确保 novel 文件夹存在与单线程、多线程逻辑一致ifnotos.path.exists(novel):os.mkdir(novel)# 基础配置与单线程、多线程完全一致无需修改url_parthttps://www.biquge365.neturl_htmlhttps://www.biquge365.net/newbook/83621/headers{User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/138.0.0.0}# 同步获取章节列表只需一次无需异步与原有逻辑一致responserequests.get(urlurl_html,headersheaders)response.encodingutf-8treeetree.HTML(response.text)litree.xpath(/html/body/div[1]/div[4]/ul/li)max_chapter10# 只取前10章控制爬取数量与单线程、多线程一致chapter_listli[:max_chapter]# 异步爬取单个章节函数核心优化部分asyncdefcrawl_chapter(session,i):try:ai.xpath(./a/href)[0]urlurl_parta# 异步发送请求替代同步的requests.getasyncwithsession.get(url,headersheaders)asresp:# 异步获取响应文本等待响应完成htmlawaitresp.text(encodingutf-8)treeetree.HTML(html)titletree.xpath(//*[idneirong]/h1/text())chapter_titletitle[0].strip()iftitleelse无标题章节text_listtree.xpath(//*[idtxt]//text())lines[t.strip()fortintext_listift.strip()]content\n\n.join(lines)full_contentf\n{chapter_title}\n\n{content}\n\n# 文件写入为同步操作无需异步withopen(fnovel/{chapter_title}.txt,w,encodingutf-8)asf:f.write(full_content)print(f已写入{chapter_title})exceptExceptionase:print(f爬取失败章节异常{e})# 异步主函数管理事件循环和任务调度asyncdefmain():# 复用异步会话提升爬取效率asyncwithaiohttp.ClientSession()assession:# 生成所有异步任务tasks[crawl_chapter(session,i)foriinchapter_list]# 并发执行所有异步任务awaitasyncio.gather(*tasks)# 运行异步主函数启动事件循环if__name____main__:asyncio.run(main())# 结束计时输出总耗时end_timetime.time()print(f\n【异步IO版】总耗时{end_time-start_time:.2f}秒)五、真实测试结果对比与核心注意事项5.1 三者耗时对比真实测试我们在同一环境Windows10、Python3.9、家用宽带、网络稳定下分别运行三个版本的脚本爬取10章小说测试结果如下数据为多次测试平均值异步IO版耗时从测试结果可以看出异步IO版的耗时仅为单线程的1/15左右比多线程还快2.3倍提速效果非常显著。随着爬取章节数量的增加这个差距会更大——比如爬取100章单线程可能需要1到2分钟多线程需要10到20秒而异步IO仅需要5~8秒效率优势一目了然。提示每个人的网络环境、电脑性能不同耗时会有差异但三者的速度排序始终是异步IO 多线程 单线程。5.2 异步IO爬取核心注意事项异步IO虽然速度快、资源占用低但在使用过程中有几个关键注意事项需要遵守避免脚本报错、被网站反爬确保爬取过程稳定。区分同步与异步避免混用aiohttp是异步请求库不能与requests同步混用在同一个异步任务中文件写入、HTML解析等操作是同步的无需添加await关键字直接执行即可。控制并发数避免被反爬异步IO的并发能力极强默认情况下会同时发送所有请求容易导致并发过高被网站识别为爬虫出现403、503等报错。如果爬取章节数量较多可通过控制任务数量或添加延迟限制并发速度。必须添加异常处理异步任务并发执行时单个章节爬取失败如链接失效、页面结构变化如果没有异常处理会导致整个事件循环崩溃其他章节无法继续爬取。我们在crawl_chapter函数中添加了try-except异常处理确保单个章节失败不影响整体任务。复用异步会话aiohttp.ClientSession建议复用不要在每个异步任务中单独创建否则会增加连接开销降低爬取效率甚至被网站识别为异常请求。注意Python版本asyncio.run()方法是Python3.7才支持的如果你的Python版本低于3.7需要替换为手动创建事件循环后续会讲解兼容方案。遵守网站规则合规爬取与单线程、多线程一样异步IO爬虫仅用于学习和个人研究请勿用于商业用途。爬取时请遵守网站的robots协议不要过度频繁爬取避免给网站服务器造成压力否则可能会被封禁IP。六、进阶思考与后续优化方向本次异步IO优化我们实现了爬虫速度的终极提升但还有很多可优化的方向后续博文会逐步讲解帮助大家进一步完善脚本提升爬取稳定性和效率添加延迟与并发控制通过asyncio.sleep()添加请求延迟或使用信号量Semaphore控制最大并发数避免被网站反爬加入代理IP池如果爬取数量过多IP容易被封禁可加入代理IP池自动切换IP提升爬取稳定性完善异常重试机制对爬取失败的章节添加自动重试逻辑避免因网络波动导致的爬取不完整添加进度条显示集成tqdm模块实现异步任务爬取进度条直观看到爬取进度批量合并章节将所有章节合并为一个完整的txt文件方便离线阅读同时添加章节索引。总结本文衔接上一篇多线程爬虫博文完成了异步IOasyncio aiohttp的终极优化实战核心亮点是“不改变原有功能、仅提升速度”新手也能无缝衔接、直接使用。通过本次优化我们不仅实现了爬虫速度比多线程再快2~4倍还掌握了异步IO的核心原理和使用方法理解了异步IO在IO密集型任务中的优势。整个优化过程分为三步替换同步请求为异步请求、定义异步函数、创建事件循环与任务调度每一步都贴合实战代码可直接复制运行。我们还对比了单线程、多线程、异步IO三种方式的耗时差异让大家直观感受到异步IO的终极提速效果。至此我们已经完成了小说爬虫从单线程到多线程、再到异步IO的完整优化流程从基础到进阶逐步提升爬取效率。如果在运行脚本过程中遇到问题比如异步报错、反爬、速度提升不明显欢迎在评论区留言我会一一解答。记得点赞、收藏、关注后续会分享更多Python爬虫进阶技巧帮助大家从新手成长为爬虫高手