第三節 協程!?
繼續…基礎框架搭好了,下面來正式的來一個專案吧
全球設計師的作品展示平臺
就從這拉幾張圖吧,具體的網頁解析方式網上有很多,在此略過,我已經取出了一些圖片地址,儲存在了list.txt裡,這次就用這些吧。
綜合多種因素,最後選用了協程來下載圖片
即asyncio
框架則用了aiohttp
實現思路:
-
目的
-
將網路上的圖片(最好是縮圖)先下載到本地,記錄圖片資訊,比如ID以便獲得更高質量的圖片,將圖片顯示到介面
-
-
問題
-
為了更快速的展示頁面,我需要同時下載一定數量的圖片…
-
我需要動態的將下載任務傳送給後臺服務…
-
這裡可以在程式啟動的時候設定一個配置列表cfg
from os import path as osPath, getcwd, mkdir
...
def __init__(self):
...
self.cfg = self.initCfg()
def initCfg(self):
cfg = {}
# 代理,沒有可不用設定
# cfg[`proxies`] = `127.0.0.1:61274`
# 載入圖片列表
filename = "list.txt"
if osPath.exists(filename):
with open(filename, "r") as f:
cfg[`picList`] = f.read().strip().split("
")
# 設定圖片的儲存位置
current_folder = getcwd()
cfg[`pic_temp`] = osPath.join( current_folder, `pic_temp`)
if not osPath.isdir( cfg[`pic_temp`] ):
mkdir(cfg[`pic_temp`])
return cfg
然後傳遞給服務程式就可以了
p = Process(target = startServiceP, args = ( self.GuiQueue, self.ServiceQueue, self.cfg ))
先來修改一下html的內容,新增一個自定義控制元件,用來存放圖片:
<section#body>
<button class="click-me">點我下載圖片</button>
<widget id="pic-view"></widget>
</section>
在服務程式ServiceEvent
裡新增一個方法getPicByList()
def getPicByList(self, msg):
# 為圖片建立佔點陣圖
imgidList = self.__creatPlaceholderImg()
for imgid in imgidList:
picHttp = self.cfg[`picList`].pop(0)
file_name = picHttp.split("/")[-1]
file_path = osPath.join( self.cfg[`pic_temp`], file_name )
# 圖片下載完成後需要執行的任務
_GuiRecvMsgDict = {
`fun` : `setImgBg`,
`msg` : {`id`:imgid,`fpath`:file_path}
}
if not osPath.isfile(file_path):
# 將下載任務動態新增到協程迴圈中
self.__run_coroutine_threadsafe(
{`id`: imgid,`http`: picHttp,`fpath`: file_path},
_GuiRecvMsgDict
)
else:
self.__putGui( `setImgBg`, {`id`:imgid,`fpath`:file_path} )
當使用者點選下載圖片的按鈕後會執行到這個方法,為了更好的體驗,在圖片下載之前先為其佔據了空間,可以在其上顯示loading動畫,更重要的一點是,通過它可以控制圖片的顯示順序,因為用協程下載圖片,你無法預知完成的順序…
def __creatPlaceholderImg(self):
# 先建立5個佔點陣圖
html = ``
imgidList = []
time_now = ``
for i in range(0, 5):
time_now = `-`.join( ( str(i), str(time()) ) )
# 儲存圖片的id
imgidList.append( time_now )
html += self.html % ( time_now )
self.__putGui(`creatPlaceholderImg`, html)
return imgidList
之後就到了動態建立協程的部分了
def __run_coroutine_threadsafe(self, data, _GuiRecvMsgDict):
asyncio.run_coroutine_threadsafe(self.dld.stream_download(
data,
_GuiRecvMsgDict
), self.new_loop)
但在正式介紹run_coroutine_threadsafe()
之前,我們需要先開啟一個協程迴圈
但我們已經開啟了一個用於處理佇列的迴圈了,沒辦法再開一個(也不排除是我們太菜),於是另開了一個執行緒專來處理協程
class ServiceEvent(object):
```服務程式```
def __init__(self, _GuiQueue, cfg):
...
# 主執行緒中建立一個事件迴圈
self.new_loop = asyncio.new_event_loop()
self.dld = AsyncioDownload( self.new_loop, self.GuiQueue, self.proxies )
class AsyncioDownload(object):
```使用協程下載圖片```
def __init__(self, loop, _GuiRecvMsg, proxies=None ):
self.GuiRecvMsg = _GuiRecvMsg
self._session = None
self.loop = loop
self.prox = ``.join((`http://`, proxies)) if proxies else proxies
self.timeout = 10
# 啟動一個執行緒,傳遞主執行緒中建立的事件迴圈
t = Thread(target=self.start_loop, args=(self.loop,))
t.setDaemon(True) # 設定子執行緒為守護執行緒
t.start()
def start_loop(self, loop):
# 啟動事件迴圈
asyncio.set_event_loop(loop)
loop.run_forever()
def __session(self):
if self._session is None:
self._session = aiohttp.ClientSession(loop=self.loop)
return self._session
async def stream_download(self, d, _GuiRecvMsgDict):
try:
client = self.__session()
async with client.get( d[`http`], proxy=self.prox, timeout=self.timeout) as response:
if response.status != 200:
print(`error`)
return
# 儲存圖片到本地
if not osPath.isfile(d[`fpath`]):
with open(d[`fpath`], `ab`) as file:
while True:
chunk = await response.content.read(1024)
if not chunk:
break
file.write(chunk)
# 圖片下載完成後告知主執行緒
self.GuiRecvMsg.put(_GuiRecvMsgDict)
except asyncio.TimeoutError:
pass
最後,主程式獲得圖片的id及路徑,顯示到視窗中
function setImgBg( d ){
var div = $(div[imgid="{d.id}"]);
if(div){
div.post( ::this.style#background-image = "url(" + d.fpath + ")" );
}
}
總結:
完成這個專案使用了
-
多程式 —- 後臺服務
-
多執行緒 —- 事件迴圈
-
協程 —- IO操作
相對一個單純的爬蟲指令碼來說,還是有點複雜的,尤其是互動,難怪這麼多人不願意寫介面…
雖然還有不足,但本次專案的內容就到這了。
謝謝。