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


398

请求是一个非常不错的库。我想用它来下载大文件(> 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 

由于某种原因,它无法按这种方式工作。仍将响应加载到内存中,然后再将其保存到文件中。

更新

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

Answers:


650

使用以下流代码,无论下载文件的大小如何,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 you have chunk encoded response uncomment if
                # and set chunk_size parameter to None.
                #if chunk: 
                f.write(chunk)
    return local_filename

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

https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflowhttps://requests.readthedocs.io/en/latest/api/#requests.Response.iter_content进一步参考。


9
@Shuman正如我所见,当您从http://切换为https://(github.com/kennethreitz/requests/issues/2043)时,您解决了该问题。您能否更新或删除您的评论,因为人们可能会认为更大的1024Mb文件的代码存在问题
Roman Podlinov

8
chunk_size是至关重要的。默认情况下为1(1个字节)。这意味着对于1MB,它将进行1百万次迭代。docs.python-requests.org/en/latest/api/…–
Eduard Gamonal

4
f.flush()似乎没有必要。您打算如何使用它?(如果删除它,则您的内存使用量不会是1.5gb)。f.write(b'')(如果iter_content()可能返回空字符串)应该是无害的,因此if chunk也可以丢弃。
jfs

11
@RomanPodlinov:f.flush()不会将数据刷新到物理磁盘。它将数据传输到OS。通常,除非断电就足够了。f.flush()无缘无故使代码变慢。当相应的文件缓冲区(应用程序内部)已满时,将发生刷新。如果您需要更频繁的写入;将buf.size参数传递给open()
jfs

9
别忘了用r.close()
0xcaff

271

如果使用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

这样就无需占用过多内存就可以将文件流式传输到磁盘,并且代码很简单。


10
请注意,在按问题2155 流压缩的响应时,您可能需要进行调整。–
ChrisP

32
这应该是正确的答案!该接受的答案让你达到2-3MB /秒。使用copyfileobj可使您达到〜40MB / s。以50-55 MB / s的速度下载(同一台机器,相同的url等)。
visoft

24
为确保释放请求连接,您可以使用第二个(嵌套的)with块来发出请求:with requests.get(url, stream=True) as r:
Christian Long

7
@ChristianLong:是的,但只是最近,因为支持的功能with requests.get()仅在2017-06-07合并!您的建议对有Requests 2.18.0或更高版本的人是合理的。参考:github.com/requests/requests/issues/4136
John Zwinck


54

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)

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

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)

我看了看这个过程:

watch 'ps -p 18647 -o pid,ppid,pmem,rsz,vsz,comm,args; ls -al *.iso'

而且我看到文件在增长,但内存使用量保持在17 MB。我想念什么吗?


2
对于Python 2.x中,使用from urllib import urlretrieve
瓦迪姆·科托夫

这导致下载速度慢...
citynorman

@citynorman你能详细说明吗?与哪种解决方案相比?为什么?
x-yuri

@ x-yuri vs shutil.copyfileobj投票最多的解决方案,请参阅我和其他评论
citynorman

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 

顺便说一句,您如何推断响应已加载到内存中?

这听起来仿佛蟒蛇没有刷新数据文件,从其他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())

1
我在Kubuntu中使用系统监视器。它告诉我python进程内存增加了(从25kb到1.5gb)。
罗曼·波德利诺夫

那种内存膨胀很烂,也许f.flush(); os.fsync()可能会迫使写一个空闲的内存。
danodonovan

2
os.fsync(f.fileno())
sebdelsol 2014年

29
您需要在request.get()调用中使用stream = True。这就是导致内存膨胀的原因。
2015年

1
小错别字:您在def DownloadFile(url)
Aubrey
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.