python aiohttp簡易使用教程
0. 前言
本文翻譯自aiohttp的官方文件,如有紕漏,歡迎指出。
aiohttp分為伺服器端和客戶端,本文只介紹客戶端。
另外我已經對 aiohttp 和 asyncio進行了封裝,可以參考我的 github 地址:
https://github.com/web-trump/ahttp
由於上下文的緣故,請求程式碼必須在一個非同步的函式中進行:
async def fn():
pass
1. aiohttp安裝
pip3 install aiohttp
1.1. 基本請求用法
-
async with aiohttp.
get(
'https://github.com')
as r:
-
await r.text()
其中r.text(), 可以在括號中指定解碼方式,編碼方式,例如
await resp.text(encoding='windows-1251')
或者也可以選擇不編碼,適合讀取影象等,是無法編碼的
await resp.read()
2.發起一個session請求
首先是匯入aiohttp模組:
import aiohttp然後我們試著獲取一個web原始碼,這裡以GitHub的公共Time-line頁面為例:
async with aiohttp.ClientSession() as session: async with session.get('https://api.github.com/events'上面的程式碼中,我們建立了一個 ClientSession 物件命名為session,然後通過session的get方法得到一個 ClientResponse 物件,命名為resp,get方法中傳入了一個必須的引數url,就是要獲得原始碼的http url。至此便通過協程完成了一個非同步IO的get請求。) as resp: print(resp.status) print(await resp.text())
有get請求當然有post請求,並且post請求也是一個協程:
session.post('http://httpbin.org/post', data=b'data')用法和get是一樣的,區別是post需要一個額外的引數data,即是需要post的資料。
除了get和post請求外,其他http的操作方法也是一樣的:
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')小記:
不要為每次的連線都建立一次session,一般情況下只需要建立一個session,然後使用這個session執行所有的請求。
每個session物件,內部包含了一個連線池,並且將會保持連線和連線複用(預設開啟)可以加快整體的效能。
3.在URL中傳遞引數
我們經常需要通過 get 在url中傳遞一些引數,引數將會作為url問號後面的一部分發給伺服器。在aiohttp的請求中,允許以dict的形式來表示問號後的引數。舉個例子,如果你想傳遞 key1=value1 key2=value2 到 httpbin.org/get 你可以使用下面的程式碼:
params = {'key1': 'value1', 'key2': 'value2'} async with session.get('http://httpbin.org/get', params=params) as resp: assert resp.url == 'http://httpbin.org/get?key2=value2&key1=value1'可以看到,程式碼正確的執行了,說明引數被正確的傳遞了進去。不管是一個引數兩個引數,還是更多的引數,都可以通過這種方式來傳遞。除了這種方式之外,還有另外一個,使用一個 list 來傳遞(這種方式可以傳遞一些特殊的引數,例如下面兩個key是相等的也可以正確傳遞):
params = [('key', 'value1'), ('key', 'value2')] async with session.get('http://httpbin.org/get', params=params) as r: assert r.url == 'http://httpbin.org/get?key=value2&key=value1'除了上面兩種,我們也可以直接通過傳遞字串作為引數來傳遞,但是需要注意,通過字串傳遞的特殊字元不會被編碼:
async with session.get('http://httpbin.org/get', params='key=value+1') as r: assert r.url == 'http://httpbin.org/get?key=value+1'
還是以GitHub的公共Time-line頁面為例,我們可以獲得頁面響應的內容:
async with session.get('https://api.github.com/events') as resp: print(await resp.text())執行之後,會打印出類似於如下的內容:
'[{"created_at":"2015-06-12T14:06:22Z","public":true,"actor":{...
resp的text方法,會自動將伺服器端返回的內容進行解碼--decode,當然我們也可以自定義編碼方式:
await resp.text(encoding='gb2312')除了text方法可以返回解碼後的內容外,我們也可以得到型別是位元組的內容:
print(await resp.read())執行的結果是:
b'[{"created_at":"2015-06-12T14:06:22Z","public":true,"actor":{...
gzip和deflate轉換編碼已經為你自動解碼。
小記:
text(),read()方法是把整個響應體讀入記憶體,如果你是獲取大量的資料,請考慮使用”位元組流“(streaming response)
5.特殊響應內容:json
如果我們獲取的頁面的響應內容是json,aiohttp內建了更好的方法來處理json:
async with session.get('https://api.github.com/events') as resp: print(await resp.json())如果因為某種原因而導致resp.json()解析json失敗,例如返回不是json字串等等,那麼resp.json()將丟擲一個錯誤,也可以給json()方法指定一個解碼方式:
print(await resp.json(
encoding='gb2312')) 或者傳遞一個函式進去:
print(await resp.json( lambda(x:x.replace('a','b')) ))
雖然json(),text(),read()很方便的能把響應的資料讀入到記憶體,但是我們仍然應該謹慎的使用它們,因為它們是把整個的響應體全部讀入了記憶體。即使你只是想下載幾個位元組大小的檔案,但這些方法卻將在記憶體中載入所有的資料。所以我們可以通過控制位元組數來控制讀入記憶體的響應內容:
async with session.get('https://api.github.com/events') as resp: await resp.content.read(10) #讀取前10個位元組一般地,我們應該使用以下的模式來把讀取的位元組流儲存到檔案中:
with open(filename, 'wb') as fd: while True: chunk = await resp.content.read(chunk_size) if not chunk: break fd.write(chunk)
如果你想新增請求頭,可以像get新增引數那樣以dict的形式,作為get或者post的引數進行請求:
import json url = 'https://api.github.com/some/endpoint' payload = {'some': 'data'} headers = {'content-type': 'application/json'}
await session.post(url,
data=json.dumps(payload),
headers=headers)
給伺服器傳送cookie,可以通過給 ClientSession 傳遞一個cookie引數:
url = 'http://httpbin.org/cookies' cookies = {'cookies_are': 'working'} async with ClientSession(cookies=cookies) as session: async with session.get(url) as resp: assert await resp.json() == { "cookies": {"cookies_are": "working"}}
可直接訪問連結 “httpbin.org/cookies”檢視當前cookie,訪問session中的cookie請見第10節。
9.post資料的幾種方式
(1)模擬表單post資料
payload = {'key1': 'value1', 'key2': 'value2'} async with session.post('http://httpbin.org/post', data=payload) as resp: print(await resp.text())
注意:data=dict的方式post的資料將被轉碼,和form提交資料是一樣的作用,如果你不想被轉碼,可以直接以字串的形式 data=str 提交,這樣就不會被轉碼。
(2)post json
import json url = 'https://api.github.com/some/endpoint' payload = {'some': 'data'}
async with session.post(url, data=json.dumps(payload)) as resp:
…
其實json.dumps(payload)返回的也是一個字串,只不過這個字串可以被識別為json格式
(3)post 小檔案
url = 'http://httpbin.org/post' files = {'file': open('report.xls', 'rb')}
await session.post(url, data=files)
可以設定好檔名和content-type:
url = 'http://httpbin.org/post' data = FormData() data.add_field('file', open('report.xls', 'rb'), filename='report.xls', content_type='application/vnd.ms-excel')
await session.post(url, data=data)
如果將檔案物件設定為資料引數,aiohttp將自動以位元組流的形式傳送給伺服器。
(4)post 大檔案
aiohttp支援多種型別的檔案以流媒體的形式上傳,所以我們可以在檔案未讀入記憶體的情況下發送大檔案。
@aiohttp.streamer def file_sender(writer, file_name=None): with open(file_name, 'rb') as f: chunk = f.read(2**16) while chunk: yield from writer.write(chunk) chunk = f.read(2**16)
# Then you can use file_sender
as a data provider:
async with session.post(‘http://httpbin.org/post’,
data=file_sender(file_name=‘huge_file’)) as resp:
print(await resp.text())
同時我們可以從一個url獲取檔案後,直接post給另一個url,並計算hash值:
async def feed_stream(resp, stream): h = hashlib.sha256()
resp = session.get(‘http://httpbin.org/post’)
stream = StreamReader()
loop.create_task(session.post(‘http://httpbin.org/post’, data=stream))
file_hash = await feed_stream(resp, stream)
因為響應內容型別是StreamReader,所以可以把get和post連線起來,同時進行post和get:
r = await session.get('http://python.org') await session.post('http://httpbin.org/post', data=r.content)(5)post預壓縮資料
在通過aiohttp傳送前就已經壓縮的資料, 呼叫壓縮函式的函式名(通常是deflate 或 zlib)作為content-encoding的值:
async def my_coroutine(session, headers, my_data): data = zlib.compress(my_data) headers = {'Content-Encoding': 'deflate'} async with session.post('http://httpbin.org/post', data=data, headers=headers) pass
ClientSession 用於在多個連線之間共享cookie:
async with aiohttp.ClientSession() as session: await session.get( 'http://httpbin.org/cookies/set?my_cookie=my_value') filtered = session.cookie_jar.filter_cookies('http://httpbin.org') assert filtered['my_cookie'].value == 'my_value' async with session.get('http://httpbin.org/cookies') as r: json_body = await r.json() assert json_body['cookies']['my_cookie'] == 'my_value'也可以為所有的連線設定共同的請求頭:
async with aiohttp.ClientSession( headers={"Authorization": "Basic bG9naW46cGFzcw=="}) as session: async with session.get("http://httpbin.org/headers") as r: json_body = await r.json() assert json_body['headers']['Authorization'] == \ 'Basic bG9naW46cGFzcw=='ClientSession 還支援 keep-alive連線和連線池(connection pooling)
11.cookie安全性
預設ClientSession使用的是嚴格模式的 aiohttp.CookieJar. RFC 2109,明確的禁止接受url和ip地址產生的cookie,只能接受 DNS 解析IP產生的cookie。可以通過設定aiohttp.CookieJar 的 unsafe=True 來配置:
jar = aiohttp.CookieJar(unsafe=True) session = aiohttp.ClientSession(cookie_jar=jar)
也可以理解為同時請求的數量,為了限制同時開啟的連線數量,我們可以將限制引數傳遞給聯結器:
conn = aiohttp.TCPConnector(limit=30)#同時最大進行連線的連線數為30,預設是100,limit=0的時候是無限制限制同時開啟限制同時開啟連線到同一端點的數量((host, port, is_ssl) 三的倍數),可以通過設定 limit_per_host 引數:
conn = aiohttp.TCPConnector(limit_per_host=30)#預設是0
我們可以指定域名伺服器的 IP 對我們提供的get或post的url進行解析:
from aiohttp.resolver import AsyncResolver
resolver = AsyncResolver(nameservers=[“8.8.8.8”, “8.8.4.4”])
conn = aiohttp.TCPConnector(resolver=resolver)
14.設定代理
aiohttp支援使用代理來訪問網頁:
async with aiohttp.ClientSession() as session: async with session.get("http://python.org", proxy="http://some.proxy.com") as resp: print(resp.status)當然也支援需要授權的頁面:
async with aiohttp.ClientSession() as session: proxy_auth = aiohttp.BasicAuth('user', 'pass') async with session.get("http://python.org", proxy="http://some.proxy.com", proxy_auth=proxy_auth) as resp: print(resp.status)或者通過這種方式來驗證授權:
session.get("http://python.org", proxy="http://user:[email protected]")
可以通過 resp.status來檢查狀態碼是不是200:
async with session.get('http://httpbin.org/get') as resp: assert resp.status == 200
我們可以直接使用 resp.headers 來檢視響應頭,得到的值型別是一個dict:
>>> resp.headers {'ACCESS-CONTROL-ALLOW-ORIGIN': '*', 'CONTENT-TYPE': 'application/json', 'DATE': 'Tue, 15 Jul 2014 16:49:51 GMT', 'SERVER': 'gunicorn/18.0', 'CONTENT-LENGTH': '331', 'CONNECTION': 'keep-alive'}或者我們可以檢視原生的響應頭:
>>> resp.raw_headers ((b'SERVER', b'nginx'), (b'DATE', b'Sat, 09 Jan 2016 20:28:40 GMT'), (b'CONTENT-TYPE', b'text/html; charset=utf-8'), (b'CONTENT-LENGTH', b'12150'), (b'CONNECTION', b'keep-alive'))
url = 'http://example.com/some/cookie/setting/url' async with session.get(url) as resp: print(resp.cookies)
18.重定向的響應頭
如果一個請求被重定向了,我們依然可以檢視被重定向之前的響應頭資訊:
>>> resp = await session.get('http://example.com/some/redirect/') >>> resp <ClientResponse(http://example.com/some/other/url/) [200]> >>> resp.history (<ClientResponse(http://example.com/some/redirect/) [301]>,)
預設的IO操作都有5分鐘的響應時間 我們可以通過 timeout 進行重寫:
async with session.get('https://github.com', timeout=60) as r: ...如果 timeout=None 或者 timeout=0 將不進行超時檢查,也就是不限時長