1. 程式人生 > >利用協程asyncio爬取搜狗美女圖片(一)——asyncio庫的介紹和使用

利用協程asyncio爬取搜狗美女圖片(一)——asyncio庫的介紹和使用

上一節,我們通過分析ajax爬取搜狗美女圖片,(連結https://blog.csdn.net/MG1723054/article/details/81735834)這樣爬取的效率相對來說比較高,在文章的末尾我們使用程序池來提高效率,但是由於爬蟲主要是密集型IO操作,利用程序對其提高時效率不高,(上節的程式碼中的time.sleep(1)若刪除,兩者的所耗時間幾乎相同),對於密集型IO操作,我們可以使用執行緒或者協程來提高爬取效率。(注意,下面的程式碼均在python3.5版本下面執行,python3.6.5版本上面執行run_until_complete()該方法會出錯。至少我這邊是這樣)

協程最大的優勢就是具有極高的執行效率。因為子程式切換不是執行緒切換,而是由程式自身控制,因此,沒有執行緒切換的開銷,和多執行緒比,執行緒數量越多,協程的效能優勢就越明顯。第二大優勢就是不需要多執行緒的鎖機制,因為只有一個執行緒,也不存在同時寫變數衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多執行緒高很多。

但是根據自己的理解目前,Python對協程的支援還非常有限,本節在基於上一節的基礎上,利用協程中asyncio庫來爬取搜狗美女圖片。

工欲善其事必先利其器,我們先了解在利用asyncio模組的概念

event_loop事件迴圈:程式開啟一個無限迴圈,程式設計師會把一些函式註冊到時間迴圈,當事件發生時呼叫相應的協程函式

coroutine協程:協程物件,指一個使用async關鍵字定義的函式,他的呼叫不會立即執行函式,而是返回一個函式物件。協程使用物件需要註冊到事件迴圈,由事件迴圈呼叫

task任務:一個協程物件就是一個原生可以掛起的函式,任務則是對協程進一步封裝,其中包含任務的各種狀態

future:代表將來執行或者未執行的任務結果

async:定義一個協程   await:掛起阻塞的非同步呼叫介面

一、建立協程

在def函式前加入async宣告,就可以定義一個協程函式,一個協程不能直接呼叫直接執行,只能把協程加入到事件迴圈loop中,asyncio.get_event_loop方法可以建立一個事件迴圈,run_until_complete將協程註冊到事件迴圈,並啟動事件迴圈。

import asyncio
async def fun():
    print('hello world')
loop=asyncio.get_event_loop()
loop.run_until_complete(fun())
####執行結果:hello world

注意:協程物件不能直接執行,在註冊事件迴圈的時候,其實run_until_complete方法將協程成為一個任務task物件。task物件是Futrue類的子類。儲存了協程執行後的狀態,用於未來獲取協程的結果

二、繫結回撥

import asyncio
async def fun():
    print('hello world')
    return 'hello'
def callback(future):
    print('callback:',future.result())
    
loop=asyncio.get_event_loop()
task=asyncio.ensure_future(fun())
task.add_done_callback(callback)
loop.run_until_complete(task)
###執行結果:hello world
   #####    callback: hello

建立task後,task在加入事件迴圈之前是pending狀態,因為fun()沒有耗時的阻塞操作,task很快執行完畢,asyncio.ensure_future和loop.creat_task均可以建立一個task,run_until_complete的引數是一個future物件。當傳入一個協程,其內部會自動封裝成task,task是一個Future子類,在task執行完畢的時候可以獲取執行的結果,回撥的最後一個引數是future物件,通過add_done_callback()呼叫。實際上可以不使用回撥,直接在task執行完成完畢後直接可以呼叫resul()方法,程式碼如下:

import asyncio
async def fun():
    print('hello world')
    return 'hello'
loop=asyncio.get_event_loop()
task=asyncio.ensure_future(fun())
loop.run_until_complete(task)
print(task.result())
###執行結果與上面的一樣

三、阻塞

使用asyncio可以定義協程物件,使用await可以針對耗時的操作進行掛起,就像生成器yield一樣,函式讓出控制權。協程遇到await,事件迴圈將會掛起該協程,執行別的協程,直到其他協程也掛起或執行完畢,再進行下一個協程的執行。例子在第五條給出

四、多工協程

上面的很多例子我們只執行了一次請求,如果我們執行多次請求如何解決,我們可以建立一個task列表,再使用asyncio中的wait方法執行即可

"""
Created on Fri Jul 27 18:31:43 2018

@author: NJUer
"""
import asyncio
import requests
import time
start = time.time()
 
async def image_download(item):
    response = requests.get(item['imgurl'])
    file='C:\\Users\\FangWei\\Desktop\\例項\\'+str(item['title'])+'.jpg'
    with open(file,'wb') as f:
        f.write(response.content)
items=[{'imgurl':'https://img02.sogoucdn.com/app/a/100520020/80de1a7c1181bb60780bee710aa9b2cc','title':12659835},  {'imgurl':'https://img04.sogoucdn.com/app/a/100520020/3186d4cc8dc54b41e62207a99341a830','title':1234},]
start=time.time()
tasks=[asyncio.ensure_future(image_download(item)) for item in items ]
loop=asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end=time.time()
print('Cost time:', end - start)
#####Cost time: 1.0180580615997314

五、協程的實現

上面我們介紹了怎麼建立一個協程以及協程的執行,但是到這裡還不是真正的實現了協程,因為並沒有因為遇到等待將協程掛起的狀態,而await可以將協程掛起,這樣我們可以將上面的程式改變,這樣就可以實現掛起操作了,但是我們可以發現,該操作程式的總用時沒有改變。

"""
Created on Fri Jul 27 18:31:43 2018

@author: NJUer
"""
import asyncio
import requests
import time
start = time.time()
 
async def image_download(item):
    response =await get(item['imgurl'])  #####注意await後面必須是原生coroutine或者Future子類
    file='C:\\Users\\FangWei\\Desktop\\例項\\'+str(item['title'])+'.jpg'
    with open(file,'wb') as f:
        f.write(response.content)
async def get(url):
    return requests.get(url)
items=[{'imgurl':'https://img02.sogoucdn.com/app/a/100520020/80de1a7c1181bb60780bee710aa9b2cc','title':12659835},  {'imgurl':'https://img04.sogoucdn.com/app/a/100520020/3186d4cc8dc54b41e62207a99341a830','title':1234},]
start=time.time()
tasks=[asyncio.ensure_future(image_download(item)) for item in items ]
loop=asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end=time.time()
print('Cost time:', end - start)
###Cost time: 1.0280702590942383

造成該原因是我們僅僅將IO操作的程式碼分裝到async修飾器中,並未進行非同步進行,所以必須使用支援非同步操作的請求方式才可以實現真正的非同步,aiohttp是專門為實現單執行緒併發IO操作的庫

我們將上面的程式碼改變一下,利用aiohttp

"""
Created on Fri Jul 27 18:31:43 2018

@author: NJUer
"""
import asyncio,aiohttp,time
async def  image_download(item):
      file='C:\\Users\\FangWei\\Desktop\\例項\\'+str(item['title'])+'.jpg'
      async with aiohttp.ClientSession() as session:
          async with session.get(item['imgurl']) as resp:
              imgcode=await resp.read()
      with open(file,'wb')as f:
          f.write(imgcode)
items=[{'imgurl':'https://img02.sogoucdn.com/app/a/100520020/80de1a7c1181bb60780bee710aa9b2cc','title':12659835},  {'imgurl':'https://img04.sogoucdn.com/app/a/100520020/3186d4cc8dc54b41e62207a99341a830','title':1234},]
start=time.time()
tasks=[asyncio.ensure_future(image_download(item)) for item in items ]
loop=asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end=time.time()
print('Cost time:', end - start)
####Cost time: 0.4710268974304199

對於爬蟲專案實戰,我們在其第二個部分再說。

 原創不易,如若轉載,請註明出處和作者,謝謝!