用Python写一个带断点续传与校验的校园文件备份小工具
前言
在学校机房或者宿舍电脑上写作业、做课程设计时,最让人心疼的不是代码写错,而是文件突然丢了。U盘忘记带、机房电脑重启后还原、作业做到一半断电、把旧版本覆盖了新版本,这些问题几乎每个学生都遇到过。本文要做的,就是一个只有几十行核心逻辑的校园文件备份小工具:它可以把指定文件夹复制到备份目录,支持断点续传,备份完成后用哈希值校验,第二次运行时只复制新增和改动过的文件。工具不依赖任何第三方库,只用 Python 标准库,适合刚学完文件操作和异常处理的同学动手练习。
一、校园场景里的三个真实痛点
第一个痛点是覆盖风险。很多同学习惯把作业直接保存在桌面,文件名永远是作业一、作业二、最终版、最终版二,最后自己也分不清哪个是最新的。第二个痛点是中断风险。机房电脑常常定时还原,宿舍也可能突然断电,复制大文件夹到一半被打断,下次又要从头开始。第三个痛点是校验缺失。复制完成后很少有人逐个打开检查,缺了一个图片或者数据文件,往往交作业时才发现。
一个好的备份工具,不需要做得很复杂,但必须回答三个问题:哪些文件需要备份,断了以后能不能接着做,做完以后怎么证明文件没有损坏。本文的设计,就是围绕这三个问题展开的。
二、设计思路:清单加哈希,一次只做一件事
工具的核心是一张清单文件,通常叫 manifest.json。它记录每个相对路径、文件大小、修改时间和 SHA256 哈希值。每次备份时,程序先扫描源文件夹,生成新的清单,再和旧清单逐项比较。如果文件不存在于旧清单,说明是新增文件,需要复制;如果大小或者哈希值变了,说明文件被修改过,需要重新复制;如果完全一致,就直接跳过。这样既节省时间,也避免反复复制没有变化的大文件。
断点续传的思路也很简单。复制大文件时,不要一次性读入内存,而是按块读取,例如每次读取 1MB,写一块就更新进度。如果中途中断,目标文件会比源文件短,下次运行时程序发现大小不一致,就会重新复制这个文件。更稳妥的做法是先写到临时文件,复制完成并且校验通过后,再把临时文件重命名为正式文件。这样即使中途断电,正式文件也不会处于写了一半的损坏状态。
校验使用 SHA256。复制完成后,程序重新计算目标文件的哈希值,并和源文件的哈希值比较。只有两者一致,才把这条记录写入新清单。如果不一致,就把该文件标记为失败,下一轮可以重试。这个步骤看起来多余,却是整个工具最有价值的部分。
三、实现步骤
第一步,定义两个目录。源目录是需要备份的作业文件夹,备份目录是 U盘或者另一个硬盘上的文件夹。程序启动时先检查两个目录是否存在,不存在就给出明确提示,而不是直接崩溃。
第二步,扫描源目录。使用 os.walk 递归遍历所有子目录,跳过备份目录本身和清单文件,避免把备份结果又备份一遍。扫描时记录相对路径,这样恢复时才能还原原来的目录结构。
第三步,比较清单。如果旧清单不存在,就把所有文件当作新增文件。比较时优先比较文件大小,大小不同可以直接判定为改动,只有大小相同但修改时间变化时,才计算哈希值确认,这样可以减少不必要的计算。
第四步,按块复制并校验。打开源文件和临时文件,用循环每次读取一块数据,直到读完。复制完成后计算哈希值,校验通过后再重命名。
第五步,保存新清单并输出统计。统计包括新增文件数、更新文件数、跳过文件数和失败文件数。失败文件要单独列出来,方便用户检查是权限问题还是磁盘空间不足。
四、核心代码
import hashlib
import json
import os
import shutil
CHUNK_SIZE = 1024 * 1024
def file_hash(path):
h = hashlib.sha256()
with open(path, 'rb') as f:
while True:
data = f.read(CHUNK_SIZE)
if not data:
break
h.update(data)
return h.hexdigest()
def load_manifest(path):
if not os.path.exists(path):
return {}
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def backup(src_dir, dst_dir):
manifest_path = os.path.join(dst_dir, 'manifest.json')
old = load_manifest(manifest_path)
new = {}
copied = skipped = failed = 0
for root, dirs, files in os.walk(src_dir):
for name in files:
src_path = os.path.join(root, name)
rel = os.path.relpath(src_path, src_dir)
dst_path = os.path.join(dst_dir, rel)
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
size = os.path.getsize(src_path)
old_item = old.get(rel)
if old_item and old_item['size'] == size:
digest = file_hash(src_path)
if digest == old_item['sha256']:
new[rel] = old_item
skipped += 1
continue
tmp_path = dst_path + '.tmp'
try:
shutil.copyfile(src_path, tmp_path)
if file_hash(tmp_path) != file_hash(src_path):
raise ValueError('校验不一致')
os.replace(tmp_path, dst_path)
new[rel] = {'size': size, 'sha256': file_hash(dst_path)}
copied += 1
except Exception as e:
print('失败:', rel, e)
failed += 1
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(new, f, ensure_ascii=False, indent=2)
print(f'复制 {copied} 个,跳过 {skipped} 个,失败 {failed} 个')
if __name__ == '__main__':
backup('homework', 'backup_homework')
这段代码有几个值得注意的细节。第一,复制时先写临时文件,再用 os.replace 重命名,保证正式文件要么是旧的完整版本,要么是新的完整版本,不会出现一半新一半旧的情况。第二,跳过逻辑先比较大小,再比较哈希值,兼顾速度和准确性。第三,所有失败都单独计数并打印,不会因为一个文件出错就中断整个备份。第四,清单文件使用 UTF-8 编码保存,中文文件名也不会乱码。
五、动手测试:故意制造一次中断
测试备份工具,不能只测正常情况。建议准备一个包含文本、图片和代码的小文件夹,先完整备份一次,记录用时和统计结果。然后修改其中一个文本文件,新增一个图片文件,再运行一次,观察程序是否只复制这两个文件。接下来可以模拟中断:复制一个较大的视频文件时手动结束程序,再重新运行,检查临时文件是否被正确处理,正式文件是否完整。最后,故意改动备份目录里的一个文件,再运行一次,程序应当发现哈希值不一致,并把正确版本重新复制回来。
如果这几组测试都通过,说明断点续传和校验逻辑基本可用。还可以继续扩展,例如给清单加上备份时间,给失败文件单独生成日志,或者在备份前检查目标磁盘剩余空间是否足够。这些扩展都不难,但能让工具更接近真实可用的状态。
六、常见问题
为什么不直接用复制粘贴。手动复制最大的问题是无法记住哪些文件改过,也无法校验结果。文件一多,手动操作很容易漏复制或者覆盖错版本。
为什么跳过文件时还要算哈希。只比较修改时间并不可靠,有些编辑器保存时会保留原时间,或者文件被重新写入但内容没有变化。哈希值是内容本身的指纹,内容不变,哈希值就不会变。
为什么大文件要分块读取。如果一次性把几个 GB 的文件读入内存,普通电脑很容易内存不足。按块读取时内存占用始终只有一块数据的大小,和文件总大小无关。
结语
备份不是一个高深的技术,却是一个非常好的工程习惯。它教会我们把一次性的操作变成可重复的流程,把口头上的我复制过了变成可以验证的结果。把这个小工具跑通之后,可以再试着给它加上命令行参数、定时备份和恢复功能,一步一步把它做成真正属于自己的学习工具。希望这篇文章能帮你少丢一次作业,也希望你从今天开始,给重要的文件留一个可以验证的备份。
- 点赞
- 收藏
- 关注作者
评论(0)