[爬蟲實戰] 多進程/多線程/協程-異步爬取豆瓣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
英文地址,請注明出處:http://en.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 主從復制 通過 “一主多從”…