示例操作
回顾之前写的单线程版本下载器,我们知道怎么获取待下载的文件大小以及如何分块下载。注意上面的分块下载是仅有一个线程在操作的,譬如文件大小为:1000 B,每次下载 100 B,那么单线程会连续地每次读取 100 B 的内容,直到没有内容可读取。
为了能让多线程下载同一个文件,我们需要为每一个线程分配属于它自己的任务,比如说要下载大小为 100 B的文件,那么线程一可以负责下载 0-50 B,线程二负责下载 50-100 B,这样子分两个线程来下载。
要实现任务以上的任务分配。我们自然得学会怎么将一个数字分为多个区间,下面以将 100 分为每一块 20 为例,展示怎么实现这样子的功能
# 总大小 total = 100 # 每一块的大小 step = 20 # 分多块 parts = [(start, min(start+step,total)) for start in range(0, total, step)] print(parts)
运行上面的代码,会达到以下输出
[(0, 20), (20, 40), (40, 60), (60, 80), (80, 100)]
可以看到,我们成功地完成了较为完美的切割操作! 同样地,为了能够更好地维护代码,我们可以尝试把它抽取为函数,示例如下
from __future__ import annotations def split(start: int, end: int, step: int) -> list[tuple[int, int]]: ''' 将指定区间的数切割为多个区间 Parameters ---------- start :起始位置 end : 终止位置 step : 区间长度 Return ------ 区间元组构成的列表 ''' # 分多块 parts = [(start, min(start+step, end)) for start in range(0, end, step)] return parts if "__main__" == __name__: # 起始位置 start = 1 # 终止位置 total = 102 # 区间长度 step = 20 parts = split(start, total, step) print(parts)
以上操作是为了后续分段下载文件做准备
下载部分文件
下面我将以下面这个链接为例,演示如何下载某一部分的文件
https://issuecdn.baidupcs.com/issue/netdisk/yunguanjia/BaiduNetdisk_7.2.8.9.exe
import requests # 待下载部分的文件起始位置 start = 0 # 待下载部分的文件终止位置 end = 1000 # 每次读取的大小 chunk_size = 128 # 记录下载的位置 seek = start # 请求头 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE' } # 这是核心!设定下载的范围 headers['Range'] = f'bytes={start}-{end}' # 下载链接 url = 'https://issuecdn.baidupcs.com/issue/netdisk/yunguanjia/BaiduNetdisk_7.2.8.9.exe' response = requests.get(url, headers=headers, stream=True) for chunk in response.iter_content(chunk_size=chunk_size): _seek = min(seek+chunk_size, end) print(f'下载: {seek}-{_seek}') seek = _seek 代码运行输出 下载: 0-128 下载: 128-256 下载: 256-384 下载: 384-512 下载: 512-640 下载: 640-768 下载: 768-896 下载: 896-1000
有了以上基础,我们来尝试把之前的单线程下载器修改为多线程的(我还没想好怎么一步步教,先贴有详细注释的代码吧,后续更新)
最终代码(带进度条的多线程下载器)
from __future__ import annotations # 用于显示进度条 from tqdm import tqdm # 用于发起网络请求 import requests # 用于多线程操作 import multitasking import signal # 导入 retry 库以方便进行下载出错重试 from retry import retry signal.signal(signal.SIGINT, multitasking.killall) # 请求头 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE' } # 定义 1 MB 多少为 B MB = 1024**2 def split(start: int, end: int, step: int) -> list[tuple[int, int]]: # 分多块 parts = [(start, min(start+step, end)) for start in range(0, end, step)] return parts def get_file_size(url: str, raise_error: bool = False) -> int: ''' 获取文件大小 Parameters ---------- url : 文件直链 raise_error : 如果无法获取文件大小,是否引发错误 Return ------ 文件大小(B为单位) 如果不支持则会报错 ''' response = requests.head(url) file_size = response.headers.get('Content-Length') if file_size is None: if raise_error is True: raise ValueError('该文件不支持多线程分段下载!') return file_size return int(file_size) def download(url: str, file_name: str, retry_times: int = 3, each_size=16*MB) -> None: ''' 根据文件直链和文件名下载文件 Parameters ---------- url : 文件直链 file_name : 文件名 retry_times: 可选的,每次连接失败重试次数 Return ------ None ''' f = open(file_name, 'wb') file_size = get_file_size(url) @retry(tries=retry_times) @multitasking.task def start_download(start: int, end: int) -> None: ''' 根据文件起止位置下载文件 Parameters ---------- start : 开始位置 end : 结束位置 ''' _headers = headers.copy() # 分段下载的核心 _headers['Range'] = f'bytes={start}-{end}' # 发起请求并获取响应(流式) response = session.get(url, headers=_headers, stream=True) # 每次读取的流式响应大小 chunk_size = 128 # 暂存已获取的响应,后续循环写入 chunks = [] for chunk in response.iter_content(chunk_size=chunk_size): # 暂存获取的响应 chunks.append(chunk) # 更新进度条 bar.update(chunk_size) f.seek(start) for chunk in chunks: f.write(chunk) # 释放已写入的资源 del chunks session = requests.Session() # 分块文件如果比文件大,就取文件大小为分块大小 each_size = min(each_size, file_size) # 分块 parts = split(0, file_size, each_size) print(f'分块数:{len(parts)}') # 创建进度条 bar = tqdm(total=file_size, desc=f'下载文件:{file_name}') for part in parts: start, end = part start_download(start, end) # 等待全部线程结束 multitasking.wait_for_tasks() f.close() bar.close() if "__main__" == __name__: # url = 'https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0d/ea/f936c14b6e886221e53354e1992d0c4e0eb9566fcc70201047bb664ce777/tensorflow-2.3.1-cp37-cp37m-macosx_10_9_x86_64.whl#sha256=1f72edee9d2e8861edbb9e082608fd21de7113580b3fdaa4e194b472c2e196d0' url = 'https://issuecdn.baidupcs.com/issue/netdisk/yunguanjia/BaiduNetdisk_7.2.8.9.exe' file_name = 'BaiduNetdisk_7.2.8.9.exe' # 开始下载文件 download(url, file_name)
代码运行过程
写在最后
本文内容丰富,涉及到的主要知识有:文件读写、发起网络请求、多线程操作、代码出错重试。由浅入深,层层递进,最终实现了一个简易的带进度条的多线程下载器。
快来试试吧!