HTTP 協定介紹
我們知道,HTTP 協定通過 ASCII 碼的形式進行傳輸,是建立在 TCP/IP 協定之上的應用層協定,HTTP/1.1 協定中規定的請求方式有八種:OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT。其中通過 POST 送出表單的方式最為常見。
HTTP/1.1 協定規定将一個完整的 HTTP 請求分為三個部分,請求行,請求頭和請求主體。大概架構如下:
<method> <request-URL> <version>
<headers>
<body>
POST http://httpbin.org HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
a_test=112233&b_test=223344
POST 請求
協定規定 POST 請求的資料必須放在請求主體中,但是并沒有說明送出的資料必須使用什麼編碼方式,是以我們完全可以自定義編碼方式,隻要格式滿足以上協定中規定的格式即可。
由于編碼方式可以自定義,在 CS 架構中,伺服器不可能提前知道用戶端的編碼方式,是以伺服器可以成功解析資料的前提就是通過某個字段告訴服務端用戶端發送資料時所使用的編碼方式,這個字段就是
Content-Type
。
在 POST 請求中,
Content-Type
和
body
是最重要的兩個資料。
POST 請求編碼方式
application/x-www-form-urlencoded
浏覽器原生
form
表單,如果不設定
enctype
,最終送出資料時所用的就是這種編碼方式。這應該是最常見的編碼方式了,由于浏覽器原生支援,是以用途最廣泛。
Content-Type
字段被設定為
application/x-www-form-urlencoded
,
body
資料為
key/value
的形式,通過 URL 編碼後使用
&
符号進行連接配接。大部分的服務端都可以支援這種編碼方式,好多用戶端(指程式設計語言,不包括浏覽器)的預設編碼方式也是這個。
POST http://httpbin.org HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
a_test=112233&b_test=223344
multipart/form-data
這個是通過浏覽器上傳檔案時所用的編碼方式,在使用浏覽器上傳檔案時,須将
form
表單的
enctype
字段設定為
multipart/form-data
。
這種編碼方式的請求體比較長,因為包含了檔案資料,通過
boundary
來分割不同字段的資料。
關于此類表單的詳細資訊可以檢視 rfc1867。
POST http://httpbin.org HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"
text
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="111.jpg"
Content-Type: image/png
PNG ... content of 111.jpg ... ... ... ... ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
application/json
由于 JSON 規範的流行,越來越多的人開始使用這種編碼方式,這種編碼方式可以送出比較複雜的結構化資料,友善各種類型的資料互動。
POST http://httpbin.org HTTP/1.1
Content-Type: application/json;charset=utf-8
{"test":"test123","test22":[111,222,333]}
text/xml
這個編碼規範一般用于使用 XML 作為編碼方式的遠端調用規範。由于接觸的不多,就不過多介紹了。
POST http://httpbin.org HTTP/1.1
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>41</i4></value>
</param>
</params>
</methodCall>
requests 中 post 請求參數差別
在解釋之前先提一下 httpbin.org 這個網站,這個網站的介紹是
A simple HTTP Request & Response Service.
,簡單來說就是它是一個調試網站,可以通過網站傳回的資料來了解我們發送給伺服器的資料是怎麼樣的,伺服器接收到了什麼類型的資料,它的功能有很多,大家可以到它的官網多多了解下。
使用 requests 這個庫可以通過不同的參數發送不同編碼類型的資料,先看一段代碼:
from pprint import pprint
import requests
url = 'http://httpbin.org/post'
data = {'a_test': 112233, 'b_test': 223344}
r = requests.post(url=url, data=data).json()
pprint(r)
url = 'http://httpbin.org/post'
data = {'a_test': 112233, 'b_test': 223344}
r = requests.post(url=url, json=data).json()
pprint(r)
我們使用同一個字典,使用 data 參數和 json 參數分别發送同樣的資料,來對比資料是如何被處理的,以及被處理的結果。先看列印出來的傳回值:
{'args': {},
'data': '',
'files': {},
'form': {'a_test': '112233', 'b_test': '223344'},
'headers': {'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate',
'Content-Length': '27',
'Content-Type': 'application/x-www-form-urlencoded',
'Host': 'httpbin.org',
'User-Agent': 'python-requests/2.24.0',
'X-Amzn-Trace-Id': 'Root=1-5f4869c6-b3834c881b400bc9b2877715'},
'json': None,
'origin': '125.36.92.42',
'url': 'http://httpbin.org/post'}
{'args': {},
'data': '{"a_test": 112233, "b_test": 223344}',
'files': {},
'form': {},
'headers': {'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate',
'Content-Length': '36',
'Content-Type': 'application/json',
'Host': 'httpbin.org',
'User-Agent': 'python-requests/2.24.0',
'X-Amzn-Trace-Id': 'Root=1-5f4869c6-a9b34c351c0adeed9669ac2a'},
'json': {'a_test': 112233, 'b_test': 223344},
'origin': '125.36.92.42',
'url': 'http://httpbin.org/post'}
通過上面資料可以看出,使用 data 參數時,發送的資料預設使用
application/x-www-form-urlencoded
編碼方式進行處理,然後發送了出去,證據就是我們發送的資料出現在了
form
表單字段中,而且
Content-Type
字段的值為
application/x-www-form-urlencoded
,并且 json 字段的資料為 None(因為它的服務端是用 python 寫的,是以會出現 None)。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5iNwcjNmRmY0MDN0UWL5EGZ40SMxIWZtATM0ATLkVmN0QmY0kzLcRnblRnbvN2Lc12bj5yZtlWZjVjLyADcvw1LcpDc0RHaiojIsJye.png)
接下來看使用 json 參數發送的資料,
Content-Type
字段的值為
application/json
,證明是通過
application/json
編碼發送的資料,并且資料出現在了 json 字段中,證明服務端正常收到了 json 類型的資料并且可以正常處理。
可能有小夥伴注意到資料還出現在了 data 字段中,由于不清楚服務端是怎麼樣的處理邏輯,為什麼發送的
application/json
編碼的資料會出現在 data 字段中,這裡暫時不做深究,感興趣的小夥伴可以嘗試到它的 GitHub 上看源代碼找找答案。
到這裡我們通過服務端傳回的資料了解到了
data/json
兩個參數的差別, 接下來我們通過使用 Charles 抓包的方式來具體看看我們發出去的資料是什麼樣的。
- data 參數
可以看到,
Content-Type
字段的值被設定為
application/x-www-form-urlencoded
,而且
body
資料的格式符合我們上邊所介紹的格式。
- json 參數
可以看到
Content-Type
字段的值被設定為
application/json
,而且
body
資料的格式是一個正常 json 格式的資料,同樣符合我們上邊所介紹的格式,而且抓包軟體下方還多出來了兩個頁籤
JSON/JSON Text
, 說明抓包軟體可以正常處理,解析,展示我們上傳的資料,進一步印證了我們發送的資料是一個正常的 json 格式的資料。
綜上所述,我們可以了解到,requests 庫發送 post 請求時,可以直接使用
data/json
參數來切換發送請求時的編碼方式,并且輸入的資料都為字典,requests 會自動處理成不同的編碼方式,并且将
Content-Type
字段的值自動設定為正确的編碼方式的值。由于本文主要讨論
data/json
的差別,這裡就不介紹如何通過 requests 發送檔案了。
跟進代碼檢視
接下來我們跟進到 requests 的源代碼中去看看 requests 對于不同的編碼方式是怎麼處理的。我這裡使用的是 Pycharm,友善單步調試并且實時檢視變量的值,大家可以選用你們喜歡的工具進行調試學習。由于是為了檢視 requests 對于不同編碼方式的處理,是以單步調試的操作方法我就不做過多說明了。
- 首先打斷點,跟進代碼。
- 這裡調用了 request 這個函數。其實其他的請求方式也是調用這個函數,通過第一個參數來區分是什麼請求類型。繼續跟進
- 由于我們直接使用
來請求,但是 requests 是通過 Session 這個類的對象作為最小機關來進行 Cookie 持久化,連接配接池等操作的,是以這裡通過 with 語句為我們建立了 Session 對象。requests.post
- 繼續跟進到
中,這個方法有很多參數,平時我們所用到的參數在這裡都有了。我們可以看到,data 和 json 參數被傳進來以後又被傳到了 Request 這個類中, 建立了一個 Request 的對象,然後調用 prepare_request 函數進行了處理,我們有理由相信,prepare_request 這個函數裡邊會對我們傳進來的 data 和 json 參數進行處理。session.request
- 進入到 prepare_request 函數中,可以看到,傳入進來的 request 對象的執行個體變量的值又被傳入到了 PreparedRequest 類的 prepare 函數中進行了處理,處理完成後傳回了 PreparedRequest 對象。我們跟進到 prepare 函數中
- 這裡分别對我們傳入的不同資料進行了校驗和處理,我們跟進到
函數中檢視我們感興趣的self.prepare_body
資料是怎麼被處理的。data/json
- 這個函數比較長,進入函數後,首先對 json 進行處理,使用系統的
将字典轉為字元串,然後轉為 UTF8 編碼,設定好json.dumps
的值為content-type
。還對于 data 為一個流的情況進行了判斷和處理,不多解釋。application/json
- 跳過處理 data 流的過程,首先進行了 files 是否為 None 的處理,如果有檔案就需要讀取檔案并且編碼。如果沒有的話處理 data 參數,通過
函數編碼 data 參數。然後将self._encode_params
的值設定為content-type
。最後将application/x-www-form-urlencoded
字段添加到請求頭中。content-type
- 最後再看一下
函數,通過各種類型判斷,最後将資料轉換為清單,清單中是由 key/value 組成的元組,最後調用系統的 urlencode 函數将 result 中的資料轉換為最終發送出去的資料。self._encode_params
手動發送 json
可能有小夥伴會問,以前發送 json 資料不是這麼發送的,是通過 json.dumps 函數轉換之後發送出去的,那可能你見到的是這樣的代碼:
import json
from pprint import pprint
import requests
url = 'http://httpbin.org/post'
data = json.dumps({'a_test': 112233, 'b_test': 223344})
r = requests.post(url=url, data=data, proxies={'http': 'http://192.168.233.186:8888'}).json()
pprint(r)
我們來看下伺服器傳回資料:
{'args': {},
'data': '{"a_test": 112233, "b_test": 223344}',
'files': {},
'form': {},
'headers': {'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate',
'Content-Length': '36',
'Host': 'httpbin.org',
'User-Agent': 'python-requests/2.24.0',
'X-Amzn-Trace-Id': 'Root=1-5f4897ee-054e38c02d5d5d60ece7f640'},
'json': {'a_test': 112233, 'b_test': 223344},
'origin': '125.36.92.42',
'url': 'http://httpbin.org/post'}
可以看到,資料被識别出來了, json 字段的值不是 None,先别高興的太早,我們來看看抓包結果:
可以看到,資料是抓到了,伺服器也可以正常接收到并解析,但是我們發現請求頭中并沒有
Content-Type
字段,但是伺服器仍然可以正常解析,這隻能說明伺服器嘗試去通過 json 解析,如果解析正常的話會當作 json 請求進行處理。
為了驗證猜想,我去翻了網站的源代碼,證明了我的猜想是正确的。
是以這裡正常傳回了資料隻能說明伺服器在嘗試,如果伺服器嘗試失敗了可能就傳回錯誤了,是以說我們在自己手動發送請求時還是要記得帶好
Content-Type
字段,不能讓别人猜測。如果伺服器不去猜測呢?或者說可能沒有對應的
Content-Type
字段伺服器會直接傳回錯誤,為了保證程式的健壯性,還是要記得手動添加。
雖然手動添加是可以的,但是還是推薦使用 json 參數,可以自動添加請求頭,并且格式化、驗證資料,防止出錯。import json
from pprint import pprint
import requests
url = 'http://httpbin.org/post'
data = json.dumps({'a_test': 112233, 'b_test': ' 你好 '})
headers = {'Content-Type': 'application/json'}
r = requests.post(url=url, data=data, proxies={'http': 'http://192.168.233.186:8888'}).json()
pprint(r)
python 之禅中有一句話叫:
顯性勝于隐性 (Explicit is better than implicit),就是要提醒我們一定要明确。既然有了更好的格式化資料的方式,就要利用好它。
好了,以上内容就是關于 requests 中 data 參數和 json 參數差別的全部内容了,沒有看明白的小夥伴可以自己動手調試下源代碼,就可以了解了。
參考連結:
https://imququ.com/post/four-ways-to-post-data-in-http.html
https://github.com/postmanlabs/httpbin/blob/master/httpbin/helpers.py
http://httpbin.org/