当前位置: 首页 > news >正文

[爬虫实战] 多进程/多线程/协程-异步爬取豆瓣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.dtcms.com/a/275979.html

相关文章:

  • 小架构step系列12:单元测试
  • 【LeetCode】算法详解#8 ---螺旋矩阵
  • Linux->基础IO
  • 佩戴头盔数据集,5498张图片,平均识别率95.3% 其中戴头盔的图有2348张,支持yolo,coco json, pasical voc xml格式的标注
  • Ansible 入门指南:自动化配置管理核心技术与实战 SELinux 配置
  • day051-ansible循环、判断与jinja2模板
  • Frida绕过SSL Pinning (证书绑定)抓包;Frida注入;app无法抓包问题解决。
  • Spring之【写一个简单的IOC容器EasySpring】
  • 2025年亚太杯(中文赛项)数学建模B题【疾病的预测与大数据分析】原创论文分享
  • UE5多人MOBA+GAS 19、创建升龙技能,以及带力的被动,为升龙技能添加冷却和消耗
  • 3. java 堆和 JVM 内存结构
  • YOLOv8
  • pytables模块安装
  • 【TOOL】ubuntu升级cmake版本
  • 单细胞分析教程 | (二)标准化、特征选择、降为、聚类及可视化
  • STM32用PWM驱动步进电机
  • 快捷跑通ultralytics下的yolo系列
  • 算法第三十一天:贪心算法part05(第八章)
  • 回溯算法-数据结构与算法
  • Pythone第二次作业
  • brpc 介绍与安装
  • Redis过期策略与内存淘汰机制面试笔记
  • 数据库连接池及其核心特点
  • AI编程下的需求规格文档的问题及新规范
  • ADSP-1802这颗ADI的最新DSP应该怎么做开发(一)
  • 【Redis实战】Widnows本地模拟Redis集群的2种方法
  • Syntax Error: TypeError: Cannot set properties of undefined (setting ‘parent‘)
  • Unity URP + XR 自定义 Skybox 在真机变黑问题全解析与解决方案(支持 Pico、Quest 等一体机)
  • Cookie、Session、Token 有什么区别?
  • Spring Boot 中使用 Lombok 进行依赖注入的示例