簡單爬蟲的通用步驟——多線程/多進程爬蟲示例

目錄

 

前言

介紹

多線程基本操作

多進程基本操作

程序示例

總結


前言

很久很久以前,我寫了篇文章《簡單爬蟲的通用步驟》,這篇文章中對於多線程/多進程/分佈式/增量爬蟲沒有具體例子進行解釋,現在來填坑了。

介紹

單線程爬蟲就像是一個人處理一堆事情,沒法同時處理很多事,效率單一。多線程爬蟲可以比作好多人組了一個team來處理這一堆事情。多進程爬蟲可以說是,有多個team(team裏面可能有一個或多個人)來處理一堆事情。大多數情況下,多線程/多進程比單線程效率高得多。多進程相比於多線程,開銷較大,在規模不大的問題處理上,可能多線程比多進程效率高,就好比team內部交流比跨team交流順暢。

多線程基本操作

僅介紹本文中用到的多線程基本操作以供入門,需要更多進階知識,請自行學習。

在python中,建議使用threading高級模塊,而不是_thread模塊。

from threading import Thread
import time


def func1(index):
    print("I'm thread {}".format(index))
    time.sleep(index)
    print("Thread {} end".format(index))


if __name__ == '__main__':
    start_time = time.time()
    thread_list = []
    # 實例化一個線程
    # target是目標函數,記得別加括號
    # args是目標函數的參數,當參數個數僅有一個時,記得後面加上','逗號
    for index in range(1,10):
        thread_list.append(Thread(target=func1, args=(index,)))
    # 用start啓動線程
    for t in thread_list:
        t.start()
    # join表示等待線程執行結束
    for t in thread_list:
        t.join()
    print("用時:{:.2f}s".format(time.time()-start_time))

多線程通信沒什麼值得注意的,用全局變量也行,用類中的公有成員也行。

但要注意一點,多線程共享一個變量時,同時讀寫會造成數據錯誤。多進程會將變量各自複製一份到自己的進程空間,變量不進行共享;但是多線程是會共享同一個變量的。例如,兩個線程同時對一個公共變量讀寫1,000,000次。

from threading import Thread
import time

public_count = 0


def func_decrease():
    global public_count
    for index in range(1000000):
        public_count -= 1


def func_increase():
    global public_count
    for index in range(1000000):
        public_count += 1


if __name__ == '__main__':
    start_time = time.time()
    increase_thread = Thread(target=func_increase)
    decrease_thread = Thread(target=func_decrease)

    increase_thread.start()
    decrease_thread.start()

    increase_thread.join()
    decrease_thread.join()
    print(public_count)

多次執行結果均不相同,且都不爲0。當多個線程對同一變量進行讀寫(尤其是寫操作)時,一定要加鎖。以上程序就變爲:

from threading import Thread, Lock
import time

public_count = 0
lock = Lock() #定義一個鎖

def func_decrease():
    global public_count
    for index in range(1000000):
        lock.acquire() #獲取鎖
        public_count -= 1 #修改
        lock.release() #釋放鎖


def func_increase():
    global public_count
    for index in range(1000000):
        lock.acquire()
        public_count += 1
        lock.release()


if __name__ == '__main__':
    start_time = time.time()
    increase_thread = Thread(target=func_increase)
    decrease_thread = Thread(target=func_decrease)

    increase_thread.start()
    decrease_thread.start()

    increase_thread.join()
    decrease_thread.join()
    print(public_count)

這樣執行結果就爲0 了,加鎖保證了這段代碼的執行完整性,中間不會被打斷。但是同時只有一個線程能獲得鎖,這樣一來保證了數據安全,失去了多線程的優勢。

注意:

使用完一定要調用lock.release()!!!!!!!!!!!避免其他線程等待事件過長造成死鎖。

當多個線程擁有不同的鎖,又等待其他線程釋放其他鎖時,會造成死鎖現象。就像是兩隊人馬交換人質,都喊着讓對面給先放人,自己人回來後再放。這樣一來,多個線程都陷入等待狀態,等待其他線程釋放鎖。這就造成了死鎖。

多進程基本操作

在unix/linux/mac中,使用fork()可以創建;但是在windows上,需要使用multiprocessing模塊。這次主要說multiprocessing。

基本操作與threading類似,使用Process類跟Thread類一樣的用法,同時有多線程通信Queue的使用方法(Pipe的使用方法自行查閱)可以看下面的程序:

