[爬虫实战] 多进程/多线程/协程-异步爬取豆瓣Top250

 相关爬虫知识点:[爬虫知识] 深入理解多进程/多线程/协程的异步逻辑

相关爬虫专栏:JS逆向爬虫实战  爬虫知识点合集  爬虫实战案例  逆向知识点合集


前言:

在之前文章中,我们深入探讨了多进程、多线程和协程这三大异步技术的工作原理及其在爬虫中的应用场景。现在,我们将通过一个具体的爬虫实战案例——爬取豆瓣电影 Top 250,来直观对比同步与异步爬取(包括多进程、多线程和协程)的实际效率差异。通过详细的代码示例和运行结果,你将亲身体验到异步化对爬虫性能带来的巨大提升。

一、同步爬取:一步一个脚印

同步爬取是最直观的爬取方式,程序会严格按照代码顺序执行,一个请求完成后才能进行下一个。这意味着在等待网络响应(I/O 操作)时,程序会一直处于阻塞状态,CPU 大部分时间都在空闲等待。对于需要访问多个页面的爬虫来说,这种方式效率极低。

代码实战与讲解

import os.path
import time
from itertools import zip_longestfrom lxml import etree
import requests
from DataRecorder import Recorder# 初始化excel文件
def get_excel():filename = 'top250电影_同步.xlsx'if os.path.exists(filename):os.remove(filename)print('\n旧文件已清除\n')recorder = Recorder(filename)recorder.show_msg = Falsereturn recorderdef get_msg():session = requests.session()recorder = get_excel()total_index = 1# 循环爬取250个数据for j in range(10):# 初始化爬取数据url = f'https://movie.douban.com/top250?start={j*25}&filter='headers = {'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36','referer':'https://movie.douban.com/top250?start=225&filter='}res = session.get(url,headers=headers).texttree = etree.HTML(res)# 获取其中关键数据titles = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]/text()')scores = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[2]/div/span[2]/text()')comments = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[2]/p[2]/span/text()')# zip_longest是防止如有某个数据不存在,无法将该数据组输出的情况for title,score,comment in zip_longest(titles,scores,comments,fillvalue='无'):print(f'{total_index}.电影名:{title},评分:{score},短评:{comment}')map_={'序号':total_index,'电影名':title,'评分':score,'短评':comment}recorder.add_data(map_)recorder.record()total_index+=1if __name__ == '__main__':# 计时start_time = time.time()get_msg()end_time = time.time()use_time = end_time - start_timeprint(f'共用时:{use_time:.2f}秒!') # 取后小数点后两位# 共用时:7.24秒!

分析: 同步爬虫会依次请求每一页,每页请求完成并处理后,才会开始下一页。总耗时累加了所有页面的网络请求时间和数据处理时间,效率最低。

二、多进程爬取:分而治之,并行加速

多进程利用操作系统级别的并行,每个进程拥有独立的内存空间和 Python 解释器。这意味着它们可以真正地同时在多个 CPU 核心上运行,从而规避了 Python GIL 的限制。对于爬虫,我们可以将爬取每一页的任务分配给不同的进程,让它们并行工作,最后再由主进程统一汇总数据。

代码实战与讲解

这里代码逻辑的编写明显不同于之前的同步爬取逻辑。

之前在同步爬取中,我们直接用自己写的for循环十次。但在后面的并发与异步编程中,我们逻辑都需要转换:将这十次for循环分开,并让每次for循环逻辑丢给并发,让并发跑。

因为如果我们直接将原先的大任务拆分成十个小任务的话,它并不能很好的执行,甚至在某些地方会出现混乱(比如原同步爬虫中的写入逻辑),必须重新规划原先的同步逻辑,将其细分

