Python requests post 的进展情况

20

我正在使用Python requests包上传一个大文件,但我找不到任何方法可以获取有关上传进度的数据。我看到了很多下载文件的进度条,但这些进度条对于文件上传是无效的。

理想的解决方案是某种回调方法,例如:

def progress(percent):
  print percent
r = requests.post(URL, files={'f':hugeFileHandle}, callback=progress)

提前感谢您的帮助 :)


3
你需要在hugeFileHandle中实现进度。我不确定为什么requests库没有提供一种清晰易懂的方法来实现此功能。 - Blender
9个回答

20

requests 不支持 上传 流式传输,例如:

import os
import sys
import requests  # pip install requests

class upload_in_chunks(object):
    def __init__(self, filename, chunksize=1 << 13):
        self.filename = filename
        self.chunksize = chunksize
        self.totalsize = os.path.getsize(filename)
        self.readsofar = 0

    def __iter__(self):
        with open(self.filename, 'rb') as file:
            while True:
                data = file.read(self.chunksize)
                if not data:
                    sys.stderr.write("\n")
                    break
                self.readsofar += len(data)
                percent = self.readsofar * 1e2 / self.totalsize
                sys.stderr.write("\r{percent:3.0f}%".format(percent=percent))
                yield data

    def __len__(self):
        return self.totalsize

# XXX fails
r = requests.post("http://httpbin.org/post",
                  data=upload_in_chunks(__file__, chunksize=10))

顺便说一句,如果您不需要报告进度; 您可以使用内存映射文件上传大文件

为了解决这个问题,您可以创建一个类似于urllib2 POST进度监控中的文件适配器:

class IterableToFileAdapter(object):
    def __init__(self, iterable):
        self.iterator = iter(iterable)
        self.length = len(iterable)

    def read(self, size=-1): # TBD: add buffer for `len(data) > size` case
        return next(self.iterator, b'')

    def __len__(self):
        return self.length

例子

it = upload_in_chunks(__file__, 10)
r = requests.post("http://httpbin.org/post", data=IterableToFileAdapter(it))

# pretty print
import json
json.dump(r.json, sys.stdout, indent=4, ensure_ascii=False)

1
@Robin:上面的方法是一种容易失败的黑客技巧。你可以尝试使用“poster”代替。它支持进度回调和多部分/表单数据的流式传输(已知内容长度)。顺便说一句,如果这个答案对你的问题不可接受,请取消勾选。 - jfs
2
kennethreitz于2013年1月10日发表评论:完成。https://github.com/kennethreitz/requests/issues/952 - Dima Tisnek
1
@qarma:我相信你。你能否写一个最小的例子(包括OP所需的进度报告),并使用大于可用内存的文件进行测试,确保行为合理:不交换,实时进行进度报告。我无法删除已接受的答案。我只能提供更好答案的链接。 - jfs
当执行requests.post()时,类upload_in_chunks(object)中的__len__方法是如何自动调用的?这个方法是否覆盖了requests库中的某个方法?我在那里找不到任何信息。如果删除该方法,则requests将无法为我上传文件。 - probat
@probat: 一般来说,内置函数len()会调用相应的__len__ 方法。我不知道这个答案是否适用于当前的请求版本。 - jfs
显示剩余5条评论

14

我建议使用一个名为requests-toolbelt的工具包,它可以使监控上传字节变得非常容易,例如:

from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
import requests

def my_callback(monitor):
    # Your callback function
    print monitor.bytes_read

