第三节 协程!?
继续…基础框架搭好了,下面来正式的来一个项目吧
全球设计师的作品展示平台
就从这拉几张图吧,具体的网页解析方式网上有很多,在此略过,我已经取出了一些图片地址,保存在了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("\n")
# 设置图片的保存位置
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操作
相对一个单纯的爬虫脚本来说,还是有点复杂的,尤其是交互,难怪这么多人不愿意写界面…
虽然还有不足,但本次项目的内容就到这了。
谢谢。