¿Cómo enviar un "multipart / form-data" con solicitudes en python?

214

¿Cómo enviar un multipart/form-datacon solicitudes en python? Entiendo cómo enviar un archivo, pero no puedo entender cómo enviar los datos del formulario por este método.

agrynchuk
fuente
Su pregunta no es realmente clara. ¿Qué quieres lograr? ¿Desea enviar "multipart / form-data" sin cargar un archivo en el formulario?
Hans Then
44
El hecho de que el filesparámetro se use para hacer ambas cosas es una API muy mala. Planteé el problema titulado Envío de datos de varias partes: necesitamos una mejor API para solucionar esto. Si acepta que el uso de filesparámetros para enviar datos de varias partes es engañoso en el mejor de los casos, solicite cambiar la API en el problema anterior.
Piotr Dobrogost
@PiotrDobrogost ese problema está cerrado. No aliente a las personas a comentar sobre temas cerrados, relevantes o no.
Ian Stapleton Cordasco
1
No importa, me acabo de dar cuenta de que tu comentario fue publicado antes de que se cerrara. Odio cómo StackOverflow no mantiene las cosas en orden cronológico.
Ian Stapleton Cordasco

Respuestas:

168

Básicamente, si especifica un filesparámetro (un diccionario), requestsenviará una multipart/form-dataPOST en lugar de una application/x-www-form-urlencodedPOST. Sin embargo, no está limitado a usar archivos reales en ese diccionario:

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

y httpbin.org te permite saber con qué encabezados publicaste; en response.json()tenemos:

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

Mejor aún, puede controlar aún más el nombre de archivo, el tipo de contenido y los encabezados adicionales para cada parte mediante el uso de una tupla en lugar de una sola cadena o un objeto de bytes. Se espera que la tupla contenga entre 2 y 4 elementos; el nombre de archivo, el contenido, opcionalmente un tipo de contenido y un diccionario opcional de encabezados adicionales.

Usaría el formulario de tupla Nonecomo nombre de archivo, de modo que el filename="..."parámetro se elimine de la solicitud de esas partes:

>>> 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 También puede ser una lista de tuplas de dos valores, si necesita ordenar y / o múltiples campos con el mismo nombre:

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

Si especifica ambos filesy data, entonces depende del valor de datalo que se utilizará para crear el cuerpo POST. Si dataes una cadena, solo se usará; de lo contrario, ambos datay filesse usan, con los elementos dataenumerados primero.

También está el excelente requests-toolbeltproyecto, que incluye soporte avanzado de múltiples partes . Toma definiciones de campo en el mismo formato que el filesparámetro, pero a diferencia requests, no establece un parámetro de nombre de archivo. Además, puede transmitir la solicitud desde objetos de archivos abiertos, donde requestsprimero construirá el cuerpo de la solicitud en la memoria:

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

Los campos siguen las mismas convenciones; use una tupla con entre 2 y 4 elementos para agregar un nombre de archivo, parte tipo mime o encabezados adicionales. A diferencia del filesparámetro, no se intenta encontrar un filenamevalor predeterminado si no usa una tupla.

Martijn Pieters
fuente
3
Si se usan archivos = {}, entonces no se deben usar encabezados = {'Content-Type': 'blah blah'}.
zaki
55
@zaki: de hecho, porque el multipart/form-dataTipo de contenido debe incluir el valor límite utilizado para delinquir las partes en el cuerpo de la publicación. No establecer el Content-Typeencabezado asegura que se requestsestablece en el valor correcto.
Martijn Pieters
Nota importante: la solicitud solo se enviará como multipart/form-datasi el valor de files=es verdadero, por lo que si necesita enviar una multipart/form-datasolicitud pero no incluye ningún archivo, puede establecer un valor verdadero pero sin sentido como {'':''}, y establecerlo data=con su cuerpo de solicitud. Si está haciendo esto, no proporcione el Content-Typeencabezado usted mismo; requestslo configurará para ti. Puedes ver la verdad aquí: github.com/psf/requests/blob/…
Daniel Situnayake
@DanielSitunayake no hay necesidad de tal truco. Simplemente coloque todos los campos en el filesdict, no tienen que ser archivos (solo asegúrese de usar el formulario de tupla y establecer el nombre de archivo en None). Mejor aún, use el requests_toolbeltproyecto.
Martijn Pieters
Gracias @MartijnPieters, ¡el truco con la forma de tupla es genial! Le vamos a dar una oportunidad.
Daniel Situnayake
107

Desde que se escribieron las respuestas anteriores, las solicitudes han cambiado. Eche un vistazo al hilo de errores en Github para obtener más detalles y este comentario como ejemplo.

En resumen, el parámetro de archivos toma un dictcon la clave que es el nombre del campo de formulario y el valor es una cadena o una tupla de 2, 3 o 4 longitudes, como se describe en la sección PUBLICAR un archivo codificado en varias partes en las solicitudes Inicio rápido:

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

En lo anterior, la tupla se compone de la siguiente manera:

(filename, data, content_type, headers)

Si el valor es solo una cadena, el nombre del archivo será el mismo que la clave, como se muestra a continuación:

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

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

72c2b6f406cdabd578c5fd7598557c52

Si el valor es una tupla y la primera entrada es Nonela propiedad del nombre de archivo no se incluirá:

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

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

