从0到1手把手教你实现一个 Python 多线程下载器(四)

简介: 从0到1手把手教你实现一个 Python 多线程下载器(四)

示例操作


回顾之前写的单线程版本下载器,我们知道怎么获取待下载的文件大小以及如何分块下载。注意上面的分块下载是仅有一个线程在操作的,譬如文件大小为: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)


代码运行过程image.png

写在最后


本文内容丰富,涉及到的主要知识有:文件读写、发起网络请求、多线程操作、代码出错重试。由浅入深,层层递进,最终实现了一个简易的带进度条的多线程下载器。

快来试试吧!

相关文章
|
25天前
|
数据采集 存储 安全
如何确保Python Queue的线程和进程安全性:使用锁的技巧
本文探讨了在Python爬虫技术中使用锁来保障Queue(队列)的线程和进程安全性。通过分析`queue.Queue`及`multiprocessing.Queue`的基本线程与进程安全特性,文章指出在特定场景下使用锁的重要性。文中还提供了一个综合示例,该示例利用亿牛云爬虫代理服务、多线程技术和锁机制,实现了高效且安全的网页数据采集流程。示例涵盖了代理IP、User-Agent和Cookie的设置,以及如何使用BeautifulSoup解析HTML内容并将其保存为文档。通过这种方式,不仅提高了数据采集效率,还有效避免了并发环境下的数据竞争问题。
如何确保Python Queue的线程和进程安全性:使用锁的技巧
|
12天前
|
API Python
探索Python中的多线程编程
探索Python中的多线程编程
33 5
|
22天前
|
调度 Python
Python 中如何实现多线程?
【8月更文挑战第29天】
40 6
|
25天前
|
API C语言 C++
C调用Python之多线程与traceback打印
C调用Python之多线程与traceback打印
26 2
|
1月前
|
数据采集 Java Python
Python并发编程:多线程(threading模块)
Python是一门强大的编程语言,提供了多种并发编程方式,其中多线程是非常重要的一种。本文将详细介绍Python的threading模块,包括其基本用法、线程同步、线程池等,最后附上一个综合详细的例子并输出运行结果。
|
29天前
|
数据采集 Java Python
Python并发编程:多线程(threading模块)
本文详细介绍了Python的threading模块,包括线程的创建、线程同步、线程池的使用,并通过多个示例展示了如何在实际项目中应用这些技术。通过学习这些内容,您应该能够熟练掌握Python中的多线程编程,提高编写并发程序的能力。 多线程编程可以显著提高程序的并发性能,但也带来了新的挑战和问题。在使用多线程时,需要注意避免死锁、限制共享资源的访问,并尽量使用线程池来管理和控制线程。
|
1月前
|
开发工具 计算机视觉 Python
大恒相机 - Python 多线程拍摄
大恒相机 - Python 多线程拍摄
33 1
|
1月前
|
调度 Python
|
23天前
|
数据采集 Java Python
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
|
27天前
|
安全 Java Python
Python 中的多线程
【8月更文挑战第24天】
16 0