本文希望達到的目標:
- 學習Queue模組
- 將Queue模組與多執行緒程式設計相結合
- 通過Queue和threading模組, 重構爬蟲, 實現多執行緒爬蟲,
- 通過以上學習希望總結出一個通用的多執行緒爬蟲小模版
1. Queue模組
Queue模組實現了多生產者多消費者佇列, 尤其適合多執行緒程式設計.Queue類中實現了所有需要的鎖原語(這句話非常重要), Queue模組實現了三種型別佇列:
- FIFO(先進先出)佇列, 第一加入佇列的任務, 被第一個取出
- LIFO(後進先出)佇列,最後加入佇列的任務, 被第一個取出(操作類似與棧, 總是從棧頂取出, 這個佇列還不清楚內部的實現)
- PriorityQueue(優先順序)佇列, 保持佇列資料有序, 最小值被先取出(在C++中我記得優先順序佇列是可以自己重寫排序規則的, Python不知道可以嗎)
1.1. 類和異常
1 2 3 4 5 6 7 8 9 10 11 12 |
import Queue #類 Queue.Queue(maxsize = 0) #構造一個FIFO佇列,maxsize設定佇列大小的上界, 如果插入資料時, 達到上界會發生阻塞, 直到佇列可以放入資料. 當maxsize小於或者等於0, 表示不限制佇列的大小(預設) Queue.LifoQueue(maxsize = 0) #構造一LIFO佇列,maxsize設定佇列大小的上界, 如果插入資料時, 達到上界會發生阻塞, 直到佇列可以放入資料. 當maxsize小於或者等於0, 表示不限制佇列的大小(預設) Queue.PriorityQueue(maxsize = 0) #構造一個優先順序佇列,,maxsize設定佇列大小的上界, 如果插入資料時, 達到上界會發生阻塞, 直到佇列可以放入資料. 當maxsize小於或者等於0, 表示不限制佇列的大小(預設). 優先順序佇列中, 最小值被最先取出 #異常 Queue.Empty #當呼叫非阻塞的get()獲取空佇列的元素時, 引發異常 Queue.Full #當呼叫非阻塞的put()向滿佇列中新增元素時, 引發異常 |
1.2. Queue物件
三種佇列物件提供公共的方法
1 2 3 4 5 6 7 8 9 10 11 |
Queue.empty() #如果佇列為空, 返回True(注意佇列為空時, 並不能保證呼叫put()不會阻塞); 佇列不空返回False(不空時, 不能保證呼叫get()不會阻塞) Queue.full() #如果佇列為滿, 返回True(不能保證呼叫get()不會阻塞), 如果佇列不滿, 返回False(並不能保證呼叫put()不會阻塞) Queue.put(item[, block[, timeout]]) #向佇列中放入元素, 如果可選引數block為True並且timeout引數為None(預設), 為阻塞型put(). 如果timeout是正數, 會阻塞timeout時間並引發Queue.Full異常. 如果block為False為非阻塞put Queue.put_nowait(item) #等價於put(itme, False) Queue.get([block[, timeout]]) #移除列隊元素並將元素返回, block = True為阻塞函式, block = False為非阻塞函式. 可能返回Queue.Empty異常 Queue.get_nowait() #等價於get(False) Queue.task_done() #在完成一項工作之後,Queue.task_done()函式向任務已經完成的佇列傳送一個訊號 Queue.join() #實際上意味著等到佇列為空,再執行別的操作 |
下面是官方文件給多出的多執行緒模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def worker(): while True: item = q.get() do_work(item) q.task_done() q = Queue() for i in range(num_worker_threads): t = Thread(target=worker) t.daemon = True t.start() for item in source(): q.put(item) q.join() # block until all tasks are done |
2. Queue模組與執行緒相結合
簡單寫了一個Queue和執行緒結合的小程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#!/usr/bin/env python # -*- coding:utf-8 -*- import threading import time import Queue SHARE_Q = Queue.Queue() #構造一個不限制大小的的佇列 _WORKER_THREAD_NUM = 3 #設定執行緒個數 class MyThread(threading.Thread) : def __init__(self, func) : super(MyThread, self).__init__() self.func = func def run(self) : self.func() def worker() : global SHARE_Q while not SHARE_Q.empty(): item = SHARE_Q.get() #獲得任務 print "Processing : ", item time.sleep(1) def main() : global SHARE_Q threads = [] for task in xrange(5) : #向佇列中放入任務 SHARE_Q.put(task) for i in xrange(_WORKER_THREAD_NUM) : thread = MyThread(worker) thread.start() threads.append(thread) for thread in threads : thread.join() if __name__ == '__main__': main() |
3. 重構爬蟲
主要針對之間寫過的豆瓣爬蟲進行重構:
3.1. 豆瓣電影爬蟲重構
通過對Queue和執行緒模型進行改寫, 可以寫出下面的爬蟲程式 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#!/usr/bin/env python # -*- coding:utf-8 -*- # 多執行緒爬取豆瓣Top250的電影名稱 import urllib2, re, string import threading, Queue, time import sys reload(sys) sys.setdefaultencoding('utf8') _DATA = [] FILE_LOCK = threading.Lock() SHARE_Q = Queue.Queue() #構造一個不限制大小的的佇列 _WORKER_THREAD_NUM = 3 #設定執行緒的個數 class MyThread(threading.Thread) : def __init__(self, func) : super(MyThread, self).__init__() #呼叫父類的建構函式 self.func = func #傳入執行緒函式邏輯 def run(self) : self.func() def worker() : global SHARE_Q while not SHARE_Q.empty(): url = SHARE_Q.get() #獲得任務 my_page = get_page(url) #爬取整個網頁的HTML程式碼 find_title(my_page) #獲得當前頁面的電影名 time.sleep(1) SHARE_Q.task_done() |
完整程式碼請檢視Github豆瓣多執行緒爬蟲
完成這個程式後, 又出現了新的問題:
無法保證資料的順序性, 因為執行緒是併發的, 思考的方法是: 設定一個主執行緒進行管理, 然後他們的執行緒工作
4. 通用的多執行緒爬蟲小模版
下面是根據上面的爬蟲做了點小改動後形成的模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
#!/usr/bin/env python # -*- coding:utf-8 -*- import threading import time import Queue SHARE_Q = Queue.Queue() #構造一個不限制大小的的佇列 _WORKER_THREAD_NUM = 3 #設定執行緒的個數 class MyThread(threading.Thread) : """ doc of class Attributess: func: 執行緒函式邏輯 """ def __init__(self, func) : super(MyThread, self).__init__() #呼叫父類的建構函式 self.func = func #傳入執行緒函式邏輯 def run(self) : """ 重寫基類的run方法 """ self.func() def do_something(item) : """ 執行邏輯, 比如抓站 """ print item def worker() : """ 主要用來寫工作邏輯, 只要佇列不空持續處理 佇列為空時, 檢查佇列, 由於Queue中已經包含了wait, notify和鎖, 所以不需要在取任務或者放任務的時候加鎖解鎖 """ global SHARE_Q while True : if not SHARE_Q.empty(): item = SHARE_Q.get() #獲得任務 do_something(item) time.sleep(1) SHARE_Q.task_done() def main() : global SHARE_Q threads = [] #向佇列中放入任務, 真正使用時, 應該設定為可持續的放入任務 for task in xrange(5) : SHARE_Q.put(task) #開啟_WORKER_THREAD_NUM個執行緒 for i in xrange(_WORKER_THREAD_NUM) : thread = MyThread(worker) thread.start() #執行緒開始處理任務 threads.append(thread) for thread in threads : thread.join() #等待所有任務完成 SHARE_Q.join() if __name__ == '__main__': main() |
我感覺其實這個多執行緒挺凌亂的, 希望以後自己能重構
5. 思考更高效的爬蟲方法
- 使用twisted進行非同步IO抓取
- 使用Scrapy框架(Scrapy 使用了 Twisted 非同步網路庫來處理網路通訊)