引言
使用Django开发WEB网站时,通常下可以通过request.POST请求获取POST请求中的参数。
但是有时候明明前端发送的是POST请求,但是在调用request.POST的时候却没有获取相关数据,而在request.body中是有数据。这种情况通常情况下是由于请求的Content-Type的类型或者传入的数据格式不符合Django的规范。
本文通过Django中的一部分源码来解释request.POST怎样才会有值。由于Django中的源码太多,因此本文只会解释源码中的部分代码。若不想看过程可以直接看总结部分。
正文
2.1 概述
在调用request.POST时,会对POST请求体中的数据进行解析。
下列为部分源码,首先会判断当前的request对象是否有 self._post,有则不做数据解析,而是直接返回 self._post。从源码中可以看出调用request.POST就是这个 self._post,在视图函数中打印两者信息可以验证。
def _get_post(self): if not hasattr(self, "_post"): # 对请求体中的数据进行解析 self._load_post_and_files() return self._post
2.2 Content-Type解析
_load_post_and_files方法实现的功能是对请求体中的数据进行解析,并将解析好的数据存在 self._post中。下面的代码为_load_post_and_files方法中的部分源码。
首先在源码中会判断当前请求方式是不是POST,如果不是POST请求,则直接返回空的QueryDict对象给self._post。
其次在对请求方式做完判断后,会对请求头中的Content-Type的值进行判断。在源码中可以看出,在解析POST请求的数据时,只有在Content-Type为multipart/form-data 或者 application/x-www-form-urlencoded 时才会执行解析数据的流程,而Django在对于除两者之外的Content-Type是不会进行数据解析的。
因此request.POST只会解析上述两种Content-Type所传的数据。
# _load_post_and_files部分源码 def _load_post_and_files(self): """ Populate self._post and self._files if the content-type is a form type """ if self.method != "POST": # 当前的请求方式不是POST请求,返回没有数据的QueryDict对象 self._post, self._files = ( QueryDict(encoding=self._encoding), MultiValueDict(), ) return if self.content_type == "multipart/form-data": # 当前POST请求中的Content-Type的内容为: multipart/form-data if hasattr(self, "_body"): # Use already read data data = BytesIO(self._body) else: data = self try: self._post, self._files = self.parse_file_upload(self.META, data) except MultiPartParserError: # An error occurred while parsing POST data. Since when # formatting the error the request handler might access # self.POST, set self._post and self._file to prevent # attempts to parse POST data again. self._mark_post_parse_error() raise elif self.content_type == "application/x-www-form-urlencoded": # 当前POST请求中的Content-Type的内容为:application/x-www-form-urlencoded self._post, self._files = ( QueryDict(self.body, encoding=self._encoding), MultiValueDict(), ) else: # 当前POST请求中的Content-Type的内容不是:application/x-www-form-urlencoded 或者 multipart/form-data,如application/json self._post, self._files = ( QueryDict(encoding=self._encoding), MultiValueDict(), )
2.3 application/x-www-form-urlencoded
在Content-Type为application/x-www-form-urlencoded时,_load_post_and_files方法中会实例化一个QueryDict对象,并将self.body作为参数传入,self.body中存放的是未解析过的请求体数据。
而在实例化QueryDict对象时会调用QueryDict类中的__init__方法,在__init__中会执行下列代码,该代码是通过parse_qsl方法对self.body中的请求体数据进行解析,并将解析完成的数据通过self.appendlist方法存起来。下列代码中的query_string参数就是self.body。
# QueryDict __init__方法中的部分源码,解析数据 for key, value in parse_qsl(query_string, **parse_qsl_kwargs): self.appendlist(key, value)
默认情况下,application/x-www-form-urlencoded在请求体中的数据格式为 1=1&2=3。
在parse_qsl中,会对application/x-www-form-urlencoded默认对应的数据格式进行解析,并返回一个列表,返回的列表中存放的是请求体中解析好后的数据。解析流程如下:
首先使用列表推导式以“&”符号作为分隔符将原始数据拆分成每项数据,即pairs=['1=1', '2=3']。
然后通过遍历pairs将每项数据对应的name和value分开,插入到列表r。
最后返回列表r。
# parse_qsl部分源码 def parse_qsl(qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8', errors='replace', max_num_fields=None): # 对数据进行拆分(数据样式:1=1&2=3) # 拆分前:1=1&2=3 # 拆分后:["1=1","2=3"] pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] r = [] for name_value in pairs: # 拆分数据的name和value("1=1" -> [1 1]) nv = name_value.split('=', 1) if len(nv[1]) or keep_blank_values: name = nv[0].replace('+', ' ') name = unquote(name, encoding=encoding, errors=errors) name = _coerce_result(name) value = nv[1].replace('+', ' ') value = unquote(value, encoding=encoding, errors=errors) value = _coerce_result(value) r.append((name, value)) return r
2.4 multipart/form-data
当Content-Type为multipart/form-data 时,_load_post_and_files方法会调用 parse_file_upload方法对上传的文件跟数据进行解析并返回,在本文中,目前只描述非文件的数据,关于文件的解析,各位可以自行浏览源码。
在 parse_file_upload方法中,会定义一个 MultiPartParser类,并返回该类parse方法的返回值。 MultiPartParser类中的parse方法就是对请求体中的数据进行解析,并将请求体中的数据赋给self._post。
# parse_file_upload部分源码 def parse_file_upload(self, META, post_data): """Return a tuple of (POST QueryDict, FILES MultiValueDict).""" parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding) return parser.parse()
对于Content-Type为multipart/form-data对应的请求体,默认情况下的数据格式为:
----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="username" z ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="age" 25 ----WebKitFormBoundary7MA4YWxkTrZu0gW
上述的数据格式在打印request.body时内容如下:
b'------WebKitFormBoundarybVqijAh6BVbgoNPX\r\nContent-Disposition: form-data; name="username"\r\n\r\nz\r\n------WebKitFormBoundarybVqijAh6BVbgoNPX\r\nContent-Disposition: form-data; name="age"\r\n\r\n25\r\n------WebKitFormBoundarybVqijAh6BVbgoNPX--\r\n'
在MultiPartParser类的 parse方法中,当请求体无内容时,会直接返回空的QueryDict对象给self._post。否则会使用for语句遍历Parser类实例化后的对象。
# parse部分源码 def parse(self): """ Parse the POST data and break it into a FILES MultiValueDict and a POST MultiValueDict. Return a tuple containing the POST and FILES dictionary, respectively. """ from django.http import QueryDict encoding = self._encoding # HTTP spec says that Content-Length >= 0 is valid # handling content-length == 0 before continuing # 判断请求体中是否有数据 if self._content_length == 0: return QueryDict(encoding=self._encoding), MultiValueDict() # Create the data structures to be used later. self._post = QueryDict(mutable=True) self._files = MultiValueDict() # Instantiate the parser and stream: stream = LazyStream(ChunkIter(self._input_data, self._chunk_size)) # Number of bytes that have been read. num_bytes_read = 0 # To limit the amount of data read from the request. read_size = None try: # 遍历Parser示例化都的对象,每次遍历都会调用parse_boundary_stream方法返回对应的数据(item_type, meta_data, field_stream) for item_type, meta_data, field_stream in Parser(stream, self._boundary): try: disposition = meta_data["content-disposition"][1] # 获取每项数据中的name field_name = disposition["name"].strip() except (KeyError, IndexError, AttributeError): continue field_name = force_str(field_name, encoding, errors="replace") if item_type == FIELD: # Avoid reading more than DATA_UPLOAD_MAX_MEMORY_SIZE. if settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None: read_size = ( settings.DATA_UPLOAD_MAX_MEMORY_SIZE - num_bytes_read ) # This is a post field, we can just set it in the post # 获取field_name对应的值,及请求体中每项数据对应的value data = field_stream.read(size=read_size) num_bytes_read += len(data) num_bytes_read += len(field_name) + 2 self._post.appendlist( field_name, force_str(data, encoding, errors="replace") ) else: # If this is neither a FIELD or a FILE, just exhaust the stream. exhaust(stream) return self._post, self._files
在Parser类中,遍历它时,它会作为迭代器,并且返回parse_boundary_stream方法的返回值:(TYPE, outdict, stream)。在parse_boundary_stream方法中,会对请求体中的数据进行解析并用字典的方式将数据存在outdict中,感兴趣的可以自行查看里面的实现。
class Parser: def __init__(self, stream, boundary): self._stream = stream self._separator = b"--" + boundary def __iter__(self): boundarystream = InterBoundaryIter(self._stream, self._separator) for sub_stream in boundarystream: # Iterate over each part yield parse_boundary_stream(sub_stream, 1024)
parse_boundary_stream() 返回值 | 说明 |
---|---|
TYPE | 当前数据的类型,取值范围:field / file / raw |
outdict | 请求体中经过解析后的数据,数据类型:字典 |
stream | 数据类型:LazyStream对象 |
总结
Django在收到POST请求候,只有当请求头中的Content-Type为multipart/form-data 或者 application/x-www-form-urlencoded 时,才会对请求体中的数据进行解析操作。除此之外,请求体中的数据只有符合Django中的数据格式,才会对请求体中的数据进行正确的解析,从而request.POST中才会有数据。
以下列举三个例子来演示结果。
Content-Type为multipart/form-data,请求内容为:
POST /test HTTP/1.1 Host: 127.0.0.1:8000 Cache-Control: no-cache Postman-Token: 57cafddf-d487-c3eb-7583-39ef7e3a09b1 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="username" z ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="age" 25 ----WebKitFormBoundary7MA4YWxkTrZu0gW
打印 request.POST的结果:
<QueryDict: {'username': ['z'], 'age': ['25']}>
Content-Type为application/x-www-form-urlencoded,请求内容为:
POST /test HTTP/1.1 Host: 127.0.0.1:8000 Cache-Control: no-cache Postman-Token: ca233eaa-55fd-f32c-6f8f-e960fdd0fc6e Content-Type: application/x-www-form-urlencoded name=Z&age=24
打印request.POST的结果:
<QueryDict: {'name': ['Z'], 'age': ['24']}>
Content-Type为application/json,请求的格式为:
POST /test HTTP/1.1 Host: 127.0.0.1:8000 Content-Type: application/json Cache-Control: no-cache Postman-Token: 3e1804c9-fa88-8258-deb3-e76099a21039 { "name":"Z", "age":23 }
打印request.POST的结果:
<QueryDict: {}>
因此当遇到 request.POST获取不到 POST请求中的数据时,可以优先考虑以下两种情况:
1.Content-Type不是multipart/form-data 或者 application/x-www-form-urlencoded 。
2.对应的请求体数据格式不对。
本文转载原文链接:https://blog.csdn.net/weixin_44284498/article/details/124742848
本文由一叶发布,不代表一叶立场,转载联系作者并注明出处:https://yiyeo.com/python/131.html