72c2b6f406cdabd578c5fd7598557c52
runejuhl
fuente
2
¿Qué sucede si necesita distinguir el namey filenamepero también tener múltiples campos con el mismo nombre?
Michael
1
Tengo un problema similar como @Michael. ¿Puedes echar un vistazo a la pregunta y sugerir algo? [enlace] ( stackoverflow.com/questions/30683352/… )
Shaardool
¿Alguien resolvió este problema al tener múltiples campos con el mismo nombre?
user3131037
1
El truco para pasar en cadena vacía como el primer valor de una filestupla ya no funciona: es necesario utilizar requests.post dataparámetro en lugar de enviar archivos no additionnal multipart/form-dataparámetros
Lucas Cimón
1
Pasar en Nonelugar de una cadena vacía parece funcionar
Alexandre Blin
74

Debe usar el filesparámetro para enviar una solicitud POST de formulario multiparte incluso cuando no necesite cargar ningún archivo.

De la fuente original de solicitudes :

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 parte relevante es: file-tuple can be a2-tuple, 3-tupleor a4-tuple .

Con base en lo anterior, la solicitud de formulario multiparte más simple que incluye tanto archivos para cargar como campos de formulario se verá así:

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)

Tenga en cuenta que Nonees el primer argumento en la tupla para campos de texto sin formato: este es un marcador de posición para el campo de nombre de archivo que solo se usa para cargar archivos, pero para los campos de texto que pasan Nonecomo primer parámetro es necesario para que los datos se envíen .

Múltiples campos con el mismo nombre.

Si necesita publicar múltiples campos con el mismo nombre, en lugar de un diccionario, puede definir su carga útil como una lista (o una tupla) de tuplas:

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 solicitudes de transmisión

Si la API anterior no es lo suficientemente pitónica para usted, entonces considere usar el toolbelt de solicitudes ( pip install requests_toolbelt), que es una extensión del módulo de solicitudes principales que proporciona soporte para la transmisión de carga de archivos, así como el MultipartEncoder que se puede usar en lugar de files, y que también permite Usted define la carga útil como un diccionario, tupla o lista.

MultipartEncoderse puede usar tanto para solicitudes de varias partes con o sin campos de carga reales. Debe asignarse al dataparámetro.

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 necesita enviar varios campos con el mismo nombre, o si el orden de los campos del formulario es importante, puede usar una tupla o una lista en lugar de un diccionario:

multipart_data = MultipartEncoder(
    fields=(
            ('action', 'ingest'), 
            ('item', 'spam'),
            ('item', 'sausage'),
            ('item', 'eggs'),
           )
    )
ccpizza
fuente
Gracias por esto. El orden de las llaves fue importante para mí y esto me ayudó mucho.
Esplendor
Asombroso. Inexplicablemente, una API con la que estoy trabajando requiere 2 valores diferentes para la misma clave. Esto es increíble. Gracias.
ajon
@ccpizza, ¿qué significa realmente esta línea? > "('file.py', abierto ('file.py', 'rb'), 'text / plain')". No funciona para mí :(
Denis Koreyba
@DenisKoreyba: este es un ejemplo de un campo de carga de archivos que supone que el archivo nombrado file.pyse encuentra en la misma carpeta que su script.
ccpizza
1
Puede usar en Nonelugar de una cadena vacía. Entonces las solicitudes no incluirán un nombre de archivo en absoluto. Así que en lugar de Content-Disposition: form-data; name="action"; filename=""eso será Content-Disposition: form-data; name="action". Para mí, esto era fundamental para que el servidor aceptara esos campos como campos de formulario y no como archivos.
Mitar
9

Aquí está el fragmento de código simple para cargar un solo archivo con parámetros adicionales mediante solicitudes:

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)

Tenga en cuenta que no necesita especificar explícitamente ningún tipo de contenido.

NOTA: Quería comentar una de las respuestas anteriores, pero no pude debido a la baja reputación, por lo que redacté una nueva respuesta aquí.

Jainik
fuente
4

Debe usar el nameatributo del archivo de carga que se encuentra en el HTML del sitio. Ejemplo:

autocomplete="off" name="image">

Lo ves name="image"> ? Puede encontrarlo en el HTML de un sitio para cargar el archivo. Debe usarlo para cargar el archivo conMultipart/form-data

guión:

import requests

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

Aquí, en lugar de la imagen, agregue el nombre del archivo de carga en HTML

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

Si la carga requiere hacer clic en el botón para cargar, puede usar así:

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

Entonces comienza la solicitud

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

Y listo, archivo cargado con éxito

Skiller Dz
fuente
3

Enviar clave y valor multiparte / datos de formulario

comando curl:

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

solicitudes de Python : solicitudes POST más complicadas :

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

Enviar archivo multipart / form-data

comando 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

solicitudes de Python : PUBLICA un archivo codificado en varias partes :

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

eso es todo.

crifan
fuente
-1

Aquí está el fragmento de Python que necesita para cargar un archivo único grande como datos de formulario multiparte. Con el middleware NodeJs Multer ejecutándose en el lado del servidor.

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

Para el lado del servidor, consulte la documentación del multer en: https://github.com/expressjs/multer aquí, el campo single ('fieldName') se usa para aceptar un solo archivo, como en:

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