如何在Python中使用requests发送“multipart/form-data”?

376
如何使用Python的requests库发送multipart/form-data格式的数据?我知道如何发送文件,但不知道如何用这种方法发送表单数据。请帮我解答。

你的问题不是很清楚。你想要实现什么?你希望在表单中发送“multipart/form-data”,但没有文件上传吗? - Hans Then
2
请查看此答案 https://stackoverflow.com/a/64586578/8826047 边界很重要! - Sona Pochybova
14个回答

354
基本上,如果你指定了一个files参数(一个字典),那么requests将发送一个multipart/form-data的POST请求,而不是application/x-www-form-urlencoded的POST请求。在该字典中,你并不局限于使用实际的文件。
>>> import requests
>>> response = requests.post('http://httpbin.org/post', files=dict(foo='bar'))
>>> response.status_code
200

而httpbin.org可以让你知道你发送的头部信息;在response.json()中我们有:
>>> from pprint import pprint
>>> pprint(response.json()['headers'])
{'Accept': '*/*',
 'Accept-Encoding': 'gzip, deflate',
 'Connection': 'close',
 'Content-Length': '141',
 'Content-Type': 'multipart/form-data; '
                 'boundary=c7cbfdd911b4e720f1dd8f479c50bc7f',
 'Host': 'httpbin.org',
 'User-Agent': 'python-requests/2.21.0'}

而且要明确一点:当您使用files参数时,不应设置Content-Type头,将此留给requests处理,因为它需要在头部中指定与请求体中使用的值匹配的(唯一的)边界值。
更好的是,您可以通过使用元组而不是单个字符串或字节对象来进一步控制每个部分的文件名、内容类型和附加头信息。该元组预期包含2到4个元素;文件名、内容(可选)、内容类型(可选)和一个可选的进一步头信息字典。
我会使用元组形式,并将文件名设为None,这样请求中的filename="..."参数就会被忽略。
>>> files = {'foo': 'bar'}
>>> print(requests.Request('POST', 'http://httpbin.org/post', files=files).prepare().body.decode('utf8'))
--bb3f05a247b43eede27a124ef8b968c5
Content-Disposition: form-data; name="foo"; filename="foo"

bar
--bb3f05a247b43eede27a124ef8b968c5--
>>> files = {'foo': (None, 'bar')}
>>> print(requests.Request('POST', 'http://httpbin.org/post', files=files).prepare().body.decode('utf8'))
--d5ca8c90a869c5ae31f70fa3ddb23c76
Content-Disposition: form-data; name="foo"

bar
--d5ca8c90a869c5ae31f70fa3ddb23c76--

files也可以是一个包含两个值的元组列表,如果你需要排序和/或具有相同名称的多个字段:

requests.post(
    'http://requestb.in/xucj9exu',
    files=(
        ('foo', (None, 'bar')),
        ('foo', (None, 'baz')),
        ('spam', (None, 'eggs')),
    )
)

如果您同时指定了filesdata,那么取决于data的值将用于创建POST请求的主体。如果data是一个字符串,则只使用它;否则,同时使用datafiles,其中data中的元素先列出。
还有一个优秀的requests-toolbelt项目,其中包括高级多部分支持。它采用与files参数相同格式的字段定义,但与requests不同,默认情况下不设置文件名参数。此外,它可以从打开的文件对象流式传输请求,而requests会首先在内存中构建请求主体。
from requests_toolbelt.multipart.encoder import MultipartEncoder

mp_encoder = MultipartEncoder(
    fields={
        'foo': 'bar',
        # plain file object, no filename or mime type produces a
        # Content-Disposition header with just the part name
        'spam': ('spam.txt', open('spam.txt', 'rb'), 'text/plain'),
    }
)
r = requests.post(
    'http://httpbin.org/post',
    data=mp_encoder,  # The MultipartEncoder is posted as data, don't use files=...!
    # The MultipartEncoder provides the content-type header with the boundary:
    headers={'Content-Type': mp_encoder.content_type}
)

字段遵循相同的约定;使用包含2到4个元素的元组来添加文件名、部分MIME类型或额外的头信息。与files参数不同,如果您不使用元组,则不会尝试找到默认的filename值。

