一、Session對(duì)象
Session 對(duì)象允許你能夠在跨請(qǐng)求的同時(shí)保持某些參數(shù),它也會(huì)在同一個(gè) Session 實(shí)例發(fā)出的所有請(qǐng)求之間保持 cookies,并且在此期間使用 urllib3 的 connection pooling(連接池)功能。所以,如果你向同一主機(jī)發(fā)送多個(gè)請(qǐng)求,底層的 TCP 連接將會(huì)被重用,從而帶來(lái)顯著的性能提升。
Session 對(duì)象擁有那些主要的 Requests API 的所有方法。
下面實(shí)驗(yàn)一下在跨請(qǐng)求中保持 cookies:
s = requests.Session()
s.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
r = s.get('http://httpbin.org/cookies')
print(r.text)
# '{"cookies": {"sessioncookie": "123456789"}}'
Sessions 也可用來(lái)為請(qǐng)求方法提供默認(rèn)數(shù)據(jù),這是通過(guò)為一個(gè) Session 對(duì)象的屬性賦值來(lái)實(shí)現(xiàn)的:
s = requests.Session()
s.auth = ('user', 'pass')
s.headers.update({'x-test': 'true'})
# both 'x-test' and 'x-test2' are sent
s.get('http://httpbin.org/headers', headers={'x-test2': 'true'})
任何你傳遞給請(qǐng)求方法的字典都會(huì)與已經(jīng)設(shè)置的會(huì)話層數(shù)據(jù)進(jìn)行合并,方法層的參數(shù)將覆蓋會(huì)話的參數(shù)。
不過(guò)需要注意的是,就算使用了會(huì)話,方法層的參數(shù)也不會(huì)被跨請(qǐng)求保持。下面的例子只會(huì)和第一個(gè)請(qǐng)求發(fā)送 cookies ,而非第二個(gè):
s = requests.Session()
r = s.get('http://httpbin.org/cookies', cookies={'from-my': 'browser'})
print(r.text)
# '{"cookies": {"from-my": "browser"}}'
r = s.get('http://httpbin.org/cookies')
print(r.text)
# '{"cookies": {}}'
如果你要手動(dòng)為會(huì)話添加 cookie,可以使用Cookie 系列函數(shù)來(lái)操縱 Session.cookies。
Sessions 還可以被用作上下文管理器:
with requests.Session() as s:
s.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
這樣就能確保 with 區(qū)塊退出后會(huì)話能被關(guān)閉,即使發(fā)生了異常也一樣。
從字典參數(shù)中移除一個(gè)值
有時(shí)候你可能會(huì)想省略字典參數(shù)中一些會(huì)話層的鍵。要做到這一點(diǎn),你只需簡(jiǎn)單地在方法層參數(shù)中將那個(gè)鍵的值設(shè)置為 None ,那個(gè)鍵就會(huì)被自動(dòng)省略掉。
包含在一個(gè)會(huì)話中的所有數(shù)據(jù)你都可以直接使用。
二、請(qǐng)求和請(qǐng)求對(duì)象
任何時(shí)候進(jìn)行了類似requests.get() 的調(diào)用,你其實(shí)做的就是兩件事兒:第一,構(gòu)建一個(gè) Request 對(duì)象,該對(duì)象將被發(fā)送到某個(gè)服務(wù)器請(qǐng)求或查詢一些資源;第二,一旦 Requests 得到一個(gè)從服務(wù)器返回的響應(yīng)就會(huì)產(chǎn)生一個(gè) Response 對(duì)象。該 Response 對(duì)象包含服務(wù)器返回的所有信息,也包含你原來(lái)創(chuàng)建的 Request 對(duì)象。
下面是一個(gè)簡(jiǎn)單的請(qǐng)求,從維基的服務(wù)器上獲取一些非常重要的信息:
>>> r = requests.get('http://en.wikipedia.org/wiki/Monty_Python')
如果我們想訪問(wèn)服務(wù)器返回的響應(yīng)頭部信息,可以這樣做:
>>> r.headers
{'content-length': '56170', 'x-content-type-options': 'nosniff', 'x-cache':
'HIT from cp1006.eqiad.wmnet, MISS from cp1010.eqiad.wmnet', 'content-encoding':
'gzip', 'age': '3080', 'content-language': 'en', 'vary': 'Accept-Encoding,Cookie',
'server': 'Apache', 'last-modified': 'Wed, 13 Jun 2012 01:33:50 GMT',
'connection': 'close', 'cache-control': 'private, s-maxage=0, max-age=0,
must-revalidate', 'date': 'Thu, 14 Jun 2012 12:59:39 GMT', 'content-type':
'text/html; charset=UTF-8', 'x-cache-lookup': 'HIT from cp1006.eqiad.wmnet:3128,
MISS from cp1010.eqiad.wmnet:80'}
然而,如果獲取我們發(fā)送到服務(wù)器的請(qǐng)求頭部,可以直接訪問(wèn)該請(qǐng)求的 headers 屬性:
>>> r.request.headers
{'Accept-Encoding': 'identity, deflate, compress, gzip',
'Accept': '*/*', 'User-Agent': 'python-requests/1.2.0'}
三、預(yù)備請(qǐng)求
當(dāng)你從 API 或者 Session 調(diào)用中收到一個(gè) Response 對(duì)象時(shí),request 屬性實(shí)際上使用的是 PreparedRequest。這玩意兒有什么用呢?比如有時(shí)候在發(fā)送請(qǐng)求之前,你如果需要對(duì)body或者 header(或者其它什么東西)做一些額外處理就很有用了,像下面這樣:
from requests import Request, Session
s = Session()
req = Request('POST', url, data=data, headers=headers)
prepped = req.prepare()
# do something with prepped.body
prepped.body = 'No, I want exactly this as the body.'
# do something with prepped.headers
del prepped.headers['Content-Type']
resp = s.send(prepped,
stream=stream,
verify=verify,
proxies=proxies,
cert=cert,
timeout=timeout
)
print(resp.status_code)
然而,上述代碼會(huì)失去 Requests Session對(duì)象的一些優(yōu)勢(shì),尤其 Session級(jí)別的狀態(tài),例如 cookies就不會(huì)被應(yīng)用到你的請(qǐng)求上去。要獲取一個(gè)帶有狀態(tài)的 PreparedRequest, 請(qǐng)使用 Session.prepare_request() 取代 Request.prepare(),像下面這樣:
from requests import Request, Session
s = Session()
req = Request('GET', url, data=data, headers=headers)
prepped = s.prepare_request(req)
# do something with prepped.body
prepped.body = 'Seriously, send exactly these bytes.'
# do something with prepped.headers
prepped.headers['Keep-Dead'] = 'parrot'
resp = s.send(prepped,
stream=stream,
verify=verify,
proxies=proxies,
cert=cert,
timeout=timeout
)
print(resp.status_code)
當(dāng)你使用預(yù)備請(qǐng)求流的時(shí)候,請(qǐng)記住它并不會(huì)考慮到用戶環(huán)境。如果你希望通過(guò)設(shè)置環(huán)境變量來(lái)改變請(qǐng)求行為的時(shí)候,這可能會(huì)帶來(lái)一些問(wèn)題。例如:在 REQUESTS_CA_BUNDLE 中指定的自簽名 SSL 證書將不被考慮。因此,SSL:CERTIFICATE_VERIFY_FAILED被拋出。你可以通過(guò)將環(huán)境設(shè)置合并到會(huì)話中來(lái)解決此問(wèn)題:
from requests import Request, Session
s = Session()
req = Request('GET', url)
prepped = s.prepare_request(req)
# Merge environment settings into session
settings = s.merge_environment_settings(prepped.url, None, None, None, None)
resp = s.send(prepped, **settings)
print(resp.status_code)
四、SSL證書驗(yàn)證
就像 web 瀏覽器一樣,Requests可以為 HTTPS 請(qǐng)求驗(yàn)證 SSL 證書。SSL 驗(yàn)證默認(rèn)是開(kāi)啟的,如果證書驗(yàn)證失敗,Requests會(huì)拋出 SSLError:
>>> requests.get('https://requestb.in')
requests.exceptions.SSLError: hostname 'requestb.in' doesn't match either of '*.herokuapp.com', 'herokuapp.com'
由于該域名我沒(méi)有設(shè)置 SSL,所以失敗了。但 Github 設(shè)置了 SSL:
>>> requests.get('https://github.com', verify=True)
<Response [200]>
你可以為 verify 傳入 CA_BUNDLE 文件的路徑,或者包含可信任 CA 證書文件的文件夾路徑
>>> requests.get('https://github.com', verify='/path/to/certfile')
或者將其保持在會(huì)話中:
s = requests.Session()
s.verify = '/path/to/certfile'
如果 verify 設(shè)為文件夾路徑,文件夾必須通過(guò) OpenSSL 提供的 c_rehash 工具處理。
你還可以通過(guò) REQUESTS_CA_BUNDLE 環(huán)境變量定義可信任 CA 列表。
如果你將verify設(shè)置為 False,Requests 也能忽略對(duì) SSL 證書的驗(yàn)證。
>>> requests.get('https://kennethreitz.org', verify=False)
<Response [200]>
默認(rèn)情況下, verify 是設(shè)置為 True,verify 選項(xiàng)僅應(yīng)用于主機(jī)證書。
五、客戶端證書
你也可以指定一個(gè)本地證書作為客戶端證書,可以是單個(gè)文件(包含密鑰和證書)或一個(gè)包含兩個(gè)文件路徑的元組:
>>> requests.get('https://kennethreitz.org', cert=('/path/client.cert', '/path/client.key'))
<Response [200]>
或者將其保持在會(huì)話中:
s = requests.Session()
s.cert = '/path/client.cert'
如果你指定了一個(gè)錯(cuò)誤路徑或一個(gè)無(wú)效的證書,Requests會(huì)扔給你一個(gè) SSLError:
>>> requests.get('https://kennethreitz.org', cert='/wrong_path/client.pem')
SSLError: [Errno 336265225] _ssl.c:347: error:140B0009:SSL routines:SSL_CTX_use_PrivateKey_file:PEM lib
本地證書的私有 key 必須是解密狀態(tài),因?yàn)楫?dāng)前 Requests 還不支持使用加密的 key。
六、CA 證書
Requests 默認(rèn)附帶了一套它信任的根證書,來(lái)自于 Mozilla trust store。然而,它們只有在每次 Requests 更新時(shí)才會(huì)被更新。這就意味著如果你固定使用某一版本的 Requests,你的證書有可能已經(jīng) OUT 了。
從 Requests 2.4.0 版之后,如果系統(tǒng)中裝了 certifi 包,Requests 會(huì)試圖使用它里邊的證書。這樣用戶就可以在不修改代碼的情況下更新他們的可信任證書啦。為了安全起見(jiàn),我們建議你經(jīng)常更新 certifi!
七、響應(yīng)體內(nèi)容工作流
默認(rèn)情況下,當(dāng)你發(fā)起網(wǎng)絡(luò)請(qǐng)求后響應(yīng)體便會(huì)立即被下載。你可以通過(guò) stream參數(shù)修改這個(gè)行為,推遲下載響應(yīng)體直到Response.content 屬性被訪問(wèn)為止:
tarball_url = 'https://github.com/requests/requests/tarball/master'
r = requests.get(tarball_url, stream=True)
你可以進(jìn)一步使用Response.iter_content()和 Response.iter_lines()方法來(lái)控制工作流,或者以 Response.raw 從底層 urllib3 的urllib3.HTTPResponse中讀取未解碼的響應(yīng)體。
如果你在請(qǐng)求中把 stream參數(shù)設(shè)為True,Requests 無(wú)法將連接釋放回連接池,除非你消耗了所有的數(shù)據(jù),或者調(diào)用了 Response.close()方法。這樣會(huì)帶來(lái)連接效率低下的問(wèn)題。如果你指定stream=True的同時(shí)還在部分讀取響應(yīng)體(或者完全沒(méi)有讀?。敲茨憔蛻?yīng)該考慮使用with 語(yǔ)句發(fā)送請(qǐng)求,這樣可以保證請(qǐng)求一定會(huì)被關(guān)閉:
with requests.get('http://httpbin.org/get', stream=True) as r:
# Do things with the response here.
八、保持活動(dòng)狀態(tài)
好消息 —— 歸功于 urllib3,在同一會(huì)話內(nèi)的持久連接是 100% 自動(dòng)處理的!同一會(huì)話中發(fā)出的任何請(qǐng)求都會(huì)自動(dòng)復(fù)用恰當(dāng)?shù)倪B接!
注意:只有等到所有的響應(yīng)體數(shù)據(jù)被讀取完畢后,連接才能被釋放回連接池以待重用;所以,請(qǐng)確保將 stream參數(shù)設(shè)置為 False 或讀取 Response 對(duì)象的content屬性。
九、流上傳
Requests 支持以流的形式上傳數(shù)據(jù),該特性允許你發(fā)送尺寸巨大的數(shù)據(jù)流或文件而無(wú)需先把它們讀入內(nèi)存。要使用流上傳,僅需為你的請(qǐng)求體提供一個(gè)類文件對(duì)象即可:
with open('massive-body', 'rb') as f:
requests.post('http://some.url/streamed', data=f)
我們強(qiáng)烈建議你用二進(jìn)制模式(binary mode)來(lái)打開(kāi)文件。因?yàn)?Requests 可能會(huì)為你提供頭部信息中的 Content-Length,在這種情況下該值會(huì)被設(shè)置為文件的字節(jié)數(shù)。如果你用文本模式打開(kāi)文件,就可能會(huì)出現(xiàn)錯(cuò)誤
十、分塊編碼請(qǐng)求
Requests 也支持分塊傳輸編碼,要發(fā)送一個(gè)分塊編碼請(qǐng)求,僅需為你的請(qǐng)求體提供一個(gè)生成器(或任意沒(méi)有具體長(zhǎng)度的迭代器):
def gen():
yield 'hi'
yield 'there'
requests.post('http://some.url/chunked', data=gen())
對(duì)于分塊編碼請(qǐng)求的響應(yīng),最好的方法是使用 Response.iter_content() 方法對(duì)其數(shù)據(jù)進(jìn)行迭代。理想的情況是你設(shè)置了 stream=True,這樣你就可以通過(guò)調(diào)用 iter_content()方法并將chunk_size參數(shù)設(shè)為 None,從而實(shí)現(xiàn)按塊進(jìn)行迭代。如果你要設(shè)置分塊的最大體積,你可以把chunk_size參數(shù)設(shè)為任意整數(shù)。
十一、POST 多個(gè)分塊編碼的文件
你可以在一個(gè)請(qǐng)求中發(fā)送多個(gè)文件。例如,假設(shè)你要上傳多個(gè)圖像文件到一個(gè) HTML 表單中,可以使用一個(gè)叫做 "images" 的多文件 field 即可:
<input type="file" name="images" multiple="true" required="true"/>
實(shí)現(xiàn)它,只需要在一個(gè)包含元組的列表中指定文件,其中元組結(jié)構(gòu)為 (form_field_name, file_info):
>>> url = 'http://httpbin.org/post'
>>> multiple_files = [
('images', ('foo.png', open('foo.png', 'rb'), 'image/png')),
('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))]
>>> r = requests.post(url, files=multiple_files)
>>> r.text
{
...
'files': {'images': 'data:image/png;base64,iVBORw ....'}
'Content-Type': 'multipart/form-data; boundary=3131623adb2043caaeb5538cc7aa0b3a',
...
}
十二、事件鉤子
Requests 有一個(gè)鉤子系統(tǒng),你可以用它來(lái)控制部分請(qǐng)求過(guò)程,或信號(hào)事件處理。
可用的鉤子:
response:
從一個(gè) Request 中產(chǎn)生的響應(yīng)。
你可以通過(guò)傳遞一個(gè) {hook_name: callback_function}字典給 hooks請(qǐng)求參數(shù)為每個(gè)請(qǐng)求分配一個(gè)鉤子函數(shù):
hooks=dict(response=print_url)
callback_function() 函數(shù)會(huì)接受一個(gè)數(shù)據(jù)塊作為它的第一個(gè)參數(shù)。
def print_url(r, *args, **kwargs):
print(r.url)
如果在執(zhí)行回調(diào)函數(shù)的時(shí)候發(fā)生錯(cuò)誤,系統(tǒng)會(huì)拋出一個(gè)警告。
如果回調(diào)函數(shù)返回一個(gè)值,默認(rèn)以該值替換傳進(jìn)來(lái)的數(shù)據(jù);如果函數(shù)并未返回任何東西,則沒(méi)有什么其他影響。
我們來(lái)在運(yùn)行期間打印一些請(qǐng)求方法的參數(shù):
>>> requests.get('http://httpbin.org', hooks=dict(response=print_url))
http://httpbin.org
<Response [200]>
十三、自定義認(rèn)證
Requests 允許你使用自定義身份驗(yàn)證機(jī)制。
任何傳遞給請(qǐng)求方法的 auth 參數(shù)的可調(diào)用對(duì)象,在請(qǐng)求發(fā)出前都有機(jī)會(huì)進(jìn)行修改。
自定義身份驗(yàn)證機(jī)制是作為 requests.auth.AuthBase的子類來(lái)實(shí)現(xiàn)的,也非常容易定義。Requests 在requests.auth 中提供了兩種常見(jiàn)的的身份驗(yàn)證方案: HTTPBasicAuth和 HTTPDigestAuth。
假設(shè)我們現(xiàn)在有一個(gè) web 服務(wù),僅在 X-Pizza 頭被設(shè)置為一個(gè)密碼值的情況下才會(huì)有響應(yīng)。雖然在現(xiàn)實(shí)中這不大可能出現(xiàn),但就以它為例來(lái)講解吧!
from requests.auth import AuthBase
class PizzaAuth(AuthBase):
"""Attaches HTTP Pizza Authentication to the given Request object."""
def __init__(self, username):
# setup any auth-related data here
self.username = username
def __call__(self, r):
# modify and return the request
r.headers['X-Pizza'] = self.username
return r
然后就可以使用我們的 PizzaAuth 來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求了:
>>> requests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth'))
<Response [200]>
十四、Requests 流
使用Response.iter_lines() 方法可以很方便地對(duì)流 API(例如 Twitter 的流 API )進(jìn)行迭代。簡(jiǎn)單地設(shè)置 stream 參數(shù)為True 即可使用 iter_lines() 方法進(jìn)行迭代:
import json
import requests
r = requests.get('http://httpbin.org/stream/20', stream=True)
for line in r.iter_lines():
# filter out keep-alive new lines
if line:
decoded_line = line.decode('utf-8')
print(json.loads(decoded_line))
當(dāng)在 Response.iter_lines()或 Response.iter_content()方法中使用 decode_unicode=True 時(shí),你需要提供一個(gè)回退編碼,以防服務(wù)器沒(méi)有提供默認(rèn)編碼而導(dǎo)致錯(cuò)誤:
r = requests.get('http://httpbin.org/stream/20', stream=True)
if r.encoding is None:
r.encoding = 'utf-8'
for line in r.iter_lines(decode_unicode=True):
if line:
print(json.loads(line))
警告:
iter_lines 不保證重進(jìn)入時(shí)的安全性。多次調(diào)用該方法 會(huì)導(dǎo)致部分收到的數(shù)據(jù)丟失。如果你要在多處調(diào)用它,就應(yīng)該使用生成的迭代器對(duì)象:
lines = r.iter_lines()
# 保存第一行以供后面使用,或者直接跳過(guò)
first_line = next(lines)
for line in lines:
print(line)
十五、代理
如果需要使用代理,你可以通過(guò)為任意請(qǐng)求方法提供proxies參數(shù)來(lái)配置請(qǐng)求:
import requests
proxies = {
'http': 'http://10.10.1.10:3128',
'https': 'http://10.10.1.10:1080',
}
requests.get('http://example.org', proxies=proxies)
你也可以通過(guò)環(huán)境變量HTTP_PROXY和 HTTPS_PROXY來(lái)配置代理:
$ export HTTP_PROXY="http://10.10.1.10:3128"
$ export HTTPS_PROXY="http://10.10.1.10:1080"
$ python
>>> import requests
>>> requests.get("http://example.org")
如果代理需要使用HTTP Basic Auth,使用http://user:password@host/的語(yǔ)法:
proxies = {'http': 'http://user:pass@10.10.1.10:3128/'}
要為某個(gè)特定的連接方式或者主機(jī)設(shè)置代理,可以直接將 scheme://hostname 作為 key 使用,它會(huì)針對(duì)指定的主機(jī)和連接方式進(jìn)行匹配:
proxies = {'http://10.20.1.128': 'http://10.10.1.10:5323'}
注意,代理 URL 中必須指明連接方式。
十六、SOCKS
除了基本的 HTTP 代理,Request 還支持 SOCKS 協(xié)議的代理。這是一個(gè)可選功能,需要安裝第三方庫(kù)方可使用,你可以使用 pip 安裝該功能:
$ pip install requests[socks]
使用 SOCKS 代理就和使用 HTTP 代理一樣簡(jiǎn)單:
proxies = {
'http': 'socks5://user:pass@host:port',
'https': 'socks5://user:pass@host:port'
}
使用 socks5 連接方式使得 DNS 解析在客戶端進(jìn)行而不是代理服務(wù)器;如果你希望域名的 DNS 解析是在代理服務(wù)器上進(jìn)行的,那么請(qǐng)使用 socks5h 連接方式代替。
十七、編碼
前面我們說(shuō)過(guò),當(dāng)你收到一個(gè)響應(yīng)時(shí),Requests 會(huì)猜測(cè)響應(yīng)的編碼方式,用于在你調(diào)用 Response.text()方法時(shí)進(jìn)行解碼。Requests 首先在 HTTP 頭部檢測(cè)是否存在指定的編碼方式,如果不存在,則會(huì)使用 chardet 來(lái)嘗試猜測(cè)編碼方式。
只有當(dāng) HTTP 頭部中沒(méi)有明確指定字符集,并且 Content-Type 頭部字段包含 text 值時(shí),Requests 才不會(huì)去猜測(cè)編碼。在這種情況下,[RFC 2616] 指定默認(rèn)字符集必須是 ISO-8859-1,Requests 遵從這一規(guī)范。如果你需要一種不同的編碼方式,你可以手動(dòng)設(shè)置 Response.encoding 屬性,或使用原始的 Response.content。
十八、HTTP 動(dòng)詞
Requests 提供了幾乎所有 HTTP 動(dòng)詞的功能:GET、OPTIONS、HEAD、POST、PUT、PATCH 和 DELETE。下面我們通過(guò)一些詳細(xì)的例子,演示在 Requests 中如何使用這些動(dòng)詞。
我們從最常使用的動(dòng)詞 GET 開(kāi)始。HTTP GET 是一個(gè)冪等方法(所謂冪等方法,是指該方法多次調(diào)用返回的效果是一致的),它的作用是從給定的 URL 返回一個(gè)資源。當(dāng)你試圖從一個(gè) web 位置獲取數(shù)據(jù)時(shí),你應(yīng)該使用這個(gè)動(dòng)詞。
下面這個(gè)例子嘗試從 GitHub 上獲取關(guān)于一個(gè)指定的 commit。假設(shè)我們像通過(guò) Requests 來(lái)獲取 commit a050faf 的數(shù)據(jù),可以這樣做:
>>> import requests
>>> r = requests.get('https://api.github.com/repos/requests/requests/git/commits/a050faf084662f3a352dd1a941f2c7c9f886d4ad')
我們應(yīng)該確認(rèn) GitHub 是否有正確地響應(yīng)請(qǐng)求,如果是,我們要繼續(xù)弄清楚響應(yīng)內(nèi)容具體是什么類型,比如這樣:
>>> if r.status_code == requests.codes.ok:
... print(r.headers['content-type'])
...
application/json; charset=utf-8
可見(jiàn),GitHub 返回了 JSON 數(shù)據(jù),非常好,這樣就可以使用 r.json方法把這個(gè)返回的數(shù)據(jù)解析成 Python 對(duì)象。
>>> commit_data = r.json()
>>> print(commit_data.keys())
[u'committer', u'author', u'url', u'tree', u'sha', u'parents', u'message']
>>> print(commit_data[u'committer'])
{u'date': u'2012-05-10T11:10:50-07:00', u'email': u'me@kennethreitz.com', u'name': u'Kenneth Reitz'}
>>> print(commit_data[u'message'])
makin' history
到目前為止,一切都非常簡(jiǎn)單?,F(xiàn)在,我們來(lái)研究一下 GitHub 的 API,我們可以翻查文檔,但如果使用 Requests 來(lái)研究也許會(huì)更有意思一些。我們可以借助 Requests 的 OPTIONS動(dòng)詞來(lái)看看我們剛使用過(guò)的 url 支持哪些 HTTP 方法。
>>> verbs = requests.options(r.url)
>>> verbs.status_code
500
什么?這是什么鬼?!返回的信息毫無(wú)幫助嘛!原來(lái) GitHub 與許多 API 提供方一樣,實(shí)際上并沒(méi)有實(shí)現(xiàn) OPTIONS 方法。真是讓人感到氣憤!但沒(méi)關(guān)系,我們還可以查找那枯燥的文檔。不過(guò)話說(shuō)回來(lái),如果 GitHub 正確地實(shí)現(xiàn)了 OPTIONS,那么服務(wù)器應(yīng)該在響應(yīng)頭中返回允許用戶使用的 HTTP 方法,例如:
>>> verbs = requests.options('http://a-good-website.com/api/cats')
>>> print(verbs.headers['allow'])
GET,HEAD,POST,OPTIONS
轉(zhuǎn)而去查看文檔,我們看到對(duì)于提交信息,另一個(gè)允許的方法是 POST,它會(huì)創(chuàng)建一個(gè)新的提交。由于我們正在使用 Requests 代碼庫(kù),我們應(yīng)盡可能避免對(duì)它發(fā)送笨拙的 POST。作為替代,我們來(lái)玩玩 GitHub 的 Issue 功能。
下面代碼訪問(wèn)的是 [Issue #482]該問(wèn)題已經(jīng)存在,我們就以它為例講解:
>>> r = requests.get('https://api.github.com/requests/kennethreitz/requests/issues/482')
>>> r.status_code
200
>>> issue = json.loads(r.text)
>>> print(issue[u'title'])
Feature any http verb in docs
>>> print(issue[u'comments'])
3
酷,有三個(gè)評(píng)論,我們看下最后一個(gè)說(shuō)的什么:
>>> r = requests.get(r.url + u'/comments')
>>> r.status_code
200
>>> comments = r.json()
>>> print(comments[0].keys())
[u'body', u'url', u'created_at', u'updated_at', u'user', u'id']
>>> print(comments[2][u'body'])
Probably in the "advanced" section
嗯,看起來(lái)似乎是個(gè)愚蠢的回復(fù)?,F(xiàn)在讓我們發(fā)表個(gè)評(píng)論來(lái)告訴這位評(píng)論者他到底有愚蠢……那么,這個(gè)評(píng)論者是誰(shuí)呢?
>>> print(comments[2][u'user'][u'login'])
kennethreitz
好吧,我們來(lái)告訴這個(gè)叫做 Kenneth 的家伙(其實(shí)是作者自己),這個(gè)例子應(yīng)該放在快速上手指南中。根據(jù) GitHub API 文檔,其方法是 POST 到該話題。我們不妨試試看:
>>> body = json.dumps({u"body": u"Sounds great! I'll get right on it!"})
>>> url = u"https://api.github.com/repos/requests/requests/issues/482/comments"
>>> r = requests.post(url=url, data=body)
>>> r.status_code
404
額,怎么返回 404 了呢?有點(diǎn)詭異……可能我們需要驗(yàn)證身份……那就有點(diǎn)麻煩了,對(duì)吧?不對(duì)。Requests 簡(jiǎn)化了多種身份驗(yàn)證形式的使用,包括非常常見(jiàn)的 Basic Auth。
>>> from requests.auth import HTTPBasicAuth
>>> auth = HTTPBasicAuth('fake@example.com', 'not_a_real_password')
>>> r = requests.post(url=url, data=body, auth=auth)
>>> r.status_code
201
>>> content = r.json()
>>> print(content[u'body'])
Sounds great! I'll get right on it.
太棒了!噢,不!我原本是想說(shuō)等我一會(huì),因?yàn)槲业萌ノ刮业呢垼](méi)想到作者也是貓奴(>ω<)喵)。如果我能夠編輯這條評(píng)論那就好了!幸運(yùn)的是,GitHub 允許我們使用另一個(gè) HTTP 動(dòng)詞 PATCH 來(lái)編輯評(píng)論。試試看吧:
>>> print(content[u"id"])
5804413
>>> body = json.dumps({u"body": u"Sounds great! I'll get right on it once I feed my cat."})
>>> url = u"https://api.github.com/repos/requests/requests/issues/comments/5804413"
>>> r = requests.patch(url=url, data=body, auth=auth)
>>> r.status_code
200
非常棒!現(xiàn)在,我們來(lái)折磨一下這個(gè)叫做 Kenneth 的家伙,我決定要讓他急得團(tuán)團(tuán)轉(zhuǎn),也不告訴他背后到底是誰(shuí)在搗蛋……我的意思說(shuō)時(shí)我現(xiàn)在想刪除這條評(píng)論了……
GitHub 允許我們使用 DELETE 方法來(lái)刪除評(píng)論:
>>> r = requests.delete(url=url, auth=auth)
>>> r.status_code
204
>>> r.headers['status']
'204 No Content'
很好,評(píng)論消失了。最后一件我想知道的事情是我已經(jīng)使用了多少限額(ratelimit)。查查看,GitHub 在響應(yīng)頭部發(fā)送這個(gè)信息,因此不必下載整個(gè)網(wǎng)頁(yè),我將使用一個(gè) HEAD 請(qǐng)求來(lái)獲取響應(yīng)頭:
>>> r = requests.head(url=url, auth=auth)
>>> print(r.headers)
...
'x-ratelimit-remaining': '4995'
'x-ratelimit-limit': '5000'
...
十九、自定義動(dòng)詞
有時(shí)候你會(huì)碰到一些服務(wù)器,基于某些原因,它們或者要求用戶使用上述 HTTP 動(dòng)詞之外的自定義動(dòng)詞。比如說(shuō) WEBDAV 服務(wù)器會(huì)要求你使用 MKCOL 方法。不過(guò)別擔(dān)心,Requests 一樣可以搞定它們。你可以使用內(nèi)建的.request方法,例如:
>>> r = requests.request('MKCOL', url, data=data)
>>> r.status_code
200 # Assuming your call was correct
這樣你就可以使用服務(wù)器要求的任意動(dòng)詞了。
二十、響應(yīng)頭鏈接字段
許多 HTTP API 都有響應(yīng)頭鏈接字段的功能,它們使得 API 能夠更好地自我描述和自我顯露。
GitHub 在 API 中為 分頁(yè) 使用這些功能,例如:
>>> url = 'https://api.github.com/users/kennethreitz/repos?page=1&per_page=10'
>>> r = requests.head(url=url)
>>> r.headers['link']
'<https://api.github.com/users/kennethreitz/repos?page=2&per_page=10>; rel="next", <https://api.github.com/users/kennethreitz/repos?page=6&per_page=10>; rel="last"'
Requests 會(huì)自動(dòng)解析這些響應(yīng)頭鏈接字段,并使得它們非常易于使用:
>>> r.links["next"]
{'url': 'https://api.github.com/users/kennethreitz/repos?page=2&per_page=10', 'rel': 'next'}
>>> r.links["last"]
{'url': 'https://api.github.com/users/kennethreitz/repos?page=7&per_page=10', 'rel': 'last'}
二十一、傳輸適配器
從 v1.0.0 以后,Requests 的內(nèi)部采用了模塊化設(shè)計(jì)。部分原因是為了實(shí)現(xiàn)傳輸適配器(Transport Adapters),你可以看看關(guān)于它的最早描述。傳輸適配器提供了一個(gè)機(jī)制,讓你可以為 HTTP 服務(wù)定義交互方法,尤其是它允許你應(yīng)用服務(wù)前的配置。
Requests 自帶了一個(gè)傳輸適配器,也就是 HTTPAdapter。這個(gè)適配器使用了強(qiáng)大的 urllib3 庫(kù),為 Requests 提供了默認(rèn)的 HTTP 和 HTTPS 交互。每當(dāng) Session 被初始化,就會(huì)有適配器附著在 Session 上,其中一個(gè)供 HTTP 使用,另一個(gè)供 HTTPS 使用。
Requests 允許用戶創(chuàng)建和使用他們自己的傳輸適配器,實(shí)現(xiàn)他們需要的特殊功能。創(chuàng)建好以后,傳輸適配器可以被加載到一個(gè)會(huì)話對(duì)象上,附帶著一個(gè)說(shuō)明,告訴會(huì)話適配器應(yīng)該應(yīng)用在哪個(gè) web 服務(wù)上。
>>> s = requests.Session()
>>> s.mount('http://www.github.com', MyAdapter())
這個(gè) mount 調(diào)用會(huì)注冊(cè)一個(gè)傳輸適配器的特定實(shí)例到一個(gè)前綴上面。加載以后,任何使用該會(huì)話的 HTTP 請(qǐng)求,只要其 URL 是以給定的前綴開(kāi)頭,該傳輸適配器就會(huì)被使用到。
傳輸適配器的眾多實(shí)現(xiàn)細(xì)節(jié)不在本文檔的覆蓋范圍內(nèi),不過(guò)你可以看看接下來(lái)這個(gè)簡(jiǎn)單的 SSL 案例。更多的用法,你也許該考慮為 BaseAdapter 創(chuàng)建子類。
二十二、阻塞和非阻塞
使用默認(rèn)的傳輸適配器,Requests 不提供任何形式的非阻塞 IO。Response.content 屬性會(huì)阻塞,直到整個(gè)響應(yīng)下載完成。如果你需要更多精細(xì)控制,該庫(kù)的數(shù)據(jù)流功能(見(jiàn)上面流請(qǐng)求章節(jié)) 允許你每次接受少量的一部分響應(yīng),不過(guò)這些調(diào)用依然是阻塞式的。
如果你對(duì)于阻塞式 IO 有所顧慮,還有很多項(xiàng)目可以供你使用,它們結(jié)合了 Requests 和 Python 的某個(gè)異步框架。典型的優(yōu)秀例子是 requests-threads、grequests 和 requests-futures。
二十三、Header 排序
在某些特殊情況下你也許需要按照次序來(lái)提供 headers,如果你向 headers 關(guān)鍵字參數(shù)傳入一個(gè) OrderedDict,就可以向提供一個(gè)帶排序的 headers。然而,Requests 使用的默認(rèn) headers 的次序會(huì)被優(yōu)先選擇,這意味著如果你在 headers 關(guān)鍵字參數(shù)中覆蓋了默認(rèn) headers,和關(guān)鍵字參數(shù)中別的 headers 相比,它們也許看上去會(huì)是次序錯(cuò)誤的。
如果這個(gè)對(duì)你來(lái)說(shuō)是個(gè)問(wèn)題,那么用戶應(yīng)該考慮在 Session 對(duì)象上面設(shè)置默認(rèn) headers,只要將 Session 設(shè)為一個(gè)定制的 OrderedDict 即可。這樣就會(huì)讓它成為優(yōu)選的次序。
二十四、超時(shí)
為防止服務(wù)器不能及時(shí)響應(yīng),大部分發(fā)至外部服務(wù)器的請(qǐng)求都應(yīng)該帶著 timeout 參數(shù)。在默認(rèn)情況下,除非顯式指定了 timeout 值,requests 是不會(huì)自動(dòng)進(jìn)行超時(shí)處理的。如果沒(méi)有 timeout,你的代碼可能會(huì)掛起若干分鐘甚至更長(zhǎng)時(shí)間。
連接超時(shí)指的是在你的客戶端實(shí)現(xiàn)到遠(yuǎn)端機(jī)器端口的連接時(shí)(對(duì)應(yīng)的是 connect()),Request 會(huì)等待的秒數(shù)。一個(gè)很好的實(shí)踐方法是把連接超時(shí)設(shè)為比 3 的倍數(shù)略大的一個(gè)數(shù)值,因?yàn)?TCP 數(shù)據(jù)包重傳窗口 的默認(rèn)大小是 3。
一旦你的客戶端連接到了服務(wù)器并且發(fā)送了 HTTP 請(qǐng)求,讀取超時(shí)指的就是客戶端等待服務(wù)器發(fā)送請(qǐng)求的時(shí)間(它指的是客戶端要等待服務(wù)器發(fā)送字節(jié)之間的時(shí)間。在 99.9% 的情況下這指的是服務(wù)器發(fā)送第一個(gè)字節(jié)之前的時(shí)間)。
如果你指定了一個(gè)值作為 timeout,像這樣:
r = requests.get('https://github.com', timeout=5)
timeout 還可以分別指定 connect 和 read 兩者的超時(shí)時(shí)間,只需要傳入一個(gè)元組即可:
r = requests.get('https://github.com', timeout=(3.05, 27))
如果遠(yuǎn)端服務(wù)器很慢,你可以讓 Requests 一直等下去……傳入一個(gè) None 作為 timeout 值:
r = requests.get('https://github.com', timeout=None)