e = MultipartEncoder(
    fields={'field0': 'value', 'field1': 'value',
            'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
    )
m = MultipartEncoderMonitor(e, my_callback)

r = requests.post('http://httpbin.org/post', data=m,
                  headers={'Content-Type': m.content_type})

而且您可能希望阅读此文以显示进度条。


这基本上就是我需要的...但是...现在有没有一种方法可以分块上传例如file.py的内容? - Georg
@Georg,根据requests-toolbelt的文档,它应该天生支持流式传输。 - swampfox357

10

我用这里的代码成功了:PyQt中简单的文件上传进度条。 我稍微修改了一下,使用BytesIO代替StringIO。

class CancelledError(Exception):
    def __init__(self, msg):
        self.msg = msg
        Exception.__init__(self, msg)

    def __str__(self):
        return self.msg

    __repr__ = __str__

class BufferReader(BytesIO):
    def __init__(self, buf=b'',
                 callback=None,
                 cb_args=(),
                 cb_kwargs={}):
        self._callback = callback
        self._cb_args = cb_args
        self._cb_kwargs = cb_kwargs
        self._progress = 0
        self._len = len(buf)
        BytesIO.__init__(self, buf)

    def __len__(self):
        return self._len

    def read(self, n=-1):
        chunk = BytesIO.read(self, n)
        self._progress += int(len(chunk))
        self._cb_kwargs.update({
            'size'    : self._len,
            'progress': self._progress
        })
        if self._callback:
            try:
                self._callback(*self._cb_args, **self._cb_kwargs)
            except: # catches exception from the callback
                raise CancelledError('The upload was cancelled.')
        return chunk


def progress(size=None, progress=None):
    print("{0} / {1}".format(size, progress))


files = {"upfile": ("file.bin", open("file.bin", 'rb').read())}

(data, ctype) = requests.packages.urllib3.filepost.encode_multipart_formdata(files)

headers = {
    "Content-Type": ctype
}

body = BufferReader(data, progress)
requests.post(url, data=body, headers=headers)

诀窍是手动从文件列表中生成数据和头部,使用urllib3中的encode_multipart_formdata()


1
另一个问题,如果文件很大怎么办? - little

7

我知道这是一个老问题,但是我在其他地方找不到简单的答案,所以希望这能帮助其他人:

import requests
import tqdm    
with open(file_name, 'rb') as f:
        r = requests.post(url, data=tqdm(f.readlines()))

这样做可能不太好,使用rb模式打开文件时,readlines可能无法完全读取文件,只能部分上传该文件。 - chiragjn
1
是的,我认为你是对的。这个解决方案似乎有时候有效,但并不一致。我现在使用了类似Glen Thompson的答案。@chiragjn - user1432738
是的,我认为你是对的。这个解决方案似乎有时可以工作,但不是一致的。我现在使用类似于Glen Thompson答案的东西。@chiragjn - user1432738

6

这个解决方案使用了requests_toolbelttqdm两个都是维护得很好且广受欢迎的库。

from pathlib import Path
from tqdm import tqdm

import requests
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor

def upload_file(upload_url, fields, filepath):

    path = Path(filepath)
    total_size = path.stat().st_size
    filename = path.name

    with tqdm(
        desc=filename,
        total=total_size,
        unit="B",
        unit_scale=True,
        unit_divisor=1024,
    ) as bar:
        with open(filepath, "rb") as f:
            fields["file"] = ("filename", f)
            e = MultipartEncoder(fields=fields)
            m = MultipartEncoderMonitor(
                e, lambda monitor: bar.update(monitor.bytes_read - bar.n)
            )
            headers = {"Content-Type": m.content_type}
            requests.post(upload_url, data=m, headers=headers)

用例示例

upload_url = 'https://uploadurl'
fields = {
  "field1": value1, 
  "field2": value2
}
filepath = '97a6fce8_owners_2018_Van Zandt.csv'

upload_file(upload_url, fields, filepath)

Demo


2
通常情况下,您会构建一个流式数据源(生成器),对文件进行分块读取并在路上报告其进度(请参见kennethreitz/requests#663)。但是,这在请求文件API中不起作用,因为请求不支持流式上传(请参见kennethreitz/requests#295)-需要上传的文件在开始处理之前需要完全存储在内存中。
但是,正如J.F. Sebastian先前证明的那样,请求可以从生成器中流式传输内容,但是此生成器需要生成包括多部分编码和边界的完整数据流。这就是poster发挥作用的地方。
海报最初是为与Python的urllib2一起使用而编写的,支持流式生成多部分请求,并在进行过程中提供进度指示。Poster主页提供了与urllib2一起使用的示例,但您真的不想使用urllib2。请查看此示例代码,了解如何使用urllib2进行HTTP基本身份验证。太可怕了。
因此,我们真正想要使用poster和requests一起进行文件上传并跟踪进度。以下是方法:
# load requests-module, a streamlined http-client lib
import requests

# load posters encode-function
from poster.encode import multipart_encode



# an adapter which makes the multipart-generator issued by poster accessable to requests
# based upon code from https://dev59.com/cGYr5IYBdhLWcg3wKHEp#13911048
class IterableToFileAdapter(object):
    def __init__(self, iterable):
        self.iterator = iter(iterable)
        self.length = iterable.total

    def read(self, size=-1):
        return next(self.iterator, b'')

    def __len__(self):
        return self.length

# define a helper function simulating the interface of posters multipart_encode()-function
# but wrapping its generator with the file-like adapter
def multipart_encode_for_requests(params, boundary=None, cb=None):
    datagen, headers = multipart_encode(params, boundary, cb)
    return IterableToFileAdapter(datagen), headers



# this is your progress callback
def progress(param, current, total):
    if not param:
        return

    # check out http://tcd.netinf.eu/doc/classnilib_1_1encode_1_1MultipartParam.html
    # for a complete list of the properties param provides to you
    print "{0} ({1}) - {2:d}/{3:d} - {4:.2f}%".format(param.name, param.filename, current, total, float(current)/float(total)*100)

# generate headers and gata-generator an a requests-compatible format
# and provide our progress-callback
datagen, headers = multipart_encode_for_requests({
    "input_file": open('recordings/really-large.mp4', "rb"),
    "another_input_file": open('recordings/even-larger.mp4', "rb"),

    "field": "value",
    "another_field": "another_value",
}, cb=progress)

# use the requests-lib to issue a post-request with out data attached
r = requests.post(
    'https://httpbin.org/post',
    auth=('user', 'password'),
    data=datagen,
    headers=headers
)

# show response-code and -body
print r, r.text

1
我的上传服务器不支持分块编码,所以我想出了这个解决方案。它基本上只是一个围绕 python IOBase 的包装器,允许 tqdm.wrapattr 无缝工作。
import io
import requests
from typing import Union
from tqdm import tqdm
from tqdm.utils import CallbackIOWrapper

class UploadChunksIterator(Iterable):
    """
    This is an interface between python requests and tqdm.
    Make tqdm to be accessed just like IOBase for requests lib.
    """

    def __init__(
        self, file: Union[io.BufferedReader, CallbackIOWrapper], total_size: int, chunk_size: int = 16 * 1024
    ):  # 16MiB
        self.file = file
        self.chunk_size = chunk_size
        self.total_size = total_size

    def __iter__(self):
        return self

    def __next__(self):
        data = self.file.read(self.chunk_size)
        if not data:
            raise StopIteration
        return data

    # we dont retrive len from io.BufferedReader because CallbackIOWrapper only has read() method.
    def __len__(self):
        return self.total_size

fp = "data/mydata.mp4"
s3url = "example.com"
_quiet = False

with open(fp, "rb") as f:
    total_size = os.fstat(f.fileno()).st_size
    if not _quiet:
        f = tqdm.wrapattr(f, "read", desc=hv, miniters=1, total=total_size, ascii=True)

    with f as f_iter:
        res = requests.put(
            url=s3url,
            data=UploadChunksIterator(f_iter, total_size=total_size),
        )
    res.raise_for_status()

1
改进@jfs的答案,使进度条更具信息量。
import math
import os
import requests
import sys


class ProgressUpload:
    def __init__(self, filename, chunk_size=1250):
        self.filename = filename
        self.chunk_size = chunk_size
        self.file_size = os.path.getsize(filename)
        self.size_read = 0
        self.divisor = min(math.floor(math.log(self.file_size, 1000)) * 3, 9)  # cap unit at a GB
        self.unit = {0: 'B', 3: 'KB', 6: 'MB', 9: 'GB'}[self.divisor]
        self.divisor = 10 ** self.divisor


    def __iter__(self):
        progress_str = f'0 / {self.file_size / self.divisor:.2f} {self.unit} (0 %)'
        sys.stderr.write(f'\rUploading {dist_file}: {progress_str}')
        with open(self.filename, 'rb') as f:
            for chunk in iter(lambda: f.read(self.chunk_size), b''):
                self.size_read += len(chunk)
                yield chunk
                sys.stderr.write('\b' * len(progress_str))
                percentage = self.size_read / self.file_size * 100
                completed_str = f'{self.size_read / self.divisor:.2f}'
                to_complete_str = f'{self.file_size / self.divisor:.2f} {self.unit}'
                progress_str = f'{completed_str} / {to_complete_str} ({percentage:.2f} %)'
                sys.stderr.write(progress_str)
        sys.stderr.write('\n')

    def __len__(self):
        return self.file_size


# sample usage
requests.post(upload_url, data=ProgressUpload('file_path'))

关键在于__len__方法。没有它,我会得到连接关闭错误。这就是为什么你不能只使用tqdm + iter来获得一个简单的进度条的唯一原因。

0

我的Python代码运行得非常好。来源:twine

import sys
import tqdm
import requests
import requests_toolbelt

class ProgressBar(tqdm.tqdm):
    def update_to(self, n: int) -> None:
        self.update(n - self.n)

with open("test.zip", "rb") as fp:

    data_to_send = []
    session = requests.session()

    data_to_send.append(
        ("files", ("test.zip", fp))
    )

    encoder = requests_toolbelt.MultipartEncoder(data_to_send)
    with ProgressBar(
        total=encoder.len,
        unit="B",
        unit_scale=True,
        unit_divisor=1024,
        miniters=1,
        file=sys.stdout,
    ) as bar:
        monitor = requests_toolbelt.MultipartEncoderMonitor(
            encoder, lambda monitor: bar.update_to(monitor.bytes_read)
        )

        r = session.post(
            'http://httpbin.org/post',
            data=monitor,
            headers={"Content-Type": monitor.content_type},
        )

print(r.text)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接