本系列:把这两年用 Python 写的十几个小项目挨个开源、挨个说清楚。不讲”我做了什么”,讲”做的过程中被什么打脸、最后怎么想明白的”。 第 11 篇 · 核痕 KeHen(上一篇:烽燧 FengSui) 源码:github.com/ghostgorge/KeHen 纯 Python 标准库,零第三方依赖,Windows / macOS / Linux,单文件 exe
一、现有工具有一个共同的前提
把 800GB 图纸从旧服务器拷到新服务器,怎么确认真的一份没少、一份没坏?
robocopy /L、rsync -n、FreeFileSync、Beyond Compare——这些工具都很好用,但它们有一个共同前提:两端同时在线,能同时访问 A 和 B。
而现实里经常做不到:
- 旧机已经下架了
- 盘已经寄走了
- 要比的是”半年前的它“和”今天的它”——A 端根本不存在于当下
核痕换了个做法:把 A 的状态冻结成一个文件。
旧服务器 ──扫描──> 图纸-旧机.dsnap ──U盘拷走──> 新服务器 ──核对──> 报告
快照压缩后大约每个文件 34 字节。600 万个文件的文件服务器,快照约 230MB,随便放。
二、它能看见别的工具看不见的一类问题
大小没变、时间没变、内容坏了。
硬盘静默损坏、bit rot、传输截断补零——这类问题在任何”比大小 + 比时间”的工具眼里都是完全正常的。
只有真的把每个字节读一遍算过指纹,才能看见。
这也是为什么快照默认是 full 模式:
| 模式 | 读多少 | 能发现静默损坏 | 适合 |
|---|---|---|---|
full(默认) | 每个字节 | ✅ | 图纸、文档、代码、照片 |
sample | 大于 16MB 的文件只读首/中/尾各 4MB | 大部分 | 几 TB 的视频/素材盘 |
none | 只记大小和时间 | ❌ | 粗略清点 |
三、最危险的一类 bug:它不会崩溃,只会悄悄说”一致”
这一节是整篇文章我最想让人看到的。
为了让内存与目录规模无关,快照按一个约定顺序写入:每层目录内按 casefold 排序,遇到子目录先输出自己再立刻递归。这个顺序恰好等于”按路径分量元组排序“,于是比对两份快照就是一次归并推进——600 万文件和 6 千文件占的内存一样多。
整个设计建立在这个排序不变量上。而它有一个陷阱:
普通字符串比较会得出
a.txt < a/b,因为.的码位(0x2E)小于/(0x2F)。 必须按路径分量比,不能按整串比。
如果这条不变量破了会怎样?
归并不会崩溃。它会错位,然后给出一份错误的”一致”报告。
你拿着这份报告去确认 800GB 图纸迁移完成、格式化了旧盘——半年后才发现少了一批文件。
所以 selftest.py 的第一项就是专门撞这个不变量的:用随机生成的病态文件名反复验证排序键。
一个”失败时会静默给出错误的正确答案”的 bug,比一个会崩溃的 bug 危险一个数量级。 崩溃至少会告诉你出事了。
(这和立方之蛇那篇里”D 键在屏幕上往左走”是同一类问题:不报错、不崩溃,只是悄悄地错。这类地方必须有专门的测试守着。)
四、几个具体的技术决定
用 blake2b(128位),不用 sha256
标准库自带(不引依赖、不多带 DLL)、比 sha256 快、128 位对”检测意外损坏”完全够用。
但要说清楚:这是完整性校验,不是防篡改。有人蓄意构造碰撞的场景请用 sha256。工具的能力边界要写在文档里,不能让用户自己猜。
哈希走线程池
hashlib 读大块数据时会释放 GIL,所以这里多线程是真并行——不是 Python 里常见的那种假并行。
而且瓶颈本来就在磁盘 IO,解释器开销被完全掩盖。实测 2 万文件 / 238MB 全文校验 1.2 秒。
快照格式是 gzip 压缩的 JSON Lines
不是自定义二进制,也不是 sqlite。理由:
十年后你还能
gzip -dc x.dsnap | head直接看懂它,也能拿 Python 或 jq 自己二次加工。
体积代价约三成,压缩后完全可接受。一个用来做长期存证的文件,可读性比省那三成体积重要得多。
“仅时间不同”默认不算差异
跨文件系统拷贝几乎必然改修改时间:FAT32 只有 2 秒精度,SMB、云盘同理。
如果把时间差异也报出来,报告里会全是噪音,跟没有报告一样。需要的话加 --show-time。
(这和上一篇烽燧是同一条思路:工具必须自己承担”什么算噪音”的判断,不能原封不动推回给人。)
CSV 一律写 UTF-8 BOM
不带 BOM 的话 Excel 按 GBK 解释,中文路径直接乱码——这是国内用户最常见的”你的工具有 bug”投诉,而它根本不是 bug。
五、三个版本更新,三个”不像它自己”的故事
1.0.1:自测在 Windows 上必然崩
selftest.py 里有一项用了一个含英文双引号的文件名。这在 POSIX 上完全合法,在 Windows 上是保留字符。
于是 Windows 下自测必崩在这一项,而 build.bat 是”自测通过才打包”的——整个构建被一个测试用例的文件名卡死了。
修法是加一个 portable_name():过滤保留字符、结尾的点和空格、CON/COM1 这类保留设备名,两个平台都合法但仍然足够刁钻。
同版本还修了另一个更严重的:Windows 上超过 MAX_PATH(260) 的深路径会被记成”读取失败”,导致快照残缺。遍历时改为自动加 \\?\ 前缀(UNC 路径走 \\?\UNC\),上限 32767 字符,不需要用户去改注册表的 LongPathsEnabled。新增的测试会真的造一条 350+ 字符的路径来验。
1.0.2:两个 exe 名字只差大小写
KeHen.exe 和 kehen.exe 在 Windows 上是同一个文件,PyInstaller 会先生成一个、再把另一个覆盖掉。改成 kehen-cli.exe。
(同一个坑我在烽燧里也踩过——两个项目的产物结构一样,坑就一样。)
1.0.3:一个看着完全不像权限问题的报错
快照默认保存位置原本是”被扫目录的上一级”。听起来很合理——直到你选了 D:\project,于是快照默认要存到 D:\ 盘符根目录。
而 Windows 上这个位置经常写不进去:受控文件夹访问、云盘映射盘、只读卷、组策略……
报出来的错误是:
Errno 22 Invalid argument
这个报错看着完全不像权限问题。 用户会去检查路径拼错没有、检查磁盘满没满,就是想不到是”根目录不让写”。
三处修改:
- 默认位置改成”第一个真的写得进去的目录“(上一级 → 被扫目录本身 → 文档 → 主目录)
- 开始扫描前先做一次可写性预检——扫描可能要几十分钟,等跑完才发现存不下是最糟糕的失败方式
- 写入失败不再甩 traceback,而是给出能照着做的说明:哪个目录、系统怎么说、换哪里存
第 2 条是这三条里最普适的:任何长耗时任务,都要在开始前把”结束时才会用到的前提”先验一遍。
六、典型用法
| 场景 | 怎么做 |
|---|---|
| 换电脑 / 换硬盘 / 迁 NAS | 旧机 scan,新机 verify |
| 服务器迁移、机房搬迁 | 迁移前 scan,迁移后 verify --html 报告.html 存档 |
| 备份是否可信 | 对备份还原出来的目录跑 verify |
| 移动硬盘有没有悄悄坏 | 每半年 verify 一次,对比同一份快照 |
| 素材/图纸交付给客户 | 随交付附一份 .dsnap,对方自己核 |
| 装软件前后系统改了什么 | 装之前 scan,装之后 verify |
| 顺手清理重复文件 | 扫描已经算过指纹,dupes 不用再读盘 |
最后一条是白捡的:既然全文哈希都算过了,查重就是零额外 IO 的副产品。
kehen scan D:\项目图纸 -o 图纸-旧机.dsnap
kehen verify 图纸-旧机.dsnap --root E:\项目图纸 --html 核对报告.html
kehen diff 旧.dsnap 新.dsnap --csv 差异.csv
kehen dupes 图纸-旧机.dsnap --min-size 5MB --html 重复.html
退出码:0 一致 · 1 发现内容变化/缺失/大小不符 · 2 出错。可以直接接进计划任务。
窗口版三页,从左到右用:扫描 → 核对 → 查重。
七、已知边界(照实写)
- 不做同步,只报告差异,不动你任何一个文件。要同步用 FreeFileSync / robocopy。
- 不判断”哪份是原件”。查重只告诉你哪些文件内容相同,删哪份你自己决定。
sample模式下超大文件中段被改写有极小概率漏检。要确定结论就用full。- 软链接/联接点默认记成 0 字节文件、不跟进去(避免成环和重复计数)。
- ACL、备用数据流、稀疏文件属性不在比对范围内。核痕比的是”文件内容与基本元数据”。
- 一次比对里若有超过 20 万条新增+缺失,会自动放弃”移动/改名识别”以控制内存,并在报告里写明。
python selftest.py # 42 项自测,全绿再往下走
python main.py # 窗口版
双击 build.bat # 先跑自测,通过才打包
尾巴
两条带得走的:
一、当所有现成方案都有同一个前提时,先问这个前提是不是必需的。 同步类工具全都要求两端在线——因为它们要”改”。而只想”证明”的话,一份指纹文件就够了,A 端可以已经不存在。
二、会静默给出错误正确答案的 bug,必须有专门的测试守着。 排序不变量一旦破了,归并不崩溃,只是给你一份错误的”一致”报告——而你会拿着它去格式化旧盘。
📦 源码:https://github.com/ghostgorge/KeHen
下一篇:奥赛题库 OlympiadMath——小初高数学奥赛题库桌面程序。回复「继续」。
