"""三层去重的指纹算法。 核心: - normalize_content:把 content 折叠成纯净文本,用于跨源比对; - content_hash:normalize 后 SHA1[:16]; - simhash64:字符 3-gram + md5 加权累加,产出 64 位无符号整数; - hamming:两个 SimHash 的汉明距离。 设计取舍: SimHash 的"分词"用字符 3-gram 而非 jieba。理由: 1. 中文场景下字符 3-gram 与词级 SimHash 在重复识别上效果接近, 而前者无外部依赖、ARM/嵌入式友好; 2. M5 Embedding 后续不依赖 jieba,引入只为 M3 不划算; 3. 重复率验收门槛 ≤ 5%(project_plan.md 第七章),3-gram 经验上 足以分辨。 """ from __future__ import annotations import hashlib import unicodedata # 64 位 SimHash 位宽 SIMHASH_BITS = 64 SIMHASH_MASK = (1 << SIMHASH_BITS) - 1 # 默认 SimHash 汉明距离阈值(<= 此值视为重复) DEFAULT_HAMMING_THRESHOLD = 3 # 字符 n-gram 长度 NGRAM_SIZE = 3 def normalize_content(text: str) -> str: """把 content 折叠成"无空白无标点"形式,用于 L2 内容 hash 与 SimHash 输入。 使用 Unicode 类别判断: - P* Punctuation(所有中英文标点) - Z* Separator(空格 / 行 / 段分隔符) - C* Control(NUL / 换行控制等) 保留 L*(字母)、N*(数字)、S*(符号,如 +/-、% 等),以及 CJK 字符。 """ if not text: return "" return "".join( ch for ch in text if unicodedata.category(ch)[0] not in ("P", "Z", "C") ) def content_hash(text: str) -> str: """对 normalize_content(text) 做 SHA1,取前 16 hex 字符。""" norm = normalize_content(text) return hashlib.sha1(norm.encode("utf-8")).hexdigest()[:16] def _ngrams(text: str, n: int = NGRAM_SIZE) -> list[str]: """字符级 n-gram。文本短于 n 时,直接整体作为单个 token。""" if len(text) < n: return [text] if text else [] return [text[i : i + n] for i in range(len(text) - n + 1)] def simhash64(text: str) -> int: """64 位 SimHash。返回无符号整数,空文本返回 0。""" norm = normalize_content(text) if not norm: return 0 grams = _ngrams(norm) if not grams: return 0 v = [0] * SIMHASH_BITS for gram in grams: h = int(hashlib.md5(gram.encode("utf-8"), usedforsecurity=False).hexdigest(), 16) # 取低 64 位 h64 = h & SIMHASH_MASK for i in range(SIMHASH_BITS): if (h64 >> i) & 1: v[i] += 1 else: v[i] -= 1 fp = 0 for i in range(SIMHASH_BITS): if v[i] > 0: fp |= 1 << i return fp def hamming(a: int, b: int) -> int: """两个 SimHash 的汉明距离。""" return bin((a ^ b) & SIMHASH_MASK).count("1")