POST 一般用来向服务端提交数据,HTTP 协议是以 ASCII 码传输,建立在 TCP/IP 协议之上的应用层规范。规范把 HTTP 请求分为三个部分:状态行、请求头、消息主体。
协议规定 POST 提交的数据必须放在消息主体(entity-body)中,但协议并没有规定数据必须使用什么编码方式。实际上,开发者完全可以自己决定消息主体的格式,只要最后发送的 HTTP 请求满足上面的格式就可以。
multipart/form-data:这是一个常见的 POST 数据提交的方式。我们使用表单上传文件时,必须让 form 的 enctype 等于这个值。
请求示例:
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"
title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png
PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
这种方式一般用来上传文件(Content-Disposition后可接form-data表示表单数据,attachment表示附件)。RFC2183
消息主体
每部分都是以 --boundary 开始,紧接着内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。如 text 的值就是 title
如果传输的是文件,还要包含文件名和文件类型(Content-Type)信息。如文件名为chrome.png的文件类型是image/png,具体内容(值)就是‘ PNG ... content of chrome.png ... ’ (二进制数据)
消息主体最后以 --boundary-- 标示结束。
参考资料:https://www.imququ.com/post/four-ways-to-post-data-in-http.html(挺好的资料)
下面分析web框架中分析post请求实体
# coding=utf-8
import cgi
def _to_unicode(s, encoding='utf-8'):
return s.decode('utf-8')
class MultipartFile(object):
def __init__(self, storage):
self.filename = _to_unicode(storage.filename)
self.file = storage.file
class Request(object):
def __init__(self, environ):
self._environ = environ
def _parse_input(self):
def _convert(item):
if isinstance(item, list):
return [_to_unicode(i.value) for i in item]
if item.filename:
return MultipartFile(item)
return _to_unicode(item.value)
fs = cgi.FieldStorage(fp=self._environ['wsgi.input'], environ=self._environ, keep_blank_values=True)
inputs = {}
for key in fs:
inputs[key] = _convert(fs[key]) #key的value可以是一个list
return inputs
def _get_raw_input(self):
if not hasattr(self, '_raw_input'):
self._raw_input = self._parse_input()
return self._raw_input
def __getitem__(self, key):
r = self._get_raw_input()[key]
if isinstance(r, list):
return r[0]
return r
def get(self, key, default=None):
r = self._get_raw_input().get(key, default)
if isinstance(r, list):
return r[0]
return r
def gets(self, key):
r = self._get_raw_input()[key]
if isinstance(r, list):
return r[:]
return [r]
from StringIO import StringIO
r = Request({'REQUEST_METHOD':'POST', 'wsgi.input':StringIO('a=1&b=M%20M&c=ABC&c=XYZ&e=')})
print r['a']
print r['c']
b = '----WebKitFormBoundaryQQ3J8kPsjFpTmqNz' #这是分割线,用于分割不同的字段,为了避免与正文内容重复,boundary 很长很复杂
pl = ['--%s' % b, 'Content-Disposition: form-data; name=\"name\"\n', 'Scofield', '--%s' % b, 'Content-Disposition: form-data; name=\\"name\\"\\n', 'Lincoln', '--%s' % b, 'Content-Disposition: form-data; name=\\"file\\"; filename=\\"test.txt\\"', 'Content-Type: text/plain\\n', 'just a test', '--%s' % b, 'Content-Disposition: form-data; name=\\"id\\"\\n', '4008009001', '--%s--' % b, '']
payload = '\n'.join(pl)
r = Request({'REQUEST_METHOD':'POST', 'CONTENT_LENGTH':str(len(payload)), 'CONTENT_TYPE':'multipart/form-data; boundary=%s' % b, 'wsgi.input':StringIO(payload)})
r.get('name')
r.gets('name')
f = r.get('file')
print f.filename
#print f.file.read()
payload(pl)就是post请求实体
相应的,如果是浏览器客户端发来上面的请求,应该是这样的:
###################################
|||
###################################
POST / HTTP/1.1
'''
#############
|||
#############
boundary是自定义的一串复杂无规律的字符串
Content-Type:
有附件时类型务必是multipart/form-data;
无附件(仅post表单)默认类型是application/x-www-form-urlencoded.
'''
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryQQ3J8kPsjFpTmqNz
################
|||
################
------WebKitFormBoundaryQQ3J8kPsjFpTmqNz
Content-Disposition: form-data; name="name"
Scofield
------WebKitFormBoundaryQQ3J8kPsjFpTmqNz
Content-Disposition: form-data; name="name"
Lincoln
------WebKitFormBoundaryQQ3J8kPsjFpTmqNz
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
just a test
------WebKitFormBoundaryQQ3J8kPsjFpTmqNz
Content-Disposition: form-data; name="id"
4008009001
------WebKitFormBoundaryQQ3J8kPsjFpTmqNz--
前端对应的HTML表单是这样的(
也就是cgi.FieldStorage()调用的东西):
注:test.txt文本文档里面的内容是 " just a test " .
可从Web框架中得到启发:
form = cgi.FieldStorage()
# 获取文件名
fileitem = form['file']
# 检测文件是否上传
if fileitem.filename:
# 设置文件路径
fn = os.path.basename(fileitem.filename)
open('/tmp/' + fn, 'wb').write(fileitem.file.read())
其中 ' file '是HTML表单name属性的名称:
结果:
如图所示,file文件类型的name属性名称是“ file ”,text文本框的name属性名称是“ trial ”,cgi.FieldStorage()就是通过“ file ”和“ trial ”来获取相关表单信息的。form['file']或form[ ' trial ' ]表示一个input标签整体,而标签有属性,如可以通过form['file'].filename或form['file'].file等来获取文件的相关信息,通过form[ ' trial ' ].value来获取text文本框里面的值。
回到web框架的源代码就容易理解了。
fs [key] 的fs是cgi.FieldStorage()实例,代表整个表单,key就是表单中各个标签的“name”属性的名称——"name","name","file","id",而 fs [key] 则表示一个input标签整体!可用标签属性来获取标签的具体内容。
name---- " name "-----Scofield(fs [name].value,标签的value属性可获取text的内容)
name---- " name "-----Lincoln (fs [name].value,标签的value属性可获取text的内容)
name----- " file "--------" test.txt "(fs [file].filename,标签的filename属性可获取文件的名称)
name----- " id "----------" 4008009001 "(fs [id].value,标签的value属性可获取text的内容)
出现了两个fs [name],可遍历后 return一个list(' return [_to_unicode(i.value) for i in item] ');对于文件类型的,通过判断其 filename 属性进行相应处理!