Como enviar um "multipart / form-data" com solicitações em python?

214

Como enviar um multipart/form-datacom pedidos em python? Como enviar um arquivo, eu entendo, mas como enviar os dados do formulário por este método não consigo entender.

agrynchuk
fonte
sua pergunta não está realmente clara. O que você quer alcançar? Deseja enviar "dados de várias partes / formulário" sem um upload de arquivo no formulário?
Hans Então,
4
O fato de esse filesparâmetro ser usado para fazer as duas coisas é uma API muito ruim. Eu levantei a questão intitulada Enviando dados de várias partes - precisamos de uma API melhor para corrigir isso. Se você concorda que o uso do filesparâmetro para enviar dados com várias partes é enganoso, na melhor das hipóteses, solicite a alteração da API na edição acima.
Piotr Dobrogost
@PiotrDobrogost esse problema está encerrado. Não incentive as pessoas a comentar sobre questões fechadas, relevantes ou não.
Ian Stapleton Cordasco
1
Deixa pra lá, acabei de perceber que seu comentário foi postado antes de ser fechado. Eu odeio como o StackOverflow não mantém as coisas em ordem cronológica.
Ian Stapleton Cordasco

Respostas:

168

Basicamente, se você especificar um filesparâmetro (um dicionário), requestsenviará um multipart/form-dataPOST em vez de um application/x-www-form-urlencodedPOST. Você não está limitado ao uso de arquivos reais nesse dicionário, no entanto:

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

e httpbin.org permite saber em quais cabeçalhos você postou; em response.json()que temos:

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

Melhor ainda, você pode controlar ainda mais o nome do arquivo, o tipo de conteúdo e os cabeçalhos adicionais de cada parte usando uma tupla em vez de uma única string ou objeto de bytes. A tupla deve conter entre 2 e 4 elementos; o nome do arquivo, o conteúdo, opcionalmente um tipo de conteúdo e um dicionário opcional de outros cabeçalhos.

Eu usaria o formulário de tupla com Noneo nome do arquivo, para que o filename="..."parâmetro seja descartado da solicitação para essas 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 também pode ser uma lista de tuplas de dois valores, se você precisar solicitar e / ou vários campos com o mesmo nome:

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

Se você especificar ambos filese data, isso dependerá do valor do dataque será usado para criar o corpo do POST. Se datafor uma string, somente ela será usada; caso contrário, ambos datae filessão usados, com os elementos datalistados primeiro.

Há também o excelente requests-toolbeltprojeto, que inclui suporte avançado a várias partes . Leva definições de campo no mesmo formato que o filesparâmetro, mas requests, ao contrário , o padrão é não definir um parâmetro de nome de arquivo. Além disso, ele pode transmitir a solicitação a partir de objetos de arquivo aberto, onde requestsprimeiro construirá o corpo da solicitação na memória:

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

Os campos seguem as mesmas convenções; use uma tupla com entre 2 e 4 elementos para adicionar um nome de arquivo, parte do tipo MIME ou cabeçalhos extras. Diferente do filesparâmetro, nenhuma tentativa é feita para encontrar um filenamevalor padrão se você não usar uma tupla.

Martijn Pieters
fonte
3
Se files = {} for usado, os cabeçalhos = {'Content-Type': 'blah blah'} não deverão ser usados!
Zaki
5
@zaki: de fato, porque o multipart/form-dataContent-Type deve incluir o valor limite usado para delinear as partes no corpo do post. Não definir o Content-Typecabeçalho garante que requestsele seja definido com o valor correto.
Martijn Pieters
Nota importante: a solicitação será enviada apenas como multipart/form-datase o valor de files=fosse verdadeiro; portanto, se você precisar enviar uma multipart/form-datasolicitação, mas não incluir nenhum arquivo, poderá definir um valor verdadeiro, mas sem sentido, como {'':''}, e definir data=com o corpo da solicitação. Se você estiver fazendo isso, não forneça o Content-Typecabeçalho; requestsirá definir para você. Você pode ver a verificação da verdade aqui: github.com/psf/requests/blob/…
Daniel Situnayake
@DanielSitunayake, não há necessidade de tal hack. Basta colocar todos os campos no filesdict, eles não precisam ser arquivos (use o formulário de tupla e defina o nome do arquivo como None). Melhor ainda, use o requests_toolbeltprojeto.
Martijn Pieters
Obrigado @MartijnPieters, o truque com a forma de tupla é ótimo! Vai dar uma chance.
Daniel Situnayake 14/03
107

Desde que as respostas anteriores foram escritas, os pedidos foram alterados. Dê uma olhada no tópico do bug no Github para obter mais detalhes e este comentário para um exemplo.

Em resumo, o parâmetro files leva a dictcom a chave sendo o nome do campo de formulário e o valor sendo uma sequência de caracteres ou uma tupla de 2, 3 ou 4 comprimentos, conforme descrito na seção POST um arquivo codificado por várias partes nas solicitações começo rápido:

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

Acima, a tupla é composta da seguinte maneira:

(filename, data, content_type, headers)

Se o valor for apenas uma cadeia, o nome do arquivo será o mesmo da chave, como no seguinte:

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

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

72c2b6f406cdabd578c5fd7598557c52

