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

一句話概括本文

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

大蕾姆鎮樓


引言

本來是準備寫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.分析環節


抓取源https://bcy.net/coser/toppost100?type=lastday

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

嗯,直接是日期耶,應該是請求參數裏的一個,F12打開開發者模式,Network
抓包開起來,隨手點開個02月08日,看下打開新鏈接的相關信息:

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

不然得出這樣的抓包信息:

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

清理一波,然後滾動下,抓下加載更多的那個接口:

同樣是Ajax加載技術,不過數據不是Json,直接就是XML,點擊Preview看下:

好傢伙,果然是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模塊格式化下日期,弄個循環,輕鬆完成;

  • 2.定義抓取今日熱門默認加載部分的函數

簡單介紹下,cpn是我自己寫的一個模塊,get_dx_proxy_ip()隨機獲取
一個大象代理的代理ip,接着的get_bs()則是獲取一個BeautifulSoup對象,
write_str_data()是往文件裏追加一串字符串的函數。最後還把異常給打印
出來了,運行下就知道了,這個是非常頻繁的,threading.current_thread()
獲得當前線程,只是方便排查,如果不想打印任何東西,這裏直接改成pass就
可以了。另外,使用Θ分隔圖片名與下載鏈接(因爲還沒學到數據庫那裏,暫時
就先寫txt裏…)

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

和2類似…

  • 4.定義一個抓取線程類

繼承threading.Thread類,init構造函數傳入一個執行函數,
重寫run函數,在此處調用傳入的執行函數。

  • 5.定義任務隊列,把日期參數傳入

  • 6.定義線程執行的函數

循環,如果隊列不爲空,從裏面取出一枚數據,執行兩個抓數據
的函數,執行完畢後,調用queue對象的task_done()通知數目-1;

  • 7.開闢線程執行任務

這裏就是創建了和任務隊列一樣數目的線程,調用daemon=True是爲了
避免因爲線程死鎖或者堵塞,然後程序無法停止的情況,保證當程序只
剩下主線程時能夠正常退出。

運行截圖

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

(PS:這裏有些重複是網站本來就重複,一開始還以爲是我程序出錯…
還有,這裏沒有抓取所有的,只抓了:20150918到20150930的,數據多得一批…)

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

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

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

  • 10.新建下載隊列,開啓線程

運行截圖

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

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


*3.queue模塊源碼解析

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

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

1)_all_

_all_在模塊級別暴露公共接口,比如在導庫的時候不建議寫
from xxx import *,因爲會把xxx模塊裏所有非下劃線開頭的成員
引入到當前命名空間中,可能會污染當前命名空間。如果顯式聲明瞭
_all_,import * 就只會導入 _all_ 列出的成員。
(不建議使用:from xxx import * 這種語法!!!)

接着看下Queue類結構,老規矩,先擼下_init_方法

文檔註釋裏寫了:創建一個maxsize大小的隊列,如果<=0,隊列大小是無窮的。
設置了maxsize,然後調用self._init(maxsize),點進去看下:

這個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()方法:

with加鎖,未完成任務數量-1,判斷未完成的任務數量,
小於0,拋出異常:task_done調用次數過多,等於0則喚醒
所有等待線程,修改未完成任務數量;

再接着到join()方法:

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

接着是put()函數:

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

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

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

3)heapq模塊

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

(這裏大概知道是二叉樹就好了,筆者數據結構也學的比較爛…)

利用: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)


本節源碼下載

https://github.com/coder-pig/ReptileSomething


來啊,Py交易啊

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

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



[36]:

發佈了306 篇原創文章 · 獲贊 1857 · 訪問量 1661萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章