Python網頁爬蟲的執行效率,相信是開發人員在蒐集網頁資料時所追求的,除了可以使用GRequests或Asyncio等套件打造非同步的Python網頁爬蟲外,應用多執行緒(Multithreading)的技巧也是不錯的選擇。
所以本文想來和大家分享程序(Process)與執行緒(Thread)的差別,以及多執行緒(Multithreading)的重要觀念,最後,實際應用在Python網頁爬蟲專案,提升執行效率。
其中的重點包含:- 程序(Process) vs 執行緒(Thread)
- 多執行緒(Multithreading)
- 多執行緒Python網頁爬蟲
一、程序(Process) vs 執行緒(Thread)
程序(Process)簡單來說,就是當我們啟動應用程式時產生的執行實體,需要一定的CPU與記憶體等資源,才有辦法完成工作。舉例來說,在電腦中同時開啟Word與Excel,就會產生兩個程序(Process),彼此互相獨立,不會共用記憶體資源。
而一個程序(Process)則是由多個執行緒(Thread)組成,所以,執行緒(Thread)就是執行工作的基本單位,以使用Word為例,能夠編輯文字與繪製圖表,就是由同一個程序(Process)的不同執行緒(Thread)來完成的,也因此,同一個程序(Process)中的執行緒(Thread)之間,可以共用記憶體資源,反之,則無法共用。
二、多執行緒(Multithreading)
多執行緒(Multithreading)顧名思義,就是在同一個程序(Process)中,建立多個執行緒同時執行任務,藉此來提升效率。
而其中的秘密,就是當主執行緒(Main Thread)閒置或等待時,CPU就會切換到另一個執行緒(Thread)執行其它的工作,完成後再回到主執行緒(Main Thread)繼續往下執行。
換句話說,就是因為多執行緒(Multithreading)能夠在閒置或等待的時候,透過不斷互相切換的動作,來節省執行的時間。
Python3想要實作多執行緒(Multithreading),可以使用內建的threading模組(Module),來建立及執執行緒(Thread),如下範例:
import threading import time def scraper(): print("start") time.sleep(10) print("sleep done") t = threading.Thread(target=scraper) #建立執行緒 t.start() #執行 print("end")
執行結果
start end sleep done
從執行結果可以證明,當執行緒(Thread)執行到第7行時,開始閒置了,這時候,CPU並不會等到10秒睡完,才執行任務,而是會切換到另一個執行緒(Thread)處理其它的任務,也就是印出end,由於之後沒有其它任務了,所以CPU將執行緒(Thread)切換回去,印出sleep done。這邊有三個觀念需要注意:
- 在建立執行緒(Thread)之前,程式碼本身就已經有1個執行緒(Thread)了,所以在以上範例第11、12行建立與執行新的執行緒後,就共有2個執行緒(Thread)。
- Python建立執行緒(Thread)時,需要指定處理的函式(Function),如第11行,所以要建立多執行緒(Multithreading)時,記得將程式碼包裝成函式(Function)。
- 如果函式(Function)有參數的需求,則在建立執行緒(Thread)時,需加入args關鍵字參數(Keyword Argument),透過指定元組(Tuple)的方式來傳入,如下範例第11行:
import threading import time def scraper(x): print("start") time.sleep(x) print("sleep done") t = threading.Thread(target=scraper, args=(3,)) # 建立執行緒 t.start() # 執行 print("end")
三、多執行緒Python網頁爬蟲
瞭解了多執行緒(Multithreading)的基本觀念後,該如何應用到Python網頁爬蟲的實作,來提升執行效率呢?
首先,來看一個爬取INSIDE硬塞的網路趨勢觀察網站1~5頁AI文章標題的Python網頁爬蟲,如下範例:
from bs4 import BeautifulSoup import requests import time # 爬取文章標題 def scrape(urls): for url in urls: response = requests.get(url) soup = BeautifulSoup(response.content, "lxml") # 爬取文章標題 titles = soup.find_all("h3", {"class": "post_title"}) for title in titles: print(title.getText().strip()) time.sleep(2) base_url = "https://www.inside.com.tw/tag/AI" urls = [f"{base_url}?page={page}" for page in range(1, 6)] # 1~5頁的網址清單 start_time = time.time() # 開始時間 scrape(urls) end_time = time.time() print(f"{end_time - start_time} 爬取 {len(urls)} 頁的文章")
執行結果
範例中,第23行利用Python Comprehension語法定義Python網頁爬蟲所要爬取的1~5頁網址清單,接著第25行傳入scrape()函式(Function),透過迴圈爬取每一頁的文章標題,為了避免同時發送過多的請求(Request),所以在第19行每一次的請求(Request)之間暫停(sleep)2秒的時間。此外,前面也有提到程式碼本身只有一個執行緒(Thread),總共花費了15秒的時間。
而要應用多執行緒(Multithreading)的技巧在Python網頁爬蟲上,就需要同時啟動多個執行緒(Thread),這時候,除了可以使用第二節所分享的方法,單獨建立及啟動外,還有一個更簡單的方式,就是使用Python3內建concurrent.futures模組的ThreadPoolExecutor類別,來同時啟動多個執行緒(Thread),如下範例第27行:
from bs4 import BeautifulSoup import concurrent.futures import requests import time def scrape(urls): response = requests.get(urls) soup = BeautifulSoup(response.content, "lxml") # 爬取文章標題 titles = soup.find_all("h3", {"class": "post_title"}) for title in titles: print(title.getText().strip()) time.sleep(2) base_url = "https://www.inside.com.tw/tag/AI" urls = [f"{base_url}?page={page}" for page in range(1, 6)] # 1~5頁的網址清單 start_time = time.time() # 開始時間 # 同時建立及啟用10個執行緒 with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: executor.map(scrape, urls) end_time = time.time() print(f"{end_time - start_time} 秒爬取 {len(urls)} 頁的文章")
執行結果
四、小結
本文帶大家認識什麼是程序(Process)和執行緒(Thread),以及兩者之間的關係,接著,介紹多執行緒(Multithreading)的原理,簡單來說,就是多個執行緒(Thread)在處理任務的過程中,當有執行緒(Thread)閒置時,透過切換不同執行緒(Thread)的動作,來節省等待的時間,也因此,非常適合應用在Python網頁爬蟲的實作,達到非同步處理及提升執行效率。希望這樣的內容能夠幫助大家對於多執行緒(Multithreading)的處理及應用,有基本的認識 :)
如果您喜歡我的文章,請幫我按五下Like(使用Google或Facebook帳號免費註冊),支持我創作教學文章,回饋由LikeCoin基金會出資,完全不會花到錢,感謝大家。
Hi Mike
回覆刪除最後的這兩個範例,time.sleep(2)建議可以刪除,比較能反映真正的爬蟲時間,刪除後我爬的時間分別是,3.59s/1.12s。
因為在多執行緒的範例應該只有最後一次才會真正休息到2秒。反正如果要加速,應該就沒有在顧慮反爬蟲或是伺服器負擔了,所以刪除sleep時間,這樣測速才會在同一基礎上。
感謝分享好文,我有很認真在看唷~
MINI您好,如果要測試最直接的爬取時間,沒錯,time.sleep(2)是可以拿掉的,本文會使用的原因一方面是想讓大家「明顯的感受」一般同步和多執行緒爬取的效率差別,那個停頓與幾乎沒停頓的感覺,相信您在實作的過程中,非常有感吧XD
刪除感謝您的建議和支持,認真觀看文章,讓我有持續的動力分享好文給大家,希望對您有所幫助,也歡迎分享給身邊有在學習Python的朋友 :)
請問一下~本篇的做法和另一篇[整合asyncio與aiohttp打造Python非同步網頁爬蟲]的範例,實作上哪一個會更快速呢??
回覆刪除您好,基本上速度幾乎一樣快,不會差很多,都是非同步的Python網頁爬蟲技巧 :)
刪除那concurrent.futures使用起來簡單多了,asyncio與aiohttp用起來好複雜
刪除是阿~ asyncio與aiohttp較為進階複雜,使用concurrent.futures相對來說較為簡潔,並且可以達到幾乎一樣的執行效率 :)
刪除請問windows環境使用原生python3.7.9+vscode用jupyter跑第一個範例,執行結果好像不完全,只會顯示startend(無分行)程式就結束了,可能是哪裡有問題呢?
回覆刪除