Download large file in python with requests
请求是一个非常好的库。我想用它下载大文件(>1GB)。问题是不可能将整个文件保存在内存中,我需要将其分块读取。以下代码有问题
1 2 3 4 5 6 7 8 9 10 11
| 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下载大文件,您可以在这里找到它。它支持多线程和重新连接(它可以监视连接),还可以调整下载任务的套接字参数。
使用以下流代码,无论下载文件的大小,python内存的使用都受到限制:
1 2 3 4 5 6 7 8 9 10 11
| 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 |
注意,使用iter_content返回的字节数并不完全是chunk_size;它应该是一个更大的随机数,并且在每次迭代中都会有所不同。
请参阅http://docs.python requests.org/en/latest/user/advanced/body content workflow以获取进一步参考。
- @Shuman此代码成功下载了大于1.5GB的文件。你能通过任何浏览器成功下载文件吗?
- 是的,在Firefox中,如果我手动下载,它会成功地保存一个1.5GB.zip文件。
- @如我所见,当从http://to https://(github.com/kennethreitz/requests/issues/2043)切换到https://时,Shuman解决了这个问题。您能更新或删除您的评论吗?因为人们可能认为1024MB以上的文件的代码有问题。
- chunk_size是至关重要的。默认为1(1字节)。这意味着对于1MB,它将进行1百万次迭代。docs.python requests.org/en/latest/api/…
- 是否可以以某种方式并行ITER_content()部分以加快下载速度?谢谢!
- 这是个好问题。我不确定有没有可能
- @罗曼波德利诺夫,你能告诉我你为什么用冲水吗?
- 也可以使用url.rsplit("/",1)[1],它不会拆分整个url,而只拆分其最后一部分。
- @Rovinbhandari:要进行并行化,请查看requests中是否支持字节范围HTTP头。
- url.split('/')[-1]可能过于简单,例如,参见url2filename()。
- f.flush()似乎没有必要。你想用它来完成什么?(如果您放弃它,您的内存使用将不会是1.5GB)。f.write(b'')(如果iter_content()可以返回空字符串)应该是无害的,因此if chunk也可以被丢弃。
- @J.F.Sebastian同意URL2FileName更好。关于Flash。其思想是将数据刷新到驱动器上的物理文件中。如果您看到没有flash()代码工作正常,只需删除它。
- @romanpodlinov:f.flush()不将数据刷新到物理磁盘。它将数据传输到操作系统。通常情况下,除非发生电源故障,否则就足够了。f.flush()无缘无故地使这里的代码变慢。刷新发生在相应的文件缓冲区(内部应用程序)已满时。如果需要更频繁的写入,请将buf.size参数传递给open()。
- @J.F.Sebastian谢谢,我在代码中注释了flush行
- 别忘了关闭与r.close()的连接。
- if chunk: # filter out keep-alive new chunks–是多余的,不是吗?由于iter_content()总是生成字符串,而从不生成None,因此看起来是过早的优化。我也怀疑它是否会产生空字符串(我无法想象有任何理由这样做)。
- 如果您使用Dropbox链接,它将以"banner-apus-1.23.zip"这样的名称保存您的文件。dl=1
- @暂停再次检查作为链接提供的内容。如果Dropbox在URL中添加了一些内容(或重定向到其他URL),您可以很容易地将其删除。只需更改设置本地文件名变量的方式。
- @Y0prst plz将您的注意力放在这行的评论"过滤掉保持活跃的新块"上。如果下载的文件大小为几个Gbs,则完全有意义。
- @罗曼波德利诺夫,我不熟悉"保持活力的新块"这个词。你能进一步解释一下吗?存在keep-alive(持久)连接(当单个TCP连接中包含多个HTTP请求)和分块响应(当没有内容长度头和内容被分块时,最后一个为零长度)。阿法克,这两个特征是独立的,没有共同点。
- @RomanPodlinov另一点:iter_content()总是生成字符串。将空字符串写入文件没有任何错误,对吧。那么,我们为什么要检查长度呢?
- @罗曼波德利诺夫和另外一点,抱歉:)在阅读了ITER_content()源之后,我得出结论,它永远不会产生空字符串:到处都有空检查。这里的主要逻辑是:请求/包/urlib3/response.py。
- 但为什么不是shutil.copyfileobj?
- @stek29 coz response and response.iter_内容不是像对象一样的文件?
- @下面是使用response.raw的带有shutil.copyfileobj的stek29示例。
- @正如你已经注意到的,Reishin response.raw是。
- @Y0prst"我不熟悉"keep alive new chunks"这个词。一方面我不知道是谁在w代码中添加了这个注释,另一方面你改变了我的话。这一行代码会删除空块,这可能是由于下载过程中的保持活动状态请求而不时出现的。
- @RomanPodlinov关于您和y0prst讨论的"保持活动块"检查;是否得出了这样的结论:由于内部检查,请求永远不会返回空字符串,因此这是不必要的?
- @romanpodlinov这一行至少在"类文件对象"的情况下是这样的:github.com/kennethreitz/requests/blob/…
- 为什么不检查r.status_code?
- @对于一个5GB以上的代码需要花费很长时间的文件,在这种情况下,理想的块大小是什么?为了提高下载速度我们能做的任何事情
- @用户3508811在您的情况下,我建议使用我的小lib github.com/keepitsimple/pyftpclient,它可以重新连接并使用多个同时连接进行下载。我用这个小库下载1-10Gbs大小的文件
- @Romanpodlinov-我不能调整pyftpclien从一个链接下载,比如说https://hostname.company.com/ui/containers/9888577,下面的行如何改为从一个链接obj = PyFTPclient('192.168.0.59', 2121, 'test', 'testftp') obj.DownloadFile('USAHD-8974-20131013-0300-0330.ts')下载?
- @为ftp协议实现了用户3508811 pyftpclient。
- 如果你想在下载后获得文件的散列值/大小,记住在用stream=True写入文件后刷新-如果不这样做,可能会丢失几个(数百)字节。
- 你怎么知道这不占用很多记忆?查看过程监视器?当我运行:import sys print(sys.getsizeof(r.text))时,无论是否使用上面的流代码,我都会得到相同的大小输出。
- @新手我不知道你用什么操作系统。我在Linux下使用htop,在Windows下使用sysinternals.com上的Process Monitor
如果您使用Response.raw和shutil.copyfileobj(),会更容易:
1 2 3 4 5 6 7 8 9 10
| import requests
import shutil
def download_file(url):
local_filename = url.split('/')[-1]
r = requests.get(url, stream=True)
with open(local_filename, 'wb') as f:
shutil.copyfileobj(r.raw, f)
return local_filename |
这将文件流式传输到磁盘,而不使用过多的内存,而且代码很简单。
- 请注意,当根据2155问题传输gzip响应时,可能需要进行调整。
- 您是否测试过此代码以下载大于1GB的大文件?
- @罗曼波德利诺夫:不,是吗?
- 是的。大多数文件都大于1GB。代码每天都在下载一堆视频文件
- 这应该是正确的答案!接受的答案可以使您达到2-3Mb/s。使用copyfileobj可以使您达到~40MB/s。curl下载(相同的机器、相同的URL等)的速度可以达到~50-55MB/s。
- @Visoft你是如何检查下载速度的?
- @moondra来自python,它划分了下载时间和文件大小。我在千兆连接上使用了漂亮的larg文件(100M-2G)。服务器或多或少位于同一网络/数据中心中。
- 为了确保释放请求连接,可以使用第二个(嵌套的)with块发出请求:with requests.get(url, stream=True) as r:。
- @克里斯蒂亚龙:没错,但只是最近,因为支持with requests.get()的功能才在2017-06-07合并!您的建议对于请求2.18.0或更高版本的人是合理的。参考:github.com/requests/requests/issues/4136
- 使用.raw的一个小警告是它不处理解码。这里的文档中提到:docs.python requests.org/en/master/user/quickstart/…
- 是否可以通过print流到stdout?
- @Vitalyzdanevich:试试shutil.copyfileobj(r.raw, sys.stdout)。
- @在我将chunk_size从1024增加到10*1024之后,visoft i能够匹配raw和iter_content之间的下载速度(debian iso,常规连接)
- 接受的答案的问题是块大小。如果您有足够快的连接,那么1KIB太小,与传输数据相比,您在开销上花费了太多时间。shutil.copyfileobj默认为16kib块。将块大小从1kib增加几乎肯定会提高下载率,但不要增加太多。我使用的是1Mib块,它工作得很好,接近全带宽使用。您可以尝试监视连接速率并根据它调整块大小,但要注意过早的优化。
- @Ericcousineau你可以修补这个行为来代替read方法:response.raw.read = functools.partial(response.raw.read, decode_content=True)
- 有没有办法将这里的流读取限制为最大值,比如说128 kib?
您的块大小可能太大,您是否尝试删除它?一次可能删除1024个字节?(也可以使用with整理语法)
1 2 3 4 5 6 7 8
| 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 |
顺便说一下,您如何推断响应已加载到内存中?
听起来好像python没有将数据刷新到文件中,从其他问题来看,您可以尝试使用f.flush()和os.fsync()来强制文件写入并释放内存;
1 2 3 4 5 6
| 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()) |
- 我在Kubuntu使用系统监视器。它向我展示了Python进程内存的增加(从25KB增加到1.5GB)。
- 内存膨胀很糟糕,可能f.flush(); os.fsync()会迫使写一个内存释放。
- 是os.fsync(f.fileno())。
- 您需要在requests.get()调用中使用stream=true。这就是导致记忆膨胀的原因。
- 小错误:在def DownloadFile(url)后面漏掉冒号(:')
不完全是OP的要求,但是…用urllib做这件事是非常容易的:
1 2 3 4
| 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) |
或者这样,如果要将其保存到临时文件中:
1 2 3 4 5 6
| 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) |
我看了这个过程:
1
| watch 'ps -p 18647 -o pid,ppid,pmem,rsz,vsz,comm,args; ls -al *.iso' |
我看到文件在增长,但是内存使用量保持在17MB。我错过什么了吗?
- 对于python 2.x,使用from urllib import urlretrieve。