1. 程式人生 > >49.[Python]使用requests包進行HTTP互動方法詳解

49.[Python]使用requests包進行HTTP互動方法詳解

簡介

Python的HTTP包有urllib、urllib2、httplib等,但是都需要了解較多的HTTP原理才能編碼,藉助requests包可以在較高的抽象層次上完成HTTP互動過程的開發。安裝requests使用pip install requests命令,requests包內嵌了urllib3,自動支援HTTP長連線、連線池等功能。

使用方法

requests支援HTTP的HEAD、GET、POST、PUT、OPTIONS、DELETE、PATCH等請求:

r = requests.options('http://localhost:5000/')
print "Options:"
, r.headers r = requests.post('http://localhost:5000/', data={'name': 'mars'}) print "Post:", r.content r = requests.put('http://localhost:5000/', data={'name': 'loo'}) print "Put:", r.content r = requests.get('http://localhost:5000/') print "Get:", r.content r = requests.delete('http://localhost:5000/') print "Delete:", r.content r = requests.get('http://localhost:5000/'
) print "Get:", r.content

變數r是一個requests.models.Response型別的響應物件,通過r可以得到HTTP響應的所有資訊。

傳遞QUERY引數

在URI的query部分傳遞引數,可以直接按照標準放在URL字串中(允許為同一個key賦值多個value):

r = requests.get('http://localhost:5000/?xx=bb&xx=cc')

也可以放在請求的params引數中:

params = {
    'xx': ['bb', 'cc'],
    'yy': None,
}
r = requests.get('http://localhost:5000/'
, params=params) print "Request URL:", r.url

使用字典做引數時,對同一個key的多個value要放在列表中,如果某個key對應的值為None,則其不會放在query中。

定製請求頭

為HTTP請求報文的頭部新增內容,可以請求時為headers引數賦值:

r = requests.get('http://localhost:5000/', headers={"mars": "loo"})

填寫cookie

cookie可以以字典的形式賦值給cookies引數:

cookies = {
    "name": "mars"
}

r = requests.post('http://localhost:5000/', cookies=cookies)

通過RequestsCookieJar物件可以設定cookie的域、path等資訊:

jar = requests.cookies.RequestsCookieJar()
jar.set('username', 'mars', domain='httpbin.org', path='/cookies')
jar.set('password', 'loo', domain='httpbin.org', path='/else')
r = requests.get('http://httpbin.org/cookies', cookies=jar)
print r.text

如果是在localhost做實驗,domain引數需要賦值為空字串''

http://httpbin.org/cookies提供的服務是:如果請求包含cookie的話,會在響應體中迴應cookie內容,所以上述程式碼返回:

{
  "cookies": {
    "username": "mars"
  }
}

因為password/else這個path,所以通過/cookies無法訪問key為password的cookie項。

填充請求體

如果採用application/x-www-form-urlencoded格式傳送HTTP請求,可以將請求內容放在data引數中。如果採用application/json格式請求,可以將內容(dict型別)放在json引數中,或者將字典轉化為JSON字串之後傳給data引數,同時指定content-type為application/json:

import json

d = {
    "mars": "loo"
}
r = requests.post('http://localhost:5000/', data = json.dumps(d),
                  headers={"content-type": "application/json"})

如果需要上傳檔案,直接將檔案以'rb'模式開啟放入字典(必須使用'rb'模式,requests才能自動推算出正確的content-length),然後傳入files引數,請求型別會自動轉換為multipart/form-data

files = {
    'image': open('sample.jpg', 'rb'),
}

r = requests.post('http://localhost:5000/', files=files)

需要上傳的檔案大小超過記憶體時,可以將檔案的讀取放在上下文管理器中,比如:

with open('verybig.zip', 'rb') as f:
    requests.post('http://localhost:5000/', data=f)

處理響應物件

