news 2026/8/14 11:09:49

HTTP断点续传原理与Python实现:从单线程到多线程下载器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HTTP断点续传原理与Python实现:从单线程到多线程下载器

1. 项目概述:为什么我们需要断点续传工具?

如果你曾经在下载一个几GB的大文件时,网络突然中断,或者电脑意外重启,然后不得不从头开始下载,那种感觉一定糟透了。我经历过太多次,尤其是在处理虚拟机镜像、高清视频素材或者大型软件安装包的时候。这种场景下,一个可靠的“断点续传”功能,就不再是锦上添花,而是雪中送炭的必需品了。

简单来说,断点续传工具的核心价值,就是让文件传输过程具备“记忆”和“恢复”能力。它把一个大文件在逻辑上切成许多小块,并精确记录下每一块的传输状态。当传输因任何原因中断后,再次启动时,工具会先检查本地已接收的部分,然后只从断掉的地方继续下载或上传剩余的部分,而不是傻乎乎地重头再来。这不仅能节省大量时间和带宽,更重要的是,它极大地提升了传输任务的可靠性和用户体验,尤其是在网络不稳定或需要长时间运行任务的场景下。

从个人用户备份照片视频,到开发者同步代码仓库,再到运维人员部署大型应用,断点续传都是一个底层但至关重要的能力。市面上虽然有很多集成此功能的下载器(如浏览器、专业下载软件),但一个独立、轻量、可编程的断点续传工具,能让我们更深入地理解其原理,并将其灵活嵌入到自己的自动化脚本或定制化应用中。接下来,我们就从设计思路开始,拆解如何打造这样一个工具。

2. 核心设计思路与协议选择

要自己实现一个断点续传工具,首先得想清楚它的工作模式和技术选型。这决定了工具的灵活性、兼容性和最终实现的复杂度。

2.1 客户端与服务器模型

最常见的模型是客户端/服务器(C/S)模型。我们的工具通常作为客户端,需要从一个支持断点续传的服务器(例如标准的HTTP/1.1服务器、FTP服务器或自定义的TCP服务器)获取文件。因此,工具的实现分为两个层面:

  1. 协议支持层:工具需要遵循特定网络协议中关于断点续传的规范。
  2. 本地管理层:工具需要在本地管理文件分块、记录下载状态、处理文件IO。

对于大多数应用场景,HTTP协议是首选。因为它是互联网上最通用的协议,几乎所有的Web服务器都支持HTTP/1.1,而HTTP/1.1标准明确规定了用于断点续传的头部字段,实现起来有规可循,兼容性最好。

2.2 HTTP断点续传原理剖析

HTTP协议实现断点续传,主要依赖两个请求头和一个响应头:

  • Range(请求头):由客户端发送,告知服务器需要文件的哪个字节范围。
    • 格式:Range: bytes=start-end
    • 例如:Range: bytes=1024-2047表示请求从第1024字节到第2047字节(共1024字节)的数据。Range: bytes=1024-表示请求从第1024字节直到文件末尾的所有数据。
  • Content-Range(响应头):由支持范围请求的服务器返回,告知客户端当前返回的数据在完整文件中的位置。
    • 格式:Content-Range: bytes start-end/total
    • 例如:对于上面的请求,服务器可能返回:Content-Range: bytes 1024-2047/10240,表示这是总共10240字节的文件中的1024-2047字节部分。
  • Accept-Ranges(响应头):服务器在响应初始请求(非Range请求)时,通过此头部声明自己是否支持范围请求。通常是Accept-Ranges: bytes

工作流程

  1. 工具首先发送一个普通的HEADGET请求(不携带Range头),获取文件的基本信息,如文件总大小(Content-Length)和是否支持断点续传(Accept-Ranges: bytes)。
  2. 检查本地是否存在部分下载的临时文件或状态记录文件。如果存在,读取已下载的字节数,假设为downloaded_size
  3. 构造并发送携带Range: bytes=downloaded_size-GET请求。
  4. 服务器返回206 Partial Content状态码(而非完整的200 OK),并在响应体中包含从downloaded_size开始的文件数据,同时在Content-Range头中指明范围。
  5. 客户端将接收到的数据追加写入到本地临时文件的末尾。
  6. 重复步骤3-5,直到Content-Range指示已传输完毕(例如bytes x-y/total中的y等于total-1)。

