前言 在 HTTP/1.1 协议中,使用 POST 请求提交数据时常用的 Content-Type 有以下几种:
application/x-www-form-urlencoded
原生 Form 默认的提交方式, 最常用的一种,支持GET/POST等方法。主要把数据编码成键值对的方式, 并且把特殊字符转义成 utf-8 字符,如空格会被转义成 %20。
application/json
由于 JSON 格式所表示的结构化数据远比键值对复杂得多,所以使用 JSON 系列化之后的字符串进行数据交换的方式越来越受人们青睐。特别适合 RESTful 类型的接口。
text/xml
使用 XML-RPC(XML Remote Procedure Call) 协议进行数据传输,相比于 JSON 的方式更为臃肿。
multipart/form-data
使用 Form 提交小文件, 直接把文件内容放在 Body 中进行传输的方式。考虑到同时上传多个字段或文件,所以需要按照一定规则随机生成或手动指定一个 boundary 用于分割数据,然后按照一定格式、顺序进行排列构成完整的 Body 进行传输。(multipart/form-data 官方定义 )
假设现在有 ./file_1.txt
和 ./file_2.txt
两个文件,内容分别如下:
1 2 3 4 5 test file 1 content!test file 2 content!
1 2 3 4 5 6 7 8 9 10 11 import requestsdata = {'key_1' : 'value_1' , 'key_2' : 'value_2' } files = [ ('file_1' , open ('./file_1.txt' , 'rb' )), ('file_2' , open ('./file_2.txt' , 'rb' )), ] resp = requests.post('http://127.0.0.1:8000/upload' , data=data, files=files) print (resp.request.body.decode('utf-8' ))
打印出来的 request body 内容是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 --bfa60c05b6631915da313e8fb696e7b2 Content-Disposition: form-data; name="key_1" value_1 --bfa60c05b6631915da313e8fb696e7b2 Content-Disposition: form-data; name="key_2" value_2 --bfa60c05b6631915da313e8fb696e7b2 Content-Disposition: form-data; name="file_1" ; filename="file_1.txt" test file 1 content!--bfa60c05b6631915da313e8fb696e7b2 Content-Disposition: form-data; name="file_2" ; filename="file_2.txt" test file 2 content!--bfa60c05b6631915da313e8fb696e7b2--
其中 bfa60c05b6631915da313e8fb696e7b2
就是上面所提到自动生成的 boundary。
值得注意的是 {'key_1': 'value_1', 'key_2': 'value_2'}
这两个本身是键值对的数据也被自动转成了 multipart/form-data
的编码方式。如果不传 files
字段时,将自动使用 application/x-www-form-urlencoded
的编码方式,所以 request body 内容应该是这样的
1 key_1=value_1&key_2=value_2
在 requests 中数据编码时,只有 data
参数为 None 时才会判断使用 json
参数,所以 data
和 json
两个参数同时存在时,只会编码 data
的数据;但 data
和 files
是可以同时存在的,而且只要有 files
存在,其它键值对数据也会一起使用 multipart/form-data
的编码方式生成 body 数据。
1 2 3 4 5 resp = requests.post('http://127.0.0.1:8000/upload' , data=data, json=xxxx) resp = requests.post('http://127.0.0.1:8000/upload' , data=data, files=files)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import asyncioimport aiohttpasync def send_request (): async with aiohttp.ClientSession() as session: data = aiohttp.FormData() data.add_field('key_1' , 'value_1' ) data.add_field('key_2' , 'value_2' ) data.add_field('file_1' , open ('./file_1.txt' , 'rb' ), filename='file_1.txt' , content_type='multipart/form-data' ) data.add_field('file_2' , open ('./file_2.txt' , 'rb' ), filename='file_2.txt' , content_type='multipart/form-data' ) async with session.post('http://127.0.0.1:8000/upload' , data=data) as resp: print (await resp.text()) asyncio.run(send_request())
打印出来的 request body 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 --a63b12cbef044b039c5c788b25a71336 Content-Type: text/plain; charset=utf-8 Content-Disposition: form-data; name="key_1" Content-Length: 7 value_1 --a63b12cbef044b039c5c788b25a71336 Content-Type: text/plain; charset=utf-8 Content-Disposition: form-data; name="key_2" Content-Length: 7 value_2 --a63b12cbef044b039c5c788b25a71336 Content-Type: multipart/form-data Content-Disposition: form-data; name="file_1"; filename="file_1.txt"; filename*=utf-8''file_1.txt Content-Length: 21 test file 1 content! --a63b12cbef044b039c5c788b25a71336 Content-Type: multipart/form-data Content-Disposition: form-data; name="file_2"; filename="file_2.txt"; filename*=utf-8''file_2.txt Content-Length: 21 test file 2 content! --a63b12cbef044b039c5c788b25a71336--
可以看到 aiohttp 对键值对默认使用了 Content-Type: text/plain
, 即纯文本的方式,这只是不同库的默认值和实现方式有些区别而已。
这里服务端使用 Sanic
框架接收数据请求,Sanic
是 python3
中性能非常好异步无阻塞的 web 框架,特别是跟 uvloop
配合着使用,性能上可以发挥到极致。用法跟Flask
非常类似。项目主页: https://github.com/huge-success/sanic 。
1 2 3 4 5 6 7 8 9 10 11 12 from sanic import Sanic, responseapp = Sanic(__name__) @app.post('/upload' ) async def upload_handler (request ): print ('request.files' , request.files) print ('request.form' , request.form) return response.text('ok' ) if __name__ == "__main__" : app.run(host='0.0.0.0' , port=8000 )
输出信息如下
1 2 request.files {'file_1': [File(type='multipart/form-data', body=b'test file 1 content!\n', name='file_1.txt')], 'file_2': [File(type='multipart/form-data', body=b'test file 2 content!\n', name='file_2.txt')]} request.form {'key_1': ['value_1'], 'key_2': ['value_2']}