批处理脚本第二次启动时,常见的做法是“发现输出文件存在就跳过”。这个判断并不可靠:如果同名输入文件已经被替换,旧输出仍然在,程序就会把旧结果当成新结果。文件名只能用来定位路径,不能代表内容版本。更稳妥的做法是用 SHA-256 记录实际读取的字节,再把输入摘要、转换配置和实现版本组合成任务标识,用于重跑判定。
一、先确定摘要对象:原始字节还是规范化文本
假设文件里只有“退款”两个字和一个换行。UTF-8 编码下,LF 版本为 7 字节,CRLF 版本为 8 字节。肉眼看到的行可能一样,但原始字节摘要不同。
如果转换任务要求保留原始文件,就对原始字节求摘要;如果任务允许统一换行,可以另外保存规范化内容摘要,并记录规范化规则。不能先用文本模式读入、默认转换了换行,再把它称为“原文件摘要”。
下面的函数以二进制模式分块读取,同时累计实际读取的字节数。65536 只是示例块大小,并非经过基准测试得出的最优值:
- import hashlib
- from pathlib import Path
- def fingerprint(path):
- digest = hashlib.sha256()
- size = 0
- with Path(path).open("rb") as f:
- for block in iter(lambda: f.read(65536), b""):
- size += len(block)
- digest.update(block)
- return {"sha256": digest.hexdigest(), "bytes": size}
复制代码
hashlib.update 分次输入字节,等价于对这些字节的串接求摘要。代码避免一次把整个文件读进内存,但仍然会完整读取每个字节。读到一半发生权限或 I/O 异常时,函数会抛错,不返回部分摘要;调用方不能捕获异常后把它登记为已完成任务。
二、输入相同,转换配置也可能已经不同
同一文件按“保留空白”和“去除首尾空白”处理,输出可能不同。只把输入摘要作为缓存键,会把两种任务混为一谈。任务标识应包含真正影响输出的配置及实现版本:
- import json
- def task_key(source, config):
- payload = {
- "input": source["sha256"],
- "config": config,
- "implementation": "demo-v1",
- }
- raw = json.dumps(
- payload, sort_keys=True, separators=(",", ":"),
- ensure_ascii=False, allow_nan=False,
- ).encode("utf-8")
- return hashlib.sha256(raw).hexdigest()
复制代码
这个函数与前一个放在同一脚本即可使用。配置限定为字符串键和 JSON 支持的值,不允许 NaN、无穷大或自定义对象。排序键、固定分隔符和 UTF-8 编码是本例固定的序列化约定,不是通用的跨语言规范化协议;跨语言共享标识时,还要统一数字和 Unicode 等表示。
demo-v1 只适用于这段演示。正式任务应记录可追溯的实现版本或内容摘要;如果行为变化后忘记更新版本,这个标识同样会失效。会影响输出的外部字典、时区或依赖版本也要纳入约定,不能只看配置文件。
三、一个可以直接复核的小例子
把下面几行接在同一脚本末尾,在自行选择的测试目录运行。这里使用临时目录,避免覆盖已有文件:
- from tempfile import TemporaryDirectory
- with TemporaryDirectory() as folder:
- path = Path(folder) / "input.txt"
- path.write_bytes("退款\n".encode("utf-8"))
- a = fingerprint(path)
- first = task_key(a, {"strip": False})
- path.write_bytes("退款\r\n".encode("utf-8"))
- b = fingerprint(path)
- print(a["bytes"], b["bytes"], a["sha256"] == b["sha256"])
- print(first == task_key(a, {"strip": False}))
- print(first == task_key(a, {"strip": True}))
复制代码
同样两组字节和配置在本地 Python 3.9.6 下核验,输出依次为:7 8 False、True、False。这说明换行字节改变会改变输入摘要,相同配置能重建相同任务标识,改变配置会改变标识。原文没有测量读写吞吐、百万文件开销或异常恢复成功率。
四、输出存在,还要核对它对应哪项任务
任务完成后,登记输入定位信息、输入摘要、任务标识、输出位置和输出摘要。下一次启动,先重新计算本次任务标识,再查登记记录;只有记录匹配、输出存在且摘要一致时,才允许跳过。
若输出被人工修改,旧记录虽然还在,摘要检查也应阻止跳过。若任务输出是多个文件,需要逐项清单,不能只对目录名字求摘要。登记与输出之间的提交一致性还需要单独设计:输出写成功但记录未提交,或者记录已写入而输出不完整,都不能靠一个哈希函数自动解决。
五、摘要不是输入快照,也不是来源证明
文件在读取时被其他进程修改,分块读取可能得到不同时间片的内容。前后比较大小与修改时间只能发现部分变化,不能给出严格快照保证。需要稳定输入时,应从不可变版本读取,或使用适合该文件系统的快照、发布与协作机制。上面的演示默认输入在读取期间保持不变。
SHA-256 可以用于内容一致性核对,却不能证明是谁写的文件。若文件和登记记录一起被修改,比较仍可能一致。可信来源、权限和记录保护是另外的问题。按这种方式保存摘要后,重跑判定就有了可核对的理由:这次输入、配置、实现和输出是否仍对应同一项计算,而路径只是其中的定位信息。 |