注意:并非所有服务器都完美支持Range请求。有些可能忽略Range头直接返回整个文件(状态码200),有些可能返回200 OK但内容却是部分数据(不符合规范)。因此,健壮的工具必须能处理这些异常情况,例如通过比较已接收数据量和Content-Range声明的大小进行校验。

2.3 多线程并发下载的考量

单纯的单线程断点续传可以解决“中断恢复”的问题,但为了最大化利用带宽(尤其是在高延迟网络或服务器限速的情况下),我们通常会引入多线程(或多连接)并发下载

其基本思路是:

  1. 获取文件总大小(total_size)。
  2. 根据设定的线程数(如4个),将文件平均分成相应的段(Segment)。
    • 例如:文件大小 10MB,线程数4,则每个线程负责下载 2.5MB。
    • 线程1:Range: bytes=0-2621439
    • 线程2:Range: bytes=2621440-5242879
    • 以此类推。
  3. 每个线程独立发起自己的Range请求,下载指定的字节段,并写入到本地文件的指定位置(需要使用支持随机写入的文件IO操作)。
  4. 所有线程下载完成后,合并成一个完整的文件。

这种方式能显著提升下载速度,但复杂度也更高:

  • 状态管理更复杂:需要记录每个分块的下载状态(未开始、下载中、已完成、错误)。
  • 文件IO需要随机写入:每个线程需要将数据写入文件的不同偏移量处。
  • 错误处理与恢复:单个线程失败,只需重试该线程负责的区块,不影响其他线程。
  • 最终合并:所有分块下载完成后,需要确保文件拼接正确。

在我们的工具实现中,我会先实现一个稳固的单线程断点续传基础框架,然后再在此基础上扩展多线程功能,这样更容易理解和调试。

3. 工具核心模块设计与实现

我们将使用Python来构建这个工具,因为它语法简洁,网络库强大,非常适合做原型和实际工具。核心模块主要分为:网络请求模块、状态管理模块、文件IO模块和主控逻辑模块。

3.1 网络请求模块:使用requests库处理Range请求

Python的requests库对HTTP协议支持非常友好,可以很容易地设置请求头。我们将用它来发送携带Range头的请求,并处理响应。

首先,我们需要一个函数来获取文件信息:

import requests import os def get_file_info(url): """ 获取远程文件信息(大小、是否支持断点续传)。 返回: (support_breakpoint, file_size) 或 (False, None) """ try: # 使用HEAD方法,只获取头部信息,不下载主体 resp = requests.head(url, timeout=10, allow_redirects=True) resp.raise_for_status() # 检查HTTP错误 # 检查是否支持字节范围请求 accept_ranges = resp.headers.get('Accept-Ranges', 'none') support_breakpoint = (accept_ranges.lower() == 'bytes') # 获取文件总大小 content_length = resp.headers.get('Content-Length') file_size = int(content_length) if content_length else None return support_breakpoint, file_size, resp.url # 返回最终URL(处理重定向后) except requests.exceptions.RequestException as e: print(f"获取文件信息失败: {e}") return False, None, url

关键点:

  1. 使用HEAD方法而非GET,避免在获取元信息时下载整个文件体。
  2. allow_redirects=True自动处理重定向,并返回最终的URL。
  3. Accept-Ranges头判断服务器支持情况。
  4. Content-Length头获取文件大小,这是后续分块的基础。

接下来,是支持断点续传的下载函数核心部分:

