1、问题: requests.post 提交的 json 数据在 django.requests.POST 里找不到
在 Python 里,我们可以用request.post
提交json
数据:
requests.post("http://example.com/foo?dog=true", json={ "cat": False})
但在 django 的 request.POST 里找不到这个数据:
def foo_callback(request):
print(request.POST["Cat"]) # raise KeyError
为什么会出现这个问题,需要从 http 协议如何 post 传输数据说起。
2、http 协议如何 post 数据
http 请求是传输了一个字节流,分为三个部分:状态行请求行、请求头、消息主体:
method request-URL version
headers
entity-body
对于 post 请求, method 即 POST , request-URL 为请求的地址, version 一般都是 HTTP/1.1。headers 里定义了客户端信息(比如浏览器版本、是否接受压缩数据等),以及一个更重要的 Content-Type 信息,它告诉服务器,后面提交的 entity-body 的数据格式。理论上 entity-body 可以以任何格式编码,但在实际应用中,一般约定的有 5 种数据格式。
2.1、application/x-www-form-urlencoded
application/x-www-form-urlencoded 是最常见的 POST 提交数据方式。浏览器原生的 form 表单如果不设置 enctype 属性,最终就会以这种方式进行提交。类似于:
POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
title=ahweg&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3
Content-Type 被指定为 application/x-www-form-urlencoded ;其次,提交的数据按照 key1=val1&key2=val2 的方式进行编码, key 和 val 都进行了 URL 转码(空格、&、=等特殊符号变成%开头带十六进制 ascii 值,比如空格变成%20 )。
2.2、multipart/form-data
另一个常见的 POST 数据提交的方式。我们使用表单上传文件时,必须让 form 表单的 enctype 等于 multipart/form-data。例如:
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="ahweg"
title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="ahweg.png"
Content-Type: image/png
PNG ... content of ahweg.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
首先生成了一个 boundary 用于分割不同的字段,为了避免与正文内容重复, boundary 很长很复杂。然后 Content-Type 里指明了数据是以 multipart/form-data 来编码,本次请求的 boundary 是什么内容。消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 --boundary 开始,紧接着是内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。如果传输的是文件,还要包含文件名和文件类型信息。消息主体最后以 --boundary-- 标示结束。
上传文件等二进制数据一般采取 multipart/form-data 这种数据格式。
2.3、application/json
由于 JSON 规范的流行,现在越来越多的人把 application/json 作为请求头,用来告诉服务端消息主体是序列化后的 JSON 字符串。例如:
POST http://www.example.com HTTP/1.1
Content-Type: application/json;charset=utf-8
{"title":"ahweg","sub":[1,2,3,4,5]}
这种方案,可以方便的提交复杂的结构化数据,特别适合 RESTful 的接口。各大抓包工具如 Chrome 自带的开发者工具、Firebug、Fiddler ,都会以树形结构展示 JSON 数据,非常友好。
2.4、text/xml
XML-RPC ( XML Remote Procedure Call )是一种使用 HTTP 作为传输协议, XML 作为编码方式的远程调用规范。典型的 XML-RPC 请求是这样的:
POST http://www.example.com HTTP/1.1
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>125</i4></value>
</param>
</params>
</methodCall>
XML-RPC 协议简单、功能够用,各种语言的实现都有。它的使用也很广泛,如 WordPress 的 XML-RPC Api ,搜索引擎的 ping 服务等等。
2.5、text/plain
plain 文本即没有任何预设格式的文本。
POST http://www.example.com HTTP/1.1
Content-Type: text/xml
plain readable text
3、为什么 django 的 request.POST 里找不到 requests.post 提交的 json 数据
通过request.post
提交时, django.requests 并不会解析所有的数据,它只会解析:
- application/x-www-form-urlencoded ,数据会保存在 django.requests.POST 里面。
- multipart/form-data , key-value 对会保存在 django.requests.POST 里,文件会保存在 django.requests.FILES 里。
当requests.post
提交一个 json 数据时,事实上是提交了一个application/json
数据,此时 django.request 并不会解析该数据,因此无法通过django.requet.POST
来访问。此时需要自己来解析,主要 POST 的原始的 entity-body 会保存在django.requests.body
里:
import json
def foo_callback(request):
body = json.loads(request.body)
print(body["cat"])
如果提交 xml 或 plain 文本数据时,也可以通过django.request.body
来解析数据。
4、requests.post 如何选择提交数据的格式
reqiest.post 有很多参数,决定提交数据的格式:
- data :为字典,以 application/x-www-form-urlencoded 格式发送, django 从 request.POST 里获取。
- json: 以 application/json 格式发送, django 用 json.loads(request.body) 还原。
- files: 上传的文件,以 multipart/form-data 格式发送, django 从 request.FILES 里获取。
- params :作为查询字符串参数发送,影响的是 request-URL , django 从 request.GET 里获取。
用户也可以通过 headers 参数直接设置数据格式:
import requests
url = "http://example.com/foo?dog=true"
data = "<data>...</data>"
headers = {"Content-Type": "application/xml"}
response = requests.post(url, data=data, headers=headers)
Q. E. D.