【2022 年】Python3 爬蟲教程 - aiohttp 的基本使用
在上一節(jié)中,我們介紹了異步爬蟲的基本原理和 asyncio 的基本用法,并且在最后簡(jiǎn)單提及了使用 aiohttp 來實(shí)現(xiàn)網(wǎng)頁(yè)爬取的過程。在本節(jié)中,我們來介紹一下 aiohttp 的常見用法。

1. 基本介紹
前面介紹的 asyncio 模塊內(nèi)部實(shí)現(xiàn)了對(duì) TCP、UDP、SSL 協(xié)議的異步操作,但是對(duì)于 HTTP 請(qǐng)求來說,我們就需要用到 aiohttp 來實(shí)現(xiàn)了。
aiohttp 是一個(gè)基于 asyncio 的異步 HTTP 網(wǎng)絡(luò)模塊,它既提供了服務(wù)端,又提供了客戶端。其中我們用服務(wù)端可以搭建一個(gè)支持異步處理的服務(wù)器,就是用來處理請(qǐng)求并返回響應(yīng)的,類似于 Django、Flask、Tornado 等一些 Web 服務(wù)器。而客戶端可以用來發(fā)起請(qǐng)求,類似于使用 requests 發(fā)起一個(gè) HTTP 請(qǐng)求然后獲得響應(yīng),但 requests 發(fā)起的是同步的網(wǎng)絡(luò)請(qǐng)求,aiohttp 則是異步的。
本節(jié)中,我們主要了解一下 aiohttp 客戶端部分的用法。
2. 基本實(shí)例
首先,我們來看一個(gè)基本的 aiohttp 請(qǐng)求案例,代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text(), response.status
async def main():
async with aiohttp.ClientSession() as session:
html, status = await fetch(session, 'https://cuiqingcai.com')
print(f'html: {html[:100]}...')
print(f'status: {status}')
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
這里我們使用 aiohttp 來爬取我的個(gè)人博客,獲得了源碼和響應(yīng)狀態(tài)碼并輸出出來,運(yùn)行結(jié)果如下:
1
2
3
4
5
6
html:
status: 200
這里網(wǎng)頁(yè)源碼過長(zhǎng),只截取輸出了一部分。可以看到,這里我們成功獲取了網(wǎng)頁(yè)的源代碼及響應(yīng)狀態(tài)碼 200,也就完成了一次基本的 HTTP 請(qǐng)求,即我們成功使用 aiohttp 通過異步的方式來進(jìn)行了網(wǎng)頁(yè)爬取。當(dāng)然,這個(gè)操作用之前講的 requests 也可以做到。
可以看到,其請(qǐng)求方法的定義和之前有了明顯的區(qū)別,主要有如下幾點(diǎn):
首先在導(dǎo)入庫(kù)的時(shí)候,我們除了必須要引入 aiohttp 這個(gè)庫(kù)之外,還必須要引入 asyncio 這個(gè)庫(kù)。因?yàn)橐獙?shí)現(xiàn)異步爬取,需要啟動(dòng)協(xié)程,而協(xié)程則需要借助于 asyncio 里面的事件循環(huán)來執(zhí)行。除了事件循環(huán),asyncio 里面也提供了很多基礎(chǔ)的異步操作。
異步爬取方法的定義和之前有所不同,在每個(gè)異步方法前面統(tǒng)一要加 async 來修飾。
with as 語(yǔ)句前面同樣需要加 async 來修飾。在 Python 中,with as 語(yǔ)句用于聲明一個(gè)上下文管理器,能夠幫我們自動(dòng)分配和釋放資源。而在異步方法中,with as 前面加上 async 代表聲明一個(gè)支持異步的上下文管理器。
對(duì)于一些返回 coroutine 的操作,前面需要加 await 來修飾。比如 response 調(diào)用 text 方法,查詢 API 可以發(fā)現(xiàn),其返回的是 coroutine 對(duì)象,那么前面就要加 await;而對(duì)于狀態(tài)碼來說,其返回值就是一個(gè)數(shù)值類型,那么前面就不需要加 await。所以,這里可以按照實(shí)際情況處理,參考官方文檔說明,看看其對(duì)應(yīng)的返回值是怎樣的類型,然后決定加不加 await 就可以了。
最后,定義完爬取方法之后,實(shí)際上是 main 方法調(diào)用了 fetch 方法。要運(yùn)行的話,必須要啟用事件循環(huán),而事件循環(huán)就需要使用 asyncio 庫(kù),然后使用 run_until_complete 方法來運(yùn)行。
注意:在 Python 3.7 及以后的版本中,我們可以使用 asyncio.run(main()) 來代替最后的啟動(dòng)操作,不需要顯示聲明事件循環(huán),run 方法內(nèi)部會(huì)自動(dòng)啟動(dòng)一個(gè)事件循環(huán)。但這里為了兼容更多的 Python 版本,依然還是顯式聲明了事件循環(huán)。
3. URL 參數(shù)設(shè)置
對(duì)于 URL 參數(shù)的設(shè)置,我們可以借助于 params 參數(shù),傳入一個(gè)字典即可,示例如下:
1
2
3
4
5
6
7
8
9
10
11
import aiohttp
import asyncio
async def main():
params = {'name': 'germey', 'age': 25}
async with aiohttp.ClientSession() as session:
async with session.get('https://httpbin.org/get', params=params) as response:
print(await response.text())
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"args": {
"age": "25",
"name": "germey"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "Python/3.7 aiohttp/3.6.2",
"X-Amzn-Trace-Id": "Root=1-5e85eed2-d240ac90f4dddf40b4723ef0"
},
"origin": "17.20.255.122",
"url": "https://httpbin.org/get?name=germey&age=25"
}
這里可以看到,其實(shí)際請(qǐng)求的 URL 為 https://httpbin.org/get?name=germey&age=25,其 URL 請(qǐng)求參數(shù)就對(duì)應(yīng)了 params 的內(nèi)容。
4. 其他請(qǐng)求類型
另外,aiohttp 還支持其他請(qǐng)求類型,如 POST、PUT、DELETE 等,這和 requests 的使用方式有點(diǎn)類似,示例如下:
1
2
3
4
5
6
session.post('http://httpbin.org/post', data=b'data')
session.put('http://httpbin.org/put', data=b'data')
session.delete('http://httpbin.org/delete')
session.head('http://httpbin.org/get')
session.options('http://httpbin.org/get')
session.patch('http://httpbin.org/patch', data=b'data')
要使用這些方法,只需要把對(duì)應(yīng)的方法和參數(shù)替換一下即可。
5. POST 請(qǐng)求
對(duì)于 POST 表單提交,其對(duì)應(yīng)的請(qǐng)求頭的 Content-Type 為 application/x-www-form-urlencoded,我們可以用如下方式來實(shí)現(xiàn),代碼示例如下:
1
2
3
4
5
6
7
8
9
10
11
import aiohttp
import asyncio
async def main():
data = {'name': 'germey', 'age': 25}
async with aiohttp.ClientSession() as session:
async with session.post('https://httpbin.org/post', data=data) as response:
print(await response.text())
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"args": {},
"data": "",
"files": {},
"form": {
"age": "25",
"name": "germey"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "18",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Python/3.7 aiohttp/3.6.2",
"X-Amzn-Trace-Id": "Root=1-5e85f0b2-9017ea603a68dc285e0552d0"
},
"json": null,
"origin": "17.20.255.58",
"url": "https://httpbin.org/post"
}
對(duì)于 POST JSON 數(shù)據(jù)提交,其對(duì)應(yīng)的請(qǐng)求頭的 Content-Type 為 application/json,我們只需要將 post 方法的 data 參數(shù)改成 json 即可,代碼示例如下:
1
2
3
4
5
async def main():
data = {'name': 'germey', 'age': 25}
async with aiohttp.ClientSession() as session:
async with session.post('https://httpbin.org/post', json=data) as response:
print(await response.text())
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"args": {},
"data": "{\"name\": \"germey\", \"age\": 25}",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "29",
"Content-Type": "application/json",
"Host": "httpbin.org",
"User-Agent": "Python/3.7 aiohttp/3.6.2",
"X-Amzn-Trace-Id": "Root=1-5e85f03e-c91c9a20c79b9780dbed7540"
},
"json": {
"age": 25,
"name": "germey"
},
"origin": "17.20.255.58",
"url": "https://httpbin.org/post"
}
可以發(fā)現(xiàn),其實(shí)現(xiàn)也和 requests 非常像,不同的參數(shù)支持不同類型的請(qǐng)求內(nèi)容。
6. 響應(yīng)
對(duì)于響應(yīng)來說,我們可以用如下方法分別獲取響應(yīng)的狀態(tài)碼、響應(yīng)頭、響應(yīng)體、響應(yīng)體二進(jìn)制內(nèi)容、響應(yīng)體 JSON 結(jié)果,示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import aiohttp
import asyncio
async def main():
data = {'name': 'germey', 'age': 25}
async with aiohttp.ClientSession() as session:
async with session.post('https://httpbin.org/post', data=data) as response:
print('status:', response.status)
print('headers:', response.headers)
print('body:', await response.text())
print('bytes:', await response.read())
print('json:', await response.json())
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
status: 200
headers:
body: {
"args": {},
"data": "",
"files": {},
"form": {
"age": "25",
"name": "germey"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "18",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Python/3.7 aiohttp/3.6.2",
"X-Amzn-Trace-Id": "Root=1-5e85f2f1-f55326ff5800b15886c8e029"
},
"json": null,
"origin": "17.20.255.58",
"url": "https://httpbin.org/post"
}
bytes: b'{\n "args": {}, \n "data": "", \n "files": {}, \n "form": {\n "age": "25", \n "name": "germey"\n }, \n "headers": {\n "Accept": "*/*", \n "Accept-Encoding": "gzip, deflate", \n "Content-Length": "18", \n "Content-Type": "application/x-www-form-urlencoded", \n "Host": "httpbin.org", \n "User-Agent": "Python/3.7 aiohttp/3.6.2", \n "X-Amzn-Trace-Id": "Root=1-5e85f2f1-f55326ff5800b15886c8e029"\n }, \n "json": null, \n "origin": "17.20.255.58", \n "url": "https://httpbin.org/post"\n}\n'
json: {'args': {}, 'data': '', 'files': {}, 'form': {'age': '25', 'name': 'germey'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Content-Length': '18', 'Content-Type': 'application/x-www-form-urlencoded', 'Host': 'httpbin.org', 'User-Agent': 'Python/3.7 aiohttp/3.6.2', 'X-Amzn-Trace-Id': 'Root=1-5e85f2f1-f55326ff5800b15886c8e029'}, 'json': None, 'origin': '17.20.255.58', 'url': 'https://httpbin.org/post'}
這里我們可以看到有些字段前面需要加 await,有的則不需要。其原則是,如果它返回的是一個(gè) coroutine 對(duì)象(如 async 修飾的方法),那么前面就要加 await,具體可以看 aiohttp 的 API,其鏈接為:https://docs.aiohttp.org/en/stable/client_reference.html。
7. 超時(shí)設(shè)置
對(duì)于超時(shí)設(shè)置,我們可以借助 ClientTimeout 對(duì)象,比如這里要設(shè)置 1 秒的超時(shí),可以這么實(shí)現(xiàn):
1
2
3
4
5
6
7
8
9
10
11
import aiohttp
import asyncio
async def main():
timeout = aiohttp.ClientTimeout(total=1)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get('https://httpbin.org/get') as response:
print('status:', response.status)
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
如果在 1 秒之內(nèi)成功獲取響應(yīng)的話,運(yùn)行結(jié)果如下:
1
200
如果超時(shí)的話,會(huì)拋出 TimeoutError 異常,其類型為 asyncio.TimeoutError,我們?cè)龠M(jìn)行異常捕獲即可。
另外,聲明 ClientTimeout 對(duì)象時(shí)還有其他參數(shù),如 connect、socket_connect 等,詳細(xì)可以參考官方文檔:https://docs.aiohttp.org/en/stable/client_quickstart.html#timeouts。
8. 并發(fā)限制
由于 aiohttp 可以支持非常大的并發(fā),比如上萬(wàn)、十萬(wàn)、百萬(wàn)都是能做到的,但對(duì)于這么大的并發(fā)量,目標(biāo)網(wǎng)站很可能在短時(shí)間內(nèi)無法響應(yīng),而且很可能瞬時(shí)間將目標(biāo)網(wǎng)站爬掛掉,所以我們需要控制一下爬取的并發(fā)量。
一般情況下,我們可以借助于 asyncio 的 Semaphore 來控制并發(fā)量,示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import asyncio
import aiohttp
CONCURRENCY = 5
URL = 'https://www.baidu.com'
semaphore = asyncio.Semaphore(CONCURRENCY)
session = None
async def scrape_api():
async with semaphore:
print('scraping', URL)
async with session.get(URL) as response:
await asyncio.sleep(1)
return await response.text()
async def main():
global session
session = aiohttp.ClientSession()
scrape_index_tasks = [asyncio.ensure_future(scrape_api()) for _ in range(10000)]
await asyncio.gather(*scrape_index_tasks)
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
這里我們聲明了 CONCURRENCY(代表爬取的最大并發(fā)量)為 5,同時(shí)聲明爬取的目標(biāo) URL 為百度。接著,我們借助于 Semaphore 創(chuàng)建了一個(gè)信號(hào)量對(duì)象,將其賦值為 semaphore,這樣我們就可以用它來控制最大并發(fā)量了。怎么使用呢?這里我們把它直接放置在對(duì)應(yīng)的爬取方法里面,使用 async with 語(yǔ)句將 semaphore 作為上下文對(duì)象即可。這樣的話,信號(hào)量可以控制進(jìn)入爬取的最大協(xié)程數(shù)量,即我們聲明的 CONCURRENCY 的值。
在 main 方法里面,我們聲明了 10000 個(gè) task,將其傳遞給 gather 方法運(yùn)行。倘若不加以限制,這 10000 個(gè) task 會(huì)被同時(shí)執(zhí)行,并發(fā)數(shù)量太大。但有了信號(hào)量的控制之后,同時(shí)運(yùn)行的 task 的數(shù)量最大會(huì)被控制在 5 個(gè),這樣就能給 aiohttp 限制速度了。
9. 總結(jié)
本節(jié)我們了解了 aiohttp 的基本使用方法,更詳細(xì)的內(nèi)容還是推薦大家到官方文檔查閱,詳見 https://docs.aiohttp.org/。
本節(jié)代碼:https://github.com/Python3WebSpider/AsyncTest。
JSON Python
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。