Python最佳实践—requests模块下载超大文件,并实时显示下载进度和速度

本文参考:链接

场景描述

使用requests模块下载文件时,通常通过请求二进制流然后以wb的方式写到本地文件。例如,从下面的网站请求zip包二进制流,保存在本地zip文件:

import requests

def download_full_zip(form_data, url, save_path):
    ''' 请求完整的zip数据, 然后以wb方式保存在本地zip '''
    print('正在下载: {}'.format(save_path))
    # 发起请求
    response = requests.post(url, data=form_data)
    # 获取完整的二进制流
    bin_data = response.content
    # 以wb二进制写的方式保存到本地zip
    with open(save_path, 'wb') as fp:
        fp.write(bin_data)
    print('下载完成: {}'.format(save_path))

if __name__ == '__main__':
    form_data = {
        'DL_TYPE': 'pdf',
        'id_0': 'F1000000000000093741',
        'id_1': 'M2016092015005059983',
        'id_2': 'M2016092015010159984',
        'id_3': 'M2016092015011159985',
        'id_4': 'M2016092015012259986',
        'id_5': 'M2016092015013259987'
    }
    url = 'https://www.digital.archives.go.jp/acv/auto_conversion/download'
    save_path = './欽定三礼義疏.zip'
    download_full_zip(form_data, url, save_path)

上面的代码运行没有问题,原因是请求下载的zip文件较小,仅有100多M

Python最佳实践—requests模块下载超大文件,并实时显示下载进度和速度_第1张图片
在这里插入图片描述

问题描述

虽然上面的示例运行不会出现问题,但存在明显的两个缺陷:

  1. 无法得知当前是否还在下载,也无法得知当前下载的进度

    如下图,这种方式会将目标服务器完整的二进制数据全部获取后再保存本地,因此网络请求的过程是堵塞的。换句话说,只有整个代码成功才知道下载的结果,期间只能白白等待,控制台在下载的过程中不会有任何的反馈;如果网络出现暂时的拥塞,也无法得知当前是否还在下载,网络传输速度、下载的进度更是无从得知。

    Python最佳实践—requests模块下载超大文件,并实时显示下载进度和速度_第2张图片

    而我们希望做到实时掌握下载的进度,就像在谷歌浏览器下载文件时会实时提示已下载多大数据这样。

    Python最佳实践—requests模块下载超大文件,并实时显示下载进度和速度_第3张图片

  2. 对于网络的超大文件(几个GB),极有可能造成内存溢出、程序终止:该方式请求下载数据的基本原理是,先把完整的二进制流全部请求加载到内存,再将内存中的数据全部通过IO写入本地文件。如果下载的数据小,本例100多M完全没问题;而如果请求的文件超级大(几G甚至十几G),那么系统分配给对应进程的内存肯定是不够的,此时程序就会因为内存不足而被迫中止。

    例如在Linux下载10多个G的zip文件,在运行了几个小时后抛出如下异常:interrupted by signal 9: SIGKILL,表示进程被强制终止或杀死了。这个错信息通常出现在一些运行时间较长或者占用系统资源较高的程序中,比如应用,服务器等。Signal 9被称为SIGKILL(对应的信号名是"KILL"),是Linux系统中的一种强制终止信号,表示要立即中断该进程,无论进程在运行什么代码,或者当前状态如何。

    在这里插入图片描述

解决

针对上述问题及需求,我们可以使用stream流的方式。

  • requests.getrequest.post方法中须指定参数stream=True
  • 然后获取流数据时不能再用response.content(这还是加载全部数据),而是response.iter_content这个迭代器,迭代器每一步的chunk就是本次请求得到的数据块
  • 迭代器中传如每次传输数据块的大小chunk_size,以字节B为单位,例如1000*1024*1024就是1000M;注意chunk_size仅仅是对传输速率做了一个"限高",实际传输不一定会达到这个峰值,主要还得看双方的带宽。而迭代器每一步的chunk就是本次请求得到的数据块,因此实际每次传输的大小是len(chunk)而不是chunk_size。例如本例中虽然我定义了chunk_size为1000M,但实际的传输速率和我的带宽差不多,大概10M/s
  • 每次请求到一个chunk数据块之后,必须以ab二进制追加的方式写到本地文件。
  • 如果想要实时展示下载进度及传输速度,可借助tqdm进度条,具体参考如下代码:
import os
import requests
from tqdm import tqdm

def download_by_stream(chunk_size, url, form_data, save_path):
    # 如果文件存在, 删除重新下
    if os.path.exists(save_path):
        os.remove(save_path)
    # tqdm可选total参数,不传递这个参数则不显示文件总大小
    desc = '下载 {}'.format(os.path.basename(save_path))
    progress_bar = tqdm(initial=0, unit='B', unit_divisor=1024, unit_scale=True, desc=desc)

    # 设置stream=True参数读取大文件
    response = requests.post(url, stream=True, data=form_data)
    with open(save_path, 'ab') as fp:
        # 每次最多读取chunk_size个字节
        for chunk in response.iter_content(chunk_size=chunk_size):
            if chunk:
                fp.write(chunk)
                progress_bar.update(len(chunk))
    progress_bar.close()

if __name__ == '__main__':
    # 每次流传输的最大数据量(限高),但不一定能达到,还要看双方的带宽
    chunk_size = 1000 * 1024 * 1024  # 1000M
    form_data = {
        'DL_TYPE': 'pdf',
        'id_0': 'F1000000000000093741',
        'id_1': 'M2016092015005059983',
        'id_2': 'M2016092015010159984',
        'id_3': 'M2016092015011159985',
        'id_4': 'M2016092015012259986',
        'id_5': 'M2016092015013259987'
    }
    url = 'https://www.digital.archives.go.jp/acv/auto_conversion/download'
    save_path = './欽定三礼義疏.zip'
    download_by_stream(chunk_size, url, form_data, save_path)

运行效果如下:

  • 在下载过程中,通过tqdm进度条可以实时获取当前下载的速度、已下载的数据量等信息
    在这里插入图片描述

  • 下载完成,可以看到和前一种下载方式得到的结果完全一样

在这里插入图片描述 在这里插入图片描述

注意:这种方式下载,每次请求len(chunk)个字节数据后就以ab二进制追加的方式保存到本地,因此程序运行没多久本地就会生成文件,然后在此基础上不断请求不断追加写入。因此这一过程中,不要对当前正在追加下载的文件做任何操作!

在这里插入图片描述
在这里插入图片描述

你可能感兴趣的:(Python,#,最佳实践,python,网络爬虫)