import os.path
import time
from itertools import zip_longestfrom lxml import etree
import requests
from DataRecorder import Recorder
import multiprocessing # 多进程# 初始化excel文件
def get_excel():filename = 'top250电影_多进程.xlsx'if os.path.exists(filename):os.remove(filename)print('\n旧文件已清除\n')recorder = Recorder(filename)recorder.show_msg = Falsereturn recorderdef get_msg(page_index):session = requests.session()# 初始化爬取数据url = f'https://movie.douban.com/top250?start={page_index*25}&filter='headers = {'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36','referer':'https://movie.douban.com/top250?start=225&filter='}res = session.get(url,headers=headers).texttree = etree.HTML(res)# 获取其中关键数据titles = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]/text()')scores = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[2]/div/span[2]/text()')comments = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[2]/p[2]/span/text()')data = []for title,score,comment in zip_longest(titles,scores,comments,fillvalue='无'):# print(f'{total_index}.电影名:{title},评分:{score},短评:{comment}')data.append({'电影名':title,'评分':score,'短评':comment})return dataif __name__ == '__main__':start_time = time.time()recorder = get_excel()# 使用多进程爬取每一页pool = multiprocessing.Pool(processes=5)results = pool.map(get_msg,range(10)) # results为嵌套列表pool.close()pool.join()# 统一处理所有数据并录入total_index = 1for movies in results:for movie in movies:movie['序号'] = total_indexprint(f"{total_index}. 电影名:{movie['电影名']}, 评分:{movie['评分']}, 短评:{movie['短评']}")recorder.add_data(movie)recorder.record()total_index+=1end_time = time.time()use_time = end_time - start_timeprint(f'\n共用时:{use_time:.2f}秒!')# 共用时:6.95秒!

分析: 多进程版本通过将每页的爬取任务分发到不同的进程并行执行,显著减少了总耗时。进程之间的数据独立性保证了爬取和写入的正确性。

三、多线程爬取:并发处理,I/O 高效

多线程在同一个进程内创建多个执行流,它们共享进程的内存。虽然 Python 的 GIL 限制了多线程无法真正并行执行 CPU 密集型任务,但在处理 I/O 密集型任务(如网络请求)时,一个线程在等待网络响应时会释放 GIL,允许其他线程运行。这使得多线程非常适合爬虫场景,能够在等待时并发地发起新的请求。

代码实战与讲解

逻辑思路与之前的多进程大致相同,仅需在原多进程的地方,将其方法改写成多线程即可。

import os.path
import time
from itertools import zip_longestfrom lxml import etree
import requests
from DataRecorder import Recorder
from concurrent.futures import ThreadPoolExecutor # 多线程# 初始化excel文件
def get_excel():filename = 'top250电影_多线程.xlsx'if os.path.exists(filename):os.remove(filename)print('\n旧文件已清除\n')recorder = Recorder(filename)recorder.show_msg = Falsereturn recorderdef get_msg(page_index):session = requests.session()# 初始化爬取数据url = f'https://movie.douban.com/top250?start={page_index*25}&filter='headers = {'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36','referer':'https://movie.douban.com/top250?start=225&filter='}res = session.get(url,headers=headers).texttree = etree.HTML(res)# 获取其中关键数据titles = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]/text()')scores = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[2]/div/span[2]/text()')comments = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[2]/p[2]/span/text()')data = []for title,score,comment in zip_longest(titles,scores,comments,fillvalue='无'):# print(f'{total_index}.电影名:{title},评分:{score},短评:{comment}')data.append({'电影名':title,'评分':score,'短评':comment})return dataif __name__ == '__main__':start_time = time.time()recorder = get_excel()# 创建一个 最多同时运行 5 个线程 的线程池 executor用于并发执行任务。 with ... as ...:用上下文管理器,自动管理线程池的创建和销毁with ThreadPoolExecutor(max_workers=5) as executor:# executor.map(func, iterable) 会为 iterable 中的每个值并发执行一次 funcresults = list(executor.map(get_msg,range(10))) # 嵌套列表# 统一处理所有数据并录入total_index = 1for movies in results:for movie in movies:movie['序号'] = total_indexprint(f"{total_index}. 电影名:{movie['电影名']}, 评分:{movie['评分']}, 短评:{movie['短评']}")recorder.add_data(movie)recorder.record()total_index+=1end_time = time.time()use_time = end_time - start_timeprint(f'\n共用时:{use_time:.2f}秒!')# 共用时:5.79秒!

分析: 多线程版本利用 GIL 在 I/O 阻塞时释放的特性,实现了并发的网络请求,从而缩短了总耗时。相对于多进程,它的资源开销更小,但仍需注意线程安全问题(此处因为每个线程有独立的 requests.session() 且数据返回后统一处理,所以未涉及复杂锁)。

四、协程爬取:极致并发,优雅高效

协程是一种用户态的轻量级并发机制,它不受 GIL 限制。协程的切换由程序主动控制,当遇到 I/O 操作时,协程会主动让出 CPU 控制权,允许其他协程运行。这种协作式多任务的特性,使得协程在处理大量并发 I/O 密集型任务时具有无与伦比的效率和极低的开销。

