查看: 242|回复: 3

Python计算文件SHA-256摘要与任务标识做增量跳过判

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
批处理脚本第二次启动时,常见的做法是“发现输出文件存在就跳过”。这个判断并不可靠:如果同名输入文件已经被替换,旧输出仍然在,程序就会把旧结果当成新结果。文件名只能用来定位路径,不能代表内容版本。更稳妥的做法是用 SHA-256 记录实际读取的字节,再把输入摘要、转换配置和实现版本组合成任务标识,用于重跑判定。

一、先确定摘要对象:原始字节还是规范化文本

假设文件里只有“退款”两个字和一个换行。UTF-8 编码下,LF 版本为 7 字节,CRLF 版本为 8 字节。肉眼看到的行可能一样,但原始字节摘要不同。

如果转换任务要求保留原始文件,就对原始字节求摘要;如果任务允许统一换行,可以另外保存规范化内容摘要,并记录规范化规则。不能先用文本模式读入、默认转换了换行,再把它称为“原文件摘要”。

下面的函数以二进制模式分块读取,同时累计实际读取的字节数。65536 只是示例块大小,并非经过基准测试得出的最优值:
  1. import hashlib
  2. from pathlib import Path
  3. def fingerprint(path):
  4.     digest = hashlib.sha256()
  5.     size = 0
  6.     with Path(path).open("rb") as f:
  7.         for block in iter(lambda: f.read(65536), b""):
  8.             size += len(block)
  9.             digest.update(block)
  10.     return {"sha256": digest.hexdigest(), "bytes": size}
复制代码

hashlib.update 分次输入字节,等价于对这些字节的串接求摘要。代码避免一次把整个文件读进内存,但仍然会完整读取每个字节。读到一半发生权限或 I/O 异常时,函数会抛错,不返回部分摘要;调用方不能捕获异常后把它登记为已完成任务。

二、输入相同,转换配置也可能已经不同

同一文件按“保留空白”和“去除首尾空白”处理,输出可能不同。只把输入摘要作为缓存键,会把两种任务混为一谈。任务标识应包含真正影响输出的配置及实现版本:
  1. import json
  2. def task_key(source, config):
  3.     payload = {
  4.         "input": source["sha256"],
  5.         "config": config,
  6.         "implementation": "demo-v1",
  7.     }
  8.     raw = json.dumps(
  9.         payload, sort_keys=True, separators=(",", ":"),
  10.         ensure_ascii=False, allow_nan=False,
  11.     ).encode("utf-8")
  12.     return hashlib.sha256(raw).hexdigest()
复制代码

这个函数与前一个放在同一脚本即可使用。配置限定为字符串键和 JSON 支持的值,不允许 NaN、无穷大或自定义对象。排序键、固定分隔符和 UTF-8 编码是本例固定的序列化约定,不是通用的跨语言规范化协议;跨语言共享标识时,还要统一数字和 Unicode 等表示。

demo-v1 只适用于这段演示。正式任务应记录可追溯的实现版本或内容摘要;如果行为变化后忘记更新版本,这个标识同样会失效。会影响输出的外部字典、时区或依赖版本也要纳入约定,不能只看配置文件。

三、一个可以直接复核的小例子

把下面几行接在同一脚本末尾,在自行选择的测试目录运行。这里使用临时目录,避免覆盖已有文件:
  1. from tempfile import TemporaryDirectory
  2. with TemporaryDirectory() as folder:
  3.     path = Path(folder) / "input.txt"
  4.     path.write_bytes("退款\n".encode("utf-8"))
  5.     a = fingerprint(path)
  6.     first = task_key(a, {"strip": False})
  7.     path.write_bytes("退款\r\n".encode("utf-8"))
  8.     b = fingerprint(path)
  9.     print(a["bytes"], b["bytes"], a["sha256"] == b["sha256"])
  10.     print(first == task_key(a, {"strip": False}))
  11.     print(first == task_key(a, {"strip": True}))
复制代码

同样两组字节和配置在本地 Python 3.9.6 下核验,输出依次为:7 8 False、True、False。这说明换行字节改变会改变输入摘要,相同配置能重建相同任务标识,改变配置会改变标识。原文没有测量读写吞吐、百万文件开销或异常恢复成功率。

四、输出存在,还要核对它对应哪项任务

任务完成后,登记输入定位信息、输入摘要、任务标识、输出位置和输出摘要。下一次启动,先重新计算本次任务标识,再查登记记录;只有记录匹配、输出存在且摘要一致时,才允许跳过。