def download_range(url, start_byte, end_byte, local_file_path): """ 下载文件的指定字节范围,并写入本地文件的指定位置。 """ headers = {'Range': f'bytes={start_byte}-{end_byte}'} try: resp = requests.get(url, headers=headers, stream=True, timeout=30) resp.raise_for_status() # 检查响应状态码,206表示部分内容,200可能表示服务器不支持Range if resp.status_code == 206: # 打开文件,定位到start_byte位置进行写入 with open(local_file_path, 'r+b') as f: f.seek(start_byte) for chunk in resp.iter_content(chunk_size=8192): # 分块写入,避免内存占用过高 if chunk: f.write(chunk) f.flush() # 及时刷入磁盘,状态更可靠 return True elif resp.status_code == 200: print("警告:服务器可能不支持断点续传,收到了完整文件响应。") # 这种情况下,需要根据策略决定是覆盖写入还是放弃 # 简单处理:直接覆盖从头写入(这会导致断点续传失效) with open(local_file_path, 'wb') as f: for chunk in resp.iter_content(chunk_size=8192): if chunk: f.write(chunk) return True else: print(f"意外的HTTP状态码: {resp.status_code}") return False except requests.exceptions.RequestException as e: print(f"下载范围 {start_byte}-{end_byte} 失败: {e}") return False

关键点:

  1. headers={'Range': ...}是发起范围请求的关键。
  2. stream=True至关重要,它让requests以流的方式获取响应体,我们可以用iter_content方法分块读取,避免大文件一次性加载到内存。
  3. status_code的判断是健壮性的体现。正确处理206200
  4. 使用'r+b'模式打开文件,seek到指定位置进行写入,这是实现“续传”和“多线程分块写入”的基石。
  5. f.flush()建议在每次写入后调用,确保数据写入磁盘,这样即使程序意外退出,已下载的数据也是持久化的。

3.2 状态管理模块:如何记录下载进度

要实现断点续传,工具必须“记住”已经下载了哪些部分。我们有两种主流方式:

方式一:临时文件法这是最直观的方法。下载时,目标文件(例如video.mp4)可能被命名为video.mp4.partvideo.mp4.download。同时,创建一个同名的状态文件,如video.mp4.part.status.json,里面用JSON格式记录:

{ "url": "http://example.com/largefile.zip", "total_size": 104857600, "downloaded_size": 52428800, "chunks": [ {"start": 0, "end": 26214399, "finished": true}, {"start": 26214400, "end": 52428799, "finished": true}, {"start": 52428800, "end": 78643199, "finished": false}, ... ] }

每次写入数据后,都更新这个状态文件。恢复时,读取状态文件就知道该从哪里开始了。

方式二:文件本身作为状态记录(推荐)更简单巧妙的方法是:直接利用下载中的文件本身。我们总是向最终目标文件名(如video.mp4)写入数据。开始时,如果文件不存在,就创建它,并将其大小“扩展”到与远程文件一样大(例如,在Windows下快速创建一个全零文件,在Linux下使用truncatefallocate)。然后,每个下载线程只负责填充文件中属于自己的那部分区间。

恢复时,我们只需要:

  1. 检查目标文件是否存在。
  2. 如果存在,获取它的当前大小(os.path.getsize),这个大小就是已下载的字节数。
  3. 从这个大小处开始继续请求(Range: bytes=current_size-)。

这种方法零额外状态文件,完全依赖文件系统的可靠性,更加简洁。但它更适合单线程顺序写入的断点续传。对于多线程分块下载,因为文件是预先分配好的,每个线程写入固定位置,恢复时需要知道每个块是否完成,此时可能仍需一个轻量级的状态文件来记录各分块完成情况,或者通过读取文件各区块的数据是否完整(例如校验和)来判断,后者实现较复杂。

在我们的单线程示例中,采用第二种方法最为简单。我们通过检查本地文件大小来决定续传的起始点。

3.3 文件IO与完整性校验

