送書 | 兩百四十多萬字,六百章的小說秒爬完
大家好!啃書君。
相信很多人喜歡在空閑的時間里看小說,甚至有小部分人為了追小說而熬夜看,那么問題來了,喜歡看小說的小伙伴在評論區(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í)行其他程序,那么就白白浪費(fèi)了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ù)的進(jìn)行切換,切換條件一般就是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爬蟲使用最多的庫。
在命令行中運(yùn)行如下代碼,即可完成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)
運(yùn)行部分結(jié)果如下圖:
首先我們導(dǎo)入requests庫,創(chuàng)建請求頭,請求頭中包含了User-Agent字段信息,也就是瀏覽器標(biāo)識信息,如果不加這個,網(wǎng)站就可能禁止抓取,然后調(diào)用get()方法發(fā)送get請求,傳入的參數(shù)為URL鏈接和請求頭,這樣簡單的網(wǎng)絡(luò)請求就完成了。
這里我們返回打印輸出的是百度的源代碼,大家可以根據(jù)需求返回輸出其他類型的數(shù)據(jù)。
需要注意的是:
百度源代碼的head部分的編碼為:utf-8,如下圖所示:
我們利用requests庫的方法來查看默認(rèn)的編碼類型是什么,具體代碼如下所示:
import requests
url = 'https://www.baidu.com'
response = requests.get(url)
print(response.encoding)
運(yùn)行結(jié)果為:ISO-8859-1
由于默認(rèn)的編碼類型不同,所以需要更改輸出的編碼類型,更改方式也很簡單,只需要在返回數(shù)據(jù)前根據(jù)head部分的編碼來添加以下代碼即可:
response.encoding='編碼類型'
除了使用get()方法實現(xiàn)get請求外,還可以使用post()、put()、delete()等方法來發(fā)送其他網(wǎng)絡(luò)請求,在這里就不一一演示了,關(guān)于更多的requests網(wǎng)絡(luò)請求庫用法可以到官方參考文檔進(jìn)行查看,我們今天主要講解可以發(fā)送異步請求的aiohttp庫和httpx庫。
asyncio模塊
在講解異步請求aiohttp庫和httpx庫請求前,我們需要先了解一下協(xié)程。
協(xié)程是一種比線程更加輕量級的存在,讓單線程跑出了并發(fā)的效果,對計算資源的利用率高,開銷小的系統(tǒng)調(diào)度機(jī)制。
Python中實現(xiàn)協(xié)程的模塊有很多,我們主要來講解asyncio模塊,從asyncio模塊中直接獲取一個EventLoop的引用,把需要執(zhí)行的協(xié)程放在EventLoop中執(zhí)行,這就實現(xiàn)了異步協(xié)程。
協(xié)程通過async語法進(jìn)行聲明為異步協(xié)程方法,await語法進(jìn)行聲明為異步協(xié)程可等待對象,是編寫asyncio應(yīng)用的推薦方式,具體示例代碼如下:
import asyncio
import time
async def function1():
print('I am Superman?。?!')
await asyncio.sleep(3)
print('function1')
async def function2():
print('I am Batman!?。?)
await asyncio.sleep(2)
print('function2')
async def function3():
print('I am iron man?。?!')
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)
運(yùn)行結(jié)果為:
I am Superman?。?!
I am Batman!??!
I am iron man!?。?br>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()運(yùn)行協(xié)程程序。
注意:當(dāng)協(xié)程程序出現(xiàn)了同步操作的時候,異步協(xié)程就中斷了。
例如把上面的示例代碼中的await asyncio.sleep()換成time.time(),運(yùn)行結(jié)果為:
I am Superman?。。?br>function1
I am Batman?。?!
function2
I am iron man?。。?br>function3
9.014737844467163
所以在協(xié)程程序中,盡量不使用同步操作。
好了,asyncio模塊我們講解到這里,想要了解更多的可以進(jìn)入asyncio官方文檔進(jìn)行查看。
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())
運(yùn)行結(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進(jìn)行聲明,然后調(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é)束。
注意:在調(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)進(jìn)行學(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即可正常運(yùn)行。
具體示例代碼如下:
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)
運(yùn)行結(jié)果如下圖所示:
注意:httpx使用的默認(rèn)utf-8進(jìn)行編碼來解碼響應(yīng)。
httpx請求庫——同步請求高級用法
當(dāng)發(fā)送請求時,httpx必須為每個請求建立一個新連接(連接不會被重用),隨著對主機(jī)的 請求數(shù)量增加,網(wǎng)絡(luò)請求的效率就是變得很低。
這時我們可以用Client實例來使用HTTP連接池,這樣當(dāng)我們主機(jī)發(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())
運(yùn)行結(jié)果為:
首先我們導(dǎo)入了httpx庫和asyncio模塊,使用async來聲明function()方法并用來聲明with塊的客戶端打開和關(guān)閉,用await來聲明異步協(xié)程可等待對象response。接著我們調(diào)用asyncio.get_event_loop()方法進(jìn)入事件循環(huán),再調(diào)用loop.run_until_complete(function())方法運(yùn)行事件循環(huán),直到function運(yùn)行結(jié)束。
好了,httpx請求庫講解到這里,想要了解更多的可以到httpx官方文檔進(jìn)行學(xué)習(xí),接下來我們正式開始爬取小說。
實戰(zhàn)演練
接下來我們將使用requests請求庫同步和httpx請求庫的異步,兩者結(jié)合爬取17k小說網(wǎng)里面的百萬字小說,利用XPath來做相應(yīng)的信息提取。
Xpath小技巧
在使用Xpath之前,我們先來介紹使用Xpath的小技巧。
技巧一:快速獲取與內(nèi)容匹配的Xpath范圍。
我們可以將鼠標(biāo)移動到我們想要獲取到內(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范圍就運(yùn)行一次程序查看匹配內(nèi)容,大大提高了我們效率。
獲取小說章節(jié)名和鏈接
首先我們選取爬取的目標(biāo)小說,并打開開發(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()方法進(jìn)入事件循環(huán),再調(diào)用loop.run_until_complete(get_link())方法運(yùn)行事件循環(huán),直到function運(yùn)行結(jié)束。具體代碼如下所示:
url='https://www.17k.com/list/2536069.html'
loop = asyncio.get_event_loop()
loop.run_until_complete(get_link(url))
結(jié)果展示
好了,異步爬蟲爬取小說就講解到這里了,感謝觀看?。?!
送書
又到了每周三的送書時刻,今天給大家?guī)淼氖恰?span style="outline: 0px;font-family: Arial, "microsoft yahei";font-size: 16px;font-weight: 700;letter-spacing: 0.544px;">Python Django Web從入門到項目實戰(zhàn)(視頻版)》
Python 的 Django 框架是目前流行的一款重量級網(wǎng)站開發(fā)框架,具備簡單易學(xué)、搭建快速、功能強(qiáng)大等特點。
本書從簡單的 HTML、CSS、JavaScript 開始介紹,再到 Django 的基礎(chǔ)知識,融入了大量的代碼案例、重點提示、圖片展示,做到了手把手教授。
本書基于 Django 3.0.7 版本、Python 3.8.5 版本、Rest Framework 3.11.1 版本、Vue.js 2.6.10 版本、數(shù)據(jù)庫 MySQL 8.0 版本進(jìn)行講解。
本書還提供了一個商業(yè)級別的項目案例,采用目前主流的前后端分離開發(fā)技術(shù),以便讀者可以體驗正式項目的開發(fā)過程。
熟練掌握本書內(nèi)容后,讀者將達(dá)到中級 Web 項目開發(fā)工程師的技術(shù)水平。


點擊下方回復(fù):送書 即可!
