使用请求在python中下载大文件

Rom*_*nov 348 python download stream python-requests

请求是一个非常好的库.我想用它来下载大文件(> 1GB).问题是不可能将整个文件保存在内存中我需要以块的形式读取它.这是以下代码的问题

import requests

def DownloadFile(url)
    local_filename = url.split('/')[-1]
    r = requests.get(url)
    f = open(local_filename, 'wb')
    for chunk in r.iter_content(chunk_size=512 * 1024): 
        if chunk: # filter out keep-alive new chunks
            f.write(chunk)
    f.close()
    return 
Run Code Online (Sandbox Code Playgroud)

由于某种原因它不起作用.在将其保存到文件之前,它仍会将响应加载到内存中.

UPDATE

如果你需要一个可以从FTP下载大文件的小客户端(Python 2.x /3.x),你可以在这里找到它.它支持多线程和重新连接(它确实监视连接),它还为下载任务调整套接字参数.

Rom*_*nov 582

使用以下流代码,无论下载文件的大小如何,Python内存使用都受到限制:

def download_file(url):
    local_filename = url.split('/')[-1]
    # NOTE the stream=True parameter below
    with requests.get(url, stream=True) as r:
        r.raise_for_status()
        with open(local_filename, 'wb') as f:
            for chunk in r.iter_content(chunk_size=8192): 
                if chunk: # filter out keep-alive new chunks
                    f.write(chunk)
                    # f.flush()
    return local_filename
Run Code Online (Sandbox Code Playgroud)

请注意,使用的字节数iter_content不完全是chunk_size; 它应该是一个通常更大的随机数,并且预计在每次迭代中都会有所不同.

