Comment envoyer un "multipart / form-data" avec des requêtes en python?

214

Comment envoyer un multipart/form-dataavec des demandes en python? Comment envoyer un fichier, je comprends, mais comment envoyer les données du formulaire par cette méthode ne peut pas comprendre.

agrynchuk
la source
votre question n'est pas vraiment claire. Que veux-tu accomplir? Souhaitez-vous envoyer "multipart / form-data" sans téléchargement de fichier dans le formulaire?
Hans Then
4
Le fait que le filesparamètre soit utilisé pour faire les deux est une très mauvaise API. J'ai soulevé un problème intitulé Envoi de données en plusieurs parties - nous avons besoin d'une meilleure API pour résoudre ce problème. Si vous acceptez que l'utilisation de filesparamètres pour envoyer des données en plusieurs parties est au mieux trompeuse, veuillez demander de changer l'API dans le problème ci-dessus.
Piotr Dobrogost
@PiotrDobrogost ce problème est clos. N'encouragez pas les gens à commenter des questions fermées, pertinentes ou non.
Ian Stapleton Cordasco
1
Peu importe, je viens de réaliser que votre commentaire a été publié avant sa fermeture. Je déteste que StackOverflow ne garde pas les choses dans l'ordre chronologique.
Ian Stapleton Cordasco

Réponses:

168

Fondamentalement, si vous spécifiez un filesparamètre (un dictionnaire), puis requestsenverra un multipart/form-dataPOST au lieu d'un application/x-www-form-urlencodedPOST. Cependant, vous n'êtes pas limité à l'utilisation de fichiers réels dans ce dictionnaire:

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

et httpbin.org vous permet de savoir avec quels en-têtes vous avez posté; en response.json()nous avons:

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

Mieux encore, vous pouvez contrôler davantage le nom de fichier, le type de contenu et les en-têtes supplémentaires pour chaque partie en utilisant un tuple au lieu d'une seule chaîne ou d'un seul octet. Le tuple devrait contenir entre 2 et 4 éléments; le nom de fichier, le contenu, éventuellement un type de contenu, et un dictionnaire facultatif d'autres en-têtes.

J'utiliserais le formulaire tuple avec Nonecomme nom de fichier, de sorte que le filename="..."paramètre soit supprimé de la demande pour ces parties:

>>> 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 peut également être une liste de tuples à deux valeurs, si vous avez besoin de commander et / ou de plusieurs champs avec le même nom:

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

Si vous spécifiez les deux fileset data, cela dépend de la valeur de datace qui sera utilisé pour créer le corps POST. Si dataest une chaîne, seule elle sera utilisée; sinon les deux dataet filessont utilisés, avec les éléments dataénumérés en premier.

Il y a aussi l'excellent requests-toolbeltprojet, qui inclut un support avancé en plusieurs parties . Il prend les définitions de champ dans le même format que le filesparamètre, mais contrairement à requests, il ne prend pas par défaut la valeur d'un paramètre de nom de fichier. De plus, il peut diffuser la demande à partir d'objets de fichier ouverts, où requestsva d'abord construire le corps de la demande en mémoire:

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

Les champs suivent les mêmes conventions; utilisez un tuple avec entre 2 et 4 éléments pour ajouter un nom de fichier, une partie de type mime ou des en-têtes supplémentaires. Contrairement au filesparamètre, aucune tentative n'est faite pour trouver une filenamevaleur par défaut si vous n'utilisez pas de tuple.

Martijn Pieters
la source
3
Si files = {} est utilisé, headers = {'Content-Type': 'blah blah'} ne doit pas être utilisé!
zaki
5
@zaki: en effet, parce que le multipart/form-dataContent-Type doit inclure la valeur limite utilisée pour délimiter les pièces dans le corps du message. Le fait de ne pas définir l'en- Content-Typetête garantit qu'il le requestsdéfinit à la valeur correcte.
Martijn Pieters
Remarque importante: la demande ne sera envoyée que multipart/form-datasi la valeur de files=est véridique, donc si vous avez besoin d'envoyer une multipart/form-datademande mais n'incluez aucun fichier, vous pouvez définir une valeur véridique mais dénuée de sens telle que {'':''}, et définir data=avec votre corps de demande. Si vous faites cela, ne fournissez pas l' Content-Typeen-tête vous-même; requestsle définira pour vous. Vous pouvez voir la vérification de la vérité ici: github.com/psf/requests/blob/…
Daniel Situnayake
@DanielSitunayake il n'y a pas besoin d'un tel hack. Mettez simplement tous les champs dans le filesdict, ils ne doivent pas nécessairement être des fichiers (assurez-vous simplement d'utiliser le formulaire tuple et définissez le nom de fichier sur None). Mieux encore, utilisez le requests_toolbeltprojet.
Martijn Pieters
Merci @MartijnPieters, l'astuce avec la forme de tuple est géniale! Je vais essayer.
Daniel Situnayake
107

Depuis que les réponses précédentes ont été écrites, les demandes ont changé. Jetez un œil au fil de bogue sur Github pour plus de détails et ce commentaire pour un exemple.

En bref, le paramètre files prend un dictavec la clé étant le nom du champ du formulaire et la valeur étant soit une chaîne soit un tuple de 2, 3 ou 4 longueurs, comme décrit dans la section POST un fichier codé en plusieurs parties dans les requêtes démarrage rapide:

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

Dans ce qui précède, le tuple est composé comme suit:

(filename, data, content_type, headers)

Si la valeur n'est qu'une chaîne, le nom de fichier sera le même que la clé, comme dans l'exemple suivant:

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

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

72c2b6f406cdabd578c5fd7598557c52

Si la valeur est un tuple et que la première entrée est Nonela propriété de nom de fichier ne sera pas incluse:

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

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

72c2b6f406cdabd578c5fd7598557c52
runejuhl
la source
2
Que se passe-t-il si vous devez distinguer le nameet filenamemais également avoir plusieurs champs avec le même nom?
Michael
1
J'ai un problème similaire à @Michael. Pouvez-vous jeter un œil à la question et suggérer quelque chose? [lien] ( stackoverflow.com/questions/30683352/… )
Shaardool
quelqu'un a-t-il résolu ce problème en ayant plusieurs champs avec le même nom?
user3131037
1
L'astuce pour passer une chaîne vide comme première valeur d'un filestuple ne fonctionne plus: vous devez utiliser un requests.post dataparamètre à la place pour envoyer des multipart/form-dataparamètres non-fichier supplémentaires
Lucas Cimon
1
Passer Noneau lieu d'une chaîne vide semble fonctionner
Alexandre Blin
74

Vous devez utiliser le filesparamètre pour envoyer une demande POST de formulaire en plusieurs parties même lorsque vous n'avez pas besoin de télécharger de fichiers.

De la source des demandes d' origine:

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.

La partie pertinente est: file-tuple can be a2-tuple, .3-tupleor a4-tuple

Sur la base de ce qui précède, la demande de formulaire en plusieurs parties la plus simple qui comprend à la fois les fichiers à télécharger et les champs de formulaire ressemblera à ceci:

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)

