Wie sende ich ein "Multipart / Formulardaten" mit Anfragen in Python?

211

Wie sende ich eine multipart/form-datamit Anfragen in Python? Wie man eine Datei sendet, verstehe ich, aber wie man die Formulardaten mit dieser Methode sendet, kann man nicht verstehen.

agrynchuk
quelle
Ihre Frage ist nicht wirklich klar. Was möchten Sie erreichen? Möchten Sie "Multipart / Formulardaten" senden, ohne dass eine Datei in das Formular hochgeladen wird?
Hans dann
4
Die Tatsache, dass filesParameter verwendet werden, um beides zu tun, ist eine sehr schlechte API. Ich habe ein Problem mit dem Titel Senden mehrteiliger Daten angesprochen - wir benötigen eine bessere API , um dies zu beheben. Wenn Sie damit einverstanden sind, dass die Verwendung von filesParametern zum Senden von Daten mit mehreren Teilen bestenfalls irreführend ist, bitten Sie darum, die API in der obigen Ausgabe zu ändern.
Piotr Dobrogost
@PiotrDobrogost dieses Problem ist geschlossen. Ermutigen Sie die Leute nicht, sich zu geschlossenen Themen zu äußern, die relevant oder anderweitig sind.
Ian Stapleton Cordasco
1
Egal, ich habe gerade festgestellt, dass Ihr Kommentar veröffentlicht wurde, bevor er geschlossen wurde. Ich hasse es, wenn StackOverflow die Dinge nicht in chronologischer Reihenfolge hält.
Ian Stapleton Cordasco

Antworten:

166

Wenn Sie einen filesParameter (ein Wörterbuch) angeben , requestswird grundsätzlich ein multipart/form-dataPOST anstelle eines application/x-www-form-urlencodedPOST gesendet. Sie sind jedoch nicht darauf beschränkt, tatsächliche Dateien in diesem Wörterbuch zu verwenden:

>>> import requests
>>> response = requests.post('http://httpbin.org/post', files=dict(foo='bar'))
>>> response.status_code
200

und httpbin.org informiert Sie darüber, mit welchen Headern Sie gepostet haben. in haben response.json()wir:

>>> 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'}

Besser noch, Sie können den Dateinamen, den Inhaltstyp und zusätzliche Überschriften für jeden Teil weiter steuern, indem Sie ein Tupel anstelle einer einzelnen Zeichenfolge oder eines Byte-Objekts verwenden. Es wird erwartet, dass das Tupel zwischen 2 und 4 Elemente enthält. den Dateinamen, den Inhalt, optional einen Inhaltstyp und ein optionales Wörterbuch mit weiteren Kopfzeilen.

Ich würde das Tupelformular mit Noneals Dateinamen verwenden, damit der filename="..."Parameter aus der Anforderung für diese Teile entfernt wird:

>>> 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 kann auch eine Liste von Tupeln mit zwei Werten sein, wenn Sie eine Bestellung und / oder mehrere Felder mit demselben Namen benötigen:

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

Wenn Sie beide angeben , filesund datadann kommt es auf den Wert von , datawas verwendet werden , um den POST - Körper zu schaffen. Wenn dataes sich um eine Zeichenfolge handelt, wird nur diese verwendet. Andernfalls werden beide dataund filesverwendet, wobei die Elemente datazuerst aufgeführt werden.

Es gibt auch das hervorragende requests-toolbeltProjekt, das erweiterte Unterstützung für mehrere Teile umfasst . Es werden Felddefinitionen im gleichen Format wie der filesParameter verwendet, im Gegensatz requestsdazu wird standardmäßig kein Dateinamenparameter festgelegt. Darüber hinaus kann die Anforderung von geöffneten Dateiobjekten gestreamt werden, wobei requestszuerst der Anforderungshauptteil im Speicher erstellt wird:

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}
)

Felder folgen den gleichen Konventionen; Verwenden Sie ein Tupel mit 2 bis 4 Elementen, um einen Dateinamen, einen Teil-MIME-Typ oder zusätzliche Header hinzuzufügen. Im Gegensatz zum filesParameter wird kein Versuch unternommen, einen Standardwert zu finden filename, wenn Sie kein Tupel verwenden.