from multiprocessing import Process, Queue
import time


def func1(index, q):
    print("I'm Process {}".format(index))
    q.put(index)
    time.sleep(index)
    print("Process {} end".format(index))


if __name__ == '__main__':
    start_time = time.time()
    Process_list = []
    q = Queue()
    # 實例化一個線程
    # target是目標函數,記得別加括號
    # args是目標函數的參數,當參數個數僅有一個時,記得後面加上','逗號
    for index in range(1, 14):
        Process_list.append(Process(target=func1, args=(index, q)))
    # 用start啓動線程
    for t in Process_list:
        t.start()
    # join表示等待線程執行結束
    for t in Process_list:
        t.join()
    for index in range(1,14):
        print(q.get())
    print("用時:{:.2f}s".format(time.time() - start_time))

當需要啓動大量進程時,可以使用進程池Pool類創建子進程。例如:

from multiprocessing import Process, Queue, Pool, Manager
import time


def func1(index, q):
    print("I'm Process {}".format(index))
    q.put(index)
    time.sleep(index)
    print("Process {} end".format(index))


if __name__ == '__main__':
    start_time = time.time()
    Process_list = []
    pool = Pool() #默認是CPU核心數
    q = Manager().Queue()
    # 實例化一個線程
    # target是目標函數,記得別加括號
    # args是目標函數的參數,當參數個數僅有一個時,記得後面加上','逗號
    for index in range(1, 14):
        #apply_async表示異步非阻塞,不用等執行完,隨時根據系統調度來切換
        #pool.apply是阻塞的,等執行完才能執行下一個。
        pool.apply_async(func=func1,args=(index,q))
    #關閉進程池,不允許其他進程再加入
    pool.close()
    #等所有進程執行完
    pool.join()
    for index in range(1,14):
        print(q.get())
    print("用時:{:.2f}s".format(time.time() - start_time))

注意:使用進程池的時候,隊列要用Manager().Queue()而不是Queue(),否則進程無法啓動!!!!!!

程序示例

下面以爬取煎蛋網無聊圖爲例,代碼結構可以參考《簡單爬蟲的通用步驟》中的多線程和多進程部分。

  • 在使用單線程(單線程爬蟲示例)的時候,用時346S,網絡IO不到1Mb/s,沒有充分發揮網絡的性能。
  • 使用多線程(多線程爬蟲示例)的時候,用時68S,網絡IO一直滿負載,CPU/內存等空閒較大。
  • 使用多進程的時候(多進程爬蟲示例),用時78S,網絡IO略有空閒,CPU/內存等空閒較大。

上面提到過,多進程開銷較多線程大,在網絡IO或者磁盤IO密集型的爬蟲中,多進程不一定能體現出優勢。但是在計算密集型程序中,多進程優勢大於多線程(因爲python中有GIL,多線程無法利用多核優勢)。

總結

(寫的比較亂,想到哪兒寫到哪兒)

有上對比結果可以看出,爬蟲類(尤其是下載類爬蟲)計算量不大的話,對網絡性能和磁盤性能要求比較高,當然現在SSD比較普及,磁盤性能不是很大問題。所以,對於爬蟲程序中的下載部分,可以開啓多線程以充分發揮網絡的性能,對於爬蟲程序中的計算部分,可以開啓較少的線程甚至是單線程運行。以達到總體效率最大。

但是有寫爬蟲抓取完數據後需要進行大量運算,這種情況下,需要分配較多的線程給計算部分,較少部分分配給下載部分。以達到總體效率最大。

要做到以上部分,需要預先對爬蟲任務進行分析,確認自己的爬蟲偏重於什麼,性能瓶頸在哪兒。同樣要針對自己的分析,進行合理的程序設計。

大家可以看到以上三個示例的代碼相差不多,下載部分/計算部分/圖片存儲都可以複用,只是需要重新寫一下線程/進程的安排就行了。

python中,多線程和多進程的代碼差距不大,主要在進程通信上面;如果不涉及進程通信問題,甚至不需要改動代碼,剩下的交給python處理。

多線程/多進程/分佈式爬蟲,結構區別不大,就是單個爬蟲的運行環境是線程/進程還是單個主機。還有就是通信方式不同,多線程處於同一進程,通信比較方便,變量都共享;多進程需要跨進程通信,通過使用Queue或Pipe;分佈式通過網絡和redis這種數據庫。

下篇文章實現一個簡單的分佈式爬蟲。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章