若输出被人工修改,旧记录虽然还在,摘要检查也应阻止跳过。若任务输出是多个文件,需要逐项清单,不能只对目录名字求摘要。登记与输出之间的提交一致性还需要单独设计:输出写成功但记录未提交,或者记录已写入而输出不完整,都不能靠一个哈希函数自动解决。

五、摘要不是输入快照,也不是来源证明

文件在读取时被其他进程修改,分块读取可能得到不同时间片的内容。前后比较大小与修改时间只能发现部分变化,不能给出严格快照保证。需要稳定输入时,应从不可变版本读取,或使用适合该文件系统的快照、发布与协作机制。上面的演示默认输入在读取期间保持不变。

SHA-256 可以用于内容一致性核对,却不能证明是谁写的文件。若文件和登记记录一起被修改,比较仍可能一致。可信来源、权限和记录保护是另外的问题。按这种方式保存摘要后,重跑判定就有了可核对的理由:这次输入、配置、实现和输出是否仍对应同一项计算,而路径只是其中的定位信息。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python计算文件SHA-256摘要与任务标识做增量跳过判

这套思路很扎实,尤其是把“输出文件存在就跳过”这个常见做法拆开来看:文件名只是定位路径,真正决定能否跳过的是输入字节、配置、实现版本和输出内容是否仍然对应同一次计算。原始字节摘要和规范化摘要分开处理这一点也很关键,否则文本模式读入时悄悄转换了换行,却还把它当成原文件摘要,后面很容易出错。 代码示例里用临时目录直接复核 7 字节和 8 字节、输入摘要变化、配置变化导致任务标识变化,验证路径很清楚。实际落地时比较值得坚持的是:读取异常时不要登记为完成;任务标识里把真正影响输出的配置和实现版本都纳入;输出存在还不够,还要核对登记的任务标识和输出摘要;多文件输出要逐项清单。最后关于摘要不是输入快照、也不是来源证明的提醒也很必要,能把“可核对的重跑理由”和“安全来源证明”区分开。整体上是一篇很适合拿来改造成实际批处理缓存判定的文章。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python计算文件SHA-256摘要与任务标识做增量跳过判

这个思路很扎实,尤其是把输入摘要、配置和实现版本组合成任务标识,再结合输出摘要判断,比单纯看输出文件在不在可靠多了。原始字节和规范化文本分开这一点也很关键,换行差异确实容易被文本模式读入掩盖。实际落地时我会再关注两点:一是登记记录里最好带上任务标识的序列化约定版本和实现版本说明,后面规则变化时不容易混用旧记录;二是输出和登记尽量用临时文件加原子替换,异常时不要登记完成,避免输出写完记录没写,或记录写了输出不完整。读取期间文件被改的风险确实只能靠不可变输入或快照机制解决,摘要本身给不了快照保证。整体上这个判定链路是清楚的。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python计算文件SHA-256摘要与任务标识做增量跳过判

楼主这篇写得挺完整的,尤其是把“输出存在就跳过”这个坏习惯拆开讲了。实践里确实很容易踩坑:文件被同名替换、配置改了却复用旧缓存、输出被手工改过,这几种情况光看路径和存在性都拦不住。 几个我觉得值得补充的点,供后面动手的人参考。 第一,摘要是按原始字节还是规范化文本,最好在登记表里显式记一个字段,比如 digest_kind,取值是 raw_bytes 或 normalized_text,再加一列 normalization_rule。不然过几个月回头看记录,没人分得清这个 hash 是对着哪种内容算的,跨脚本共享时更容易出问题。 第二,任务标识里除了配置和实现版本,建议把外部依赖也列出来,比如词表文件的摘要、时区名、Python 和关键库的版本。这些东西变了输出也可能变,但配置文件本身看不出来。实现版本用 git commit 或发布号比手写的 demo-v1 靠谱,改行为忘了升版本的问题会少很多。 第三,输出校验我一般会再加一次“清单文件”的摘要,而不是把每个输出文件的 hash 直接散落在记录里。多文件任务可以生成一个 manifest,按相对路径排序后写成固定格式,再对 manifest 求摘要;跳过判定时校验 manifest 摘要和其中每个文件的摘要是否一致。这样目录里多出、少了或改了文件都能被发现。 第四,楼主提到的提交一致性确实是独立问题。简单做法是先把输出写到临时文
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-11 03:53 , Processed in 0.022348 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部