2009-03-25 13 views
44

Problem: Podczas zapisywania danych za pomocą urllib2 w Pythonie wszystkie dane są zakodowane i wysyłane jako Content-Type: application/x-www-form-urlencoded. Podczas przesyłania plików typ zawartości powinien być ustawiony jako wieloczęściowy/formularz-dane, a treść musi być zakodowana MIME. Omówienie tego problemu jest tutaj: http://code.activestate.com/recipes/146306/Używanie MultipartPostHandler do POST-danych formularzy z Pythonem

Aby obejść to ograniczenie, niektóre ostre programistów stworzył bibliotekę o nazwie MultipartPostHandler który tworzy OpenerDirector można korzystać z urllib2 się przeważnie automatycznie post z multipart/form-data. Kopia tej biblioteki znajduje się tutaj: http://peerit.blogspot.com/2007/07/multipartposthandler-doesnt-work-for.html

Jestem nowicjuszem w Pythonie i nie mogę uruchomić tej biblioteki. Zasadniczo napisałem poniższy kod. Kiedy przechwytywam je w lokalnym proxy HTTP, widzę, że dane są nadal zakodowane za pomocą adresu URL, a nie wieloczęściowe kodowanie MIME. Proszę pomóż mi dowiedzieć się, co robię źle lub lepszy sposób, aby to zrobić. Dzięki :-)

FROM_ADDR = '[email protected]' 

try: 
    data = open(file, 'rb').read() 
except: 
    print "Error: could not open file %s for reading" % file 
    print "Check permissions on the file or folder it resides in" 
    sys.exit(1) 

# Build the POST request 
url = "http://somedomain.com/?action=analyze"  
post_data = {} 
post_data['analysisType'] = 'file' 
post_data['executable'] = data 
post_data['notification'] = 'email' 
post_data['email'] = FROM_ADDR 

# MIME encode the POST payload 
opener = urllib2.build_opener(MultipartPostHandler.MultipartPostHandler) 
urllib2.install_opener(opener) 
request = urllib2.Request(url, post_data) 
request.set_proxy('127.0.0.1:8080', 'http') # For testing with Burp Proxy 

# Make the request and capture the response 
try: 
    response = urllib2.urlopen(request) 
    print response.geturl() 
except urllib2.URLError, e: 
    print "File upload failed..." 

EDIT1: Dzięki za odpowiedź. Zdaję sobie sprawę z tego rozwiązania HTTP na platformie ActiveState (dołączyłem do tego powyżej). Wolałbym usunąć problem i użyć minimalnej ilości kodu, aby nadal używać urllib2, jak byłem. Każdy pomysł, dlaczego otwieracz nie jest instalowany i używany?

Odpowiedz

57

Wydaje się, że najprostszym i najbardziej kompatybilny sposób, aby ominąć ten problem jest użycie „plakat” moduł.

# test_client.py 
from poster.encode import multipart_encode 
from poster.streaminghttp import register_openers 
import urllib2 

# Register the streaming http handlers with urllib2 
register_openers() 

# Start the multipart/form-data encoding of the file "DSC0001.jpg" 
# "image1" is the name of the parameter, which is normally set 
# via the "name" parameter of the HTML <input> tag. 

# headers contains the necessary Content-Type and Content-Length 
# datagen is a generator object that yields the encoded parameters 
datagen, headers = multipart_encode({"image1": open("DSC0001.jpg")}) 

# Create the Request object 
request = urllib2.Request("http://localhost:5000/upload_image", datagen, headers) 
# Actually do the request, and get the response 
print urllib2.urlopen(request).read() 

To działało idealnie i nie musiałem marudzić z httplib. Moduł jest dostępny tutaj: http://atlee.ca/software/poster/index.html

+1

To jest dokładnie to, czego potrzebowałem! Sława. –

+1

Wiem, że jest to stary post, ale otrzymuję to z plakatu: 'AttributeError: instancja multipart_yielder nie ma atrybutu" __len __ ", zastanawiając się, czy ktoś inny ma ten problem. – Andy

+5

@nalroff Nie nazwałeś 'poster.streaminghttp.register_openers()' –

32

Znaleziony ten przepis odpowiedzieć wieloczęściowy użyciu httplib bezpośrednio (bez zewnętrzne biblioteki zaangażowany)

import httplib 
import mimetypes 

def post_multipart(host, selector, fields, files): 
    content_type, body = encode_multipart_formdata(fields, files) 
    h = httplib.HTTP(host) 
    h.putrequest('POST', selector) 
    h.putheader('content-type', content_type) 
    h.putheader('content-length', str(len(body))) 
    h.endheaders() 
    h.send(body) 
    errcode, errmsg, headers = h.getreply() 
    return h.file.read() 

def encode_multipart_formdata(fields, files): 
    LIMIT = '----------lImIt_of_THE_fIle_eW_$' 
    CRLF = '\r\n' 
    L = [] 
    for (key, value) in fields: 
     L.append('--' + LIMIT) 
     L.append('Content-Disposition: form-data; name="%s"' % key) 
     L.append('') 
     L.append(value) 
    for (key, filename, value) in files: 
     L.append('--' + LIMIT) 
     L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) 
     L.append('Content-Type: %s' % get_content_type(filename)) 
     L.append('') 
     L.append(value) 
    L.append('--' + LIMIT + '--') 
    L.append('') 
    body = CRLF.join(L) 
    content_type = 'multipart/form-data; boundary=%s' % LIMIT 
    return content_type, body 

def get_content_type(filename): 
    return mimetypes.guess_type(filename)[0] or 'application/octet-stream' 
+6