有关进一步的参考,请参见http://docs.python-requests.org/en/latest/user/advanced/#body-content-workflow.

  • @RomanPodlinov:`f.flush()`不会将数据刷新到物理磁盘.它将数据传输到OS.通常,除非出现电源故障,否则就足够了.`f.flush()`无条件地使代码变慢.当相应的文件缓冲区(内部应用程序)已满时,会发生刷新.如果你需要更频繁的写作; 将buf.size参数传递给`open()`. (11认同)
  • @Shuman正如我看到你在从http://切换到https://(https://github.com/kennethreitz/requests/issues/2043)时解决了这个问题.您可以请更新或删除您的评论,因为人们可能会认为大型1024Mb的文件代码存在问题 (9认同)
  • 不要忘记用`r.close()`关闭连接 (9认同)
  • `chunk_size`至关重要.默认情况下,它是1(1个字节).这意味着对于1MB,它将进行1百万次迭代.http://docs.python-requests.org/en/latest/api/#requests.Response.iter_content (7认同)
  • `f.flush()`似乎没必要.你想用它做什么?(如果丢弃它,你的内存使用量不会是1.5gb).`f.write(b'')`(如果`iter_content()`可能返回一个空字符串)应该是无害的,因此`if chunk`也可以被删除. (4认同)
  • @RomanPodlinov还有一点,抱歉:)读完iter_content()后我得出结论,它不能产生一个空字符串:到处都有空白检查.这里的主要逻辑:[requests/packages/urllib3/response.py](https://github.com/kennethreitz/requests/blob/master/requests/packages/urllib3/response.py#L332). (3认同)
  • ``if chunk:#过滤掉keep-alive new chunks`` - 这是多余的,不是吗?因为``iter_content()``总是产生字符串而从不产生``None``,所以它看起来像是过早的优化.我也怀疑它可以产生空字符串(我无法想象任何原因). (2认同)
  • @RomanPodlinov我对“保持活动的新块”一词不熟悉。您能再解释一下吗?有保持活动(持久)连接(当单个TCP连接中包含多个HTTP请求时)和分块响应(当没有Content-Length头并且内容分为块时,最后一个为零长度)。AFAIK,这两个功能是独立的,它们没有共同之处。 (2认同)
  • @RomanPodlinov 另一点: iter_content() 总是产生字符串。将空字符串写入文件并没有错,对吧。那么,为什么要检查长度呢? (2认同)

Joh*_*nck 213

如果你使用Response.raw和更容易shutil.copyfileobj():

import requests
import shutil

def download_file(url):
    local_filename = url.split('/')[-1]
    with requests.get(url, stream=True) as r:
        with open(local_filename, 'wb') as f:
            shutil.copyfileobj(r.raw, f)

    return local_filename
Run Code Online (Sandbox Code Playgroud)

这会将文件流式传输到磁盘而不会占用过多内存,代码很简单.

  • 这应该是正确的答案![接受](/sf/answers/1168742221/)答案可以达到2-3MB/s.使用copyfileobj可以达到~40MB/s.卷曲下载(相同的机器,相同的URL等)~50-55 MB/s. (22认同)
  • 为了确保释放Requests连接,您可以使用第二个(嵌套的)`with`块来发出请求:`with requests.get(url,stream = True)为r:` (22认同)
  • 请注意,您可能需要根据问题2155调整[流式gzipped响应](https://github.com/kennethreitz/requests/issues/2155). (7认同)
  • @ChristianLong:这是真的,但仅限于最近,因为支持`with requests.get()`的功能仅在2017-06-07合并!对于拥有请求2.18.0或更高版本的用户,您的建议是合理的.参考:https://github.com/requests/requests/issues/4136 (6认同)
  • @EricCousineau你可以修补这个行为[替换`read`方法:`response.raw.read = functools.partial(response.raw.read, decode_content=True)`](https://github.com/requests/请求/问题/2155#issuecomment-50771010) (5认同)
  • 添加长度参数让我获得更好的下载速度 `shutil.copyfileobj(r.raw, f, length=16*1024*1024)` (5认同)
  • 使用`.raw`的一个小警告是它不处理解码.在这里的文档中提到:http://docs.python-requests.org/en/master/user/quickstart/#raw-response-content (3认同)
  • @VitalyZdanevich:尝试“shutil.copyfileobj(r.raw,sys.stdout)”。 (2认同)
  • @visoft 在将 `chunk_size` 从 `1024` 增加到 `10*1024`(debian ISO,常规连接)后,我能够匹配 `raw` 和 `iter_content` 之间的下载速度 (2认同)
  • 接受的答案的问题是块大小.如果连接速度足够快,1KiB太小,与传输数据相比,您在开销上花费的时间太多.`shutil.copyfileobj`默认为16KiB块.从1KiB增加块大小几乎肯定会提高下载速度,但不会增加太多.我正在使用1MiB块,它运行良好,它接近全带宽使用.您可以尝试监视连接速率并根据它调整块大小,但要注意过早优化. (2认同)
  • 与此同时,它是 2019 年。我自由地将缺少的 @with requests.get(url, stream=True) 作为 r:@ 编辑到答案中。没有理由不使用它。 (2认同)
  • 更新了关于流式传输 gzip 响应的 github [issue 2155](https://github.com/psf/requests/issues/2155) 链接(ChrisP 的答案中的链接不再有效)。 (2认同)

x-y*_*uri 47

不完全是OP所要求的,但是......用urllib以下方法做到这一点非常容易:

from urllib.request import urlretrieve
url = 'http://mirror.pnl.gov/releases/16.04.2/ubuntu-16.04.2-desktop-amd64.iso'
dst = 'ubuntu-16.04.2-desktop-amd64.iso'
urlretrieve(url, dst)
Run Code Online (Sandbox Code Playgroud)

或者这样,如果要将其保存到临时文件:

from urllib.request import urlopen
from shutil import copyfileobj
from tempfile import NamedTemporaryFile
url = 'http://mirror.pnl.gov/releases/16.04.2/ubuntu-16.04.2-desktop-amd64.iso'
with urlopen(url) as fsrc, NamedTemporaryFile(delete=False) as fdst:
    copyfileobj(fsrc, fdst)
Run Code Online (Sandbox Code Playgroud)

我看了看这个过程:

watch 'ps -p 18647 -o pid,ppid,pmem,rsz,vsz,comm,args; ls -al *.iso'
Run Code Online (Sandbox Code Playgroud)

我看到文件正在增长,但内存使用率保持在17 MB.我错过了什么吗?

  • 对于Python 2.x,请使用`from urllib import urlretrieve` (2认同)

dan*_*van 41

您的块大小可能太大,您是否尝试删除它 - 可能一次只有1024个字节?(另外,你可以with用来整理语法)

def DownloadFile(url):
    local_filename = url.split('/')[-1]
    r = requests.get(url)
    with open(local_filename, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024): 
            if chunk: # filter out keep-alive new chunks
                f.write(chunk)
    return 
Run Code Online (Sandbox Code Playgroud)

顺便提一下,你如何推断​​响应已被加载到内存中?

这听起来仿佛蟒蛇没有数据刷新到文件,从其他SO问题,你可以尝试f.flush(),并os.fsync()迫使文件的写入和释放内存;

    with open(local_filename, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024): 
            if chunk: # filter out keep-alive new chunks
                f.write(chunk)
                f.flush()
                os.fsync(f.fileno())
Run Code Online (Sandbox Code Playgroud)

  • 您需要在requests.get()调用中使用stream = True.这就是导致内存臃肿的原因. (28认同)
  • 它是`os.fsync(f.fileno())` (2认同)

小智 13

使用wgetpython 的模块代替。这是一个片段

import wget
wget.download(url)
Run Code Online (Sandbox Code Playgroud)

  • 这是一个非常古老且未维护的模块。 (10认同)

Ben*_*tch 6

基于上面罗马人最受好评的评论,这是我的实现,包括“下载为”和“重试”机制:

def download(url: str, file_path='', attempts=2):
    """Downloads a URL content into a file (with large file support by streaming)

    :param url: URL to download
    :param file_path: Local file name to contain the data downloaded
    :param attempts: Number of attempts
    :return: New file path. Empty string if the download failed
    """
    if not file_path:
        file_path = os.path.realpath(os.path.basename(url))
    logger.info(f'Downloading {url} content to {file_path}')
    url_sections = urlparse(url)
    if not url_sections.scheme:
        logger.debug('The given url is missing a scheme. Adding http scheme')
        url = f'http://{url}'
        logger.debug(f'New url: {url}')
    for attempt in range(1, attempts+1):
        try:
            if attempt > 1:
                time.sleep(10)  # 10 seconds wait time between downloads
            with requests.get(url, stream=True) as response:
                response.raise_for_status()
                with open(file_path, 'wb') as out_file:
                    for chunk in response.iter_content(chunk_size=1024*1024):  # 1MB chunks
                        out_file.write(chunk)
                logger.info('Download finished successfully')
                return file_path
        except Exception as ex:
            logger.error(f'Attempt #{attempt} failed with error: {ex}')
    return ''
Run Code Online (Sandbox Code Playgroud)