13

Python Requestsライブラリを使用して URL からファイルを取得し、それをポスト リクエストでマルチパート エンコード ファイルとして使用したいと考えています。問題は、ファイルが非常に大きくなる可能性があり (50MB ~ 2GB)、メモリにロードしたくないことです。(コンテキストはこちら。)

ドキュメントの例(multipartstream downstream up)に従って、次のようなものを作成しました:

    with requests.get(big_file_url, stream=True) as f:
        requests.post(upload_url, files={'file': ('filename', f.content)})

しかし、私はそれを正しく行っているかどうかわかりません。実際には、このエラーがスローされています-トレースバックから編集されました:

    with requests.get(big_file_url, stream=True) as f:
    AttributeError: __exit__

助言がありますか?

4

4 に答える 4

4

他の回答がすでに指摘しているようrequestsに、マルチパートでエンコードされたファイルをメモリにロードせずに POSTing をサポートしていません

multipart/form-data を使用してメモリにロードせずに大きなファイルをアップロードするには、次を使用できますposter

#!/usr/bin/env python
import sys
from urllib2 import Request, urlopen

from poster.encode import multipart_encode # $ pip install poster
from poster.streaminghttp import register_openers

register_openers() # install openers globally

def report_progress(param, current, total):
    sys.stderr.write("\r%03d%% of %d" % (int(1e2*current/total + .5), total))

url = 'http://example.com/path/'
params = {'file': open(sys.argv[1], "rb"), 'name': 'upload test'}
response = urlopen(Request(url, *multipart_encode(params, cb=report_progress)))
print response.read()

ローカル ファイルの代わりに GET 応答オブジェクトを許可するように適応させることができます。

import posixpath
import sys
from urllib import unquote
from urllib2 import Request, urlopen
from urlparse import urlsplit

from poster.encode import MultipartParam, multipart_encode # pip install poster
from poster.streaminghttp import register_openers

register_openers() # install openers globally

class MultipartParamNoReset(MultipartParam):
    def reset(self):
        pass # do nothing (to allow self.fileobj without seek() method)

get_url = 'http://example.com/bigfile'
post_url = 'http://example.com/path/'

get_response = urlopen(get_url)
param = MultipartParamNoReset(
    name='file',
    filename=posixpath.basename(unquote(urlsplit(get_url).path)), #XXX \ bslash
    filetype=get_response.headers['Content-Type'],
    filesize=int(get_response.headers['Content-Length']),
    fileobj=get_response)

params = [('name', 'upload test'), param]
datagen, headers = multipart_encode(params, cb=report_progress)
post_response = urlopen(Request(post_url, datagen, headers))
print post_response.read()

このソリューションではContent-Length、GET 応答に有効なヘッダー (既知のファイル サイズ) が必要です。ファイル サイズが不明な場合は、チャンク転送エンコーディングを使用して multipart/form-data コンテンツをアップロードできます。ライブラリurllib3.filepostに同梱されているものを使用して同様のソリューションを実装できます。requestsposter

于 2013-04-27T00:41:04.677 に答える
1

実際には、Kenneth Reitz の GitHub repoに問題があります。私は同じ問題を抱えていました(ローカルファイルをアップロードしているだけですが)、リクエストのさまざまな部分に対応するストリームのリストであるラッパークラスを追加し、リストを反復処理する read() 属性を使用して、各部分を読み取り、ヘッダー (境界とコンテンツの長さ) に必要な値も取得します。

# coding=utf-8

from __future__ import unicode_literals
from mimetools import choose_boundary
from requests.packages.urllib3.filepost import iter_fields, get_content_type
from io import BytesIO
import codecs

writer = codecs.lookup('utf-8')[3]

class MultipartUploadWrapper(object):

    def __init__(self, files):
        """
        Initializer

        :param files:
            A dictionary of files to upload, of the form {'file': ('filename', <file object>)}
        :type network_down_callback:
            Dict
        """
        super(MultipartUploadWrapper, self).__init__()
        self._cursor = 0
        self._body_parts = None
        self.content_type_header = None
        self.content_length_header = None
        self.create_request_parts(files)

    def create_request_parts(self, files):
        request_list = []
        boundary = choose_boundary()
        content_length = 0

        boundary_string = b'--%s\r\n' % (boundary)
        for fieldname, value in iter_fields(files):
            content_length += len(boundary_string)

            if isinstance(value, tuple):
                filename, data = value
                content_disposition_string = (('Content-Disposition: form-data; name="%s"; ''filename="%s"\r\n' % (fieldname, filename))
                                            + ('Content-Type: %s\r\n\r\n' % (get_content_type(filename))))

            else:
                data = value
                content_disposition_string =  (('Content-Disposition: form-data; name="%s"\r\n' % (fieldname))
                                            + 'Content-Type: text/plain\r\n\r\n')
            request_list.append(BytesIO(str(boundary_string + content_disposition_string)))
            content_length += len(content_disposition_string)
            if hasattr(data, 'read'):
                data_stream = data
            else:
                data_stream = BytesIO(str(data))

            data_stream.seek(0,2)
            data_size = data_stream.tell()
            data_stream.seek(0)

            request_list.append(data_stream)
            content_length += data_size

            end_string = b'\r\n'
            request_list.append(BytesIO(end_string))
            content_length += len(end_string)

        request_list.append(BytesIO(b'--%s--\r\n' % (boundary)))
        content_length += len(boundary_string)

        # There's a bug in httplib.py that generates a UnicodeDecodeError on binary uploads if
        # there are *any* unicode strings passed into headers as part of the requests call.
        # For this reason all strings are explicitly converted to non-unicode at this point.
        self.content_type_header = {b'Content-Type': b'multipart/form-data; boundary=%s' % boundary}
        self.content_length_header = {b'Content-Length': str(content_length)}
        self._body_parts = request_list

    def read(self, chunk_size=0):
        remaining_to_read = chunk_size
        output_array = []
        while remaining_to_read > 0:
            body_part = self._body_parts[self._cursor]
            current_piece = body_part.read(remaining_to_read)
            length_read = len(current_piece)
            output_array.append(current_piece)
            if length_read < remaining_to_read:
                # we finished this piece but haven't read enough, moving on to the next one
                remaining_to_read -= length_read
                if self._cursor == len(self._body_parts) - 1:
                    break
                else:
                    self._cursor += 1
            else:
                break
        return b''.join(output_array)

したがって、「files」キーワード引数を渡す代わりに、このオブジェクトを「data」属性として Request.request オブジェクトに渡します。

編集

コードをクリーンアップしました

于 2013-04-25T17:26:08.457 に答える