Python多線程在爬蟲中的應用


題記:作為測試工程師經常需要解決測試數據來源的問題,解決思路無非是三種:(1)直接從生產環境拷貝真實數據 (2)從互聯網上爬取數據 (3)自己用腳本或者工具造數據。前段時間,為了獲取更多的測試數據,筆者就做了一個從互聯網上爬取數據的爬蟲程序,雖然功能上基本滿足項目的需求,但是爬取的效率還是不太高。作為一個精益求精的測試工程師,決定研究一下多線程在爬蟲領域的應用,以提高爬蟲的效率。

一、為什么需要多線程
  
  凡事知其然也要知其所以然。在了解多線程的相關知識之前,我們先來看看為什么需要多線程。打個比方吧,你要搬家了,單線程就類似於請了一個搬家工人,他一個人負責打包、搬運、開車、卸貨等一系列操作流程,這個工作效率可想而知是很慢的;而多線程就相當於請了四個搬家工人,甲打包完交給已搬運到車上,然后丙開車送往目的地,最后由丁來卸貨。
  由此可見多線程的好處就是高效、可以充分利用資源,壞處就是各個線程之間要相互協調,否則容易亂套(類似於一個和尚挑水喝、兩個和尚抬水喝、三個和尚沒水喝的窘境)。所以為了提高爬蟲效率,我們在使用多線程時要格外注意多線程的管理問題。

二、多線程的基本知識
  
  進程:由程序,數據集,進程控制塊三部分組成,它是程序在數據集上的一次運行過程。如果同一段程序在某個數據集上運行了兩次,那就是開啟了兩個進程。進程是資源管理的基本單位。在操作系統中,每個進程有一個地址空間,而且默認就有一個控制進程。

  線程:是進程的一個實體,是CPU調度和分派的基本單位,也是最小的執行單位。它的出現降低了上下文切換的消耗,提高了系統的並發性,並克服了一個進程只能干一件事的缺陷。線程由進程來管理,多個線程共享父進程的資源空間。

  進程和線程的關系:
  一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。
  資源分配給進程,同一進程的所有線程共享該進程的所有資源。
  CPU分給線程,即真正在CPU上運行的是線程。

  線程的工作方式:
  如下圖所示,串行指線程一個個地在CPU上執行;並行是在多個CPU上運行多個線程;而並發是一種“偽並行”,一個CPU同一時刻只能執行一個任務,把CPU的時間分片,一個線程只占用一個很短的時間片,然后各個線程輪流,由於時間片很短所以在用戶看來所有線程都是“同時”的。並發也是大多數單CPU多線程的實際運行方式。

  進程的工作狀態:
  一個進程有三種狀態:運行、阻塞、就緒。三種狀態之間的轉換關系如下圖所示:運行態的進程可能由於等待輸入而主動進入阻塞狀態,也可能由於調度程序選擇其他進程而被動進入就緒狀態(一般是分給它的CPU時間到了);阻塞狀態的進程由於等到了有效的輸入而進入就緒狀態;就緒狀態的進程因為調度程序再次選擇了它而再次進入運行狀態。

  
三、多線程通信實例

  還是回到爬蟲的問題上來,我們知道爬取博客文章的時候都是先爬取列表頁,然后根據列表頁的爬取結果再來爬取文章詳情內容。而且列表頁的爬取速度肯定要比詳情頁的爬取速度快。
  這樣的話,我們可以設計線程A負責爬取文章列表頁,線程B、線程C、線程D負責爬取文章詳情。A將列表URL結果放到一個類似全局變量的結構里,線程B、C、D從這個結構里取結果。
  在PYTHON中,有兩個支持多線程的模塊:threading模塊--負責線程的創建、開啟等操作;queque模塊--負責維護那個類似於全局變量的結構。這里還要補充一點:也許有同學會問直接用一個全局變量不就可以了么?干嘛非要用queue?因為全局變量並不是線程安全的,比如說全局變量里(列表類型)只有一個url了,線程B判斷了一下全局變量非空,在還沒有取出該url之前,cpu把時間片給了線程C,線程C將最后一個url取走了,這時cpu時間片又輪到了B,B就會因為在一個空的列表里取數據而報錯。而queue模塊實現了多生產者、多消費者隊列,在放值取值時是線程安全的。

  廢話不多說了,直接上代碼給大伙看看:

import threading # 導入threading模塊
from queue import Queue #導入queue模塊
import time  #導入time模塊

# 爬取文章詳情頁
def get_detail_html(detail_url_list, id):
    while True:
        url = detail_url_list.get() #Queue隊列的get方法用於從隊列中提取元素
        time.sleep(2)  # 延時2s,模擬網絡請求和爬取文章詳情的過程
        print("thread {id}: get {url} detail finished".format(id=id,url=url)) #打印線程id和被爬取了文章內容的url

# 爬取文章列表頁
def get_detail_url(queue):
    for i in range(10000):
        time.sleep(1) # 延時1s,模擬比爬取文章詳情要快
        queue.put("http://testedu.com/{id}".format(id=i))#Queue隊列的put方法用於向Queue隊列中放置元素,由於Queue是先進先出隊列,所以先被Put的URL也就會被先get出來。
        print("get detail url {id} end".format(id=i))#打印出得到了哪些文章的url

#主函數
if __name__ == "__main__":
    detail_url_queue = Queue(maxsize=1000) #用Queue構造一個大小為1000的線程安全的先進先出隊列
    # 先創造四個線程
    thread = threading.Thread(target=get_detail_url, args=(detail_url_queue,)) #A線程負責抓取列表url
    html_thread= []
    for i in range(3):
        thread2 = threading.Thread(target=get_detail_html, args=(detail_url_queue,i))
        html_thread.append(thread2)#B C D 線程抓取文章詳情
    start_time = time.time()
    # 啟動四個線程
    thread.start()
    for i in range(3):
        html_thread[i].start()
    # 等待所有線程結束,thread.join()函數代表子線程完成之前,其父進程一直處於阻塞狀態。
    thread.join()
    for i in range(3):
        html_thread[i].join()

    print("last time: {} s".format(time.time()-start_time))#等ABCD四個線程都結束后,在主進程中計算總爬取時間。

 

  運行結果:


  后記:從運行結果可以看出各個線程之間井然有序地工作着,沒有出現任何報錯和告警的情況。可見使用Queue隊列實現多線程間的通信比直接使用全局變量要安全很多。而且使用多線程比不使用多線程的話,爬取時間上也要少很多,在提高了爬蟲效率的同時也兼顧了線程的安全,可以說在爬取測試數據的過程中是非常實用的一種方式。希望小伙伴們能夠GET到哦!

轉自:https://mp.weixin.qq.com/s/LsRNxAVJywKwEXxo8WuwLw

---------------------------------------------------------------------------------

關注微信公眾號即可在手機上查閱,並可接收更多測試分享~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM