小豬的Python學習之旅 —— 12.Python併發之queue模組

coder-pig發表於2018-02-09

一句話概括本文

本節對queue.py模組進行了詳細的講解,寫了一個實戰例子: 多執行緒抓取半次元Cos頻道的所有今日熱門圖片,最後分析了 一波模組的原始碼,瞭解他的實現套路。

大蕾姆鎮樓

小豬的Python學習之旅 —— 12.Python併發之queue模組


引言

本來是準備寫multiprocessing程式模組的,然後呢,白天的時候隨手 想寫一個爬半次元COS頻道小姐姐的指令碼,接著呢,就遇到了一個令人 非常困擾的問題:國內免費的高匿代理ip都被玩壞了(很多站點都鎖了), 幾千個裡可能就十個不到能用的,對於這種情況,有一種應付的策略 就是:寫While True死迴圈,一直換代理ip直到能拿到資料為止。 但是,假如是我們之前的那種單執行緒的話,需要等待非常久的時間, 想想一個個代理去試,然後哪怕你設定了5s的超時,也得花上不少 時間,而你抓取的網頁不止一個的話,這個時間就不是一般的長了, 這個時候不用多執行緒還等什麼?我們可以把要請求的頁面都丟到 一個容器裡,然後加鎖,然後新建頁面數量 x 訪問執行緒,然後每個 執行緒領取一個訪問任務,然後各自執行任訪問,直到全部訪問完畢, 最後反饋完成資訊。在學完threading模組後,相信你第一個想到的 會是條件變數Contition,acquire對集合加鎖,取出一枚頁面連結, notify喚醒一枚執行緒,然後release鎖,接著重複這個操作,直到集合 裡的不再有元素為止,大概套路就是這樣,如果你有興趣可以自己 試著去寫下,在Python的**queue模組**裡已經實現了一個執行緒安全的 多生產者,多消費者佇列,自帶鎖,多執行緒併發資料交換必備。


1.語法簡介:

內建三種型別的佇列

  • Queue:FIFO(先進先出);
  • LifoQueue:LIFO(後進先出);
  • PriorityQueue:優先順序最小的先出;

建構函式的話,都是(maxsize=0),設定佇列的容量,如果 設定的maxsize小於1,則表示佇列的長度無限長

兩個異常

Queue.Empty:當呼叫非堵塞的get()獲取空佇列元素時會引發; Queue.Full:當呼叫非堵塞的put()滿佇列裡新增元素時會引發;

相關函式

  • qsize():返回佇列的近似大小,注意:qsize()> 0不保證隨後的get()不會 阻塞也不保證qsize() < maxsize後的put()不會堵塞;
  • empty():判斷佇列是否為空,返回布林值,如果返回True,不保證後續 呼叫put()不會阻塞,同理,返回False也不保證get()呼叫不會被阻塞;
  • full():判斷佇列是否滿,返回布林值如果返回True,不保證後續 呼叫get()不會阻塞,同理,返回False也不保證put()呼叫不會被阻塞;
  • put(item, block=True, timeout=None):往佇列中放入元素,如果block 為True且timeout引數為None(預設),為堵塞型put(),如果timeout是 正數,會堵塞timeout時間並引發Queue.Full異常,如果block為False則 為非堵塞put()
  • put_nowait(item):等價於put(item, False),非堵塞put()
  • get(block=True, timeout=None):移除一個佇列元素,並返回該元素, 如果block為True表示堵塞函式,block = False為非堵塞函式,如果設定 了timeout,堵塞時最多堵塞超過多少秒,如果這段時間內沒有可用的 項,會引發Queue.Empty異常,如果為非堵塞狀態,有資料可用返回資料 無資料立即丟擲Queue.Empty異常;
  • get_nowait():等價於get(False),非堵塞get()
  • task_done():完成一項工作後,呼叫該方法向佇列傳送一個完成訊號,任務-1;
  • join():等佇列為空,再執行別的操作;

官方給出的多執行緒例子

def worker():
    while True:
        item = q.get()
        if item is None:
            break
        do_work(item)
        q.task_done()

