Python:教你如何实现多线程下载器

hello,大家好,我是wangzirui32,今天我们来学习如何使用Python实现多线程下载器,开始学习吧!

1. 流程&原理

  1. HEAD请求发送到目标URL,获取文件的大小
  2. 根据文件大小对下载任务进行分配
  3. 每个线程发送含有Range参数的请求,获取文件的一部分
  4. 把每一个部分进行合成,下载完成

HEAD请求是用来获取对方文件的基本信息,而不会返回具体内容,响应头中的Content-Length便是文件的大小(单位:字节)。发送请求头含有Range参数的GET请求时,不会返回文件的全部内容,只会返回Range指定的部分内容,如Range='0-3000'就只获取文件字节0-3000的部分。

2. 项目准备

请在工作目录下创建app.py和文件夹files,执行命令下载所需包:

pip install requests

3. 代码

3.1 导入所需包

from queue import Queue  # 队列
import requests          # 网络请求库
import threading         # 多线程
import os                # 操作文件

3.2 下载设置

我们要对下载的文件URL,文件名等进行设置:

# download settings
url = "http://img1.baidu.com/it/u=2476325767,3197989021&fm=26&fmt=auto"  # 目标文件URL
filename = "img.jpg"   # 下载后保存的文件名
thread_count = 5       # 启用线程数
copies_count = 20      # 将文件分为多少个部分作为单个下载任务

3.3 获取文件大小

def get_file_size(url) -> int:
    response = requests.head(url)   # HEAD请求
    file_length = int(response.headers['Content-Length'])  # 获取大小

    return file_length    # 返回大小

3.4 计算单个部分下载大小

def get_thread_download(file_length) -> list:
    bytes = Queue(copies_count)   # 创建字节队列

    start_bytes = -1        # 开始字节为-1
    for i in range(copies_count):
        bytes_size = int(file_length/copies_count)*i  # 计算目前字节
        # 最后一个时 末尾字节为文件大小 避免落下一些字节未下载
        if i == copies_count-1: bytes_size = file_length
        # 字节范围
        # start_bytes用来保存上一次的字节末尾
        bytes_length = "{}-{}".format(start_bytes+1, bytes_size)
        
        bytes.put([i, bytes_length])  # 加入队列 并赋予编号(i)
        start_bytes = bytes_size      # 将开始字节重新赋值

    return bytes

3.5 线程类

class DownloadThread(threading.Thread):
    def __init__(self, bytes_queue: Queue, url):
        super().__init__(daemon=True)
        self.bytes_queue = bytes_queue
        self.url = url

    def run(self):
        while not self.bytes_queue.empty():  # 如果字节队列不为空
            bytes_range = self.bytes_queue.get()  # 读取范围信息
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 Edg/92.0.902.84",
                "Range": "bytes={}".format(bytes_range[1])  # 'Range'设置
            }
            response = requests.get(self.url, headers=headers) # 请求发送
            with open("files/{}.tmp".format(bytes_range[0]), "wb") as f:
                f.write(response.content)   # 根据id生成临时文件

3.6 创建线程 开启下载

def create_threading(bytes_queue):
    thread_list = []
    for i in range(thread_count):
        thread = DownloadThread(bytes_queue, url)
        thread.start()
        thread_list.append(thread)

    for thread in thread_list:
        thread.join()

3.7 合成文件

def composite_file():
	# 如果文件存在 先移除
    if os.path.isfile(filename): os.remove(filename)
    with open(filename, "ab") as f:   # 以追加模式打开文件
        for i in range(copies_count): # 根据id查找文件
            with open("files/{}.tmp".format(i), "rb") as bytes_f:
                f.write(bytes_f.read())

    for i in os.listdir("files"):  # 清理临时文件
        os.remove("files/{}".format(i))

3.8 入口函数

def main():
    file_length = get_file_size(url)
    copies_queue = get_thread_download(file_length)
    create_threading(copies_queue)
    composite_file()

if __name__ == '__main__':
    main()

3.9 完整代码

from queue import Queue
import requests
import threading
import os

# download settings
url = "http://img1.baidu.com/it/u=2476325767,3197989021&fm=26&fmt=auto"
filename = "img.jpg"
thread_count = 5
copies_count = 20

class DownloadThread(threading.Thread):
    def __init__(self, bytes_queue: Queue, url):
        super().__init__(daemon=True)
        self.bytes_queue = bytes_queue
        self.url = url

    def run(self):
        while not self.bytes_queue.empty():
            bytes_range = self.bytes_queue.get()
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 Edg/92.0.902.84",
                "Range": "bytes={}".format(bytes_range[1])
            }
            response = requests.get(self.url, headers=headers)
            with open("files/{}.tmp".format(bytes_range[0]), "wb") as f:
                f.write(response.content)

def get_file_size(url) -> int:
    response = requests.head(url)
    file_length = int(response.headers['Content-Length'])

    return file_length

def get_thread_download(file_length) -> list:
    bytes = Queue(copies_count)

    start_bytes = -1
    for i in range(copies_count):
        bytes_size = int(file_length/copies_count)*i
        
        if i == copies_count-1: bytes_size = file_length
        bytes_length = "{}-{}".format(start_bytes+1, bytes_size)
        
        bytes.put([i, bytes_length])
        start_bytes = bytes_size

    return bytes

def create_threading(bytes_queue):
    thread_list = []
    for i in range(thread_count):
        thread = DownloadThread(bytes_queue, url)
        thread.start()
        thread_list.append(thread)

    for thread in thread_list:
        thread.join()

def composite_file():
    if os.path.isfile(filename): os.remove(filename)
    with open(filename, "ab") as f:
        for i in range(copies_count):
            with open("files/{}.tmp".format(i), "rb") as bytes_f:
                f.write(bytes_f.read())

    for i in os.listdir("files"):
        os.remove("files/{}".format(i))

def main():
    file_length = get_file_size(url)
    copies_queue = get_thread_download(file_length)
    create_threading(copies_queue)
    composite_file()

if __name__ == '__main__':
    main()

设置好urlfilename,运行代码,便可以开启愉快的下载之路了!
注意:大部分URL(指向文件的URL)都支持HEAD请求,如果HEAD请求失效,程序将无法运行。


好了,今天的课程就到这里,我是wangzirui32,喜欢的可以点个收藏和关注,我们下次再见!

  • 7
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值