1. 程式人生 > >python爬蟲——多執行緒+協程(threading+gevent)

python爬蟲——多執行緒+協程(threading+gevent)

以下摘自這篇文章:https://blog.csdn.net/qq_23926575/article/details/76375337

在爬蟲中廣泛運用的多執行緒+協程的解決方案,親測可提高效率至少十倍以上。
本文既然提到了執行緒和協程,我覺得有必要在此對程序、執行緒、協程做一個簡單的對比,瞭解這三個程之間的區別。
以下摘自這篇文章:http://www.cnblogs.com/guokaixin/p/6041237.html

1、程序
程序是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,程序是系統進行資源分配和排程的一個獨立單位。每個程序都有自己的獨立記憶體空間,不同程序通過程序間通訊來通訊。由於程序比較重量,佔據獨立的記憶體,所以上下文程序間的切換開銷(棧、暫存器、虛擬記憶體、檔案控制代碼等)比較大,但相對比較穩定安全。
2、執行緒
執行緒是程序的一個實體,是CPU排程和分派的基本單位,它是比程序更小的能獨立執行的基本單位.執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程序的其他的執行緒共享程序所擁有的全部資源。執行緒間通訊主要通過共享記憶體,上下文切換很快,資源開銷較少,但相比程序不夠穩定容易丟失資料。
3、協程
協程是一種使用者態的輕量級執行緒,協程的排程完全由使用者控制。協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧,直接操作棧則基本沒有核心切換的開銷,可以不加鎖的訪問全域性變數,所以上下文的切換非常快。
簡單來說,協程相比執行緒來說有一個優勢,就是在協程間切換時不需要很大的資源開銷。在使用時可以開多個程序,然後每個程序開多個執行緒,每個執行緒開多個協程來綜合使用。

from bs4 import BeautifulSoup
import datetime
import sys
import threading
reload(sys)
sys.setdefaultencoding('utf-8')
import gevent.monkey
gevent.monkey.patch_all()   
import socket
socket.setdefaulttimeout(10)
path = sys.path[0] + '/data/'

多執行緒可以使用的包一般有兩個:Thread和threading,threading更強大和常用一點,可以利用threading.Thread來自定義多執行緒類。gevent為python下的協程包。
本篇例項場景與上一篇相同,依舊為爬取外文資料庫,可參考

http://blog.csdn.net/qq_23926575/article/details/76375042

def main():
    """將任務切割,開啟多執行緒"""
    listf = open(path + 'urllist.txt', 'r')
    urllist = listf.readlines()
    length = len(urllist)
    print length
    queList = []
    threadNum = 6  #執行緒數量
    #將urllist按照執行緒數目進行切割
    for i in range(threadNum)
: que = []#Queue.Queue() left = i * (length//threadNum) if (i+1)*(length//threadNum)<length: right = (i+1) * (length//threadNum) else: right = length for url in urllist[left:right]: que.append(url.strip()) queList.append(que) threadList = [] for i in range(threadNum): threadList.append(threadDownload(queList[i])) for thread in threadList: thread.start() #啟動執行緒 for thread in threadList: thread.join() #這句是必須的,否則執行緒還沒開始執行就結束了

其中threadDownload是自定義的執行緒類,傳入引數為url列表。在這個執行緒類中開啟多個協程。

class threadDownload(threading.Thread):
    """使用threading.Thread初始化自定義類"""
    def __init__(self, que):
        threading.Thread.__init__(self)
        self.que = que
    def run(self):               
        length = len(self.que)
        coroutineNum = 20  #協程數量
        for i in range(coroutineNum):
            jobs = []
            left = i * (length//coroutineNum)
            if (i+1)*(length//coroutineNum)<length:            
                right = (i+1) * (length//coroutineNum)
            else:
                right = length
            for url in self.que[left:right]:
                jobs.append(gevent.spawn(getThesis, url))
            gevent.joinall(jobs)

在上述程式碼中我開啟了6個執行緒,並且在每個執行緒中開啟了20個協程,因為需要抓取的資料量較大,故對資料進行了切割。其實也可以使用quene佇列的方式來實現,多個協程共用同一個佇列資料,但是管理起來稍微麻煩一點。
我在執行的過程中發現有這樣幾個問題,執行速度很快,是多程序的十幾倍,但是抓很多資料時會抓取失敗,報各種錯誤,最常見的是too many open files和new connection failed之類的錯誤,應該是每個協程都獲得了一個檔案的控制代碼,所以你可能只打開了幾個檔案,但是系統會認為你開啟了很多,網上找了解決方案(有一個是修改ulimit,即系統設定的最大開啟檔案數量,在ubuntu下輸入ulimit -n得到的1024是系統預設的,可以通過ulimit - n 5000修改為5000,但是也沒能解決問題),但是都沒有很好的方法能避免這類問題,希望懂的高手能夠告知一下。
多執行緒+協程的方法效率高,但是很不穩定,會出現很多錯誤,所以在編寫程式碼的過程中,需要做一些錯誤的處理,使程式更加robust,在抓取一些連結出問題的時候能夠不掛掉繼續抓取其他頁面。建議將出錯的url儲存到一個檔案內,最後再對這些url進行抓取。

我的urllist檔案中其實有20萬條資料,但是全部一次性執行會掛掉,所以我是每次讀取4萬條記錄,然後6個執行緒,每個執行緒分別20個協程進行抓取,大概1小時搞定。

以上。有問題歡迎評論交流,如有錯誤也歡迎指出。