文件操作是数据落地的最后一步,必须保证正确性和效率。

  • 追加写入与定位:如上述代码所示,使用open(file_path, 'r+b')模式,seek到文件末尾,然后进行写入。
  • 分块写入:使用resp.iter_content(chunk_size=8192)循环读取和写入。chunk_size可以根据实际情况调整(如 64KB),平衡内存使用和IO效率。
  • 完整性校验(可选但重要):下载完成后,为了确保文件在网络传输中没有发生错误,应该进行校验。常见做法是,服务器在提供文件时也提供其哈希值(如MD5、SHA1、SHA256),放在响应头(如ETag,但ETag不一定基于内容)或单独的校验文件中。下载完成后,工具计算本地文件的哈希值并与服务器提供的进行比对。
    import hashlib def calculate_file_hash(file_path, algorithm='sha256'): hash_func = hashlib.new(algorithm) with open(file_path, 'rb') as f: for chunk in iter(lambda: f.read(4096), b""): hash_func.update(chunk) return hash_func.hexdigest()
    如果校验失败,说明文件损坏,需要删除重新下载或重新下载出错的部分。

4. 单线程断点续传工具完整实现与演示

结合以上模块,我们可以组装一个完整的、命令行的单线程断点续传下载工具。

#!/usr/bin/env python3 """ 简易单线程HTTP断点续传下载器 """ import requests import os import sys import time def download_file_with_resume(url, local_filename=None): """ 支持断点续传的下载函数(单线程) """ if not local_filename: local_filename = url.split('/')[-1] or 'downloaded_file' # 1. 获取文件信息 print(f"正在检查文件信息...") support_breakpoint, total_size, final_url = get_file_info(url) if total_size is None: print("无法获取文件大小,退出。") return False print(f"文件总大小: {total_size / (1024*1024):.2f} MB") if not support_breakpoint: print("警告:服务器可能不支持断点续传,将尝试完整下载。") # 2. 处理本地文件,确定起始点 start_byte = 0 if os.path.exists(local_filename): start_byte = os.path.getsize(local_filename) print(f"发现已存在的文件 '{local_filename}',大小: {start_byte} 字节") if start_byte >= total_size: print("文件已存在且大小不小于远程文件,跳过下载。") return True if start_byte > 0: print(f"将从字节 {start_byte} 处继续下载。") else: print(f"将开始下载新文件到 '{local_filename}'") # 3. 设置请求头,请求剩余部分 headers = {} if start_byte > 0: headers['Range'] = f'bytes={start_byte}-' # 4. 发起请求并流式下载 try: with requests.get(final_url, headers=headers, stream=True, timeout=30) as r: r.raise_for_status() # 检查响应状态 if start_byte > 0 and r.status_code != 206: print(f"警告:请求续传但服务器返回状态码 {r.status_code},可能不支持断点续传,将重新下载。") # 可以选择删除已存在部分,这里我们选择覆盖 start_byte = 0 mode = 'wb' else: mode = 'ab' if start_byte > 0 else 'wb' # 获取本次实际要下载的内容长度 content_length = r.headers.get('Content-Length') current_total = int(content_length) if content_length else 0 if start_byte > 0 and r.status_code == 206: # 对于206响应,Content-Length是本次返回的部分的大小 remaining_size = current_total else: # 对于200响应或从头下载,Content-Length是总大小(如果已知) remaining_size = total_size - start_byte if total_size else None print(f"开始下载剩余数据...") downloaded = start_byte last_print_time = time.time() with open(local_filename, mode) as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) # 简单进度显示 current_time = time.time() if current_time - last_print_time > 1.0: # 每秒更新一次 if remaining_size: percent = (downloaded / total_size) * 100 print(f"\r进度: {downloaded}/{total_size} bytes ({percent:.1f}%)", end='', flush=True) else: print(f"\r已下载: {downloaded} bytes", end='', flush=True) last_print_time = current_time print() # 换行 print(f"下载完成!文件保存为: {local_filename}") return True except requests.exceptions.RequestException as e: print(f"\n下载过程中发生错误: {e}") return False except KeyboardInterrupt: print(f"\n\n下载被用户中断。文件 '{local_filename}' 已保存了 {os.path.getsize(local_filename)} 字节。") print("下次运行将自动从断点处继续。") sys.exit(0) if __name__ == '__main__': if len(sys.argv) < 2: print("用法: python resume_downloader.py <文件URL> [本地文件名]") sys.exit(1) url = sys.argv[1] local_name = sys.argv[2] if len(sys.argv) > 2 else None success = download_file_with_resume(url, local_name) sys.exit(0 if success else 1)

