一句話概括本文:
本節對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.分析環節
拉到底部(中途載入了更多圖片,猜測又是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)
本節原始碼下載
來啊,Py交易啊
想加群一起學習Py的可以加下,智障機器人小Pig,驗證資訊裡包含: Python,python,py,Py,加群,交易,屁眼 中的一個關鍵詞即可通過;
驗證通過後回覆 加群 即可獲得加群連結(不要把機器人玩壞了!!!)~~~ 歡迎各種像我一樣的Py初學者,Py大神加入,一起愉快地交流學♂習,van♂轉py。