阿小信大人的头像
Where there is a Python, there is a way. 阿小信大人

requests使用multipart/formdata提交post请求的hack姿势2016-10-31 21:19

今天调别人一个接口,需要用formdata的方式发post,没有报错信息,还真是折腾了好一会儿。

一直以为就是常用的post方法里面传递一个dict给data参数发请求,结果没想到不是,还是基本功不够啊。

先来看中规中矩的请求方式:(自己构建formdata的请求体)

from pprint import pprint
import requests


BOUNDARY = 'my-cute-multipart-formdata-boundary'
CRLF = '\r\n'
URL = 'http://httpbin.org/post'


def get_headers():
    return {
        'content-type': 'multipart/form-data; boundary={0}'.format(BOUNDARY)
    }


def get_multipart_formdata(data):
    post_data = []
    for key, value in data.iteritems():
        if value is None:
            continue
        post_data.append('--' + BOUNDARY)
        post_data.append('Content-Type: text/plain; charset=utf-8')
        post_data.append('Content-Disposition: form-data; name="{0}"'
                        .format(key))
        post_data.append('')
        if isinstance(value, int):
            value = str(value)
        post_data.append(value)
    post_data.append('--' + BOUNDARY + '--')
    post_data.append('')
    body = CRLF.join(post_data)
    return body.encode('utf-8')



def post_formdata(title, content):
    data = {
        'title': title,
        'content': content
    }
    form_data = get_multipart_formdata(data)
    resp = requests.post(URL, headers=get_headers(), data=form_data)
    if resp.ok:
        pprint(resp.json())
    else:
        print resp.reason


post_formdata('test', 'test')

运行输出结果:

{u'args': {},
 u'data': u'',
 u'files': {},
 u'form': {u'content': u'test', u'title': u'test'},
 u'headers': {u'Accept': u'*/*',
              u'Accept-Encoding': u'gzip, deflate',
              u'Cache-Control': u'max-age=259200',
              u'Content-Length': u'344',
              u'Content-Type': u'multipart/form-data; boundary=my-cute-multipart-formdata-boundary',
              u'Host': u'httpbin.org',
              u'User-Agent': u'python-requests/2.11.1'},
 u'json': None,
 u'origin': u'103.7.28.136',
 u'url': u'http://httpbin.org/post'}

构建请求的formdata还是比较烦人,以前只有在调用新浪微博接口上传图片的时候用过,像这种没有附件的这样搞真是太累了,有没有比较简单的方式呢?

有,使用requests的files参数。先来看几个请求的结果(没看源码,黑盒操作,感觉自己萌萌哒):

from pprint import pprint
import requests

URL = 'http://httpbin.org/post'
DATA = {'title': 'title', 'content': 'content'}
HEADERS = {'content-type': 'multipart/form-data; boundary={0}'.format('test')}


def post_data():
    resp = requests.post(URL, data=DATA)
    pprint(resp.json())


def post_with_formdata_header():
    resp = requests.post(URL, headers=HEADERS, data=DATA)
    pprint(resp.json())


def post_use_files_for_hack():
    resp = requests.post(URL, files=DATA)
    pprint(resp.json())


def post_data_but_empty_files():
    resp = requests.post(URL, data=DATA, files={})
    pprint(resp.json())


def post_data_and_files():
    resp = requests.post(URL, data=DATA, files={'files': 'hack'})
    pprint(resp.json())


post_data()
#  {u'args': {},
#   u'data': u'',
#   u'files': {},
#   u'form': {u'content': u'content', u'title': u'title'},
#   u'headers': {u'Accept': u'*/*',
#                u'Accept-Encoding': u'gzip, deflate',
#                u'Cache-Control': u'max-age=259200',
#                u'Content-Length': u'27',
#                u'Content-Type': u'application/x-www-form-urlencoded',
#                u'Host': u'httpbin.org',
#                u'User-Agent': u'python-requests/2.11.1'},
#   u'json': None,
#   u'origin': u'103.7.28.136',
#   u'url': u'http://httpbin.org/post'}

post_with_formdata_header()
#  {u'args': {},
#  u'data': u'',
#  u'files': {},
#  u'form': {},
#  u'headers': {u'Accept': u'*/*',
#               u'Accept-Encoding': u'gzip, deflate',
#               u'Cache-Control': u'max-age=259200',
#               u'Content-Length': u'23',
#               u'Content-Type': u'multipart/form-data; boundary=test',
#               u'Host': u'httpbin.org',
#               u'User-Agent': u'python-requests/2.11.1'},
#  u'json': None,
#  u'origin': u'103.7.28.136',
#  u'url': u'http://httpbin.org/post'}

post_use_files_for_hack()
#  {u'args': {},
#   u'data': u'',
#   u'files': {u'content': u'content', u'title': u'title'},
#   u'form': {},
#   u'headers': {u'Accept': u'*/*',
#                u'Accept-Encoding': u'gzip, deflate',
#                u'Cache-Control': u'max-age=259200',
#                u'Content-Length': u'262',
#                u'Content-Type': u'multipart/form-data; boundary=28d6573b04524ce38c500e92e53d2145',
#                u'Host': u'httpbin.org',
#                u'User-Agent': u'python-requests/2.11.1'},
#   u'json': None,
#   u'origin': u'103.7.28.136',
#   u'url': u'http://httpbin.org/post'}