使用演示

  1. 将代码保存为resume_downloader.py
  2. 在命令行中运行:python resume_downloader.py https://example.com/path/to/largefile.zip
  3. 下载过程中,可以按Ctrl+C中断。
  4. 再次运行相同的命令,工具会自动检测到已存在的.zip文件,并从断点处继续下载。

这个工具已经具备了核心的断点续传能力。它结构清晰,包含了错误处理、进度显示和用户中断处理,是一个可用的基础版本。

5. 进阶:多线程并发下载的实现与优化

单线程工具在稳定性上表现很好,但速度可能受限于单TCP连接的带宽。接下来,我们将其升级为多线程并发下载工具。

5.1 分块策略与线程池

我们使用Python的concurrent.futures模块中的ThreadPoolExecutor来管理下载线程。

import concurrent.futures import threading class MultiThreadedDownloader: def __init__(self, url, local_path, num_threads=4): self.url = url self.local_path = local_path self.num_threads = num_threads self.total_size = 0 self.chunk_size = 0 self.lock = threading.Lock() # 用于线程安全地更新进度 self.downloaded_chunks = 0 def prepare(self): """准备工作:获取文件大小,创建空文件,计算分块""" support, self.total_size, _ = get_file_info(self.url) if not support or self.total_size is None: raise Exception("无法获取文件信息或不支持断点续传") # 计算每个线程负责的字节范围 self.chunk_size = self.total_size // self.num_threads self.ranges = [] for i in range(self.num_threads): start = i * self.chunk_size # 最后一个线程获取所有剩余字节 end = (self.total_size - 1) if (i == self.num_threads - 1) else (start + self.chunk_size - 1) self.ranges.append((start, end)) # 预创建(或清空)目标文件 with open(self.local_path, 'wb') as f: f.truncate(self.total_size) # 关键:预分配磁盘空间 print(f"文件已预分配,总大小: {self.total_size} bytes, 线程数: {self.num_threads}") def download_chunk(self, thread_id, start, end): """单个线程下载指定范围的数据""" chunk_filename = f"{self.local_path}.part{thread_id}" # 临时分块文件 headers = {'Range': f'bytes={start}-{end}'} try: with requests.get(self.url, headers=headers, stream=True, timeout=60) as r: r.raise_for_status() if r.status_code != 206: print(f"线程{thread_id}: 服务器未返回206,可能不支持分块下载。") return False downloaded = 0 with open(chunk_filename, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) # 下载完成后,将分块文件内容写入最终文件的正确位置 with open(self.local_path, 'r+b') as final_f: final_f.seek(start) with open(chunk_filename, 'rb') as chunk_f: final_f.write(chunk_f.read()) # 删除临时分块文件 os.remove(chunk_filename) with self.lock: self.downloaded_chunks += 1 print(f"线程{thread_id} 完成 ({start}-{end})。 总体进度: {self.downloaded_chunks}/{self.num_threads}") return True except Exception as e: print(f"线程{thread_id} 下载失败: {e}") return False def run(self): """启动多线程下载""" self.prepare() print("开始多线程下载...") with concurrent.futures.ThreadPoolExecutor(max_workers=self.num_threads) as executor: # 提交所有任务 future_to_chunk = { executor.submit(self.download_chunk, i, start, end): i for i, (start, end) in enumerate(self.ranges) } # 等待所有任务完成,并处理结果 results = [] for future in concurrent.futures.as_completed(future_to_chunk): chunk_id = future_to_chunk[future] try: result = future.result() results.append(result) except Exception as e: print(f"线程{chunk_id} 产生异常: {e}") results.append(False) if all(results): print(f"\n恭喜!文件 '{self.local_path}' 下载完成。") return True else: print(f"\n下载过程中部分线程失败。") # 这里可以实现更复杂的重试逻辑 return False

