同一份文档被多次上传时,系统怎样判断内容是否重复?一种常见办法是先对内容计算哈希,再用摘要定位已有记录。
哈希值可以类比为数据的数字指纹,但这个比喻有边界:它不是数学上绝对唯一的身份证,也不会自动完成去重或权限控制。
从输入字节到摘要
对固定算法而言,相同字节输入会得到相同摘要。以 SHA-256 为例,输出为
256 bit,也就是 32 字节;写成十六进制字符串后有 64 个字符。
import hashlib
text = "课程支持回放。"
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
print(digest)
print(len(digest)) # 64
注意比较的是字节:大小写、换行、空格、编码不同,都可能改变输入。要做内容去重,应先明确是否保留这些差异,再决定规范化规则。Python
hashlib
相同摘要不代表绝对没有碰撞
固定长度输出的数量有限,而输入空间更大,因此不同输入可能产生相同摘要,这叫哈希碰撞。
安全哈希算法旨在让主动寻找碰撞或从摘要寻找原像在计算上足够困难,但不能把所有哈希函数都描述为“不可逆且碰撞概率极低”。某些哈希函数主要用于哈希表分桶,并不提供密码学保证。
同样,“不可逆”也不等于短输入无法被猜中。攻击者可以把常见输入逐个计算后比对。因此,对可预测内容做哈希,不能当成保密措施。
核对一个常见例子
下面仅演示历史上常见的 MD5 输出,不将它推荐为安全用途:
import hashlib
print(hashlib.md5(b"Hello").hexdigest())
# 8b1a9953c4611296a827abf8c47804d7
print(hashlib.md5(b"hello").hexdigest())
# 5d41402abc4b2a76b9719d911017c592
这里两个摘要不同,但不能据此推出“任何不同输入都必然有不同摘要”。MD5
与 SHA-1 已存在已知碰撞风险,安全设计中不应依赖它们的抗碰撞能力。
哈希 ID 与自增 ID 怎样选择
| 维度 | 自增 ID | 内容哈希 ID |
|---|---|---|
| 表达对象 | 一条业务记录的身份 | 某个确定内容版本 |
| 相同内容再次写入 | 默认产生另一条记录 | 可得到相同候选 ID |
| 内容更新 | 通常保持 ID | 摘要通常变化 |
| 生成方式 | 依赖分配机制 | 相同算法和输入可独立计算 |
| 去重能力 | 可另加内容摘要与唯一约束 | 仍需要查询或唯一约束等流程 |
| 访问控制 | 必须校验权限 | 同样必须校验权限 |
常见设计是同时保留两者:业务 ID
稳定标识文档,内容哈希标识版本或缓存结果。多个用户上传相同文件,也不一定应该合并权限和归属关系。
在 RAG 中设计缓存键
只对原文做哈希还不够。相同文本经过不同切分配置或不同嵌入模型处理,结果可能不同。
import hashlib
import json
payload = {
"text": "课程支持回放。",
"embedding_model": "example-embedding-v1",
"chunking_version": "paragraph-v2",
}
serialized = json.dumps(
payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
cache_key = hashlib.sha256(serialized.encode("utf-8")).hexdigest()
print(cache_key)
这让缓存键反映影响输出的条件。实际系统还应按需要加入租户、权限或数据版本信息,并由存储层处理并发写入。
完整性校验与密码存储不是一回事
文件摘要可以检测内容是否与预期一致,但用于安全校验时,预期摘要本身必须来自可信渠道。攻击者若能同时替换文件和摘要,比较二者并不能证明文件可信。
密码存储应使用专门的密码哈希方案,如配置合适成本的
Argon2id,而不是直接对密码做一次 SHA-256。盐值与计算成本承担不同职责。OWASP
密码存储指南