為QUERY傳遞引數的例子中可以看到,使用響應物件的url屬性可以訪問請求的URL。status_code屬性可以獲取響應狀態碼。raise_for_status方法,當狀態碼為4XX或5XX時,丟擲對應的客戶端或服務端異常,如果是2XX或3XX錯誤,返回None。比如:

r = requests.get('http://localhost:5000/')
print r.raise_for_status()

輸出可能是None,或者是:

Traceback (most recent call last):
  File "a.py", line 38, in <module>
    print r.raise_for_status()
  File "/Users/linglingguo/Envs/learnselenium/lib/python2.7/site-packages/requests/models.py", line 862, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 403 Client Error: FORBIDDEN for url: http://localhost:5000/

encoding屬性獲取響應編碼,text屬性會嘗試按照encoding屬性自動將響應內容轉碼後返回,如果encoding屬性為None,requests會根據chardet猜測正確的編碼。針對響應內容是二進位制檔案(如圖片)的場景,content屬性獲取響應的原始內容(以位元組為單位),比如:

from PIL import Image
from io import BytesIO

r = requests.post('http://localhost:5000/picture')
i = Image.open(BytesIO(r.content))
i.save('sample.jpg', 'jpeg')

如果響應內容的大小超過了機器記憶體,需要分段讀取響應內容,可以在請求時使用stream=True然後呼叫響應物件的iter_content方法:

r = requests.post('http://localhost:5000/', stream=True)
with open('download.zip', 'wb') as f:
    for chunk in r.iter_content(chunk_size=10*1024*1024):
        if chunk:
            f.write(chunk)

針對application/json格式的響應內容,requests內建了json方法將結果轉換為字典後返回:

r = requests.post('http://localhost:5000/', data={'name': 'mars'})
print "JSON:", r.json()

如果響應內容不能轉換為字典,丟擲異常:ValueError: No JSON object could be decoded

通過headers屬性可以訪問響應的頭部。

重定向與訪問歷史

如果請求過程發生了重定向,requests預設返回最後一個成功的響應,如果要獲取中間重定向過程的響應,可以訪問history屬性(按照訪問先後順序的響應物件列表),比如:

r = requests.get("http://localhost:5000")
print r.history
for re in r.history:
    print re.status_code, re.headers

上述程式碼輸出為:

[<Response [302]>, <Response [301]>]
302 {'Date': 'Wed, 28 Sep 2016 07:58:28 GMT', 'Content-Length': '211', 'Content-Type': 'text/html; charset=utf-8', 'Location': 'http://localhost:5000/a', 'Server': 'Werkzeug/0.11.10 Python/2.7.10'}
301 {'Date': 'Wed, 28 Sep 2016 07:58:28 GMT', 'Content-Length': '215', 'Content-Type': 'text/html; charset=utf-8', 'Location': 'http://localhost:5000/404', 'Server': 'Werkzeug/0.11.10 Python/2.7.10'}

可以發現請求先被重定向到http://localhost:5000/a,最後被重定向到http://localhost:5000/404拿到了結果。如果想禁用requests預設的處理轉發的行為,可以使用allow_redirect=False,比如:

r = requests.get("http://localhost:5000", allow_redirects=False)
print r.status_code, r.headers
for re in r.history:
    print re.status_code, re.headers

上述程式碼輸出為:

302 {'Date': 'Wed, 28 Sep 2016 08:04:04 GMT', 'Content-Length': '211', 'Content-Type': 'text/html; charset=utf-8', 'Location': 'http://localhost:5000/a', 'Server': 'Werkzeug/0.11.10 Python/2.7.10'}

超時

requests預設不設定超時,一直等待服務端響應。requests中將超時分為兩個部分:連線超時和響應讀取超時,分別表示Socket建立TCP連結超時和TCP連結建立以後,客戶端讀取服務端響應超時。如果設定timeout引數為一個數值,則連線超時和響應讀取超時設定為同樣的值,如果timeout引數是一個包含兩個數值的元組,則分別代表連線超時和響應讀取超時。如果過了超時時間設定以後,未成功建立TCP連結或者未成功讀取服務端響應,會拋異常,比如響應讀取超時的異常如下(以設定3秒超時為例):