关键改进与解释

  1. 预分配文件f.truncate(self.total_size)在开始下载前就创建了一个大小与远程文件一致的空文件(或稀疏文件)。这有两个好处:一是提前检查磁盘空间是否足够,二是为多线程并行写入固定位置做好了准备。
  2. 分块临时文件:每个线程先将自己的数据块下载到一个独立的临时文件(如.part0),完成后再一次性写入最终文件的指定位置。这比多个线程同时seek和写入同一个文件更安全,避免了复杂的文件锁和写入位置冲突。
  3. 线程安全进度更新:使用threading.Lock来保护self.downloaded_chunks这个共享变量,确保进度打印不会错乱。
  4. 错误隔离:一个线程的失败不会直接影响其他线程,所有任务提交后由ThreadPoolExecutor统一管理。最后检查所有线程的结果,判断整体成功与否。

5.2 更健壮的状态管理与恢复

上述多线程版本在任务中断后,重新运行会从头开始,因为它没有记录每个分块的状态。为了支持多线程的断点续传,我们需要一个状态文件来记录每个范围(chunk)的完成情况。

我们可以设计一个状态文件(如.status.json):

{ "url": "http://...", "total_size": 104857600, "num_chunks": 4, "chunks": [ {"id": 0, "start": 0, "end": 26214399, "finished": true, "temp_file": "largefile.zip.part0"}, {"id": 1, "start": 26214400, "end": 52428799, "finished": false, "temp_file": "largefile.zip.part1"}, ... ] }

prepare()阶段,先检查状态文件是否存在。如果存在,就加载状态,只对那些"finished": false的分块重新创建下载任务。每个分块下载完成后,立即更新状态文件并将finished改为true。这样,即使程序中途崩溃,重启后也能精确恢复。

5.3 性能调优与注意事项

  • 线程数设置:不是线程越多越快。受限于本地CPU、磁盘IO和服务器并发连接限制,通常4-8个线程是甜点区间。可以做成可配置参数。
  • 分块大小:避免分块过小(导致大量HTTP请求开销)或过大(失去并发意义)。通常1MB到10MB是一个合理的范围。可以根据文件总大小动态计算。
  • 磁盘IO瓶颈:多个线程同时写入多个临时文件,最后再合并,可能会造成磁盘IO竞争。如果下载速度远超磁盘写入速度,多线程带来的提升会有限,甚至可能变慢。使用SSD会好很多。
  • 服务器限制:有些服务器会对同一IP的并发连接数或请求频率进行限制。过多的线程可能导致IP被暂时封禁。增加重试机制和指数退避策略是必要的。
  • 内存使用:使用stream=True和分块读取写入,确保即使下载超大文件,内存占用也保持稳定。

6. 常见问题排查与实战技巧

在实际使用和开发断点续传工具时,你会遇到各种各样的问题。下面是我总结的一些典型场景和解决思路。

6.1 服务器返回状态码200而不是206

现象:你发送了带Range头的请求,但服务器返回了200 OK和整个文件。原因

  1. 服务器根本不支持Range请求(Accept-Ranges头为none或不存在)。
  2. 服务器支持,但你请求的范围无效(例如,起始位置大于文件大小)。
  3. 某些服务器或CDN配置问题,对某些文件类型或路径不支持断点续传。应对
  • 首先检查HEAD请求返回的Accept-Ranges头。
  • 如果服务器明确不支持,工具应降级为普通单线程下载,并给出明确提示。
  • 在代码中做好兼容,当收到200响应时,根据策略决定是覆盖还是放弃。

6.2 下载的文件大小不正确或损坏

