用 Python 和 SHA-256 清单检查实验数据是否改变
输入文件变了,固定随机种子也无法复现原来的实验。标签被修正、划分表被重新生成,或者 FASTA 文件被替换后,文件名可能没变,实验输入却已经不同。
这篇教程用一个小型 Python 命令记录目录中的文件路径和 SHA-256 摘要,并在下次运行前核对。它可以接在 FASTA 检查和按组划分数据集之后:先检查数据,再固定划分,最后记录实际使用的文件。
1. 确定清单记录哪些文件
把一次实验需要的输入放进独立目录。模型输出、缓存、日志和清单本身都放在目录之外。
experiment/
inputs/
proteins.fasta
labels.csv
split.csv
inputs-v1.json
dataset_manifest.py
清单把每个文件的相对路径映射到摘要。整体移动输入目录不会改变这份映射。重命名文件则会被记录为旧路径缺失、新路径新增。
这里检查的是文件字节。重新折行 FASTA 序列或转换换行符,都会改变文件摘要,即使解析后的序列相同。原始文件摘要用于追踪输入版本;如果还需要按序列去重,应另存规范化后的序列标识,并说明规范化规则。
脚本包含隐藏文件,拒绝符号链接和特殊文件,忽略空目录。它不记录权限和修改时间。请对已经停止写入的本地目录使用它;扫描过程不是原子的文件系统快照。
2. 下载并运行示例
需要 Python 3.11 或更新版本,无须安装第三方依赖。示例已在 macOS 的 Python 3.14.7 上测试。哈希计算使用 Python 3.11 引入的 hashlib.file_digest,以二进制模式读取文件。
下载 dataset_manifest.py,或复制后面的完整代码。把脚本保存到一个新的工作目录,再执行以下命令。Shell 示例适用于 Bash 或 Zsh。
python3 --version
python3 dataset_manifest.py --help
mkdir inputs
python3 - <<'PY'
from pathlib import Path
Path("inputs/proteins.fasta").write_bytes(b">p1\nACDE\n")
Path("inputs/labels.csv").write_bytes(b"sample_id,label\np1,1\n")
Path("inputs/split.csv").write_bytes(b"sample_id,split\np1,train\n")
PY
python3 dataset_manifest.py create inputs inputs-v1.json
python3 dataset_manifest.py verify inputs inputs-v1.json
最后两条命令应输出:
CREATED: 3 files
OK: 3 files match
这三个文件是人为构造的测试输入,不能用于实际训练。脚本只创建新清单:如果 inputs-v1.json 已经存在,会报错而不会覆盖。排查意外改动时,需要保留原始清单。
3. 完整脚本
"""Byte-level manifest for a quiet local dataset directory. Python 3.11+."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import stat
import sys
def scan(root):
def fail(error):
raise error
files = {}
for folder, dirs, names in os.walk(root, onerror=fail, followlinks=False):
for name in dirs + names:
path = Path(folder) / name
mode = path.lstat().st_mode
if stat.S_ISLNK(mode):
raise ValueError(f"Symlink is not supported: {path}")
if stat.S_ISDIR(mode):
continue
if not stat.S_ISREG(mode):
raise ValueError(f"Not a regular file: {path}")
with path.open("rb") as handle:
digest = hashlib.file_digest(handle, "sha256").hexdigest()
files[path.relative_to(root).as_posix()] = digest
return files
def read_manifest(path):
data = json.loads(path.read_text(encoding="utf-8"))
if (not isinstance(data, dict) or data.get("version") != 1
or data.get("algorithm") != "sha256"
or not isinstance(data.get("files"), dict)):
raise ValueError("Unsupported manifest format")
for name, digest in data["files"].items():
if (not name or not isinstance(digest, str)
or re.fullmatch(r"[0-9a-f]{64}", digest) is None):
raise ValueError("Invalid manifest entry")
return data["files"]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=["create", "verify"])
parser.add_argument("root", type=Path)
parser.add_argument("manifest", type=Path)
args = parser.parse_args()
try:
if args.root.is_symlink():
raise ValueError("Dataset root must not be a symlink")
root = args.root.resolve(strict=True)
if not root.is_dir():
raise ValueError("Dataset root must be a directory")
manifest = args.manifest.resolve()
if manifest.is_relative_to(root):
raise ValueError("Keep the manifest outside the dataset directory")
expected = read_manifest(manifest) if args.action == "verify" else None
current = scan(root)
if args.action == "create":
data = {"version": 1, "algorithm": "sha256", "files": current}
with manifest.open("x", encoding="utf-8", newline="\n") as handle:
handle.write(json.dumps(data, sort_keys=True, indent=2) + "\n")
print(f"CREATED: {len(current)} files")
return 0
missing = sorted(expected.keys() - current.keys())
added = sorted(current.keys() - expected.keys())
changed = sorted(name for name in expected.keys() & current.keys()
if expected[name] != current[name])
for label, names in [("MISSING", missing), ("ADDED", added),
("CHANGED", changed)]:
for name in names:
print(f"{label}: {json.dumps(name, ensure_ascii=False)}")
if missing or added or changed:
return 1
print(f"OK: {len(current)} files match")
return 0
except (OSError, ValueError) as error:
print(f"ERROR: {error}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())
目录扫描使用 os.walk,并设置会抛出异常的错误处理函数。无法读取的目录不能被悄悄漏掉。每个条目都先检查类型,再计算摘要,符号链接目录也会被检查。
输出按 JSON 键排序,并使用以 / 分隔的相对路径,方便在版本控制中比较。验证时,路径来自重新扫描的目录;程序比较清单中的键,不会按照 JSON 提供的路径去打开文件。
4. 验证它能发现改动
改动示例中的一个残基,同时保持文件字节数不变:
python3 - <<'PY'
from pathlib import Path
Path("inputs/proteins.fasta").write_bytes(b">p1\nACDF\n")
PY
python3 dataset_manifest.py verify inputs inputs-v1.json
printf 'exit code: %s\n' "$?"
预期输出:
CHANGED: "proteins.fasta"
exit code: 1
只比较文件大小会漏掉这次改动。新增文件会输出 ADDED,删除已记录的文件会输出 MISSING。一次验证可以同时报告三类差异。文件名采用 JSON 引号形式,名称中的换行符等特殊字符也能被区分。
| 退出码 | 含义 | 后续操作 |
|---|---|---|
0 |
清单创建成功,或所有路径及摘要均匹配 | 可以继续流程。空目录也可能通过,需要检查输出的文件数。 |
1 |
存在新增、缺失或内容变化 | 先核对差异,再运行实验。 |
2 |
参数不正确、输入不可读、文件类型不支持或清单格式错误 | 修复报错后重新验证。 |
在流水线中,让验证成功成为下一条命令的前提。核对改动并确定要使用的输入版本后,可以用下面的包装脚本在验证失败时退出:
#!/usr/bin/env bash
set -euo pipefail
python3 dataset_manifest.py verify inputs inputs-v1.json
# Place the real training or analysis command after the verification line.
这只能检查验证时的目录状态。分析程序读取数据期间,也要保持目录不变,或使用不可变快照。
5. 把清单和实验结果一起保存
清单应随结果保存。一起记录代码版本、环境锁文件、模型标识或检查点摘要、训练配置和随机种子。比较蛋白质模型时,输入目录应包含实际使用的划分表和标签文件,不能只记录 FASTA。
如果确实需要修正数据,写明原因,再生成 inputs-v2.json。旧结果继续保留对应的 inputs-v1.json。验证刚失败就重新生成清单,会丢掉原本要检查的版本差异。
校验和不会检查标签是否正确,也不能发现同源序列泄漏或保证模型输出一致。本地 JSON 文件同样不能证明数据来源:能同时替换数据和清单的人,可以生成一对匹配的文件。应把参考清单放在可信、有版本记录的位置。这个脚本用于受控研究流程中的意外改动检查,不用于对抗恶意的并发文件系统操作。
6. 常见问题
| 报错或现象 | 处理办法 |
|---|---|
hashlib 没有 file_digest |
使用 Python 3.11 或更新版本,通过 python3 --version 确认解释器。 |
Keep the manifest outside the dataset directory |
使用同级路径 inputs-v1.json,不要保存为 inputs/manifest.json。 |
创建时出现 File exists |
保留现有清单,改用 verify;如果是有意更新,选择新的版本文件名。 |
Symlink is not supported |
准备包含实际文件的输入目录,或另行设计并记录链接处理规则。 |
缓存或 .DS_Store 被标记为 ADDED |
检查文件后,把与实验无关的文件移出输入目录。脚本有意不设置忽略规则。 |
序列相同却显示 CHANGED |
检查 FASTA 折行、头部、换行编码和文件顺序。如果关心生物学意义上的等价性,另行比较解析后的序列。 |
下载脚本已检查正常验证、同长度内容改动、新增和缺失文件、目录迁移、确定性输出、已有清单保护、错误输入、符号链接、特殊文件和空目录。这些检查验证的是示例的文件处理行为,不代表任何生物学数据集已经通过验证。
保存准确的输入记录
固定输入目录,生成清单,并把它与实验结果一起保存。再次使用输入前先验证,把不匹配的情况查清楚。清单配合划分表和软件环境,能为后续比较保留明确的文件版本记录。
封面为概念插画,不代表实验测得的蛋白质结构。Python 文档核对日期:2026 年 9 月 27 日。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-dataset-sha256-manifest.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。