断点续传依赖写入成功后立即持久化进度而非异常触发;需在f.write(chunk)且os.fsync(f)或文件关闭后更新.progress文件,校验文件大小优先于进度文件,异常时重试并保持offset不变。

断点续传不是靠捕获异常来“触发续传”,而是靠每次成功写入一块数据后,立刻把当前已下载字节数持久化下来;异常捕获只负责兜底——当网络中断、超时或请求失败时,确保已有进度不丢失、下次能接着用。
关键逻辑:状态保存必须在数据落盘之后
不能在发请求前记进度,也不能在收到响应但还没写入文件时就更新状态。只有调用 f.write(chunk) 成功、且 os.fsync(f) 或文件关闭后,才能认为这段数据真正安全。此时再更新本地记录才可靠。
- 推荐用一个独立的元数据文件(如 file.zip.progress)存当前字节数,纯文本一行即可
- 每次写入 chunk 后,立即重写该文件:with open('file.zip.progress', 'w') as p: p.write(str(current_offset))
- 避免用内存变量(如 offset = 0)全程跟踪,进程崩溃即丢失
异常处理要覆盖三类典型失败
网络请求可能卡在连接、读取、或响应解析阶段。requests 提供统一异常基类,需分别应对:
- requests.exceptions.Timeout:设置 timeout=(3, 30) 后仍超时,记录当前 offset,等待后重试
- requests.exceptions.ConnectionError:DNS失败、拒绝连接等,建议休眠 2–5 秒再试
- 非 206/200 状态码(如 416 Requested Range Not Satisfiable):说明文件已下完,可清理 .progress 文件并退出
循环分块写入时的状态衔接
每次进入下载循环前,先读取 .progress 文件获取起始偏移;若文件不存在或读取出错,默认从 0 开始。核心结构如下:
- 用 os.path.getsize('file.zip') 校验实际文件大小,与 .progress 中记录值比对,不一致则以文件大小为准(更可信)
- 构造 headers:{'Range': f'bytes={start_offset}-'},注意末尾不写结束位置,让服务器自动传到结尾
- 打开文件始终用 'ab' 模式,确保追加;不要依赖 .progress 文件决定写模式
一个最小可用的异常+状态协同片段
以下代码省略重试计数和日志,突出状态保存与异常恢复主线:
def download_with_resume(url, filename):
progress_file = filename + '.progress'
# 读取上次进度,优先信任文件实际大小
if os.path.exists(filename):
start = os.path.getsize(filename)
elif os.path.exists(progress_file):
try:
with open(progress_file) as p:
start = int(p.read().strip())
except (ValueError, OSError):
start = 0
else:
start = 0
<pre class='brush:java;toolbar:false;'>headers = {'Range': f'bytes={start}-'} if start > 0 else {}
while True:
try:
res = requests.get(url, headers=headers, stream=True, timeout=(3, 30))
if res.status_code == 416: # 已完成
if os.path.exists(progress_file):
os.remove(progress_file)
break
if res.status_code not in (200, 206):
raise RuntimeError(f'HTTP {res.status_code}')
with open(filename, 'ab') as f:
for chunk in res.iter_content(8192):
if chunk:
f.write(chunk)
# 实时更新进度(可选:每写 1MB 更新一次,减少IO)
start += len(chunk)
with open(progress_file, 'w') as p:
p.write(str(start))
except (requests.exceptions.RequestException, OSError) as e:
print(f'中断发生:{e},当前已下载 {start} 字节')
time.sleep(2) # 等待后重试
headers = {'Range': f'bytes={start}-'}
continue

