requests.exceptions.ReadTimeout: HTTPConnectionPool(host='localhost', port=5000): Read timed out. (read timeout=3)

Session物件

這裡的Session是指一系列有相關意義的HTTP請求/響應的集合。使用requests.Session物件可以在多個HTTP請求之間保持變數、共用cookie、保持長連線從而提高效能(由urllib3實現)等。在請求間自動保持cookie的例子:

s = requests.Session()
r = s.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
print "1 - Headers:", r.headers
print "1 - Content:", r.content
r = s.get('http://httpbin.org/cookies')
print "2 - Text:", r.text
print "2 - Headers:", r.request.headers

上述程式碼輸出為:

1 - Headers: {'Content-Length': '56', 'Server': 'nginx', 'Connection': 'keep-alive', 'Access-Control-Allow-Credentials': 'true', 'Date': 'Fri, 30 Sep 2016 07:35:18 GMT', 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json'}
1 - Content: {
  "cookies": {
    "sessioncookie": "123456789"
  }
}

2 - Text: {
  "cookies": {
    "sessioncookie": "123456789"
  }
}

2 - Headers: {'Connection': 'keep-alive', 'Cookie': 'sessioncookie=123456789', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'User-Agent': 'python-requests/2.11.1'}

通過為Session物件賦值可以在請求間提供預設值,HTTP VERB方法呼叫中,對於新增的值會追加,已有的值會覆蓋:

s = requests.Session()
s.headers.update({'1': '2'})
r = s.get('http://localhost:5000/headers', headers={'3': '4', '1': 'mars'})

伺服器收到的header中'3'->'4''1'->'mars'。但是對於這種在呼叫時新增的方法,不會在請求間保持,比如:

s = requests.Session()

r = s.get('http://httpbin.org/cookies', cookies={'mars': 'loo'})
print(r.text)

r = s.get('http://httpbin.org/cookies')
print(r.text)

程式碼輸出為:

{
  "cookies": {
    "mars": "loo"
  }
}

{
  "cookies": {}
}

可以把Session物件放在上下文管理器中,這樣發生異常時可以自動銷燬會話,從而釋放連線池中的連線,提高程式效能:

with requests.Session() as s:
    r = s.get('http://httpbin.org/cookies/set/sessioncookie/123456789')

根據響應獲取請求

使用響應物件的request屬性可以訪問響應對應的請求物件,比如:

r = requests.get("http://localhost:5000", headers={"Perm": "Authroized"})
print "Request headers:", r.request.headers

上述程式碼輸出可能為:

Request headers: {'Perm': 'Authroized', 'Connection': 'keep-alive', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'User-Agent': 'python-requests/2.11.1'}

其實通過響應物件的request屬性得到的是一個PreparedRequest物件,可以修改其某些屬性後通過Session物件的send方法重發修改後的請求:

from requests import Session

r = requests.get('http://www.baidu.com')
print r.status_code, r.content

s = Session()
r2 = s.send(r.request)
print r2.status_code, r2.content

也可以先構造Request物件,然後呼叫其prepare方法返回一個PreparedRequest物件,再將該物件傳給Session物件的send方法:

s = requests.Session()
s.cookies.update({'mars': 'loo'})

req = requests.Request('GET', 'http://httpbin.org/cookies')
prepared = req.prepare()

r = s.send(prepared)
print r.text

上述程式碼的輸出為:

{
  "cookies": {}
}

說明通過Request物件的prepare方法生成的PreparedRequest物件不會讀取Session物件層次上的預設值(如cookie的設定)。Session物件的prepare_request方法也可以接受一個Request物件作為引數,返回PreparedRequest物件,但是會讀取Session物件層次上的預設值(如cookie的設定):

s = requests.Session()
s.cookies.update({'mars': 'loo'})

req = requests.Request('GET', 'http://httpbin.org/cookies')
prepared = s.prepare_request(req)

r = s.send(prepared)
print r.text

上述程式碼的輸出為:

{
  "cookies": {
    "mars": "loo"
  }
}

SSL認證

requests預設內建了Mozilla公佈的受信CA,requests預設會對伺服器端SSL證書進行認證,如果證書非法會報如下錯誤:

requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590)