Martijn Pieters
quelle
3
Wenn files = {} verwendet wird, dürfen headers = {'Content-Type': 'bla bla'} nicht verwendet werden!
Zaki
5
@zaki: In der Tat, weil der multipart/form-dataInhaltstyp den Grenzwert enthalten muss, der zur Abgrenzung der Teile im Post-Body verwendet wird. Wenn Sie den Content-TypeHeader nicht setzen , wird requestser auf den richtigen Wert gesetzt.
Martijn Pieters
Wichtiger Hinweis: Die Anfrage wird nur so gesendet, als multipart/form-dataob der Wert von files=wahr ist. Wenn Sie also eine multipart/form-dataAnfrage senden müssen, aber keine Dateien enthalten, können Sie einen wahrheitsgemäßen, aber bedeutungslosen Wert wie {'':''}und data=mit Ihrem Anfragetext festlegen . Wenn Sie dies tun, geben Sie den Content-TypeHeader nicht selbst an. requestswird es für Sie einstellen. Sie können die Wahrheitsüberprüfung hier sehen: github.com/psf/requests/blob/…
Daniel Situnayake
@ DanielSitunayake gibt es keine Notwendigkeit für einen solchen Hack. filesFügen Sie einfach alle Felder in das Diktat ein, es müssen keine Dateien sein (verwenden Sie einfach das Tupelformular und setzen Sie den Dateinamen auf None). Besser noch, nutzen Sie das requests_toolbeltProjekt.
Martijn Pieters
Danke @MartijnPieters, der Trick mit der Tupelform ist großartig! Ich werde es versuchen.
Daniel Situnayake
107

Seit die vorherigen Antworten geschrieben wurden, haben sich die Anfragen geändert. Schauen Sie sich den Bug-Thread bei Github an, um mehr Details zu erfahren, und diesen Kommentar als Beispiel.

Kurz gesagt, der Parameter files nimmt a an, dictwobei der Schlüssel der Name des Formularfelds und der Wert entweder eine Zeichenfolge oder ein Tupel mit 2, 3 oder 4 Längen ist, wie im Abschnitt POST a Multipart-Encoded File in den Anforderungen beschrieben Schnellstart:

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

Oben ist das Tupel wie folgt zusammengesetzt:

(filename, data, content_type, headers)

Wenn der Wert nur eine Zeichenfolge ist, entspricht der Dateiname dem Schlüssel wie im Folgenden:

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

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

72c2b6f406cdabd578c5fd7598557c52

Wenn der Wert ein Tupel ist und der erste Eintrag Noneder Dateiname ist, wird er nicht berücksichtigt:

>>> files = {'obvius_session_id': (None, '72c2b6f406cdabd578c5fd7598557c52')}

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

72c2b6f406cdabd578c5fd7598557c52
Runejuhl
quelle
2
Was ist, wenn Sie das nameund unterscheiden müssen, filenameaber auch mehrere Felder mit demselben Namen haben?
Michael
1
Ich habe ein ähnliches Problem wie @Michael. Können Sie sich die Frage ansehen und etwas vorschlagen? [Link] ( stackoverflow.com/questions/30683352/… )
Shaardool
Hat jemand dieses Problem gelöst, indem er mehrere Felder mit demselben Namen hatte?
user3131037
1
Der Trick, eine leere Zeichenfolge als ersten Wert eines filesTupels zu übergeben, funktioniert nicht mehr: Sie müssen requests.post datastattdessen Parameter verwenden, um zusätzliche Nicht-Datei- multipart/form-dataParameter zu senden
Lucas Cimon
1
Passing Noneanstelle einer leeren Zeichenfolge scheint zu funktionieren
Alexandre Blin
73

Sie müssen den filesParameter verwenden, um eine mehrteilige POST-Anfrage zu senden, auch wenn Sie keine Dateien hochladen müssen.

Aus der ursprünglichen Anforderungsquelle :

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.

Der relevante Teil ist: file-tuple can be a2-tuple, .3-tupleor a4-tuple

Basierend auf dem oben Gesagten sieht die einfachste mehrteilige Formularanforderung, die sowohl hochzuladende Dateien als auch Formularfelder enthält, folgendermaßen aus:

multipart_form_data = {
    'file2': ('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)

Beachten Sie das Noneals erstes Argument im Tupel für Nur-Text-Felder - dies ist ein Platzhalter für das Dateinamenfeld, das nur für Datei-Uploads verwendet wird, aber für Textfelder, die Noneals erster Parameter übergeben werden, ist erforderlich, damit die Daten gesendet werden .

Mehrere Felder mit demselben Namen

Wenn Sie mehrere Felder mit demselben Namen veröffentlichen müssen, können Sie anstelle eines Wörterbuchs Ihre Nutzdaten als Liste (oder Tupel) von Tupeln definieren:

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 für Streaming-Anforderungen

Wenn die oben genannte API für Sie nicht pythonisch genug ist, sollten Sie das Anforderungs-Toolbelt ( pip install requests_toolbelt) verwenden, eine Erweiterung des Kernanforderungsmoduls, das das Streaming von Datei-Uploads unterstützt, sowie den MultipartEncoder , der anstelle von verwendet werden fileskann und der dies auch ermöglicht Sie definieren die Nutzdaten als Wörterbuch, Tupel oder Liste.

MultipartEncoderkann sowohl für mehrteilige Anfragen mit als auch ohne tatsächliche Upload-Felder verwendet werden. Es muss dem dataParameter zugewiesen werden.

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})

