记录维维的每一篇Hello World

previewFix.jpg

需要用到的库

  1. import json
  2. import tqdm
  3. import typing
  4. import os
  5. import requests
  6. import retry
  7. import signal
  8. import multitasking

完整源码

  1. # import time
  2. import json
  3. from tqdm import tqdm
  4. from typing import List, Tuple
  5. import os
  6. import requests
  7. from retry import retry
  8. import signal
  9. import multitasking
  10. signal.signal(signal.SIGINT, multitasking.killall)
  11. class Download:
  12. def __init__(self):
  13. """
  14. 多线程下载文件
  15. """
  16. self.__file_size = None # 文件大小/单位b
  17. self.__file_url = None # 文件路径
  18. self.__MB = pow(1024, 2) # 1MB == 1024^2B
  19. self.savePath = "D:\\AppData\\Download" # 保存路径
  20. self.__headers = {
  21. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
  22. 'Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.46'
  23. }
  24. self.__split_max = 20 # 分块数
  25. self.__file_min_mb = 10 # 每块最小内存
  26. self.__retry_tries = 3 # 每次连接失败重试次数
  27. @property
  28. def split_max(self):
  29. """
  30. 最大线程数
  31. :return:
  32. """
  33. return self.__split_max
  34. @split_max.setter
  35. def split_max(self, value):
  36. """
  37. 设置最大线程数
  38. :param value:
  39. :return:
  40. """
  41. if isinstance(value, int) or value.isdigit():
  42. self.__split_max = int(value)
  43. else:
  44. raise ValueError("split_max value not is int")
  45. def download(self, file_url: str, file_name: str):
  46. """
  47. 下载文件
  48. :param file_url: 文件链接
  49. :param file_name: 文件保存名
  50. :return:
  51. """
  52. # 如果保存目录不存在,创建
  53. if not os.path.exists(self.savePath):
  54. os.makedirs(self.savePath)
  55. self.__file_url = file_url
  56. # 获取文件大小/单位b
  57. self.__file_size = self.__get_file_size()
  58. self.__download(file_name=file_name)
  59. def __download(self, file_name):
  60. @retry(tries=self.__retry_tries)
  61. @multitasking.task
  62. def split_download(start, ending):
  63. """
  64. 分块下载
  65. :param start: 开头
  66. :param ending: 结尾
  67. :return:
  68. """
  69. # 拷贝,避开线程之间的影响
  70. part_headers = self.__headers.copy()
  71. # 分段下载的核心/设置下载的文件部分
  72. part_headers['Range'] = 'bytes={start}-{ending}'.format(start=start, ending=ending)
  73. # 发起请求并获取响应(流式)
  74. response = session.get(self.__file_url, headers=part_headers, stream=True)
  75. # 每次读取的流式响应大小/128b
  76. data_size = 128
  77. # 暂存已获取的数据
  78. datas = []
  79. for data in response.iter_content(chunk_size=data_size):
  80. # 暂存获取的响应
  81. datas.append(data)
  82. # 更新进度条
  83. download_tqdm.update(data_size)
  84. file.seek(start)
  85. for data in datas:
  86. file.write(data)
  87. # 释放已写入的资源
  88. del datas
  89. # session保持浏览器状态
  90. session = requests.Session()
  91. # 打开文件
  92. file = open(os.path.join(self.savePath, file_name), "wb")
  93. # 创建进度条
  94. download_tqdm = tqdm(total=self.__file_size, desc=f'下载文件[{file_name}]')
  95. # 开始分线下载
  96. for part in self.__split_list():
  97. split_download(part[0], part[1])
  98. # 等待全部线程结束
  99. multitasking.wait_for_tasks()
  100. file.close()
  101. download_tqdm.close()
  102. def __split_list(self) -> List[Tuple[int, int]]:
  103. """
  104. 获取分块列表
  105. :return:
  106. """
  107. # 获取文件大小/单位mb
  108. file_size = self.__file_size / self.__MB
  109. # 判断文件大小是否小于最小分块内存
  110. if file_size < self.__file_min_mb:
  111. return [(0, self.__file_size)]
  112. # 获取实际线程数
  113. split_sum = round(file_size / self.__file_min_mb)
  114. split_sum = self.__split_max if split_sum > self.__split_max else split_sum
  115. # 获取每块下载的内存大小
  116. split_size = self.__file_size // split_sum
  117. # print("file_size:", self.__file_size)
  118. # print("file_MB:", file_size)
  119. # print("split_sum:", split_sum)
  120. # print("split_size:", split_size)
  121. # print("split_size_mb:", split_size / self.__MB)
  122. # 分块列表
  123. split_list = []
  124. # 获取分块列表
  125. for start in range(0, self.__file_size, split_size):
  126. if start + 2 * split_size > self.__file_size:
  127. ending = self.__file_size
  128. split_list.append((start, ending))
  129. break
  130. else:
  131. ending = start + split_size
  132. split_list.append((start, ending))
  133. return split_list
  134. def __get_file_size(self):
  135. """
  136. 获取文件大小/单位b
  137. :return:
  138. """
  139. response = requests.head(self.__file_url)
  140. file_size = response.headers.get('Content-Length')
  141. # print(json.dumps(dict(response.headers), indent=4, ensure_ascii=False))
  142. if file_size is None:
  143. raise ValueError('该文件不支持多线程分段下载!')
  144. return eval(file_size)

示例代码

  1. if __name__ == '__main__':
  2. # Anaconda3
  3. # 文件下载路径
  4. url = "https://repo.anaconda.com/archive/Anaconda3-2021.11-Windows-x86_64.exe"
  5. # 文件保存名字
  6. file_name = "Anaconda3-2021.11-Windows-x86_64.exe"
  7. # 实例化
  8. d = Download()
  9. # 设置最大线程数
  10. # d.split_max = 36
  11. d.download(file_url=url, file_name=file_name)

注意事项

1、使用时更改文件下载路径和文件保存的名字

2、文件下载后默认保存在”D:\AppData\Download”下(可以自行更改)

3、下载速度和网速、线程数相关,小于10M的文件单线程下载(可以自行更改)

4、当网速不佳时,开启过多的线程反而会比正常下载更慢

python环境

电脑:Windows10

python:3.10.4