Node.js 中用 SHA-256 检测重复正文:先定义规范化规则
同一篇 Markdown 文章可能来自网页输入框、Windows 编辑器和自动生成服务。直接比较字符串时,文件开头的 BOM 或不同的换行符就可能让两份内容看起来不同。反过来,如果为了去重把所有空白都删除,又可能误伤代码块中的缩进和字符串。
本文给出一个不依赖第三方包的最小示例:先明确允许忽略的文本差异,再计算 SHA-256,使用指纹检测相同正文。这里讨论的是内容去重,不是密码存储,也不保证识别语义相同的改写文章。
一、从可解释的规范化开始
示例只处理三类差异:文件开头的 BOM、CRLF 与 LF 换行,以及正文末尾的多余换行。不删除每一行的前导空格,不压缩代码块内部的空白,也不把全文变成小写。这样做的目标,是让规则足够明确,便于作者和维护者理解。
把以下内容保存为 content-hash.mjs,使用 Node.js 22 或以上版本执行:
import { createHash } from 'node:crypto';
import assert from 'node:assert/strict';
function normalizeMarkdown(text) {
return text
.replace(/^\uFEFF/, '')
.replace(/\r\n?/g, '\n')
.replace(/\n*$/, '\n');
}
function fingerprint(text) {
return createHash('sha256')
.update(normalizeMarkdown(text), 'utf8')
.digest('hex');
}
const seen = new Set();
function accept(text) {
const hash = fingerprint(text);
if (seen.has(hash)) return false;
seen.add(hash);
return true;
}
const first = '# 示例\r\n\r\n正文内容\r\n';
const equivalent = '\uFEFF# 示例\n\n正文内容\n\n';
assert.equal(fingerprint(first), fingerprint(equivalent));
assert.equal(accept(first), true);
assert.equal(accept(equivalent), false);
// 缩进可能影响代码语义,因此保留这种差异。
assert.notEqual(fingerprint(' run()'), fingerprint('run()'));
console.log('首次接收成功;等价正文被识别为重复;缩进得到保留。');
运行 node content-hash.mjs,断言通过后会输出一行说明。把测试样例放在示例旁边,有助于在以后修改规范化规则时发现行为变化。
二、标题不应该掩盖重复正文
如果指纹的输入是“标题加正文”,只改标题就会得到新的指纹。需要判断正文是否重复时,应直接对正文建立指纹。标题、标签、作者和发布时间可以作为独立字段保存,供界面展示、检索和权限判断使用。
同样,不宜为了提高命中率把 Markdown 中所有空白合并。Python 缩进、代码字符串、表格和段落边界都可能受到影响。规范化不是越激进越好,应当由业务明确哪些差异可以忽略。
三、内存集合只能用于演示
示例中的 Set 在进程退出后会清空,因此不能单独作为长期运行任务的去重记录。实际应用需要将指纹、内容编号、所属账户和提交状态持久化。若多个进程会同时写入,应使用数据库唯一约束或其他能原子判断并写入的机制,不能仅靠“先查询,再插入”。
还应区分“内容重复”和“操作已经成功”。文章进入本地队列、保存云端草稿、提交发布、审核通过是不同阶段。保存过指纹并不能证明发布已经成功;网络超时也不能证明发布失败。写操作结果不明时,应查询官方状态后再决定是否继续,避免在自动重试中产生重复内容。
四、规则变更需要保留版本
如果将来增加 Unicode 规范化或调整末尾换行规则,旧指纹和新指纹可能不再可比。可以为指纹记录增加 normalizationVersion 字段,在迁移时根据原始正文重新计算,并先用样例验证差异。
SHA-256 的作用是把输入映射为固定长度的指纹。它不能判断两篇文章是否只是同义改写,也不能代替内容质量检查。去重规则、操作状态和人工核对各自解决不同的问题,组合起来才能让内容队列的行为清楚、可追踪。
- 点赞
- 收藏
- 关注作者
评论(0)