代码实战与讲解

代码运行逻辑与多进程/多线程也基本相同,但很多细微处需要注意下:

  1. requests库需要替换成aiohttp库,requests本身并不支持异步。
  2. async和 await 的使用

    这是异步 Python 的核心语法。

    1. async def: 任何包含 await 关键字的函数,或者你希望它能被 await 的函数,都必须用 async def 定义,使其成为一个协程函数

    2. await: await 关键字只能在 async def 定义的函数内部使用。它用于等待一个“可等待对象”(如另一个协程、asyncio.sleep()aiohttp 的 I/O 操作等)完成。当 await 遇到 I/O 阻塞时,它会将控制权交还给事件循环,让事件循环去调度其他可执行的协程。

    3. async with: 对于需要上下文管理(如文件的打开、网络会话的建立和关闭)的异步资源,要使用 async with 语句。例如,aiohttp.ClientSessionresponse 对象都应该这样使用:

      async with aiohttp.ClientSession() as session:async with await session.get(url) as response:# ...
  3. 事件循环(Event Loop)的理解与管理

    1. 入口点: 异步程序的入口通常是 asyncio.run(main_async_function())。这个函数会负责创建、运行和关闭事件循环。

    2. 不要手动创建/管理循环(通常情况): 对于简单的异步脚本,避免直接使用 asyncio.get_event_loop()loop.run_until_complete() 等低级 API,asyncio.run() 已经为你处理了这些。

  4. 并发任务的组织

    为了真正实现异步的并发优势,通常需要将多个独立的异步任务组织起来并行执行。

    1. asyncio.gather(): 这是最常用的方法,用于同时运行多个协程,并等待它们全部完成。

      tasks = []
      for url in urls:asyncio.ensure_future(fetch_data(url, session)) # 创建任务tasks.append(task)
      results = await asyncio.gather(*tasks) # 并发执行所有任务
      
    2. asyncio.ensure_future() : 把协程变成一个任务,并交给事件循环去执行。现在一般更推荐用 asyncio.create_task() 来实现这个功能。

以下是协程代码实例:

import os.path
import time
from itertools import zip_longestfrom lxml import etree
from DataRecorder import Recorder
import asyncio
import aiohttp # 协程异步# 初始化excel文件
def get_excel():filename = 'top250电影_协程.xlsx'if os.path.exists(filename):os.remove(filename)print('\n旧文件已清除\n')recorder = Recorder(filename)recorder.show_msg = Falsereturn recorder# 协程获取页面数据
async def get_msg(page_index):# session = requests.session()# 初始化爬取数据url = f'https://movie.douban.com/top250?start={page_index*25}&filter='headers = {'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36','referer':'https://movie.douban.com/top250?start=225&filter='}async with aiohttp.ClientSession() as sess:async with await sess.get(url,headers=headers)as resp:res = await resp.text()tree = etree.HTML(res)# 获取其中关键数据titles = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]/text()')scores = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[2]/div/span[2]/text()')comments = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[2]/p[2]/span/text()')data = []for title,score,comment in zip_longest(titles,scores,comments,fillvalue='无'):# print(f'{total_index}.电影名:{title},评分:{score},短评:{comment}')data.append({'电影名':title,'评分':score,'短评':comment})return data# 主协程函数
async def main():start_time = time.time()recorder = get_excel()# 建立异步请求sessiontasks = []for i in range(10):task = asyncio.ensure_future(get_msg(i))tasks.append(task)results = await asyncio.gather(*tasks)# 统一处理所有数据并录入total_index = 1for movies in results:for movie in movies:movie['序号'] = total_indexprint(f"{total_index}. 电影名:{movie['电影名']}, 评分:{movie['评分']}, 短评:{movie['短评']}")recorder.add_data(movie)recorder.record()total_index+=1end_time = time.time()use_time = end_time - start_timeprint(f'\n共用时:{use_time:.2f}秒!')if __name__ == '__main__':asyncio.run(main())# 共用时:5.23秒!

分析: 协程版本通过 aiohttpasyncio 实现了高效的并发。在 I/O 操作时,协程会主动切换,充分利用等待时间,使得总耗时最短。这是在 Python 中实现高并发 I/O 密集型爬虫的最佳实践。