Se o valor for uma tupla e a primeira entrada for Nonea propriedade filename, não será incluída:

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

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

72c2b6f406cdabd578c5fd7598557c52
runejuhl
fonte
2
E se você precisa distinguir a namee filenamemas também tem vários campos com o mesmo nome?
Michael Michael
1
Eu tenho um problema semelhante ao @Michael. Você pode dar uma olhada na pergunta e sugerir algo? [link] ( stackoverflow.com/questions/30683352/… )
Shaardool 07/07/2015
alguém resolveu esse problema com vários campos com o mesmo nome?
user3131037
1
O truque para passar en cadeia vazia como o primeiro valor de uma filestupla não funciona mais: você precisa usar requests.post dataparâmetro em vez de enviar non-arquivo additionnal multipart/form-dataparâmetros
Lucas Cimon
1
Passando Noneem vez de uma seqüência vazia parece funcionar
Alexandre Blin
74

Você precisa usar o filesparâmetro para enviar uma solicitação POST de formulário com várias partes, mesmo quando não precisar fazer upload de nenhum arquivo.

Na fonte de solicitações originais :

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.

A parte relevante é: file-tuple can be a2-tuple, .3-tupleor a4-tuple

Com base no exposto acima, a solicitação de formulário multipartes mais simples que inclui os arquivos para upload e os campos do formulário será semelhante a:

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)

Observe Nonecomo o primeiro argumento na tupla para campos de texto sem formatação - este é um espaço reservado para o campo nome do arquivo que é usado apenas para upload de arquivos, mas para campos de texto que passam Nonecomo o primeiro parâmetro é necessário para que os dados sejam enviados .

Vários campos com o mesmo nome

Se você precisar postar vários campos com o mesmo nome, em vez de um dicionário, poderá definir sua carga como uma lista (ou uma 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 solicitações de streaming

Se a API acima não for suficientemente pitônica para você, considere o uso de orders toolbelt ( pip install requests_toolbelt), que é uma extensão do módulo de solicitações principais que fornece suporte para streaming de upload de arquivos, bem como o MultipartEncoder que pode ser usado em vez de files, e que também permite você define a carga útil como dicionário, tupla ou lista.

MultipartEncoderpode ser usado para solicitações de várias partes com ou sem campos de upload reais. Ele deve ser atribuído ao 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})

Se você precisar enviar vários campos com o mesmo nome ou se a ordem dos campos do formulário for importante, uma tupla ou uma lista poderá ser usada em vez de um dicionário:

multipart_data = MultipartEncoder(
    fields=(
            ('action', 'ingest'), 
            ('item', 'spam'),
            ('item', 'sausage'),
            ('item', 'eggs'),
           )
    )
ccpizza
fonte
Obrigado por isso. A ordem das chaves foi importante para mim e isso ajudou muito.
Splendour
Surpreendente. Inexplicavelmente, uma API com a qual estou trabalhando exige 2 valores diferentes para a mesma chave. Isso é incrível. Obrigado.
ajon
@ccpizza, o que realmente significa esta linha? > "('arquivo.py', aberto ('arquivo.py', 'rb'), 'texto / sem formatação')". Não funciona para mim :(
Denis Koreyba 31/01
@ DenisKoreyba: este é um exemplo de um campo de upload de arquivo que pressupõe que o arquivo nomeado file.pyesteja localizado na mesma pasta que o seu script.
Ccpizza
1
Você pode usar em Nonevez da string vazia. As solicitações não incluirão um nome de arquivo. Então, ao invés disso Content-Disposition: form-data; name="action"; filename="", será Content-Disposition: form-data; name="action". Isso foi fundamental para mim para o servidor aceitar esses campos como campos de formulário e não como arquivos.
Mitar
9

Aqui está o trecho de código simples para fazer upload de um único arquivo com parâmetros adicionais usando solicitações:

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)

Observe que você não precisa especificar explicitamente nenhum tipo de conteúdo.

NOTA: Queria comentar uma das respostas acima, mas não pôde, devido à baixa reputação, por isso elaborou uma nova resposta aqui.

Jainik
fonte
4

Você precisa usar o nameatributo do arquivo de upload que está no HTML do site. Exemplo:

autocomplete="off" name="image">

Você vê name="image">? Você pode encontrá-lo no HTML de um site para fazer upload do arquivo. Você precisa usá-lo para fazer upload do arquivo comMultipart/form-data

roteiro:

import requests

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

Aqui, no lugar da imagem, adicione o nome do arquivo de upload em HTML

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

Se o upload exigir clicar no botão para envio, você poderá usar assim:

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

Em seguida, inicie a solicitação

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

E pronto, o arquivo foi enviado com sucesso

Skiller Dz
fonte
3

Enviar chave e valor multipart / dados de formulário

comando curl:

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

requisições python - solicitações POST mais complicadas :

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

Enviar arquivo multipart / dados de formulário

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

solicitações python - POST um arquivo codificado por várias partes :

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

Isso é tudo.

crifan
fonte
-1

Aqui está o snippet python necessário para carregar um arquivo único grande como dados de formulário de várias partes. Com o middleware do NodeJs Multer em execução no lado do 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 o servidor, verifique a documentação do multer em: https://github.com/expressjs/multer aqui, o campo single ('fieldName') é usado para aceitar um único arquivo, como em:

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