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