q = queue.Queue()
threads = []
for i in range(num_worker_threads):
    t = threading.Thread(target=worker)
    t.start()
    threads.append(t)

for item in source():
    q.put(item)

# block until all tasks are done
q.join()

# stop workers
for i in range(num_worker_threads):
    q.put(None)
for t in threads:
    t.join()
複製程式碼

關於文件的解讀大概就這些了,還是比較簡單的,接下來實戰 寫個用到Queue佇列的多執行緒爬蟲例子~


2.Queue實戰:多執行緒抓取半次元Cos頻道的所有今日熱門圖片


1.分析環節


抓取源bcy.net/coser/toppo…

小豬的Python學習之旅 —— 12.Python併發之queue模組

拉到底部(中途載入了更多圖片,猜測又是ajax):

小豬的Python學習之旅 —— 12.Python併發之queue模組

嗯,直接是日期耶,應該是請求引數裡的一個,F12開啟開發者模式,Network 抓包開起來,隨手點開個02月08日,看下開啟新連結的相關資訊:

小豬的Python學習之旅 —— 12.Python併發之queue模組

開啟目錄結構看看,要找的元素都在這裡,數了下30個:

小豬的Python學習之旅 —— 12.Python併發之queue模組

不然得出這樣的抓包資訊:

抓取地址:https://bcy.net/coser/toppost100 請求方式Get 請求引數: type(固定):lastday date:20180208

清理一波,然後滾動下,抓下載入更多的那個介面:

小豬的Python學習之旅 —— 12.Python併發之queue模組

同樣是Ajax載入技術,不過資料不是Json,直接就是XML,點選Preview看下:

小豬的Python學習之旅 —— 12.Python併發之queue模組

好傢伙,果然是XML,然後不難看出**<li class="_box">**包著的就是 一個元素,搜了下有20個,就是每次載入20個咯,算一算每日最熱 每天的圖片就是30+20 = 50個咯,整理下抓包資訊:

抓取地址:https://bcy.net/coser/index/ajaxloadtoppost 請求方式Post 請求引數: p(固定):1 type(固定):lastday date:20180207

嗯,兩個要抓的介面都一清二楚了,然後就是獲得日期的範圍了, 這個就要自己慢慢試了,二分查詢套路,慢慢縮減範圍,知道得 出日期的前一天和日期內容相同,日期的後一天與內容不同為止, 這裡直接給出起始時間:20150918,開始抓的時間就是這個, 截止時間就是今天,比如:2018.02.09

分析完畢,接下來就一步步寫程式碼了~


2.程式碼實現環節


  • 1.定義獲取兩個日期間所有日期列表的函式

比較簡單,利用datetime模組格式化下日期,弄個迴圈,輕鬆完成;

小豬的Python學習之旅 —— 12.Python併發之queue模組

  • 2.定義抓取今日熱門預設載入部分的函式

小豬的Python學習之旅 —— 12.Python併發之queue模組

簡單介紹下,cpn是我自己寫的一個模組,**get_dx_proxy_ip()隨機獲取 一個大象代理的代理ip,接著的get_bs()**則是獲取一個BeautifulSoup物件, write_str_data()是往檔案裡追加一串字串的函式。最後還把異常給列印 出來了,執行下就知道了,這個是非常頻繁的,threading.current_thread() 獲得當前執行緒,只是方便排查,如果不想列印任何東西,這裡直接改成pass就 可以了。另外,使用Θ分隔圖片名與下載連結(因為還沒學到資料庫那裡,暫時 就先寫txt裡...)

  • 3.定義抓取今日熱門載入更多的函式

小豬的Python學習之旅 —— 12.Python併發之queue模組

和2類似...

  • 4.定義一個抓取執行緒類

小豬的Python學習之旅 —— 12.Python併發之queue模組

繼承threading.Thread類,__init__建構函式傳入一個執行函式, 重寫run函式,在此處呼叫傳入的執行函式。

  • 5.定義任務佇列,把日期引數傳入

小豬的Python學習之旅 —— 12.Python併發之queue模組

  • 6.定義執行緒執行的函式

小豬的Python學習之旅 —— 12.Python併發之queue模組

