用Python标准库生成SHA-256文件清单:定位实验目录的新增、删除与修改
为什么实验文件也需要一份清单
做云实验或整理训练资料时,经常会遇到这样的情况:配置改了几处,数据又复制了一次,结果文件名看起来都一样,却已经无法确定前后两次运行用的是不是同一份输入。只看文件大小和修改时间,也难以直接回答这个问题。
本文实现一个小型的只读核对工具:对目录中的普通文件逐个计算 SHA-256,保存“相对路径 → 内容摘要”的 JSON 清单,再比较两份清单,列出新增、删除和内容发生变化的路径。示例只依赖 Python 标准库,本次在本地 Python 3.11 环境中使用自建临时目录验证,没有连接云服务,也没有使用真实业务文件。
先确定比较规则
清单以相对路径为键,这样同一目录复制到另一台机器后,只要内部目录结构一致,仍可以比较。路径分隔符统一输出为斜杠,方便人工阅读。文件按二进制读取,既适用于文本,也适用于图片或压缩包;不会在计算前擅自更改编码、换行或压缩格式。
这里比较的是字节内容。文本从 CRLF 转成 LF,即使显示效果一样,也会产生不同摘要;只改变文件修改时间、内容没有变化,则不应出现在 changed 中。文件重命名暂不做智能识别,会表现为旧路径删除、新路径增加。这些规则需要在实际使用前讲清楚,避免把工具结果误解为业务语义上的变化。
完整实现
将下面代码保存为 file_manifest.py。snapshot 命令读取输入目录并写入清单,diff 命令只读取两份清单并输出结果。为避免把清单本身纳入下一次扫描,程序要求清单保存在输入目录之外。
"""Read-only file inventory; keep the output manifest outside the scanned tree."""
import argparse
import hashlib
import json
from pathlib import Path
def snapshot(root):
root = Path(root).resolve(strict=True)
if not root.is_dir():
raise ValueError("root must be a directory")
result = {}
for path in sorted(root.rglob("*")):
if path.is_symlink() or not path.is_file():
continue
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
result[path.relative_to(root).as_posix()] = digest.hexdigest()
return result
def compare(before, after):
old, new = set(before), set(after)
return {
"added": sorted(new - old),
"removed": sorted(old - new),
"changed": sorted(k for k in old & new if before[k] != after[k]),
}
def main():
parser = argparse.ArgumentParser()
commands = parser.add_subparsers(dest="command", required=True)
save = commands.add_parser("snapshot")
save.add_argument("root")
save.add_argument("output")
diff = commands.add_parser("diff")
diff.add_argument("before")
diff.add_argument("after")
args = parser.parse_args()
if args.command == "snapshot":
root = Path(args.root).resolve(strict=True)
output = Path(args.output).resolve()
if output.is_relative_to(root):
parser.error("save the manifest outside the scanned directory")
data = snapshot(root)
output.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8")
print(f"saved {len(data)} files")
else:
before = json.loads(Path(args.before).read_text(encoding="utf-8"))
after = json.loads(Path(args.after).read_text(encoding="utf-8"))
print(json.dumps(compare(before, after), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
每次读取最多 1 MiB,不会一次把单个大文件全部读入内存。不过清单字典和排序后的路径列表仍会占用内存,所以这仍是适合实验目录的小工具,而不是面向海量对象的流式索引系统。
snapshot 跳过符号链接,只统计普通文件;此处不把链接目标目录当作待扫描实验内容。如果在打开或读取文件时出现权限不足、路径消失等异常,程序会报错,不会捕获错误后继续写出本次新清单。不过,扫描前已消失的路径仍可能不被遍历到,目录遍历也不能保证在并发变更下完整一致,因此应针对稳定副本操作。已有的历史清单应另外保留。示例未实现清单输出的原子替换,重要基线请使用新文件名,不要覆盖唯一副本。
如何使用
假设实验输入目录是 data,程序文件和清单都位于它的上一级目录。在实验前后分别运行:
python file_manifest.py snapshot data before.json
# 在 data 中完成文件调整,然后再次生成清单
python file_manifest.py snapshot data after.json
python file_manifest.py diff before.json after.json
只扫描准备核对的资料目录,避免把正在增长的日志、下载缓存、虚拟环境和敏感配置混在同一棵目录里。程序不删除、不改写输入文件,也不会把摘要或文件内容上传到外部服务。注意清单会保留相对路径:如果路径名称本身包含个人信息,分享清单前仍需要处理。
自建样例的验证结果
本次验证先创建 keep.txt、change.txt 和 remove.txt 三个文件,保存基线。随后修改 change.txt 的内容,移除临时样例 remove.txt,在 nested 子目录中新建一个中文文件名的文本,同时仅修改 keep.txt 的时间戳。核对结果如下:
{
"added": ["nested/新增.txt"],
"removed": ["remove.txt"],
"changed": ["change.txt"]
}
断言同时确认:同一份清单与自身比较时,三个列表都为空;只改时间戳的 keep.txt 没有被判为内容变化。整个演示使用临时目录,未对实际实验资料做删除操作。测试覆盖的是新增、删除、修改、内容不变、仅时间戳变化以及嵌套中文路径,没有据此声称覆盖所有文件系统异常。
用在实验流程里的位置
比较适合在上传实验输入之前生成一次基线,下载或整理完成后再生成一次快照。若出现 changed,先根据路径检查是否确实计划修改;若出现 removed,确认是否漏拷贝;若新增的是临时输出,可决定下次是否将输入与输出目录分开管理。定位变化之后,再运行格式校验、单元测试或实际实验,摘要比较不能替代这些检查。
还要留意两个边界。第一,普通目录扫描不是一致性快照:扫描期间仍在写入的文件可能被读到中间状态,建议先暂停写入或针对已冻结的副本核对。第二,SHA-256 清单本身没有身份认证能力;如果文件和基线清单都能被同时替换,这个脚本不能证明发布者身份或抵抗恶意篡改。它主要帮助核对受控实验资料的字节变化,也不判断数据标注是否正确、模型效果是否提升。
本文示例代码与文字由 AI 辅助编写,示例已在本地自建数据上运行验证。文中没有包含账号、密钥或真实训练内容。
- 点赞
- 收藏
- 关注作者
评论(0)