Notez le Nonecomme premier argument dans le tuple pour les champs de texte brut - il s'agit d'un espace réservé pour le champ de nom de fichier qui n'est utilisé que pour les téléchargements de fichiers, mais pour les champs de texte passant Nonecomme premier paramètre est requis pour que les données soient soumises .

Plusieurs champs avec le même nom

Si vous devez publier plusieurs champs avec le même nom, au lieu d'un dictionnaire, vous pouvez définir votre charge utile comme une liste (ou un tuple) de tuples:

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 de requêtes de streaming

Si l'API ci-dessus n'est pas assez pythonique pour vous, alors envisagez d'utiliser la barre d' outils de requêtes ( pip install requests_toolbelt) qui est une extension du module de requêtes principal qui prend en charge le streaming de téléchargement de fichiers ainsi que le MultipartEncoder qui peut être utilisé à la place defiles , et qui permet également vous définissez la charge utile comme un dictionnaire, un tuple ou une liste.

MultipartEncoderpeut être utilisé à la fois pour les demandes en plusieurs parties avec ou sans champs de téléchargement réels. Il doit être affecté au dataparamètre.

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

Si vous devez envoyer plusieurs champs avec le même nom, ou si l'ordre des champs du formulaire est important, alors un tuple ou une liste peut être utilisé à la place d'un dictionnaire:

multipart_data = MultipartEncoder(
    fields=(
            ('action', 'ingest'), 
            ('item', 'spam'),
            ('item', 'sausage'),
            ('item', 'eggs'),
           )
    )
ccpizza
la source
Merci pour ça. L'ordre des clés était important pour moi et cela m'a beaucoup aidé.
Splendor
Incroyable. Inexplicablement, une API avec laquelle je travaille nécessite 2 valeurs différentes pour la même clé. Ceci est incroyable. Je vous remercie.
Ajon
@ccpizza, que signifie réellement cette ligne? > "('file.py', open ('file.py', 'rb'), 'text / plain')". Ça ne marche pas pour moi :(
Denis Koreyba
@DenisKoreyba: ceci est un exemple de champ de téléchargement de fichier qui suppose que le fichier nommé file.pyse trouve dans le même dossier que votre script.
ccpizza
1
Vous pouvez utiliser Noneau lieu d'une chaîne vide. Les demandes n'incluront alors aucun nom de fichier. Au lieu de Content-Disposition: form-data; name="action"; filename=""cela, ce sera le cas Content-Disposition: form-data; name="action". C'était essentiel pour moi que le serveur accepte ces champs comme champs de formulaire et non comme fichiers.
Mitar
9

Voici l'extrait de code simple pour télécharger un seul fichier avec des paramètres supplémentaires à l'aide de requêtes:

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)

Veuillez noter que vous n'avez pas besoin de spécifier explicitement un type de contenu.

REMARQUE: Je voulais commenter l'une des réponses ci-dessus, mais je n'ai pas pu le faire en raison de sa faible réputation, j'ai donc rédigé une nouvelle réponse ici.

Jainik
la source
4

Vous devez utiliser l' nameattribut du fichier de téléchargement qui se trouve dans le code HTML du site. Exemple:

autocomplete="off" name="image">

Tu vois name="image">? Vous pouvez le trouver dans le code HTML d'un site pour télécharger le fichier. Vous devez l'utiliser pour télécharger le fichier avecMultipart/form-data

scénario:

import requests

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

Ici, à la place de l'image, ajoutez le nom du fichier upload en HTML

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

Si le téléchargement nécessite de cliquer sur le bouton de téléchargement, vous pouvez utiliser comme ça:

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

Ensuite, lancez la demande

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

Et terminé, le fichier a été téléchargé avec succès

Skiller Dz
la source
3

Envoyer la clé et la valeur multipart / form-data

commande curl:

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

requêtes python - Requêtes POST plus compliquées :

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

Envoyer un fichier multipart / form-data

commande 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

requêtes python - POST un fichier codé en plusieurs parties :

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

c'est tout.

crifan
la source
-1

Voici l'extrait de code python dont vous avez besoin pour télécharger un grand fichier unique en tant que données de formulaire en plusieurs parties. Avec le middleware NodeJs Multer exécuté côté serveur.

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

Pour le côté serveur, veuillez consulter la documentation du multer sur: https://github.com/expressjs/multer ici le champ single ('fieldName') est utilisé pour accepter un seul fichier, comme dans:

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