如果要禁用SSL的驗證,可以請求時設定verify引數為False:

r = requests.get('https://xxx.com', verify=False)

依賴requests版本預設的CA的話,只有更新requests包的時候才會更新受信CA。新版本的requests會嘗試使用certifi(如果使用了certifi的話,建議經常升級certifi)。

如果服務端的SSL/TLS版本與requests預設的不一致,可以藉助HTTPAdapter物件更改Session物件使用的協議版本:

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.poolmanager import PoolManager
import ssl

class MyAdapter(HTTPAdapter):
    def init_poolmanager(self, connections, maxsize, block=False):
        self.poolmanager = PoolManager(num_pools=connections,
                                      maxsize=maxsize,
                                      block=block,
                                      ssl_version=ssl.PROTOCOL_TLSv1)

s = requests.Session()
# 掛載Adapter(https請求底層採用TLSv1協議)
s.mount('https://', MyAdapter())
s.get('https://xxx.com')

HTTP認證

關於HTTP認證可以閱讀我的上一篇部落格

基本認證

import requests
from requests.auth import HTTPBasicAuth

r = requests.get('http://localhost:5000/', auth=HTTPBasicAuth('mars', 'loo'))
print r.status_code, r.content

摘要認證

from requests.auth import HTTPDigestAuth

r = requests.get('http://localhost:5000/', auth=HTTPDigestAuth('mars', 'loo'))
print "1 - Response Header:", r.headers
print "1 - Request Header:", r.request.headers

上述程式碼的輸出樣例為:

1 - Response Header: {'Date': 'Thu, 29 Sep 2016 15:05:08 GMT', 'Content-Length': '12', 'Content-Type': 'text/html; charset=utf-8', 'Server': 'Werkzeug/0.11.10 Python/2.7.10'}
1 - Request Header: {'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'User-Agent': 'python-requests/2.11.1', 'Connection': 'keep-alive', 'Cookie': 'session=.eJyrVkosLcmIz8vPS05VsqpWUkhSslKKrHI18HdJr_B396z0c_fK8atyNAXSWb7hrgZRWckVfu6BQDm37MiQUFulWh2IEfkFiYWlSGaEeBr7hkeaRlbl5PplBWX7GXka-Rn5Gvrm-gLNCMr0dckGyrsaRob4As2oBQDP-yuE.Cs6_JA.kHRL3DmOS5wJavbd34C8cY7Fvl8', 'Authorization': 'Digest username="mars", realm="Authentication Required", nonce="c148818b24be7094bc1a4f714d18ada5", uri="/", response="ed0fd2e1bf8bbe2c3bccc52c60b63a11", opaque="a271f9c9f64d7b67c52c4f4b0971a5a3"'}

代理場景

HTTP/HTTPS代理

請求時可以通過proxies引數設定代理,代理中也可以設定認證方式,比如:

proxies = {
    "https": "https://username:[email protected]",
    "http://stackoverflow.com": "http://username:[email protected]"
}

r = requests.get('https://www.google.com', proxies=proxies)
print r.status_code
r = requests.get('http://stackoverflow.com', proxies=proxies)
print r.status_code

SOCKS代理

SOCKS代理從傳輸層轉發代理報文,並不關注應用層採用的是什麼協議,速度一般比HTTP/HTTPS代理速度快。如果代理使用了SOCKS協議,則需要安裝相應的包支援:pip install requests[socks],然後在代理的URL中按照SOCKS協議書寫代理的URL即可。

如果覺得我的文章對您有幫助,歡迎關注我(CSDN:Mars Loo的部落格)或者為這篇文章點贊,謝謝