迴圈,如果佇列不為空,從裡面取出一枚資料,執行兩個抓資料 的函式,執行完畢後,呼叫queue物件的task_done()通知數目-1;

  • 7.開闢執行緒執行任務

小豬的Python學習之旅 —— 12.Python併發之queue模組

這裡就是建立了和任務佇列一樣數目的執行緒,呼叫daemon=True是為了 避免因為執行緒死鎖或者堵塞,然後程式無法停止的情況,保證當程式只 剩下主執行緒時能夠正常退出。

執行截圖

小豬的Python學習之旅 —— 12.Python併發之queue模組

是的,這種HTTPSConnectionPool的異常就是那麼頻發,代理ip問題,不是 你程式的原因,開啟bcycos_url.xml,驗證下資料有沒有問題:

小豬的Python學習之旅 —— 12.Python併發之queue模組

(PS:這裡有些重複是網站本來就重複,一開始還以為是我程式出錯... 還有,這裡沒有抓取所有的,只抓了:20150918到20150930的,資料多得一批...)

  • 8.定義下載圖片的函式

小豬的Python學習之旅 —— 12.Python併發之queue模組

就是處理字串,獲得下載連結,還有圖片名的拼接而已~

  • 9.定義下載圖片程式執行的函式

小豬的Python學習之旅 —— 12.Python併發之queue模組

  • 10.新建下載佇列,開啟執行緒

小豬的Python學習之旅 —— 12.Python併發之queue模組

執行截圖

小豬的Python學習之旅 —— 12.Python併發之queue模組

可以開啟輸入目錄驗證下:

小豬的Python學習之旅 —— 12.Python併發之queue模組

使用Queue編寫一個多執行緒爬蟲就是那麼簡單~ 接下來會摳下Queue的原始碼,有興趣的可以繼續看,沒興趣的話直接跳過即可~


*3.queue模組原始碼解析

直接點進去queue.py,原始碼只有249行,還好,看下原始碼結構

小豬的Python學習之旅 —— 12.Python併發之queue模組

點開兩個異常,非常簡單,繼承Exception而已,我們更關注**__all__**

小豬的Python學習之旅 —— 12.Python併發之queue模組

1)all

all在模組級別暴露公共介面,比如在導庫的時候不建議寫 *from xxx import ,因為會把xxx模組裡所有非下劃線開頭的成員都 引入到當前名稱空間中,可能會汙染當前名稱空間。如果顯式宣告瞭 all,import * 就只會匯入 all 列出的成員。 (不建議使用:**from xxx import *** 這種語法!!!)

接著看下Queue類結構,老規矩,先擼下**init**方法

小豬的Python學習之旅 —— 12.Python併發之queue模組

文件註釋裡寫了:建立一個maxsize大小的佇列,如果<=0,佇列大小是無窮的。 設定了maxsize,然後呼叫self._init(maxsize),點進去看下:

小豬的Python學習之旅 —— 12.Python併發之queue模組

這個deque是什麼?

2)deque類

其實是collections模組提供的雙端佇列,可以從佇列頭部快速 增加和取出物件,對應兩個方法:popleft()與appendleft(), 時間複雜度只有O(1),相比起**list物件的insert(0,v)和pop(0)**的 時間複雜度為O(N),列表元素越多,元素進出耗時會越長!

回到原始碼,接著還定義了: mutex:threading.Lock(),定義一個互斥鎖 not_empty = threading.Condition(self.mutex):定義一個非空的條件變數 not_full = threading.Condition(self.mutex):定義一個非滿的條件變數 all_tasks_done = threading.Condition(self.mutex):定義一個任務都完成的條件變數 unfinished_tasks = 0:初始化未完成的任務數量為0

接著到**task_done()**方法:

小豬的Python學習之旅 —— 12.Python併發之queue模組

with加鎖,未完成任務數量-1,判斷未完成的任務數量, 小於0,丟擲異常:task_done呼叫次數過多,等於0則喚醒 所有等待執行緒,修改未完成任務數量;

再接著到**join()**方法:

小豬的Python學習之旅 —— 12.Python併發之queue模組