五、总结与性能对比

通过以上四种爬取方式的实战对比,我们可以清晰地看到异步化带来的性能提升:

爬取方式

平均耗时(秒)

核心原理

优点

缺点/注意点

同步

~7.24

串行执行

编码简单

效率最低,I/O 阻塞严重

多进程

~6.95

真正并行(多 CPU)

规避 GIL,利用多核 CPU,隔离性强

资源开销大,进程间通信复杂

多线程

~5.79

I/O 并发(GIL 释放)

资源开销小,I/O 效率提升显著

受 GIL 限制,线程安全问题

协程

~5.23

I/O 协作式多任务

极高并发,开销小,效率最优

异步传染性,需异步库支持,调试稍复杂

观察结果: 在这个 I/O 密集型的爬虫任务中,协程的性能表现最佳,多线程次之,多进程虽然也能并行但因为进程创建开销略高,效果不如协程和多线程(当然,在极端 CPU 密集型任务中,多进程的优势会更明显)。同步爬取无疑是效率最低的。

实际选择建议:

  • 对于大多数需要高效率的爬虫项目:优先考虑使用 协程(asyncio + aiohttp

  • 如果项目规模较小,或不愿引入异步编程的复杂性多线程 是一个简单有效的提速方案。

  • 当爬虫涉及大量 CPU 密集型任务,或者需要更强的隔离性和稳定性时多进程则是其中的优选。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/88616.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88616.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git系列--1.初始Git

一、背景 目录 一、背景 二、认识 三、如何在Linux上安装Git 3.1检测git是否存在和版本 3.2安装和卸载git 3.2.1Centos 3.2.2Ubuntu 四、基本操作 4.1创建本地仓库 4.2必须的配置项 4.3宏观认识基本分区 我们会根据需求不断更改我们的文件内容,但有时我们会…

QWidget的属性

QWidget的属性 windowOpacityAPI说明windowOpacity()获取不透明数值,返回float,取值为0.0到1.0,其中0.0为全透明,1.0为完全不透明setWindowOpacity()设置控件的不透明数值注意点:窗口不透明度的变化并非精确的&#xf…

【PTA数据结构 | C语言版】后缀表达式求值

本专栏持续输出数据结构题目集,欢迎订阅。 文章目录题目代码题目 请编写程序,求给定的后缀表达式的值。 输入格式: 输入在一行中给出一个非空后缀表达式,其中操作数为 int 型整数,操作符包括加、减、乘、除、取模。各…

装配式建筑4.0:当房子像汽车一样被“智造”

传统建筑方式,如同手工打造艺术品一般,大部分工作依赖现场施工,工人在建筑工地进行混凝土浇筑、砖块堆砌、钢筋绑扎等繁杂工作。这种方式受天气、工人技术水平等因素影响极大,不仅施工周期漫长,质量也参差不齐。据统计…

Go语言生态成熟度分析:为何Go还无法像Java那样实现注解式框架?

近年来,Go语言因其性能高效、部署简单、并发模型优秀等特性,成为云原生与微服务架构中的热门语言。然而,在实际的企业级项目开发中,开发者普遍会发现一个现象:Go的开发效率,尤其在快速构建中大型业务系统时…

oc分类和swift扩展有哪些区别

目录1. 语言环境2. 主要目的3. 核心能力对比4. 关键差异详解4.1. 属性支持4.2. Swift 扩展4.3. 初始化器4.4. 方法冲突与覆盖4.5. 关联类型与泛型5. 设计哲学6. 总结表在 Objective-C 和 Swift 中,分类(Category)和扩展(Extension…

go.work

一般学习一个小东西时,无非两点,1、怎么用? 2、为啥用?在写一个小的项目demo时,忽然看到一个奇怪的东西“go.work”?这是啥?好奇😯,想知道。我是这么问AI的:g…

Kimi K2万亿参数开源模型原理介绍

Kimi K2 技术全解:1T MoE 大模型如何炼成开放智能体 文章目录Kimi K2 技术全解:1T MoE 大模型如何炼成开放智能体1. 模型架构与特点:1 T MoE 的「大」与「省」2. 主要创新点:MuonClip、Agentic RL 与工具调用2.1 MuonClip 优化器&…

【CMake】CMake构建项目入门

一、CMake介绍 CMake 是一个跨平台的自动化构建工具,用于管理软件项目的编译过程。它通过简单的配置文件(CMakeLists.txt)生成特定平台的构建文件(如 Makefile、Visual Studio 项目),让开发者可以专注于代…

贪心算法题解——划分字母区间【LeetCode】

763. 划分字母区间 本题目,“同一字母最多出现在一个片段中”,因为这句话,所以本质上 这道题目属于合并区间 一、算法逻辑(逐步思路) ✅ 目标: 将字符串 s 划分成尽可能多的片段,要求&#xf…

Python----目标检测(使用YOLOV8网络训练人脸)

一、Ultralytics安装 网址:主页 -Ultralytics YOLO 文档 Ultralytics提供了各种安装方法,包括pip、conda和Docker。通过 ultralytics pip包安装最新稳定版本的YOLOv8,或克隆Ultralytics GitHub 存储库以获取最新版本。可以使用Docker在隔离的…

Filament引擎(三) ——引擎渲染流程

通过Filament引擎(二) ——引擎的调用及接口层核心对象的介绍我们知道,要在项目中使用filament,首先我们需要构建出filament的Engine的对象,然后通过filament::Engine对象实例,来构建其他对象,组装渲染场景&#xff0c…

Oracle存储过程导出数据到Excel:全面实现方案详解

技术背景与需求分析 数据导出是企业级应用的核心功能,Oracle存储过程因其高性能执行(减少网络传输)、代码复用性(封装业务逻辑)和事务安全性(ACID保障)成为理想载体。Excel作为使用率$ \geq 95% $的办公工具,其兼容性需求尤为突出。典型场景包括: 财务报表自动生成物…

解决el-table右下角被挡住部分

一部分展示不全&#xff0c;被遮挡&#xff0c;因为 最右边加了fixed"right"<el-table-column fixed"right" label"操作" width"120">解决&#xff1a;1、去除fixed"right"或2、设置样式单页面<style lang"sc…

Waiting for server response 和 Content Download

在浏览器网络调试&#xff08;如 Chrome DevTools 的 Network 面板&#xff09;中&#xff0c;Timing 选项卡下的 Waiting for server response 和 Content Download 是两个关键性能指标&#xff0c;它们分别代表了 HTTP 请求生命周期的不同阶段。以下是详细解释和优化方案&…

《Java Web程序设计》实验报告五 Java Script学习汇报

目 录 一、实验目的 二、实验环境 三、实验步骤和内容 1、小组成员分工&#xff08;共计4人&#xff09; 2、实验方案 3、实验结果与分析 Ⅰ、简述JavaScript的产生过程与Java的关系 Ⅱ、简述JavaScript的特点有哪些 Ⅲ、简述ECMAScript的历史 Ⅳ、简述ECMAScript与J…

C#与FX5U进行Socket通信

实现效果实现步骤&#xff1a;注意&#xff1a;详细的参数这里就不说明了&#xff0c;自己网上搜即可&#xff1b;打开GX Works3 创建FX5U项目系统参数设置PLC的具体型号&#xff08;我有实物PLC&#xff09;设置IP及组态参数添加通讯设备&#xff08;这里PLC做客户端&#xff…

ubuntu20.04基于tensorRT和c++跑yolo11

设备 系统&#xff1a;Ubuntu 20.04 显卡&#xff1a;NVIDIA GeForce RTX 3050 显卡驱动&#xff1a; Driver Version: 535.183.01 CUDA Version: 12.2 关键软件版本总结 Cmake: 3.28.6 Cuda&#xff1a; 12.2.2 Cudnn: 8.9.7 TensorRT: 10.8.0.43 Python&#xff1a;3.10.1…

玖玖NFT数字藏品源码(源码下载)

玖玖NFT数字藏品源码 这套还是很不错的&#xff0c;前端uniapp&#xff0c;后端FastAdmin&#xff0c;对接汇元支付&#xff0c;富友支付&#xff0c;对接avata链&#xff0c;感兴趣的自行下载研究 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/9133…

【Redis-05】高可用方案-主从哨兵

1 概述 高可用&#xff08;High Availability&#xff09;指系统在部分节点故障时仍能持续提供服务的能力。Redis 作为核心缓存组件&#xff0c;主流的高可用方案有主从复制、哨兵模式、集群模式三种。本文介绍主从复制、哨兵模式两种高可用方案。 2 主从复制 通过 “一主多从”…