Python3結合Sciter編寫桌面程式第三節

ZeronoFreya發表於2019-02-16

第三節 協程!?

繼續…基礎框架搭好了,下面來正式的來一個專案吧

behance

全球設計師的作品展示平臺

就從這拉幾張圖吧,具體的網頁解析方式網上有很多,在此略過,我已經取出了一些圖片地址,儲存在了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操作

相對一個單純的爬蟲指令碼來說,還是有點複雜的,尤其是互動,難怪這麼多人不願意寫介面…

雖然還有不足,但本次專案的內容就到這了。

謝謝。

相關文章