Wenn Sie mehrere Felder mit demselben Namen senden müssen oder wenn die Reihenfolge der Formularfelder wichtig ist, kann anstelle eines Wörterbuchs ein Tupel oder eine Liste verwendet werden:

multipart_data = MultipartEncoder(
    fields=(
            ('action', 'ingest'), 
            ('item', 'spam'),
            ('item', 'sausage'),
            ('item', 'eggs'),
           )
    )
ccpizza
quelle
Danke dafür. Die Reihenfolge der Schlüssel war mir wichtig und das hat mir sehr geholfen.
Pracht
Tolle. Unerklärlicherweise erfordert eine API, mit der ich arbeite, zwei verschiedene Werte für denselben Schlüssel. Das ist großartig. Danke dir.
Ajon
@ccpizza, was bedeutet diese Zeile eigentlich? > "('file.py', open ('file.py', 'rb'), 'text / plain')". Es funktioniert nicht für mich :(
Denis Koreyba
@DenisKoreyba: Dies ist ein Beispiel für ein Feld zum Hochladen von Dateien, bei dem davon ausgegangen wird, dass sich die benannte Datei file.pyim selben Ordner wie Ihr Skript befindet.
ccpizza
1
Sie können Noneanstelle einer leeren Zeichenfolge verwenden. Dann enthalten Anfragen überhaupt keinen Dateinamen. Also stattdessen Content-Disposition: form-data; name="action"; filename=""wird es sein Content-Disposition: form-data; name="action". Dies war für mich entscheidend, damit der Server diese Felder als Formularfelder und nicht als Dateien akzeptiert.
Mitar
8

Hier ist das einfache Code-Snippet zum Hochladen einer einzelnen Datei mit zusätzlichen Parametern mithilfe von Anforderungen:

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)

Bitte beachten Sie, dass Sie keinen Inhaltstyp explizit angeben müssen.

HINWEIS: Wollte eine der oben genannten Antworten kommentieren, konnte dies jedoch aufgrund der geringen Reputation nicht. Daher wurde hier eine neue Antwort verfasst.

Jainik
quelle
4

Sie müssen das nameAttribut der Upload-Datei verwenden, das sich im HTML-Code der Site befindet. Beispiel:

autocomplete="off" name="image">

Siehst du name="image">? Sie finden es im HTML-Code einer Site zum Hochladen der Datei. Sie müssen es verwenden, um die Datei mit hochzuladenMultipart/form-data

Skript:

import requests

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

Fügen Sie hier anstelle des Bildes den Namen der Upload-Datei in HTML hinzu

up = {'image':(filename, open(filename, 'rb'), "multipart/form-data")}

Wenn für den Upload die Schaltfläche zum Hochladen erforderlich ist, können Sie Folgendes verwenden:

data = {
     "Button" : "Submit",
}

Starten Sie dann die Anfrage

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

Und fertig, Datei erfolgreich hochgeladen

Skiller Dz
quelle
3

Senden Sie einen mehrteiligen / Formulardatenschlüssel und -wert

Curl-Befehl:

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

Python- Anfragen - Kompliziertere POST-Anfragen :

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

Senden Sie eine mehrteilige / Formulardatendatei

Curl-Befehl:

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- Anforderungen - POST eine Multipart-codierte Datei :

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

das ist alles.

Crifan
quelle
-1

Hier ist das Python-Snippet, das Sie benötigen, um eine große einzelne Datei als mehrteilige Formulardaten hochzuladen. Mit NodeJs Multer-Middleware, die auf der Serverseite ausgeführt wird.

import requests
latest_file = 'path/to/file'
url = "http://httpbin.org/apiToUpload"
files = {'fieldName': open(latest_file, 'rb')}
r = requests.put(url, files=files)

Für die Serverseite überprüfen Sie bitte die Multer-Dokumentation unter: https://github.com/expressjs/multer. Hier wird das Feld single ('fieldName') verwendet, um eine einzelne Datei zu akzeptieren, wie in:

var upload = multer().single('fieldName');
vinaymk
quelle