Python進階:理解Python中的非同步IO和協程(Coroutine),並應用在爬蟲中
基礎知識
(1)什麼是同步IO和非同步IO,它們之間有什麼區別?
答:舉個現實例子,假設你需要打開4個不同的網站,但每個網站都比較卡。IO過程就相當於你打開網站的過程,CPU就是你的點擊動作。你的點擊動作很快,但是網站打開很慢。同步IO是指你每點擊一個網址,都等待該網站徹底顯示,才會去點擊下一個網址。非同步IO是指你點擊完一個網址,不等對方伺服器返回結果,立馬新開瀏覽器窗口去打開另外一個網址,以此類推,最後同時等待4個網站徹底打開。很明顯非同步IO的效率更高。
(2)什麼是協程,為什麼要使用協程?
Python中解決IO密集型任務(打開多個網站)的方式有很多種,比如多進程、多線程。但理論上一台電腦中的線程數、進程數是有限的,而且進程、線程之間的切換也比較浪費時間。所以就出現了「協程」的概念。協程允許一個執行過程A中斷,然後轉到執行過程B,在適當的時候再一次轉回來,有點類似於多線程。但協程有以下2個優勢:
- 協程的數量理論上可以是無限個,而且沒有線程之間的切換動作,執行效率比線程高。
- 協程不需要「鎖」機制,即不需要lock和release過程,因為所有的協程都在一個線程中。
- 相對於線程,協程更容易調試debug,因為所有的代碼是順序執行的。
Python中的非同步IO和協程
Python中的協程是通過「生成器(generator)」的概念實現的。這裡引用廖雪峰Python教程中的例子,並做一點修改和「裝飾」:
def consumer(): # 定義消費者,由於有yeild關鍵詞,此消費者為一個生成器n print("[Consumer] Init Consumer ......")n r = "init ok" # 初始化返回結果,並在啟動消費者時,返回給生產者n while True:n n = yield r # 消費者通過yield接收生產者的消息,同時返給其結果n print("[Consumer] conusme n = %s, r = %s" % (n, r))n r = "consume %s OK" % n # 消費者消費結果,下個循環返回給生產者nndef produce(c): # 定義生產者,此時的 c 為一個生成器n print("[Producer] Init Producer ......")n r = c.send(None) # 啟動消費者生成器,同時第一次接收返回結果n print("[Producer] Start Consumer, return %s" % r)n n = 0n while n < 5:n n += 1n print("[Producer] While, Producing %s ......" % n)n r = c.send(n) # 向消費者發送消息並準備接收結果。此時會切換到消費者執行n print("[Producer] Consumer return: %s" % r)n c.close() # 關閉消費者生成器n print("[Producer] Close Producer ......")nnproduce(consumer())n
代碼中添加了很詳細的print語句和注釋,幫助大家更好的理解。這裡刪除了源代碼consumer中的「return」語句。如果還是不太明白,可以在編輯器中進行debug調試,一步步跟蹤程序的運行過程。
關於非同步IO,在Python3.4中可以使用asyncio標準庫。該標準庫支持一個時間循環模型(EventLoop),我們聲明協程,然後將其加入到EventLoop中,即可實現非同步IO。
Python中也有一個關於非同步IO的很經典的HelloWorld程序(同樣參考於廖雪峰教程):
# 非同步IO例子:適配Python3.4,使用asyncio庫n@asyncio.coroutinendef hello(index): # 通過裝飾器asyncio.coroutine定義協程n print(Hello world! index=%s, thread=%s % (index, threading.currentThread()))n yield from asyncio.sleep(1) # 模擬IO任務n print(Hello again! index=%s, thread=%s % (index, threading.currentThread()))nnloop = asyncio.get_event_loop() # 得到一個事件循環模型ntasks = [hello(1), hello(2)] # 初始化任務列表nloop.run_until_complete(asyncio.wait(tasks)) # 執行任務nloop.close() # 關閉事件循環列表n
同樣這裡的代碼添加了注釋,並增加了index參數。輸出currentThread的目的是演示當前程序都是在一個線程中執行的。返回結果如下:
Hello world! index=1, thread=<_MainThread(MainThread, started 14816)>nHello world! index=2, thread=<_MainThread(MainThread, started 14816)>nHello again! index=1, thread=<_MainThread(MainThread, started 14816)>nHello again! index=2, thread=<_MainThread(MainThread, started 14816)>n
在Python3.5中引入了關於非同步IO的新語法:async和await關鍵字。
# 非同步IO例子:適配Python3.5,使用async和await關鍵字nasync def hello(index): # 通過關鍵字async定義協程n print(Hello world! index=%s, thread=%s % (index, threading.currentThread()))n await asyncio.sleep(1) # 模擬IO任務n print(Hello again! index=%s, thread=%s % (index, threading.currentThread()))nnloop = asyncio.get_event_loop() # 得到一個事件循環模型ntasks = [hello(1), hello(2)] # 初始化任務列表nloop.run_until_complete(asyncio.wait(tasks)) # 執行任務nloop.close() # 關閉事件循環列表n
從代碼中可以看出,使用async代替@asyncio.coroutine,使用await代替yield from,使得協程代碼更加簡潔易懂。
async關鍵字將一個函數聲明為協程函數,函數執行時返回一個協程對象。
await關鍵字將暫停協程函數的執行,等待非同步IO返回結果。
這裡還出現了一個新詞:事件循環模型EventLoop,這又是個什麼呢?
EventLoop是一個程序結構,用於等待和發送消息和事件。簡單說,就是在程序中設置兩個線程:一個負責程序本身的運行,稱為"主線程";另一個負責主線程與其他進程(主要是各種I/O操作)的通信,被稱為"Event Loop線程"(可以譯為"消息線程")。
在爬蟲中使用協程實現非同步IO
非同步IO特別適合爬蟲的工作,因為爬蟲中所有的請求都屬於IO密集型任務,想得到比較好的爬蟲效率,使用協程很重要。關於Http非同步請求,建議使用aiohttp庫,一個非同步的HTTP客戶端/伺服器框架。這裡舉個例子,更多用法可以參考其官方文檔。
async def get(url):n async with aiohttp.ClientSession() as session:n async with session.get(url) as resp:n print(url, resp.status)n print(url, await resp.text())nnloop = asyncio.get_event_loop() # 得到一個事件循環模型ntasks = [ # 初始化任務列表n get("http://zhushou.360.cn/detail/index/soft_id/3283370"),n get("http://zhushou.360.cn/detail/index/soft_id/3264775"),n get("http://zhushou.360.cn/detail/index/soft_id/705490")n]nloop.run_until_complete(asyncio.wait(tasks)) # 執行任務nloop.close() # 關閉事件循環列表n
老規矩,以上所有代碼均上傳至Github:https://github.com/xianhu/LearnPython
另外關於aiohttp庫的使用,我也會整理一份代碼,上傳到GitHub。
=============================================================
作者主頁:笑虎(Python愛好者,關注爬蟲、數據分析、數據挖掘、數據可視化等)
作者專欄主頁:擼代碼,學知識 - 知乎專欄
作者GitHub主頁:擼代碼,學知識 - GitHub
歡迎大家拍磚、提意見。相互交流,共同進步!
==============================================================
推薦閱讀:
※為什麼Python裡面的locals()是只讀的
※強烈推薦 | 數據分析師的必讀書單
※用python-pandas作圖矩陣
※Python進階課程筆記(四)
※Scrapy學習實例(二)採集無限滾動頁面