现象:下载完成,但文件无法打开,或对比哈希值不一致。原因

  1. 网络传输错误:TCP协议能保证数据顺序和可靠性,但在应用层,如果HTTP连接异常中断,最后收到的数据包可能不完整。
  2. 服务器动态内容:你下载的是一个动态生成的页面或文件,其Content-Length在两次请求间可能发生变化。
  3. 多线程写入冲突或错误:多线程工具中,如果文件写入逻辑有bug,可能导致数据覆盖或错位。排查与解决
  4. 强制校验:如果服务器提供了Content-MD5ETag(强校验类型),下载完成后务必进行校验。没有官方校验值时,可以尝试从其他可信源获取哈希值进行比对。
  5. 日志与重试:为工具增加详细日志,记录每个分块的下载开始、结束和大小。对于校验失败的分块,自动进行重试(例如最多3次)。
  6. 使用更可靠的协议:对于关键文件,考虑使用支持完整性校验的协议,如rsyncBitTorrent

6.3 连接超时与不稳定网络处理

现象:下载经常中断,超时错误频发。技巧

  1. 设置合理的超时requests.get(timeout=(连接超时, 读取超时))。例如timeout=(10, 30)表示10秒连接超时,30秒读取超时。
  2. 自动重试机制:使用urllib3Retry或第三方库如tenacity,为请求配置重试策略(例如,对连接错误、超时、5xx状态码进行最多3次重试,并加入随机间隔避免拥塞)。
    from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry retry_strategy = Retry( total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504] ) adapter = HTTPAdapter(max_retries=retry_strategy) session = requests.Session() session.mount("http://", adapter) session.mount("https://", adapter) # 然后用session代替requests进行请求
  3. 分块大小自适应:在网络极差的环境下,可以减小分块大小,这样每次重试的代价更小。

6.4 实战技巧:提升工具的用户体验

  1. 友好的进度显示:除了百分比,可以显示下载速度(MB/s)、剩余时间。计算速度时,建议用平滑算法(如移动平均)避免数字跳动过快。
  2. 支持暂停/继续:捕捉Ctrl+C信号,优雅地保存状态后退出。这就是我们之前代码中except KeyboardInterrupt做的事情。
  3. 配置文件与命令行参数:使用argparse库增强命令行工具,允许用户指定线程数、输出目录、代理等。
  4. 代理支持:在requests.get()中传入proxies参数,方便内网用户或需要代理的场景。
  5. 递归下载目录(高级):如果服务器支持目录列表(如Apache的Indexes选项),可以扩展工具,使其能解析HTML页面,递归下载整个目录下的所有文件,并为每个文件应用断点续传。

开发一个断点续传工具,从简单的单线程版本到功能齐全的多线程版本,是一个逐步深入理解HTTP协议、网络编程、文件系统和并发处理的过程。它虽然不复杂,但涉及到的细节很多,每一个细节都影响着工具的稳定性和用户体验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/14 11:06:58

Codex官方9大进阶技巧:从环境配置到Agent构建的实战指南

这次我们来看一个来自 Codex 官方团队的进阶使用技巧合集。对于开发者而言&#xff0c;掌握一个工具的基础操作只是第一步&#xff0c;真正提升效率、解锁高级功能的关键在于那些“进阶技巧”。Codex 官方团队的开发工程师 Jason Liu 亲自编写并分享了 9 个核心进阶技巧&#x…

作者头像 李华
网站建设 2026/8/14 11:04:29

手机号查QQ号有多快?一个开源小工具 30 秒帮你找回账号

手机号查QQ号有多快&#xff1f;一个开源小工具 30 秒帮你找回账号 【免费下载链接】phone2qq 项目地址: https://gitcode.com/gh_mirrors/ph/phone2qq 如果你曾经把 QQ 账号记在小本本上&#xff0c;又恰好在最需要它的时候找不到那个本子&#xff0c;那么 phone2qq 这…

作者头像 李华
网站建设 2026/8/14 11:03:19

B站视频下载工具实用指南:downkyi功能全解读与避坑手册

B站视频下载工具实用指南&#xff1a;downkyi功能全解读与避坑手册 【免费下载链接】downkyi 哔哩下载姬downkyi&#xff0c;哔哩哔哩网站视频下载工具&#xff0c;支持批量下载&#xff0c;支持8K、HDR、杜比视界&#xff0c;提供工具箱&#xff08;音视频提取、去水印等&…

作者头像 李华