Python 协程 asyncio 极简入门与爬虫实战
在了解了 Python 并發編程的多線程和多進程之后,我們來了解一下基于 asyncio 的異步IO編程--協程
01
協程簡介
協程(Coroutine)又稱微線程、纖程,協程不是進程或線程,其執行過程類似于 Python 函數調用,Python 的 asyncio 模塊實現的異步IO編程框架中,協程是對使用 async 關鍵字定義的異步函數的調用;
一個進程包含多個線程,類似于一個人體組織有多種細胞在工作,同樣,一個程序可以包含多個協程。多個線程相對獨立,線程的切換受系統控制。同樣,多個協程也相對獨立,但是其切換由程序自己控制。
02
一個簡單例子
我們來使用一個簡單的例子了解協程,首先看看下面的代碼:
import?time def?display(num):time.sleep(1) print(num) for?num?in?range(10):display(num)很容易看得懂,程序會輸出0到9的數字,每隔1秒中輸出一個數字,因此整個程序的執行需要大約10秒 時間。值得注意的是,因為沒有使用多線程或多進程(并發),程序中只有一個執行單元(只有一個線程在 執行),而 time.sleep(1) 的休眠操作會讓整個線程停滯1秒鐘,
對于上面的代碼來說,在這段時間里面 CPU是閑置的沒有做什么事情。
我們再來看看使用協程會發生什么:
import?asyncio async?def?display(num):?#?在函數前使用async關鍵字,變成異步函數?await?asyncio.sleep(1) print(num)異步函數不同于普通函數,調用普通函數會得到返回值,而調用異步函數會得到一個協程對象。我們需要將協程對象放到一個事件循環中才能達到與其他協程對象協作的效果,因為事件循環會負責處理子程 序切換的操作。
簡單的說就是讓阻塞的子程序讓出CPU給可以執行的子程序。
03
基本概念
異步IO是指程序發起一個IO操作(阻塞等待)后,不用等IO操作結束,可以繼續其它操作;做其他事情,當IO操作結束時,會得到通知,然后繼續執行。異步IO編程是實現并發的一種方式,適用于IO密集型任務
Python 模塊 asyncio 提供了一個異步編程框架,全局的流程圖大致如下:
下面對每個函數都從代碼層面進行介紹
async: 定義一個方法(函數),這個方法在后面的調用中不會被立即執行而是返回一個協程對象;
async?def?test():?print('hello?異步') test()?#?調用異步函數輸出:RuntimeWarning: coroutine 'test'?was?never?awaitedcoroutine: 協程對象,也可以將協程對象添加到時間循環中,它會被事件循環調用;
async?def?test():?print('hello?異步') c?=?test()?#?調用異步函數,得到協程對象-->c? print(c)輸出:<coroutine?object?test?at?0x0000023FD05AA360>event_loop: 事件循環,相當于一個無限循環,可以把一些函數添加到這個事件中,函數不會立即執行, 而是滿足某些條件的時候,函數就會被循環執行;
async?def?test():?print('hello?異步') c?=?test()?#?調用異步函數,得到協程對象-->c loop?=?asyncio.get_event_loop()?#?創建事件循環? loop.run_until_complete(c)?#?把協程對象丟給循環,并執行異步函數內部代碼輸出:hello?異步await: 用來掛起阻塞方法的執行;
import?asyncio def?running1():async?def?test1():print('1')await?test2()print('2')async?def?test2():print('3')print('4')loop?=?asyncio.get_event_loop()loop.run_until_complete(test1()) if?__name__?==?'__main__':running1()輸出:
task: 任務,對協程對象的進一步封裝,包含任務的各個狀態;
async?def?test():?print('hello?異步') c?=?test()?#?調用異步函數,得到協程對象-->c loop?=?asyncio.get_event_loop()?#?創建事件循環? task?=?loop.create_task(c)?#?創建task任務? print(task) loop.run_until_complete(task)?#?執行任務輸出: <Task?pending?coro=<test()?running?at?D:?/xxxx.py>>?#?task hello?異步?#?異步函數內部代碼一樣執行future: 代表以后執行或者沒有執行的任務,實際上和task沒有本質區別;這里就不做代碼展示;
首先使用一般方式方法創建一個函數:
def?func(url):?print(f'正在對{url}發起請求:')?print(f'請求{url}成功!') func('www.baidu.com')結果如下所示:
正在對www.baidu.com發起請求: 請求www.baidu.com成功04
基本操作
創建協程對象
通過 async 關鍵字定義一個異步函數,調用異步函數返回一個協程對象。
異步函數就是在函數執行過程中掛起,去執行其他異步函數,等待掛起條件(time.sleep(n))消失后,再回來執行,接著我們來修改上述代碼:
async?def?func(url):?print(f'正在對{url}發起請求:')?print(f'請求{url}成功!') func('www.baidu.com')結果如下:
RuntimeWarning:?coroutine?'func'?was?never?awaited這就是之前提到的,使用async關鍵字使得函數調用得到了一個協程對象,協程不能直接運行,需要把協程 加入到事件循環中,由后者在適當的時候調用協程;
創建task任務對象
task任務對象是對協程對象的進一步封裝;
import?asyncio async?def?func(url):?print(f'正在對{url}發起請求:')?print(f'請求{url}成功!') c?=?func('www.baidu.com')?#?函數調用的寫成對象-->?c loop?=?asyncio.get_event_loop()?#?創建一個時間循環對象? task?=?loop.create_task(c)? loop.run_until_complete(task)?#?注冊加啟動? print(task)結果如下:
正在對www.baidu.com發起請求: 請求www.baidu.com成功! <Task?finished?coro=<func()?done,?defined?at?D:/data_/test.py:10>?result=None>future的使用
前面我們提及到future和task沒有本質區別
async def func(url): print(f'正在對{url}發起請求:') print(f'請求{url}成功!') c = func('www.baidu.com') # 函數調用的寫成對象--> c loop = asyncio.get_event_loop() # 創建一個時間循環對象 future_task = asyncio.ensure_future(c) print(future_task,'未執行') loop.run_until_complete(future_task) # 注冊加啟動 print(future_task,'執行完了')結果如下:
<Task?pending?coro=<func()?running?at?D:/data/test.py:10>>未執行 正在對www.baidu.com發起請求: 請求www.baidu.com成功! <Task?finished?coro=<func()?done,?defined?at?D:/data/test.py:10>?result=None>?執行完了await關鍵字的使用
在異步函數中,可以使用await關鍵字,針對耗時的操作(例如網絡請求、文件讀取等IO操作)進行掛起,比如異步程序執行到某一步時需要很長時間的等待,就將此掛起,去執行其他異步函數
import?asyncio,?time async?def?do_some_work(n):?#使用async關鍵字定義異步函數print('等待:{}秒'.format(n))await?asyncio.sleep(n)?#休眠一段時間?return?'{}秒后返回結束運行'.format(n) start_time?=?time.time()?#開始時間 coro?=?do_some_work(2) loop?=?asyncio.get_event_loop()?#?創建事件循環對象? loop.run_until_complete(coro) print('運行時間:?',?time.time()?-?start_time)運行結果如下:
等待:2秒 運行時間:?2.00131201744079605
多任務協程
任務(Task)對象用于封裝協程對象,保存了協程運行后的狀態,使用 run_until_complete() 方法將任務注冊到事件循環;
如果我們想要使用多任務,那么我們就需要同時注冊多個任務的列表,可以使用 run_until_complete(asyncio.wait(tasks)),
這里的tasks,表示一個任務序列(通常為列表)
注冊多個任務也可以使用run_until_complete(asyncio. gather(*tasks))
import?asyncio,?time async?def?do_some_work(i,?n):?#使用async關鍵字定義異步函數print('任務{}等待:?{}秒'.format(i,?n))await?asyncio.sleep(n)?#休眠一段時間return?'任務{}在{}秒后返回結束運行'.format(i,?n) start_time?=?time.time()?#開始時間 tasks?=?[asyncio.ensure_future(do_some_work(1,?2)),asyncio.ensure_future(do_some_work(2,?1)),asyncio.ensure_future(do_some_work(3,?3))] loop?=?asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(tasks)) for?task?in?tasks:print('任務執行結果:?',?task.result())? print('運行時間:?',?time.time()?-?start_time)運行結果如下:
任務1等待:?2秒 任務2等待:?1秒 任務3等待:?3秒 任務執行結果:?任務1在2秒后返回結束運行?任務執行結果:?任務2在1秒后返回結束運行?任務執行結果:?任務3在3秒后返回結束運行?運行時間:?3.002867698669433606
實戰|爬取LOL皮膚
首先打開官網:
可以看到英雄列表,這里就不詳細展示了,我們知道一個英雄有多個皮膚,我們的目標就是爬取每個英雄的所有皮膚,保存到對應的文件夾里;
打開一個英雄的皮膚頁面,如下所示:
黑暗之女,下面的小兔對應的就是該隱兄弟皮膚,然后通過查看network發現對應的皮膚數據在js文件里;
然后我們發現了英雄皮膚存放的url鏈接規律:
url1?=?'https://game.gtimg.cn/images/lol/act/img/js/hero/1.js'? url2?=?'https://game.gtimg.cn/images/lol/act/img/js/hero/2.js'? url3?=?'https://game.gtimg.cn/images/lol/act/img/js/hero/3.js'我們發現只有id參數是動態構造的,規律是:
'https://game.gtimg.cn/images/lol/act/img/js/hero/{}.js'.format(i)但是這個id只有前面的是按順序的,在展示全部英雄的頁面找到對應英雄的id,
這里截取的是最后幾個英雄的id,所以要全部爬取,需要先設置好id,由于前面的是按順序的,這里我們就爬 取前20個英雄的皮膚;
1. 獲取英雄皮膚ulr地址:
前面的英雄id是按順序的所有可以使用range(1,21),動態構造url;
def?get_page():page_urls?=?[]?for?i?in?range(1,21):url?=?'https://game.gtimg.cn/images/lol/act/img/js/hero/{}.js'.format(i)print(url)page_urls.append(url)?return?page_urls2. 請求每一頁的url地址
并對網頁進行解析獲取皮膚圖片的url地址:
def?get_img():img_urls?=?[]?page_urls?=?get_page()?for?page_url?in?page_urls:res?=?requests.get(page_url,?headers=headers)result?=?res.content.decode('utf-8')res_dict?=?json.loads(result)skins?=?res_dict["skins"]for?hero?in?skins:item?=?{}item['name']?=?hero["heroName"]item['skin_name']?=?hero["name"]if?hero["mainImg"]?==?'':continueitem['imgLink']?=?hero["mainImg"]print(item)img_urls.append(item)return?img_urls說明:
res_dict = json.loads(result) : 將得到的json格式字符串轉化為字典格式;
heroName:英雄名字(這個一定是一樣的,方便我們后面根據英雄名創建文件夾);
name:表示完整的 名字,包括皮膚名(這個一定是不一樣的) 有的'mainImg'是空的,我們需要進行一個判斷;
3. 創建協程函數
這里我們根據英雄名創建文件夾,然后就是注意圖片的命名,不要忘記/,目錄結構確立
async?def?save_img(index,?img_url):path?=?"皮膚/"?+?img_url['name']if?not?os.path.exists(path):os.makedirs(path)content?=?requests.get(img_url['imgLink'],?headers=headers).contentwith?open('./皮膚/'?+?img_url['name']?+?'/'?+?img_url['skin_name']?+?str(index)?+?'.jpg',?'wb')?as?f:f.write(content)主函數:
def?main():loop?=?asyncio.get_event_loop()?img_urls?=?get_img()?print(len(img_urls))?tasks?=?[save_img(img[0],?img[1])?for?img?in?enumerate(img_urls)]?try:loop.run_until_complete(asyncio.wait(tasks))?finally:loop.close()4. 程序運行
if?__name__?==?'__main__':start?=?time.time()?main()?end?=?time.time()?print(end?-?start)運行結果:
下載233張圖花費了42s,可以看到速度還行,文件目錄結果如下:
與requests對比
異步爬取圖片之后,我們有必要使用requests去進行同步數據爬取,進行效率對比,所以在原有代碼的 基礎上進行修改,這里直接略過,思路都是一樣的,這是把一部當中的事件循環替換成循環即可:
img_urls?=?get_img()? print(len(img_urls))? for?i,img_url?in?enumerate(img_urls):save_img(i,img_url)我們可以看到,使用協程的速度要比 requests 快了一些。
以上就是本文的全部內容,感興趣的讀者可以自己動手敲一遍代碼~
E?N?D
各位伙伴們好,詹帥本帥假期搭建了一個個人博客和小程序,匯集各種干貨和資源,也方便大家閱讀,感興趣的小伙伴請移步小程序體驗一下哦!(歡迎提建議)
推薦閱讀
牛逼!Python常用數據類型的基本操作(長文系列第①篇)
牛逼!Python的判斷、循環和各種表達式(長文系列第②篇)
牛逼!Python函數和文件操作(長文系列第③篇)
牛逼!Python錯誤、異常和模塊(長文系列第④篇)
總結
以上是生活随笔為你收集整理的Python 协程 asyncio 极简入门与爬虫实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 重磅!微软发布 vscode.dev,把
- 下一篇: 绘制方法太单一!?这三个宝藏在线学习资源