大家好宙拉!我是霖hero宾尚。
相信很多人喜歡在空閑的時間里看小說,甚至有小部分人為了追小說而熬夜看谢澈,那么問題來了央勒,喜歡看小說的小伙伴在評論區(qū)告訴我們?yōu)槭裁聪矚g看小說,今天我們手把手教你使用異步協(xié)程20秒爬完兩百四十多萬字澳化,六百章的小說崔步,讓你一次看個夠。
在爬取之前我們先來簡單了解一下什么是同步缎谷,什么是異步協(xié)程井濒?
同步與異步
同步
同步是有序,為了完成某個任務(wù)列林,在執(zhí)行的過程中瑞你,按照順序一步一步執(zhí)行下去,直到任務(wù)完成希痴。
爬蟲是IO密集型任務(wù)者甲,我們使用requests請求庫來爬取某個站點時,網(wǎng)絡(luò)順暢無阻塞的時候砌创,正常情況如下圖所示:
但在網(wǎng)絡(luò)請求返回數(shù)據(jù)之前虏缸,程序是處于阻塞狀態(tài)的,程序在等待某個操作完成期間嫩实,自身無法繼續(xù)干別的事情刽辙,如下圖所示:
當(dāng)然阻塞可以發(fā)生在站點響應(yīng)后的執(zhí)行程序那里,執(zhí)行程序可能是下載程序甲献,大家都知道下載是需要時間的宰缤。
當(dāng)站點沒響應(yīng)或者程序卡在下載程序的時候,CPU一直在等待而不去執(zhí)行其他程序,那么就白白浪費了CPU的資源慨灭,導(dǎo)致我們的爬蟲效率很低朦乏。
異步
異步是一種比多線程高效得多的并發(fā)模型,是無序的氧骤,為了完成某個任務(wù)呻疹,在執(zhí)行的過程中,不同程序單元之間過程中無需通信協(xié)調(diào)语淘,也能完成任務(wù)的方式诲宇,也就是說不相關(guān)的程序單元之間可以是異步的。如下圖所示:
當(dāng)請求程序發(fā)送網(wǎng)絡(luò)請求1并收到某個站點的響應(yīng)后惶翻,開始執(zhí)行程序中的下載程序姑蓝,由于下載需要時間或者其他原因使處于阻塞狀態(tài),請求程序和下載程序是不相關(guān)的程序單元吕粗,所以請求程序發(fā)送下一個網(wǎng)絡(luò)請求纺荧,也就是異步。
- 微觀上異步協(xié)程是一個任務(wù)一個任務(wù)的進行切換颅筋,切換條件一般就是IO操作宙暇;
- 宏觀上異步協(xié)程是多個任務(wù)一起在執(zhí)行;
注意:上面我們所講的一切都是在單線程的條件下實現(xiàn)议泵。
請求庫
我們發(fā)送網(wǎng)絡(luò)請求一定要用到請求庫占贫,在Python從多的HTTP客戶端中,最常用的請求庫莫過于requests先口、aiohttp型奥、httpx。
在不借助其他第三方庫的情況下碉京,requests只能發(fā)送同步請求厢汹;aiohttp只能發(fā)送異步請求;httpx既能發(fā)送同步請求谐宙,又能發(fā)送異步請求烫葬。
接下來我們將簡單講解這三個庫。
requests庫
相信大家對requests庫不陌生吧凡蜻,requests庫簡單搭综、易用,是python爬蟲使用最多的庫咽瓷。
在命令行中運行如下代碼设凹,即可完成requests庫的安裝:
pip install requests
使用requests發(fā)送網(wǎng)絡(luò)請求非常簡單,
在本例中茅姜,我們使用get網(wǎng)絡(luò)請求來獲取百度首頁的源代碼,具體代碼如下:
import requests
headers={
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
}
response=requests.get('https://baidu.com',headers=headers)
response.encoding='utf-8'
print(response.text)
運行部分結(jié)果如下圖:
首先我們導(dǎo)入requests庫,創(chuàng)建請求頭钻洒,請求頭中包含了User-Agent字段信息奋姿,也就是瀏覽器標識信息,如果不加這個素标,網(wǎng)站就可能禁止抓取称诗,然后調(diào)用get()方法發(fā)送get請求,傳入的參數(shù)為URL鏈接和請求頭头遭,這樣簡單的網(wǎng)絡(luò)請求就完成了寓免。
這里我們返回打印輸出的是百度的源代碼,大家可以根據(jù)需求返回輸出其他類型的數(shù)據(jù)计维。
需要注意的是:
百度源代碼的head部分的編碼為:utf-8袜香,如下圖所示:
我們利用requests庫的方法來查看默認的編碼類型是什么,具體代碼如下所示:
import requests
url = 'https://www.baidu.com'
response = requests.get(url)
print(response.encoding)
運行結(jié)果為:ISO-8859-1
由于默認的編碼類型不同鲫惶,所以需要更改輸出的編碼類型蜈首,更改方式也很簡單,只需要在返回數(shù)據(jù)前根據(jù)head部分的編碼來添加以下代碼即可:
response.encoding='編碼類型'
除了使用get()方法實現(xiàn)get請求外欠母,還可以使用post()欢策、put()、delete()等方法來發(fā)送其他網(wǎng)絡(luò)請求赏淌,在這里就不一一演示了踩寇,關(guān)于更多的requests網(wǎng)絡(luò)請求庫用法可以到官方參考文檔進行查看,我們今天主要講解可以發(fā)送異步請求的aiohttp庫和httpx庫六水。
asyncio模塊
在講解異步請求aiohttp庫和httpx庫請求前俺孙,我們需要先了解一下協(xié)程。
協(xié)程是一種比線程更加輕量級的存在缩擂,讓單線程跑出了并發(fā)的效果鼠冕,對計算資源的利用率高,開銷小的系統(tǒng)調(diào)度機制胯盯。
Python中實現(xiàn)協(xié)程的模塊有很多懈费,我們主要來講解asyncio模塊,從asyncio模塊中直接獲取一個EventLoop的引用博脑,把需要執(zhí)行的協(xié)程放在EventLoop中執(zhí)行憎乙,這就實現(xiàn)了異步協(xié)程。
協(xié)程通過async語法進行聲明為異步協(xié)程方法叉趣,await語法進行聲明為異步協(xié)程可等待對象泞边,是編寫asyncio應(yīng)用的推薦方式,具體示例代碼如下:
import asyncio
import time
async def function1():
print('I am SupermanA粕肌U笱琛蚕礼!')
await asyncio.sleep(3)
print('function1')
async def function2():
print('I am Batman!I沂病奠蹬!')
await asyncio.sleep(2)
print('function2')
async def function3():
print('I am iron man!N宋纭囤躁!')
await asyncio.sleep(4)
print('function3')
async def Main():
tasks=[
asyncio.create_task(function1()),
asyncio.create_task(function2()),
asyncio.create_task(function3()),
]
await asyncio.wait(tasks)
if __name__ == '__main__':
t1=time.time()
asyncio.run(Main())
t2=time.time()
print(t2-t1)
運行結(jié)果為:
I am Superman!@蠖谩狸演!
I am Batman!F宵距!
I am iron man!V薪消玄!
function2
function1
function3
4.0091118812561035
首先我們用了async來聲明三個功能差不多的方法,分別為function1丢胚,function2翩瓜,function3,在方法中使用了await聲明為可等待對象携龟,并使用asyncio.sleep()方法使函數(shù)休眠一段時間兔跌。
再使用async來聲明Main()方法,通過調(diào)用asyncio.create_task()方法將方法封裝成一個任務(wù)峡蟋,并把這些任務(wù)存放在列表tasks中坟桅,這些任務(wù)會被自動調(diào)度執(zhí)行;
最后通過asyncio.run()運行協(xié)程程序蕊蝗。
注意:當(dāng)協(xié)程程序出現(xiàn)了同步操作的時候仅乓,異步協(xié)程就中斷了。
例如把上面的示例代碼中的await asyncio.sleep()換成time.time()蓬戚,運行結(jié)果為:
I am Superman?溟埂!子漩!
function1
I am BatmanTバ!幢泼!
function2
I am iron man=粝浴!缕棵!
function3
9.014737844467163
所以在協(xié)程程序中孵班,盡量不使用同步操作涉兽。
好了,asyncio模塊我們講解到這里重父,想要了解更多的可以進入asyncio官方文檔進行查看花椭。
aiohttp庫
aiohttp是基于asyncio實現(xiàn)的HTTP框架忽匈,用于HTTP服務(wù)器和客戶端房午。安裝方法如下:
pip install aiohttp
aiohttp只能發(fā)送異步請求,示例代碼如下所示:
import aiohttp
import asyncio
headers={
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
}
async def Main():
async with aiohttp.ClientSession() as session:
async with session.get('https://www.baidu.com',headers=headers) as response:
html = await response.text()
print(html)
loop=asyncio.get_event_loop()
loop.run_until_complete(Main())
運行結(jié)果和前面介紹的requests網(wǎng)絡(luò)請求一樣丹允,如下圖所示:
大家可以對比requests網(wǎng)絡(luò)請求發(fā)現(xiàn)郭厌,其實aiohttp.ClientSession() as session相當(dāng)于將requests賦給session,也就是說session相當(dāng)于requests雕蔽,而發(fā)送網(wǎng)絡(luò)請求折柠、傳入的參數(shù)、返回響應(yīng)內(nèi)容都和requests請求庫大同小異批狐,只是aiohttp請求庫需要用async和await進行聲明扇售,然后調(diào)用asyncio.get_event_loop()方法進入事件循環(huán),再調(diào)用loop.run_until_complete(Main())方法運行事件循環(huán)嚣艇,直到Main方法運行結(jié)束承冰。
注意:在調(diào)用Main()方法時,不能使用下面這條語句:
asyncio.run(Main())
雖然會得到想要的響應(yīng)食零,但會報:RuntimeError: Event loop is closed錯誤困乒。
我們還可以在返回的內(nèi)容中指定解碼方式或編碼方式,例如:
await response.text(encoding='utf-8')
或者選擇不編碼贰谣,讀取圖像:
await resp.read()
好了aiohttp請求庫我們學(xué)到這里娜搂,想要了解更多的可以到pypi官網(wǎng)進行學(xué)習(xí)。
httpx請求庫
在前面我們簡單地講解了requests請求庫和aiohttp請求庫吱抚,requests只能發(fā)送同步請求百宇,aiohttp只能發(fā)送異步請求,而httpx請求庫既可以發(fā)送同步請求秘豹,又可以發(fā)送異步請求携御,而且比上面兩個效率更高。
安裝方法如下:
pip install httpx
httpx請求庫——同步請求
使用httpx發(fā)送同步網(wǎng)絡(luò)請求也很簡單憋肖,與requests代碼重合度99%因痛,只需要把requests改成httpx即可正常運行。
具體示例代碼如下:
import httpx
headers={
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36'
}
response=httpx.get('https://www.baidu.com',headers=headers)
print(response.text)
運行結(jié)果如下圖所示:
注意:httpx使用的默認utf-8進行編碼來解碼響應(yīng)岸更。
httpx請求庫——同步請求高級用法
當(dāng)發(fā)送請求時鸵膏,httpx必須為每個請求建立一個新連接(連接不會被重用),隨著對主機的 請求數(shù)量增加怎炊,網(wǎng)絡(luò)請求的效率就是變得很低谭企。
這時我們可以用Client實例來使用HTTP連接池廓译,這樣當(dāng)我們主機發(fā)送多個請求時,Client將重用底層的TCP連接债查,而不是為重新創(chuàng)建每個請求非区。
with塊用法如下:
with httpx.Client() as client:
...
我們把Client作為上下文管理器,并使用with塊盹廷,當(dāng)執(zhí)行完with語句時征绸,程序會自動清理連接。
當(dāng)然我們可以使用.close()顯式關(guān)閉連接池俄占,用法如下:
client = httpx.Client()
try:
...
finally:
client.close()
為了我們的代碼更簡潔管怠,我們推薦使用with塊寫法,具體示例代碼如下:
import httpx
headers={
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36'
}
with httpx.Client(headers=headers)as client:
response=client.get('https://www.baidu.com')
print(response.text)
其中httpx.Client()as client相當(dāng)于把httpx的功能傳遞給client缸榄,也就是說示例中的client相當(dāng)于httpx渤弛,接著我們就可以使用client來調(diào)用get請求。
注意:我們傳遞的參數(shù)可以放在httpx.Client()里面甚带,也可以傳遞到get()方法里面她肯。
httpx請求庫——異步請求
要發(fā)送異步請求時,我們需要調(diào)用AsyncClient鹰贵,具體示例代碼如下:
import httpx
import asyncio
headers={
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36'
}
async def function():
async with httpx.AsyncClient()as client:
response=await client.get('https://www.baidu.com',headers=headers)
print(response.text)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(function())
運行結(jié)果為:
首先我們導(dǎo)入了httpx庫和asyncio模塊晴氨,使用async來聲明function()方法并用來聲明with塊的客戶端打開和關(guān)閉,用await來聲明異步協(xié)程可等待對象response砾莱。接著我們調(diào)用asyncio.get_event_loop()方法進入事件循環(huán)瑞筐,再調(diào)用loop.run_until_complete(function())方法運行事件循環(huán),直到function運行結(jié)束腊瑟。
好了聚假,httpx請求庫講解到這里,想要了解更多的可以到httpx官方文檔進行學(xué)習(xí)闰非,接下來我們正式開始爬取小說膘格。
實戰(zhàn)演練
接下來我們將使用requests請求庫同步和httpx請求庫的異步,兩者結(jié)合爬取17k小說網(wǎng)里面的百萬字小說财松,利用XPath來做相應(yīng)的信息提取瘪贱。
Xpath小技巧
在使用Xpath之前,我們先來介紹使用Xpath的小技巧辆毡。
技巧一:快速獲取與內(nèi)容匹配的Xpath范圍菜秦。
我們可以將鼠標移動到我們想要獲取到內(nèi)容div的位置并右擊選擇copy,如下圖所示:
這樣我們就可以成功獲取到內(nèi)容匹配的Xpath范圍了舶掖。
技巧二:快速獲取Xpath范圍匹配的內(nèi)容球昨。
當(dāng)我們寫好Xpath匹配的范圍后,可以通過Chrome瀏覽器的小插件Xpath Helper眨攘,該插件的安裝方式很簡單主慰,在瀏覽器應(yīng)用商店中搜索Xpath Helper嚣州,點擊添加即可,如下圖所示:
使用方法也很簡單共螺,如下圖所示:
首先我們點擊剛剛添加的插件该肴,然后把已經(jīng)寫好的Xpath范圍寫到上圖2的方框里面,接著Xpath匹配的內(nèi)容將出現(xiàn)在上圖3方框里面藐不,接著被匹配內(nèi)容的背景色全部變成了金色匀哄,那么我們匹配內(nèi)容就一目了然了。
這樣我們就不需要每寫一個Xpath范圍就運行一次程序查看匹配內(nèi)容佳吞,大大提高了我們效率拱雏。
獲取小說章節(jié)名和鏈接
首先我們選取爬取的目標小說,并打開開發(fā)者工具底扳,如下圖所示:
我們通過上圖可以發(fā)現(xiàn),<div class="Main List"存放著我們所有小說章節(jié)名贡耽,點擊該章節(jié)就可以跳轉(zhuǎn)到對應(yīng)的章節(jié)頁面衷模,所以可以使用Xpath來通過這個div來獲取到我們想要的章節(jié)名和URL鏈接。
由于我們獲取的章節(jié)名和URL鏈接的網(wǎng)絡(luò)請求只有一個蒲赂,直接使用requests請求庫發(fā)送同步請求阱冶,主要代碼如下所示:
async def get_link(url):
response=requests.get(url)
response.encoding='utf-8'
Xpath=parsel.Selector(response.text)
dd=Xpath.xpath('/html/body/div[5]')
for a in dd:
#獲取每章節(jié)的url鏈接
links=a.xpath('./dl/dd/a/@href').extract()
linklist=['https://www.17k.com'+link for link in links]
#獲取每章節(jié)的名字
names=a.xpath('./dl/dd/a/span/text()').extract()
namelist=[name.replace('\n','').replace('\t','') for name in names]
#將名字和url鏈接合并成一個元組
name_link_list=zip(namelist,linklist)
首先我們用async聲明定義的get_text()方法使用requests庫發(fā)送get請求并把解碼方式改成'utf-8',接著使用parsel.Selector()方法將文本構(gòu)成Xpath解析對象滥嘴,最后我們將獲取到的URL鏈接和章節(jié)名合并成一個元組木蹬。
獲取到URL鏈接和章節(jié)名后,需要構(gòu)造一個task任務(wù)列表來作為異步協(xié)程的可等待對象若皱,具體代碼如下所示:
task=[]
for name,link in name_link_list:
task.append(get_text(name,link))
await asyncio.wait(task)
我們創(chuàng)建了一個空列表镊叁,用來存放get_text()方法,并使用await調(diào)用asyncio.wait()方法保存創(chuàng)建的task任務(wù)走触。
獲取每章節(jié)的小說內(nèi)容
由于需要發(fā)送很多個章節(jié)的網(wǎng)絡(luò)請求晦譬,所以我們采用httpx請求庫來發(fā)送異步請求。
主要代碼如下所示:
async def get_text(name,link):
async with httpx.AsyncClient() as client:
response=await client.get(link)
html=etree.HTML(response.text)
text=html.xpath('//*[@id="readArea"]/div[1]/div[2]/p/text()')
await save_data(name,text)
首先我們將上一步的獲取到的章節(jié)名和URL鏈接傳遞到用async聲明定義的get_text()方法互广,使用with塊調(diào)用httpx.AsyncClient()方法敛腌,并使用await來聲明client.get()是可等待對象,然后使用etree模塊來構(gòu)造一個XPath解析對象并自動修正HTML文本惫皱,將獲取到的小說內(nèi)容和章節(jié)名傳入到自定義方法save_data中像樊。
保存小說內(nèi)容到text文本中
好了,我們已經(jīng)把章節(jié)名和小說內(nèi)容獲取下來了旅敷,接下來就要把內(nèi)容保存在text文本中生棍,具體代碼如下所示:
async def save_data(name,text):
f=open(f'小說/{name}.txt','w',encoding='utf-8')
for texts in text:
f.write(texts)
f.write('\n')
print(f'正在爬取{name}')
老規(guī)矩,首先用async來聲明save_data()協(xié)程方法save_data()扫皱,然后使用open()方法足绅,將text文本文件打開并調(diào)用write()方法把小說內(nèi)容寫入文本中捷绑。
最后調(diào)用asyncio.get_event_loop()方法進入事件循環(huán),再調(diào)用loop.run_until_complete(get_link())方法運行事件循環(huán)氢妈,直到function運行結(jié)束粹污。具體代碼如下所示:
url='https://www.17k.com/list/2536069.html'
loop = asyncio.get_event_loop()
loop.run_until_complete(get_link(url))
結(jié)果展示
好了,異步爬蟲爬取小說就講解到這里了首量,感謝觀看W撤浴!加缘!