with加鎖,如果還有未完成的任務,wait堵塞呼叫者程式; 接下來是qsize,empty和full函式,with加鎖返回大小而已:

小豬的Python學習之旅 —— 12.Python併發之queue模組

接著是**put()**函式:

小豬的Python學習之旅 —— 12.Python併發之queue模組

with加鎖,判斷maxsize是否大於0,上面也講了maxsize<=0代表 佇列是可以無限擴充套件的,那就不存在佇列滿的情況,maxsize<=0 的話直接就往佇列裡放元素就可以了,同時未完成任務數+1,隨機 喚醒等待執行緒。

如果maxsize大於0代表有固定容量,就會出現佇列滿的情況,就需要 進行細分了:

  • 1.block為False:非堵塞佇列,判斷當前大小是否大於等於容量,是,丟擲Full異常;
  • 2.block為True,沒設定超時:堵塞佇列,判斷當前大小是否大於等於容量, 是,堵塞執行緒;
  • 3.block為True,超時時間<0:直接丟擲ValueError異常,超時時間應為非負數;
  • 4.block為True,超時時間>=0,沒倒時間堵塞執行緒,到時間丟擲Full異常;

再接著是get()函式,和put()類似,只是丟擲的異常為:Empty

小豬的Python學習之旅 —— 12.Python併發之queue模組

這兩個就不用說了,非堵塞put()和get(),最後就是操作雙端佇列的方法而已;

小豬的Python學習之旅 —— 12.Python併發之queue模組

另外兩種型別的佇列也非常簡單,繼承Queue類,然後重寫對應的四個 方法而已~

小豬的Python學習之旅 —— 12.Python併發之queue模組

3)heapq模組

PriorityQueue優先順序隊裡的heappush()和heappop()是heapq模組 提供的兩個方法,heap佇列q佇列,堆一般可看做是一棵樹的 陣列物件(二叉樹堆),規則如下: 某個節點的值總是不大於或不小於其孩子節點的值 然後又分最大堆和最小堆:

小豬的Python學習之旅 —— 12.Python併發之queue模組

(這裡大概知道是二叉樹就好了,筆者資料結構也學的比較爛...)

利用:heappush()可以把資料放到堆裡,會自動按照二叉樹的結構進行儲存; 利用:heappop(heap):從heap堆中刪除最小元素,並返回,heap再按完全二叉樹規範重排;

queue.py模組大概的流程就是這個樣子咯,總結下套路把:

關鍵點核心:三個條件變數

not_empty:get的時候,佇列空或在超時時間內,堵塞讀取執行緒,非空喚醒讀取執行緒; not_full:put的時候,佇列滿或在超時時間內,堵塞寫入執行緒,非滿喚醒寫入執行緒; all_tasks_done:未完成任務unfinished_tasks不為0的時候堵塞呼叫佇列的執行緒, 未完成任務不為0時喚醒所有呼叫佇列的執行緒;

大概就這樣~


4.小結

本節把queue模組個擼了一遍,不止是熟悉API,還把原始碼給擼了, 擼原始碼感覺就是在一件件脫妹子的衣服一樣,每次總能發現新大陸~ 嘿嘿,挺好玩的,就說那麼多吧~

(PS:Coser的質量真是參差不齊,大部分是靠的化妝和濾鏡,我還是喜歡素顏 小姐姐還有萌大奶~,最後來個辣眼睛的Coser給你洗洗眼。O(∩_∩)O)

小豬的Python學習之旅 —— 12.Python併發之queue模組


本節原始碼下載

github.com/coder-pig/R…


來啊,Py交易啊

想加群一起學習Py的可以加下,智障機器人小Pig,驗證資訊裡包含: PythonpythonpyPy加群交易屁眼 中的一個關鍵詞即可通過;

小豬的Python學習之旅 —— 12.Python併發之queue模組

驗證通過後回覆 加群 即可獲得加群連結(不要把機器人玩壞了!!!)~~~ 歡迎各種像我一樣的Py初學者,Py大神加入,一起愉快地交流學♂習,van♂轉py。

小豬的Python學習之旅 —— 12.Python併發之queue模組


相關文章