异步爬虫-aiohttp库、Twisted库简介
為什么要用異步爬蟲?
?爬蟲本質上就是模擬客戶端與服務端的通訊過程。以瀏覽器端的爬蟲為例,我們在爬取不同網頁過程中,需要根據url構建很多HTTP請求去爬取,而如果以單個線程為參考對象,平常我們所采取的編碼習慣,通常是基于同步模式的,也就是串行的方式去執行這些請求,只有當一個url爬取結束后才會進行下一個url的爬取,由于網絡IO的延時存在,效率非常低。
?到這里可能會有人說,那么我們可以使用多進程+多線程來提高效率啊,為什么要使用異步編程,畢竟異步編程會大大增加編程難度。【進程、線程、協程簡單梳理】在這篇整理文章中有提到,多進程/多線程雖然能提高效率,但是在進程/線程切換的時候,也會消耗很多資源。而且就IO密集型任務來說,雖然使用多線程并發可以提高CUP使用率,提升爬取效率,但是卻還是沒有解決IO阻塞問題。無論是多進程還是多線程,在遇到IO阻塞時都會被操作系統強行剝奪走CPU的執行權限,爬蟲的執行效率因此就降低了下來。而異步編程則是我們在應用程序級別來檢測IO阻塞然后主動切換到其他任務執行,以此來'降低'我們爬蟲的IO,使我們的爬蟲更多的處于就緒態,這樣操作系統就會讓CPU盡可能多地臨幸我們的爬蟲,從而提高爬蟲的爬取效率。
補充:常見的IO模型有阻塞、非阻塞、IO多路復用,異步。下面舉個小栗子來簡單描述一下這四個場景。
當快樂的敲代碼時光結束時,沒有女朋友的單身狗只能約上好基友去召喚師峽谷傲游,當我秒選快樂風男,然后發送“亞索中單,不給就送后”,在隊友一片歡聲笑語中進入加載界面,奈何遇到小霸王,加載異常緩慢。。。此時!
下面開始進入正題
asyncio
在介紹aiohttp、tornado、twisted之前,先了解下python3.4版本引入的標準庫asyncio。它可以幫助我們檢測IO(只能是網絡IO),實現應用程序級別的切換。它的編程模型是一個消息循環。我們可以從asyncio模塊中直接獲取一個EventLoop的引用,然后把需要執行的協程扔到EventLoop中執行,就實現了異步IO。
基本使用
?
import asyncio import random import datetimeurls=['www.baidu.com','www.qq.com','www.douyu.com']@asyncio.coroutine def crawl(url):print("正在抓取:{}-{}".format(url,datetime.datetime.now().time()))io_time = random.random()*3 #隨機模擬網絡IO時間yield from asyncio.sleep(io_time) #模擬網絡IOprint('{}-抓取完成,用時{}s'.format(url,io_time))loop = asyncio.get_event_loop() #獲取EventLoop loop.run_until_complete(asyncio.wait(map(crawl,urls))) #執行coroutine loop.close()運行結果:
?
正在抓取:www.baidu.com-12:45:26.517226 正在抓取:www.douyu.com-12:45:26.517226 正在抓取:www.qq.com-12:45:26.517226 www.douyu.com-抓取完成,用時0.1250027573049739s www.baidu.com-抓取完成,用時0.450045918339271s www.qq.com-抓取完成,用時0.6967129499714361s [Finished in 0.9s]運行的時候可以發現三個請求幾乎是同時發出的,而返回順序則是根據網絡IO完成時間順序返回的。
由于asyncio主要應用于TCP/UDP socket通訊,不能直接發送http請求,因此,我們需要自己定義http報頭。
補充:
- 客戶端發送一個HTTP請求到服務器的請求消息包括以下格式:請求行、消息報頭和請求正文三個部分。
例如:GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n - 用asyncio提供的@asyncio.coroutine可以把一個generator標記為coroutine類型,然后在coroutine內部用yield from調用另一個coroutine實現異步操作。為了簡化并更好地標識異步IO,從Python 3.5開始引入了新的語法async和await,可以讓coroutine的代碼更簡潔易讀。
概念補充
- event_loop:事件循環,相當于一個無限循環,我們可以把一些函數注冊到這個事件循環上,當滿足條件發生的時候,就會調用對應的處理方法。
- coroutine:中文翻譯叫協程,在 Python 中常指代為協程對象類型,我們可以將協程對象注冊到時間循環中,它會被事件循環調用。我們可以使用 async 關鍵字來定義一個方法,這個方法在調用時不會立即被執行,而是返回一個協程對象。
- task:任務,它是對協程對象的進一步封裝,包含了任務的各個狀態。
- future:代表將來執行或沒有執行的任務的結果,實際上和 task 沒有本質區別。
有了以上知識基礎,就可以擼代碼啦
?
import asyncio import uuiduser_agent='Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0'def parse_page(host,res):print('%s 解析結果 %s' %(host,len(res)))with open('%s.html' %(uuid.uuid1()),'wb') as f:f.write(res)async def get_page(host,port=80,url='/',callback=parse_page,ssl=False,encode_set='utf-8'):print('下載 http://%s:%s%s' %(host,port,url))if ssl:port=443#發起tcp連接, IO阻塞操作recv,send=await asyncio.open_connection(host=host,port=port,ssl=ssl) #封裝http協議的報頭,因為asyncio模塊只能封裝并發送tcp包,因此這一步需要我們自己封裝http協議的包request_headers="""GET {} HTTP/1.0\r\nHost: {}\r\nUser-agent: %s\r\n\r\n""".format(url,host,user_agent) request_headers=request_headers.encode(encode_set)#發送構造好的http請求(request),IO阻塞send.write(request_headers)await send.drain()#接收響應頭 IO阻塞操作while True:line=await recv.readline()if line == b'\r\n':breakprint('%s Response headers:%s' %(host,line))#接收響應體 IO阻塞操作text=await recv.read()#執行回調函數callback(host,text)#關閉套接字send.close() #沒有recv.close()方法,因為是四次揮手斷鏈接,雙向鏈接的兩端,一端發完數據后執行send.close()另外一端就被動地斷開if __name__ == '__main__':tasks=[get_page('www.gov.cn',url='/',ssl=False),get_page('www.douyu.com',url='/',ssl=True),]loop=asyncio.get_event_loop()loop.run_until_complete(asyncio.wait(tasks))loop.close()用上async/await關鍵字,是不是既簡潔,也更便于理解了
自己動手封裝HTTP(S)報頭確實很麻煩,所以接下來就要請出這一小節的正主aiohttp了,它里面已經幫我們封裝好了。
補充:asyncio可以實現單線程并發IO操作。如果僅用在客戶端,發揮的威力不大。如果把asyncio用在服務器端,例如Web服務器,由于HTTP連接就是IO操作,因此可以用單線程+coroutine實現多用戶的高并發支持。asyncio實現了TCP、UDP、SSL等協議,aiohttp則是基于asyncio實現的HTTP框架。它分為兩部分,一部分是Client(我們將要使用的部分,因為我們爬蟲是模擬客戶端操作嘛),一部分是 Server,詳細的內容可以參考官方文檔。
下面我們用aiohttp來改寫上面的代碼:
?
import asyncio import uuid import aiohttpuser_agent='Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0'def parse_page(url,res):print('{} 解析結果 {}'.format(url,len(res)))with open('{}.html'.format(uuid.uuid1()),'wb') as f:f.write(res)async def get_page(url,callback=parse_page):session = aiohttp.ClientSession()response = await session.get(url)if response.reason == 'OK':result = await response.read()session.close()callback(url,result)if __name__ == '__main__':tasks=[get_page('http://www.gov.cn'),get_page('https://www.douyu.com'),]loop=asyncio.get_event_loop()loop.run_until_complete(asyncio.wait(tasks))loop.close()是不是更加簡潔了呢?
Twisted
twisted是一個網絡框架,哪怕剛剛接觸python爬蟲的萌新都知道的Scrapy爬蟲框架,就是基于twisted寫的。它其中一個功能就是發送異步請求,檢測IO并自動切換。
基于twisted修改上面的代碼如下:
?
from twisted.web.client import getPage,defer from twisted.internet import reactor import uuiddef tasks_done(arg):reactor.stop() #停止reactor#定義回調函數 def parse_page(res):print('解析結果 {}'.format(len(res)))with open('{}.html'.format(uuid.uuid1()),'wb') as f:f.write(res)defer_list=[]#初始化一個列表來存放getPage返回的defer對象urls=['http://www.gov.cn','https://www.douyu.com', ]for url in urls:obj = getPage(url.encode('utf-8'),) #getPage會返回一個defer對象obj.addCallback(parse_page) #給defer對象添加回調函數defer_list.append(obj) #將defer對象添加到列表中defer.DeferredList(defer_list).addBoth(tasks_done) #任務列表結束后停止reactor.stopreactor.run #啟動監聽這只是一個簡單的應用,后面會看情況可能會寫一篇Twisted的整理文章。
作者:放風箏的富蘭克林
鏈接:https://www.jianshu.com/p/aa93b7ae2a56
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
總結
以上是生活随笔為你收集整理的异步爬虫-aiohttp库、Twisted库简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Python】Matplotlib绘图
- 下一篇: 【Python】Matplotlib绘制