大家好铅协!我是霖hero
Python并發(fā)編程有三種方式:多線程(Threading)坏平、多進(jìn)程(Process)贬蛙、協(xié)程(Coroutine),使用并發(fā)編程會(huì)大大提高程序的效率宇弛,今天我們將學(xué)習(xí)如何選擇多線程、多進(jìn)程和協(xié)程來(lái)提高代碼的效率源请、如何使用異步協(xié)程枪芒,并用協(xié)程來(lái)獲取同程旅行酒店的評(píng)論數(shù)據(jù)彻况。
并發(fā)編程
多線程(Threading)
多線程(Threading):從軟件或者硬件上實(shí)現(xiàn)多個(gè)線程并發(fā)執(zhí)行的技術(shù)。能夠在同一時(shí)間執(zhí)行多于一個(gè)線程舅踪,進(jìn)而提升整體處理性能纽甘,利用CPU和IO可以同時(shí)執(zhí)行的原理,讓CPU不會(huì)干巴巴地等待IO完成抽碌。
工作方式:多線程由三個(gè)部分組成:新任務(wù)贷腕、任務(wù)隊(duì)列、線程咬展。由于新建線程系統(tǒng)是需要分配資源的泽裳,終止線程系統(tǒng)是需要回收資源的,為了重用線程破婆,把線程放在一個(gè)線程池中涮总,如下圖所示:
線程池:里面提前建好N個(gè)線程,這些都會(huì)被重復(fù)利用祷舀;
任務(wù)隊(duì)列:當(dāng)有新任務(wù)的時(shí)候瀑梗,會(huì)把任務(wù)放在任務(wù)隊(duì)列中。
當(dāng)任務(wù)隊(duì)列里有任務(wù)時(shí)裳扯,線程池的線程會(huì)從任務(wù)隊(duì)列中取出任務(wù)并執(zhí)行抛丽,執(zhí)行完任務(wù)后,線程會(huì)執(zhí)行下一個(gè)任務(wù)饰豺,直到?jīng)]有任務(wù)執(zhí)行后亿鲜,線程會(huì)回到線程池中等待任務(wù)。
多進(jìn)程(Process)
多進(jìn)程:一個(gè)程序的執(zhí)行實(shí)例就是一個(gè)進(jìn)程冤吨,每一個(gè)進(jìn)程提供執(zhí)行程序所需的所有資源蒿柳,利用多核CPU的能力,真正的并行執(zhí)行任務(wù)漩蟆。
工作方式:通過(guò)CPU調(diào)度器來(lái)并發(fā)執(zhí)行進(jìn)程垒探,如下圖所示:
協(xié)程(Coroutine)
協(xié)程:一種比多線程高效得多的并發(fā)模型,是無(wú)序的怠李,為了完成某個(gè)任務(wù)圾叼,在執(zhí)行的過(guò)程中,不同程序單元之間過(guò)程中無(wú)需通信協(xié)調(diào)捺癞,也能完成任務(wù)的方式夷蚊,在單線程利用CPU和IO同時(shí)執(zhí)行的原理,實(shí)現(xiàn)函數(shù)異步執(zhí)行翘簇。
工作方式:如下圖所示:
當(dāng)請(qǐng)求程序發(fā)送網(wǎng)絡(luò)請(qǐng)求1并收到某個(gè)站點(diǎn)的響應(yīng)后撬码,開(kāi)始執(zhí)行程序中的下載程序,由于下載需要時(shí)間或者其他原因使處于阻塞狀態(tài)版保,請(qǐng)求程序和下載程序是不相關(guān)的程序單元呜笑,所以請(qǐng)求程序發(fā)送下一個(gè)網(wǎng)絡(luò)請(qǐng)求夫否。
并發(fā)編程對(duì)比
簡(jiǎn)單了解了并發(fā)編程的三種方式,接下來(lái)將對(duì)比這三種方式的優(yōu)缺點(diǎn)并根據(jù)任務(wù)來(lái)選擇對(duì)應(yīng)的技術(shù)叫胁。
多進(jìn)程Process
優(yōu)點(diǎn):可以利用多核CPU并行運(yùn)算凰慈;
缺點(diǎn):占用資源最多、可啟動(dòng)數(shù)目比線程少驼鹅;
適用于:CPU密集型計(jì)算微谓。
多線程threading
優(yōu)點(diǎn):比進(jìn)程更輕量級(jí),占用資源少输钩;
缺點(diǎn):只能并發(fā)執(zhí)行豺型,不能利用多CPU,啟動(dòng)數(shù)目有限买乃,占用內(nèi)存資源姻氨,有線程切換開(kāi)銷(xiāo);
適用于:IO密集型計(jì)算剪验、同時(shí)運(yùn)行的任務(wù)數(shù)目要求不多肴焊。
協(xié)程Corutine
優(yōu)點(diǎn):內(nèi)存開(kāi)銷(xiāo)最少,啟動(dòng)協(xié)程數(shù)量最多功戚;
缺點(diǎn):之前的庫(kù)有限制娶眷、代碼實(shí)現(xiàn)復(fù)雜;
適用于:IO密集型計(jì)算啸臀、需要超多任務(wù)運(yùn)行届宠。
有人可能不知道什么是CPU、IO密集型計(jì)算壳咕,我們來(lái)簡(jiǎn)單講解一下:
CPU密集型:指I/O在很短的時(shí)間就可以完成席揽,CPU需要大量的計(jì)算機(jī)和處理,CPU占用率很高谓厘,壓縮解壓、加密解密寸谜、正則表達(dá)式搜索等都屬于CPU密集型計(jì)算竟稳。
IO密集型:指I/O(硬盤(pán)/內(nèi)存)的讀寫(xiě)操作,CPU占用率較低熊痴,文件處理程序他爸、網(wǎng)絡(luò)爬蟲(chóng)、讀寫(xiě)數(shù)據(jù)庫(kù)都屬于IO密集型計(jì)算果善。
好了诊笤,最后通過(guò)一張圖來(lái)總結(jié)如何選擇并發(fā)編程的方式:
異步協(xié)程
asyncio模塊
asyncio模塊是Python中實(shí)現(xiàn)協(xié)程的模塊之一,其語(yǔ)法格式如下:
import asyncio
#定義協(xié)程
async def myfunc(url):
await 響應(yīng)的數(shù)據(jù)或調(diào)用下一個(gè)方法等等
#獲取事件循環(huán)
loop=asyncio.get_event_loop()
#創(chuàng)建task列表
tasks=[loop.create_task(myfunc(url))]
#執(zhí)行爬蟲(chóng)事件列表
loop.run_until_complete(asyncio.wait(tasks))
其中:
async:聲明該方法是異步協(xié)程方法巾陕;
await:聲明為異步協(xié)程可等待對(duì)象讨跟;
create_task:創(chuàng)建線程任務(wù)纪他;
注意:異步協(xié)程操作不能出現(xiàn)同步操作,否則異步操作將失效或報(bào)錯(cuò)晾匠。
aiohttp庫(kù)
requests請(qǐng)求庫(kù)是不支持異步協(xié)程的茶袒,所以我們使用aiohttp請(qǐng)求庫(kù),這個(gè)請(qǐng)求庫(kù)只能發(fā)送異步請(qǐng)求凉馆,下篇文章我們學(xué)習(xí)更強(qiáng)大的異步請(qǐng)求庫(kù)httpx薪寓,httpx請(qǐng)求庫(kù)既可以發(fā)送同步請(qǐng)求,也可以發(fā)送異步請(qǐng)求澜共。
aiohttp基本使用
使用語(yǔ)法為:
import aiohttp
import asyncio
async def main():
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
print(await 響應(yīng)的數(shù)據(jù)或調(diào)用下一個(gè)方法等等)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
先導(dǎo)入aiohttp模塊和asyncio模塊向叉,aiohttp.ClientSession() 相當(dāng)于把a(bǔ)iohttp的功能傳遞給session,也就是說(shuō)語(yǔ)法中的ClientSession()相當(dāng)于aiohttp嗦董,接著我們就可以使用client來(lái)調(diào)用get請(qǐng)求調(diào)用asyncio.get_event_loop()方法進(jìn)入事件循環(huán)植康,再調(diào)用loop.run_until_complete(main())方法運(yùn)行事件循環(huán),直到main運(yùn)行結(jié)束展懈。
post請(qǐng)求
網(wǎng)頁(yè)請(qǐng)求中有兩種類(lèi)型销睁,一種是Form Data類(lèi)型和Request Playload類(lèi)型
當(dāng)我們發(fā)送post請(qǐng)求中,需要根據(jù)請(qǐng)求類(lèi)型來(lái)傳遞是否使用data參數(shù)還是使用json參數(shù)
params = [('key', 'value1'), ('key', 'value2')]
async with aiohttp.ClientSession() as session:
#data參數(shù)
async with session.post(url,data=params) as resp:
...
#json參數(shù)
async with session.post(url,json=params) as resp:
有時(shí)候存崖,我們URL參數(shù)構(gòu)造正確卻獲取不到數(shù)據(jù)冻记,有可能是因?yàn)閜ost請(qǐng)求中的參數(shù)選擇不正確,
那么什么時(shí)候使用data參數(shù)来惧,什么時(shí)候json參數(shù)呢冗栗?
判斷方法很簡(jiǎn)單,當(dāng)出現(xiàn)如下圖的情況時(shí)供搀,該網(wǎng)頁(yè)請(qǐng)求是Form Data類(lèi)型隅居,要使用data參數(shù):
當(dāng)出現(xiàn)如下圖情況時(shí),該網(wǎng)頁(yè)請(qǐng)求是Request Playload類(lèi)型葛虐,要使用json參數(shù):
超時(shí)
在某些情況下胎源,我們調(diào)用第三方接口時(shí),響應(yīng)時(shí)間無(wú)法估計(jì)屿脐,這時(shí)可以指定超時(shí)時(shí)間涕蚤,如果超時(shí)未處理成功,則直接跳過(guò)它的诵,繼續(xù)向下執(zhí)行万栅。aiohttp提供了ClientTimeout方法來(lái)設(shè)置超時(shí),其語(yǔ)法格式如下:
timeout=aiohttp.ClientTimeout(total=5*60, connect=None,sock_connect=None, sock_read=None)
async with aiohttp.ClientSession(timeout=timeout) as session:
...
默認(rèn)情況下西疤,aiohttp使用總共300 秒(5 分鐘)的超時(shí)時(shí)間烦粒,這意味著整個(gè)操作應(yīng)該在 5 分鐘內(nèi)完成。
其中:
total:整個(gè)操作的最大秒數(shù)代赁,包括連接建立扰她、請(qǐng)求發(fā)送和響應(yīng)讀仁揸;
connect:連接建立新連接或在超過(guò)池連接限制時(shí)等待來(lái)自池的空閑連接的最大秒數(shù)义黎;
sock_connect:連接到新連接的對(duì)等點(diǎn)的最大秒數(shù)禾进;
sock_read:從對(duì)等方讀取新數(shù)據(jù)部分之間允許的最大秒數(shù)。
限制連接池大小
當(dāng)我們要獲取的數(shù)據(jù)量很大時(shí)廉涕,為了提高程序效率或防止訪問(wèn)網(wǎng)頁(yè)過(guò)多造成對(duì)方網(wǎng)頁(yè)癱瘓泻云,通常情況下要設(shè)置連接池的大小,aiohttp提供了TCPConnector方法來(lái)限制連接池的大小狐蜕,其語(yǔ)法格式為:
timeout=aiohttp.ClientTimeout(total=5*60, connect=None,sock_connect=None, sock_read=None)
async with aiohttp.ClientSession(timeout=timeout) as session:
...
上面的設(shè)置是并行連接總數(shù)限制為30宠纯,默認(rèn)情況下限制數(shù)是100,當(dāng)我們不想設(shè)置限制數(shù)目時(shí)层释,可以把limit參數(shù)中的值改為0即可婆瓜。
好了,aiohttp請(qǐng)求庫(kù)講解到這里贡羔,接下來(lái)我們正式開(kāi)始爬取同程旅行酒店評(píng)論廉白。
實(shí)戰(zhàn)演練
這次的爬蟲(chóng)過(guò)程是:
評(píng)論數(shù)據(jù)網(wǎng)頁(yè)分析;
酒店列表網(wǎng)頁(yè)分析乖寒;
同步請(qǐng)求獲取酒店id
異步請(qǐng)求獲取酒店評(píng)論猴蹂;
保存數(shù)據(jù)到MongoDB中;
繪制詞云圖楣嘁。
評(píng)論數(shù)據(jù)網(wǎng)頁(yè)分析
隨意進(jìn)入一個(gè)酒店的詳情頁(yè)并打開(kāi)開(kāi)發(fā)者工具磅轻,如下圖所示:
經(jīng)過(guò)查找,我們發(fā)現(xiàn)評(píng)論數(shù)據(jù)保存在getCommentList數(shù)據(jù)包中逐虚,如下圖所示:
那么我們觀察一下getCommentList數(shù)據(jù)包的headers信息聋溜,如下圖所示:
請(qǐng)求方式是post請(qǐng)求,而且是Request payload類(lèi)型叭爱,所以我們?cè)谑褂胘son參數(shù)撮躁。
經(jīng)過(guò)觀察,我們發(fā)現(xiàn)objectId是酒店的id涤伐,pageIndex是翻頁(yè)參數(shù)馒胆,pageSize是每個(gè)url存放的數(shù)量,其他的參數(shù)要么是空凝果,要么是定值,所以我們只要知道酒店的id就可以構(gòu)造url來(lái)獲取酒店的評(píng)論數(shù)據(jù)了睦尽。
酒店列表頁(yè)網(wǎng)頁(yè)分析
進(jìn)入同程旅行酒店列表網(wǎng)頁(yè)并打開(kāi)開(kāi)發(fā)者工具器净,如下圖所示:
經(jīng)過(guò)查找,發(fā)現(xiàn)酒店基本數(shù)據(jù)(酒店id等)存放在上圖紅框的數(shù)據(jù)包中当凡,其URL鏈接為:
https://www.ly.com/hotelapi/v2/list?pageSize=20&t=1634638146507&city=80&inDate=2021-10-19&outDate=2021-10-20&filterList=8888_1&pageIndex=0&sugActInfo=
觀察URL鏈接山害,可以推測(cè)pageSize每一頁(yè)的酒店數(shù)據(jù)量纠俭,t是時(shí)間戳,city是城市編號(hào)浪慌,inDate冤荆、outDate為入住和離店的時(shí)間,filterList為定值权纤,pageIndex是翻頁(yè)钓简。
好了,網(wǎng)頁(yè)分析就到這里了汹想,接下來(lái)正式開(kāi)始編寫(xiě)代碼來(lái)爬取評(píng)論數(shù)據(jù)外邓。
同步請(qǐng)求獲取酒店id
由于我們請(qǐng)求的酒店列表頁(yè)的URL只要三四個(gè),那么使用同步請(qǐng)求即可古掏,主要代碼如下圖所示:
async def get_link(url):
response=requests.get(url, headers=headers)
json = response.json()
data = json.get('data').get('hotelList')
hotelName_list = []
task=[]
for i in data:
hotelId = i.get('hotelId')
hotelName=i.get('hotelName')
task.append(get_commtent(hotelId,hotelName))
hotelName_list.append(hotelName)
await asyncio.wait(task)
首先定義協(xié)程方法get_link()损话,發(fā)送get網(wǎng)絡(luò)請(qǐng)求,返回的數(shù)據(jù)類(lèi)型為json格式槽唾,再通過(guò)get()方法來(lái)獲取酒店名及酒店id丧枪,創(chuàng)建一個(gè)任務(wù)列表task,并將自定義的get_commtent()方法放在task任務(wù)列表中庞萍。
獲取酒店評(píng)論
酒店id拧烦、酒店名已經(jīng)獲取了,接下來(lái)構(gòu)造參數(shù)挂绰,主要代碼如下所示:
#構(gòu)造參數(shù)
data = {
'keyword': '',
'objectId': hotelId,
'pageIndex': i,
'pageSize': '20',
'searchFeatures': [],
'sortingInfo': {
'sortingDirection': '1',
'sortingMethod': '0'
}
}
發(fā)送異步請(qǐng)求屎篱,主要代碼如下圖所示:
async with aiohttp.ClientSession()as session:
async with session.post(url,json=data,headers=headers)as response:
Json = await response.json()
data = Json.get('data').get('comments')
if data != None:
for i in data:
commtent_list = {
'hotelName': hotelName,
'content': i.get('content').replace(' ', ''),
'commentScore': i.get('commentScore'),
'createTime': i.get('createTime')
}
await saving_data(commtent_list)
elif data == None:
commtent_list = {
'hotelName': hotelName,
'content': '0',
'commentScore': '0',
'createTime': '0'
}
await saving_data(commtent_list)
首先通過(guò)async語(yǔ)法來(lái)聲明為異步協(xié)程操作,再通過(guò)post()來(lái)發(fā)送網(wǎng)絡(luò)請(qǐng)求葵蒂,使用await方法來(lái)聲明響應(yīng)內(nèi)容為異步協(xié)程可等待對(duì)象交播,再通過(guò)get()方法來(lái)提取數(shù)據(jù)并把數(shù)據(jù)傳遞到自定義方法saving_data()中。
注意:有些酒店可能是新開(kāi)業(yè)的原因践付,沒(méi)有評(píng)論秦士,所以我們使用if-elif語(yǔ)句來(lái)防止因?yàn)闆](méi)有評(píng)論而報(bào)錯(cuò)。
數(shù)據(jù)保存
數(shù)據(jù)已經(jīng)獲取了永高,接下來(lái)把數(shù)據(jù)保存在MongoDB數(shù)據(jù)庫(kù)中隧土,首先創(chuàng)建MongoDB數(shù)據(jù)庫(kù)和數(shù)據(jù)集合,主要代碼如下所示:
async def create_db(hotelName):
#連接數(shù)據(jù)庫(kù)
client = pymongo.MongoClient(host='localhost', port=27017)
#創(chuàng)造數(shù)據(jù)庫(kù)
db = client['comment_data']
#創(chuàng)建數(shù)據(jù)表
colist=db.list_collection_names()
for i in hotelName:
if i not in colist:
db.create_collection(i)
創(chuàng)建數(shù)據(jù)庫(kù)后命爬,接下來(lái)將保存數(shù)據(jù)曹傀,主要代碼如下圖所示:
async def saving_data(commtent_list):
#連接數(shù)據(jù)庫(kù)
client=pymongo.MongoClient(host='localhost',port=27017)
db=client['comment_data']
collection=db[commtent_list['hotelName']]
#插入數(shù)據(jù)到數(shù)據(jù)庫(kù)中
result=collection.insert_one(commtent_list)
由于代碼比較簡(jiǎn)單,我就直接在代碼里面寫(xiě)注釋饲宛。
最后調(diào)用asyncio.get_event_loop()方法進(jìn)入事件循環(huán)皆愉,再調(diào)用loop.run_until_complete(get_link())方法運(yùn)行事件循環(huán),直到function運(yùn)行結(jié)束。主要代碼如下所示:
for i in range(0,3):
t=time.time()
url = f'https://www.ly.com/hotelapi/v2/list?pageSize=20&t={t}&city=80&inDate=2021-10-17&outDate=2021-10-18&filterList=8888_1&pageIndex={i}&sugActInfo='
loop=asyncio.get_event_loop()
loop.run_until_complete(get_link(url))
運(yùn)行結(jié)果如下圖所示:
好了幕庐,aiohttp異步協(xié)程爬取同程旅行酒店評(píng)論就講到這里了久锥,感謝觀看!R彀瑟由!