post_data_but_empty_files()
#  {u'args': {},
#   u'data': u'',
#   u'files': {},
#   u'form': {u'content': u'content', u'title': u'title'},
#   u'headers': {u'Accept': u'*/*',
#                u'Accept-Encoding': u'gzip, deflate',
#                u'Cache-Control': u'max-age=259200',
#                u'Content-Length': u'27',
#                u'Content-Type': u'application/x-www-form-urlencoded',
#                u'Host': u'httpbin.org',
#                u'User-Agent': u'python-requests/2.11.1'},
#   u'json': None,
#   u'origin': u'103.7.28.136',
#   u'url': u'http://httpbin.org/post'}

post_data_and_files()
#  {u'args': {},
#   u'data': u'',
#   u'files': {u'files': u'hack'},
#   u'form': {u'content': u'content', u'title': u'title'},
#   u'headers': {u'Accept': u'*/*',
#                u'Accept-Encoding': u'gzip, deflate',
#                u'Cache-Control': u'max-age=259200',
#                u'Content-Length': u'332',
#                u'Content-Type': u'multipart/form-data; boundary=0f46e83f5f90440288b820a72a8eeb17',
#                u'Host': u'httpbin.org',
#                u'User-Agent': u'python-requests/2.11.1'},
#   u'json': None,
#   u'origin': u'103.7.28.136',
#   u'url': u'http://httpbin.org/post'}

可以看到,在直接使用dict传给data时,请求的content-typeapplication/x-www-form-urlencoded,所以这种方式不行,

那么在这个基础上给他设置content-type为formdata呢,结果content-type正确了,请求的数据却不见了,所以也不行,

但是在我们使用了files参数的时候,看上去好像是那么回事,但是form里面没有值,于是同时使用data和files就出现了我们想要的结果,注意files必须要传没空dict。


不能这么不负责任,还是跟一下代码吧:

post方法进去,他调用了sessions.py里面Session类的request方法, request方法里面调用了models.py里面的Request的prepare_request函数, prepare_request调了PreparedRequest的prepare,这里面会进行prepare_body处理,将data,files和json做一些处理

if files:
    (body, content_type) = self._encode_files(files, data)
else:
    if data:
        body = self._encode_params(data)
        if isinstance(data, basestring) or hasattr(data, 'read'):
            content_type = None
        else:
            content_type = 'application/x-www-form-urlencoded'

如果没有files参数或者为空,那么content-type要么为None要么为form-urlencoded,这就是要传files的原因,在_encode_files里面会将data和files的值都放到new_fields的列表中, 然后通过packages.urllib3.filepos.encode_multipart_formdata生成我们之前中规中矩写的请求体和form-data的content-type

def encode_multipart_formdata(fields, boundary=None):
    """
    Encode a dictionary of ``fields`` using the multipart/form-data MIME format.
    :param fields:
        Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`).
    :param boundary:
        If not specified, then a random boundary will be generated using
        :func:`mimetools.choose_boundary`.
    """
    body = BytesIO()
    if boundary is None:
        boundary = choose_boundary()

    for field in iter_field_objects(fields):
        body.write(b('--%s\r\n' % (boundary)))

        writer(body).write(field.render_headers())
        data = field.data

        if isinstance(data, int):
            data = str(data)  # Backwards compatibility

        if isinstance(data, six.text_type):
            writer(body).write(data)
        else:
            body.write(data)

        body.write(b'\r\n')

    body.write(b('--%s--\r\n' % (boundary)))

    content_type = str('multipart/form-data; boundary=%s' % boundary)

    return body.getvalue(), content_type

这样就可以免去自己去拼写请求体了,科科。


当然也有requests-toolbelt帮你实现,参看:https://github.com/sigmavirus24/requests-toolbelt#multipartform-data-encoder

如果您觉得从我的分享中得到了帮助,并且希望我的博客持续发展下去,请点击支付宝捐赠,谢谢!

若非特别声明,文章均为阿小信的个人笔记,转载请注明出处。文章如有侵权内容,请联系我,我会及时删除。

#Python#  
分享到:
阅读[2146] 评论[4]

你可能也感兴趣的文章推荐

本文最近访客

发表评论

#1 网友202.*.*.45[深圳]31708 :
你的项目就这么写吗? 看着像是CGI的方式。不用python的web框架?类似django什么的
2017-01-17 16:27 回复
#2 网友103.*.*.7[香港]43740 回复 #1 网友202.*.*.45[深圳] :
你好,这个只是一部分python代码,不涉及项目。
2017-02-06 14:56 回复
#3 网友47.*.*.40[香港]48090 :
围观
2017-02-15 12:14 回复
#4 网友127.*.*.1[火星]202 :
可以啊
2017-07-28 18:58 回复