ta wydaje się słuszna, ale moduł plakat jest bardziej w prawo. – bentford

+3

Myślę, że to podejście jest bardziej "niezależne", ponieważ nie wymaga żadnych nowych modułów i może być "skompilowane" z py2exe do uruchomienia w systemie Windows, jako plik * .exe. Dodanie modułu "plakatu" też jest OK, ale nie działało. Ta metoda była dla mnie najlepsza. Hurra do autora! – garmoncheg

+1

Czy plik "wartość" musi być w jakiś sposób zakodowany, czy jest to tylko czysty bytream? –

29

Wystarczy użyć python-requests będzie ustawić odpowiednie nagłówki i nie przesyłać do Ciebie:

import requests 
files = {"form_input_field_name": open("filename", "rb")} 
requests.post("http://httpbin.org/post", files=files) 
+1

Dzięki, działa jak urok! – bszom

+0

+1 doskonały, krótki i dokładnie to, czego potrzebowałem! – Bogatyr

+0

Podanie nazwy pola wprowadzania html ma kluczowe znaczenie! Dzięki –

0

I co to zbieżne, 2 lata, 6 miesięcy temu tworzę projekt

https://pypi.python.org/pypi/MultipartPostHandler2, która naprawia MultipartPostHandler dla systemów utf-8. Zrobiłem też kilka drobnych ulepszeń, możesz go przetestować :)

+0

Hej, bracie! Myślę, że wybrana nazwa pakietu sprawia, że ​​nie sprawdzam tego. – pylover

+0

Niestety nie rozumiem, nie mogłem zmodyfikować MultipartPostHandler, więc muszę nazwać go MultipartPostHandler2 –

+0

pypi obsługuje wiele wersji w pakiecie, jeśli nazwa jest już zajęta, powinieneś wybrać inną dobrą nazwę pakietu. Pypi jest nasz. Wszyscy jesteśmy odpowiedzialni za to, co z nim robimy. – pylover

1

Wpadłem na ten sam problem i musiałem wykonać wieloczęściowy post formularza bez korzystania z zewnętrznych bibliotek. Napisałem cały blogpost about the issues I ran into.

Skończyło się na użyciu zmodyfikowanej wersji http://code.activestate.com/recipes/146306/. Kod w tym adresie URL po prostu dołącza zawartość pliku jako ciąg znaków, co może powodować problemy z plikami binarnymi. Oto mój działający kod.

import mimetools 
import mimetypes 
import io 
import http 
import json 


form = MultiPartForm() 
form.add_field("form_field", "my awesome data") 

# Add a fake file  
form.add_file(key, os.path.basename(filepath), 
    fileHandle=codecs.open("/path/to/my/file.zip", "rb")) 

# Build the request 
url = "http://www.example.com/endpoint" 
schema, netloc, url, params, query, fragments = urlparse.urlparse(url) 

try: 
    form_buffer = form.get_binary().getvalue() 
    http = httplib.HTTPConnection(netloc) 
    http.connect() 
    http.putrequest("POST", url) 
    http.putheader('Content-type',form.get_content_type()) 
    http.putheader('Content-length', str(len(form_buffer))) 
    http.endheaders() 
    http.send(form_buffer) 
except socket.error, e: 
    raise SystemExit(1) 

r = http.getresponse() 
if r.status == 200: 
    return json.loads(r.read()) 
else: 
    print('Upload failed (%s): %s' % (r.status, r.reason)) 

class MultiPartForm(object): 
    """Accumulate the data to be used when posting a form.""" 

    def __init__(self): 
     self.form_fields = [] 
     self.files = [] 
     self.boundary = mimetools.choose_boundary() 
     return 

    def get_content_type(self): 
     return 'multipart/form-data; boundary=%s' % self.boundary 

    def add_field(self, name, value): 
     """Add a simple field to the form data.""" 
     self.form_fields.append((name, value)) 
     return 

    def add_file(self, fieldname, filename, fileHandle, mimetype=None): 
     """Add a file to be uploaded.""" 
     body = fileHandle.read() 
     if mimetype is None: 
      mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' 
     self.files.append((fieldname, filename, mimetype, body)) 
     return 

    def get_binary(self): 
     """Return a binary buffer containing the form data, including attached files.""" 
     part_boundary = '--' + self.boundary 

     binary = io.BytesIO() 
     needsCLRF = False 
     # Add the form fields 
     for name, value in self.form_fields: 
      if needsCLRF: 
       binary.write('\r\n') 
      needsCLRF = True 

      block = [part_boundary, 
       'Content-Disposition: form-data; name="%s"' % name, 
       '', 
       value 
      ] 
      binary.write('\r\n'.join(block)) 

     # Add the files to upload 
     for field_name, filename, content_type, body in self.files: 
      if needsCLRF: 
       binary.write('\r\n') 
      needsCLRF = True 

      block = [part_boundary, 
       str('Content-Disposition: file; name="%s"; filename="%s"' % \ 
       (field_name, filename)), 
       'Content-Type: %s' % content_type, 
       '' 
       ] 
      binary.write('\r\n'.join(block)) 
      binary.write('\r\n') 
      binary.write(body) 


     # add closing boundary marker, 
     binary.write('\r\n--' + self.boundary + '--\r\n') 
     return binary 
0

Aby odpowiedzieć na pytanie, dlaczego OP oryginalny kod nie działa, uchwyt przekazany nie był instancją klasy. Linia

# MIME encode the POST payload 
opener = urllib2.build_opener(MultipartPostHandler.MultipartPostHandler) 

powinien przeczytać

opener = urllib2.build_opener(MultipartPostHandler.MultipartPostHandler())