14
如果使用 files={},则不能使用 headers={'Content-Type':'blah blah'}! - Zaki
10
@zaki: 的确,因为 multipart/form-data 的 Content-Type 必须 包括用于划分 post 请求主体中各部分的边界值。不设置 Content-Type 标头会导致 requests 自动设置正确的值。 - Martijn Pieters
@Zaki - 你在一条简明扼要的评论中解决了我数小时的困惑!这个评论应该用大号粗体字写出来。谢谢!!! - mdev

141

自某些之前的回答以来,Requests 已经有所改变。请查看Github 上的此问题以获取更多详细信息,并查看此评论以获得示例。

简而言之,files 参数接受一个字典,其中键是表单字段的名称,而值可以是字符串或2、3或4个长度的元组,如在 Requests 快速入门指南中的POST a Multipart-Encoded File部分所述:

>>> url = 'http://httpbin.org/post'
>>> files = {'file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel', {'Expires': '0'})}

上述中,元组的组成如下:

(filename, data, content_type, headers)

如果值只是一个字符串,则文件名将与键相同,如下所示:

>>> files = {'obvius_session_id': '72c2b6f406cdabd578c5fd7598557c52'}

Content-Disposition: form-data; name="obvius_session_id"; filename="obvius_session_id"
Content-Type: application/octet-stream

72c2b6f406cdabd578c5fd7598557c52
如果值是元组且第一项为None,则不包括文件名属性:
>>> files = {'obvius_session_id': (None, '72c2b6f406cdabd578c5fd7598557c52')}

Content-Disposition: form-data; name="obvius_session_id"
Content-Type: application/octet-stream

72c2b6f406cdabd578c5fd7598557c52

2
如果您需要区分“名称”和“文件名”,但同时又有多个具有相同名称的字段,该怎么办? - Michael
1
我有一个和@Michael类似的问题。你能看一下这个问题并给些建议吗?链接 - Shaardool
有人解决了使用相同名称的多个字段的问题吗? - user3131037
1
现在不能再使用将空字符串作为files元组的第一个值来传递非文件multipart/form-data参数了。您需要改用requests.postdata参数来发送附加的非文件multipart/form-data参数。 - Lucas Cimon
1
传递None而不是空字符串似乎有效。 - Alexandre Blin
显示剩余3条评论

134

即使您不需要上传任何文件,也需要使用files参数来发送多部分表单POST请求。

来自requests的原始代码:

def request(method, url, **kwargs):
    """Constructs and sends a :class:`Request <Request>`.

    ...
    :param files: (optional) Dictionary of ``'name': file-like-objects``
        (or ``{'name': file-tuple}``) for multipart encoding upload.
        ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``,
        3-tuple ``('filename', fileobj, 'content_type')``
        or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``,
        where ``'content-type'`` is a string
        defining the content type of the given file
        and ``custom_headers`` a dict-like object 
        containing additional headers to add for the file.

相关部分是:file-tuple,它可以是:

  • 2-tuple文件名,文件对象
  • 3-tuple文件名,文件对象,内容类型
  • 4-tuple文件名,文件对象,内容类型,自定义标头)。

☝可能不明显的是,fileobj可以是实际文件对象(处理文件时),也可以是字符串(处理纯文本字段时)。

基于上述信息,包含要上传的文件和表单字段的最简单的多部分请求如下所示:

import requests

multipart_form_data = {
    'upload': ('custom_file_name.zip', open('myfile.zip', 'rb')),
    'action': (None, 'store'),
    'path': (None, '/path1')
}

response = requests.post('https://httpbin.org/post', files=multipart_form_data)

print(response.content)

请注意纯文本字段元组中第一个参数为None —— 这是用于文件上传的文件名字段的占位符,但对于文本字段,必须将None作为第一个参数传递才能提交数据。

具有相同名称的多个字段

如果您需要发布具有相同名称的多个字段,则可以将载荷定义为元组(或列表)的列表,而不是字典:

multipart_form_data = (
    ('file2', ('custom_file_name.zip', open('myfile.zip', 'rb'))),
    ('action', (None, 'store')),
    ('path', (None, '/path1')),
    ('path', (None, '/path2')),
    ('path', (None, '/path3')),
)

流式传输请求API

如果上述API对你来说不够Pythonic,那么考虑使用requests toolbeltpip install requests_toolbelt),它是核心requests模块的扩展,提供了文件上传流和MultipartEncoder,可以替代files,还可以让你将负载定义为字典、元组或列表。

MultipartEncoder可用于带有或不带有实际上传字段的多部分请求。它必须分配给data参数。

import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder

multipart_data = MultipartEncoder(
    fields={
            # a file upload field
            'file': ('file.zip', open('file.zip', 'rb'), 'text/plain')
            # plain text fields
            'field0': 'value0', 
            'field1': 'value1',
           }
    )

response = requests.post('http://httpbin.org/post', data=multipart_data,
                  headers={'Content-Type': multipart_data.content_type})

如果您需要发送具有相同名称的多个字段,或者表单字段的顺序很重要,则可以使用元组或列表代替字典:

multipart_data = MultipartEncoder(
    fields=(
            ('action', 'ingest'), 
            ('item', 'spam'),
            ('item', 'sausage'),
            ('item', 'eggs'),
           )
    )

1
太神奇了。不知何故,我正在使用的一个API需要同一键的两个不同值。这真是太神奇了。谢谢。 - ajon
@ccpizza,这行代码的实际含义是什么?> "('file.py',open('file.py','rb'),'text/plain')"。但它对我不起作用 :( - Denis Koreyba
2
你可以使用None代替空字符串。这样,请求将不会包括文件名。因此,代替Content-Disposition: form-data; name="action"; filename="",它将是Content-Disposition: form-data; name="action"。对于服务器将这些字段视为表单字段而不是文件,这对我非常关键。 - Mitar
1
我正在寻找类似于JavaScript的东西,例如https://developer.mozilla.org/en-US/docs/Web/API/FormData,以便发送具有相同名称的多个字段。非常感谢您如此详细的回答。您是一个英雄! - Umair Aslam
关于多个具有相同名称的字段 - 我必须将其变成元组列表而不是元组嵌套的元组才能正常工作。 - grantr
显示剩余3条评论

30

这里是使用requests上传单个文件且带有额外参数的简单代码片段:

url = 'https://<file_upload_url>'
fp = '/Users/jainik/Desktop/data.csv'

files = {'file': open(fp, 'rb')}
payload = {'file_id': '1234'}

response = requests.put(url, files=files, data=payload, verify=False)
请注意,您不需要显式指定任何内容类型。 注:想要评论以上答案之一,但由于声望低无法评论,因此在此撰写了新的回复。

5
最简洁易懂的翻译:无论如何,使用'rb'选项打开文件是最清晰易懂的方式吗? - ghchoi
1
是的,这正是它的核心:filesdata都作为字典。 - WestCoastProjects
1
在上面许多冗长而复杂的答案之后,这个直接进入核心并且有效! - thenewasker

12
通过在POST请求中指定files参数,请求的Content-Type自动设置为multipart/form-data(后面跟随着用于在多部分负载中分隔每个主体部分的boundary字符串),无论您只发送files,还是同时发送form-datafiles(因此,在这种情况下不应尝试手动设置Content-Type)。而如果只发送form-data,则Content-Type将自动设置为application/x-www-form-urlencoded
您可以打印出请求的Content-Type标头以验证上述内容,使用下面给出的示例,该示例显示如何上传多个文件(或单个文件),并且(可选)具有相同的key(即,在下面的情况下为'files'),以及可选的form-data(即,在下面的示例中为data=data)。关于如何POST单个和多个files的文档分别可以在这里这里找到。如果您需要上传大型文件而无需将其读入内存,请查看流式上传
对于服务器端——如果需要的话——请查看此答案,其中包含下面代码片段,并使用FastAPI Web框架。

例子

import requests

url = 'http://127.0.0.1:8000/submit'
files = [('files', open('a.txt', 'rb')), ('files', open('b.txt', 'rb'))]
#file = {'file': open('a.txt','rb')} # to send a single file
data ={"name": "foo", "point": 0.13, "is_accepted": False}
r = requests.post(url=url, data=data, files=files) 
print(r.json())
print(r.request.headers['content-type'])

8
您需要使用网站HTML中上传文件的name属性。例如:
autocomplete="off" name="image">

你看到了name="image">吗?你可以在上传文件的网站的HTML中找到它。你需要使用它来使用Multipart/form-data上传文件。
脚本:
import requests

site = 'https://prnt.sc/upload.php' # the site where you upload the file
filename = 'image.jpg'  # name example

在这里,替换图片位置,将上传文件的名称添加到HTML中。
up = {'image':(filename, open(filename, 'rb'), "multipart/form-data")}

如果上传需要点击上传按钮,你可以使用如下方式:
data = {
     "Button" : "Submit",
}

然后开始请求

request = requests.post(site, files=up, data=data)

完成了,文件已成功上传。


5
import requests
# assume sending two files
url = "put ur url here"
f1 = open("file 1 path", 'rb')
f2 = open("file 2 path", 'rb')
response = requests.post(url,files={"file1 name": f1, "file2 name":f2})
print(response)

4
import json
import os
import requests
from requests_toolbelt import MultipartEncoder

AUTH_API_ENDPOINT = "http://localhost:3095/api/auth/login"

def file_upload(path_img, token ):
    url = 'http://localhost:3095/api/shopping/product/image'
    name_img = os.path.basename(path_img)

    mp_encoder = MultipartEncoder(
        fields={
            'email': 'mcm9@gmail.com',
            'source': 'tmall',
            'productId': 'product_0001',
            'image': (name_img, open(path_img, 'rb'), 'multipart/form-data')
        #'spam': ('spam.txt', open('spam.txt', 'rb'), 'text/plain'),
        }
    )

    head = {'Authorization': 'Bearer  {}'.format(token),
            'Content-Type': mp_encoder.content_type}

    with requests.Session() as s:
        result = s.post(url, data=mp_encoder, headers=head)

    return result

def do_auth(username, password, url=AUTH_API_ENDPOINT):
    data = {
        "email": username,
        "password": password
    }

    # sending post request and saving response as response object
    r = requests.post(url=url, data=data)

    # extracting response text
    response_text = r.text

    d = json.loads(response_text)
    # print(d)

    return d


if __name__ == '__main__':
    result = do_auth('mcm4@gmail.com','123456')
    token = result.get('data').get('payload').get('token')
    print(token)
    result = file_upload('/home/mcm/Pictures/1234.png',token)
    print(result.json())

1
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

4
为了澄清上面给出的示例,
即使您不需要上传任何文件,也需要使用“files”参数来发送多部分表单POST请求。
“files = {}”很不幸无法起作用。
您需要放一些虚拟值在里面,例如:
files={"foo": "bar"}

当我尝试将文件上传到Bitbucket的REST API时,遇到了这个问题,不得不编写这个可怕的东西,以避免可怕的“不支持的媒体类型”错误:

url = "https://my-bitbucket.com/rest/api/latest/projects/FOO/repos/bar/browse/foobar.txt"
payload = {'branch': 'master', 
           'content': 'text that will appear in my file',
           'message': 'uploading directly from python'}
files = {"foo": "bar"}
response = requests.put(url, data=payload, files=files)

:O=


你不能使用 requests.put(url, files=payload) 吗? - Nizam Mohamed

3

发送multipart/form-data格式的键值对数据

curl命令:

curl -X PUT http://127.0.0.1:8080/api/xxx ...
-H 'content-type: multipart/form-data; boundary=----xxx' \
-F taskStatus=1

Python requests - 更复杂的POST请求

    updateTaskUrl = "http://127.0.0.1:8080/api/xxx"
    updateInfoDict = {
        "taskStatus": 1,
    }
    resp = requests.put(updateTaskUrl, data=updateInfoDict)

发送multipart/form-data文件

curl命令:

curl -X POST http://127.0.0.1:8080/api/xxx ...
-H 'content-type: multipart/form-data; boundary=----xxx' \
-F file=@/Users/xxx.txt

Python Requests - 发送一个多部分编码的文件:

    filePath = "/Users/xxx.txt"
    fileFp = open(filePath, 'rb')
    fileInfoDict = {
        "file": fileFp,
    }
    resp = requests.post(uploadResultUrl, files=fileInfoDict)

这是全部内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接