Django—request.POST解析

1 引言

        使用Django开发WEB网站时,通常下可以通过request.POST请求获取POST请求中的参数。

        但是有时候明明前端发送的是POST请求,但是在调用request.POST的时候却没有获取相关数据,而在request.body中是有数据。这种情况通常情况下是由于请求的Content-Type的类型或者传入的数据格式不符合Django的规范。

        本文通过Django中的一部分源码来解释request.POST怎样才会有值。由于Django中的源码太多,因此本文只会解释源码中的部分代码。若不想看过程可以直接看总结部分。

2 正文

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对象

 

3 总结

        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. 对应的请求体数据格式不对。

4 结语

        以上是本人查阅Django的相关源码时的个人见解,如果有不对的地方,欢迎大家指出~

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值