"""MinHash 指纹与去重。 纯 Python 实现 MinHash(字符 3-gram shingles + 稳定哈希 + 随机置换 模拟),生成稳定十六进制指纹。用于相同 (host, service, message_fingerprint) 短窗口去重(architecture.md 5.3.2,Redis `hms:log:fp:{fingerprint}`)。 """ from __future__ import annotations import hashlib import random import re import struct import zlib from typing import Dict, Iterable, Optional, Set _MAX_HASH = (1 << 32) - 1 def _h(token: str) -> int: return zlib.crc32(token.encode("utf-8")) & _MAX_HASH def shingles(text: str, k: int = 3) -> Set[str]: """将文本切分为 k-gram 集合(字符级,跨空白压缩)。""" cleaned = re.sub(r"\s+", " ", text or "").strip() if len(cleaned) < k: # 短文本退化为整串 return {cleaned} if cleaned else set() return {cleaned[i : i + k] for i in range(len(cleaned) - k + 1)} class MinHash: """MinHash 指纹器,相同(或高度相似)文本产生相同/相近指纹。""" def __init__(self, num_perm: int = 64, seed: int = 42): self.num_perm = num_perm rng = random.Random(seed) self.a = [rng.randint(1, _MAX_HASH) for _ in range(num_perm)] self.b = [rng.randint(0, _MAX_HASH) for _ in range(num_perm)] def signature(self, tokens: Iterable[str]) -> list: hashes = [_h(t) for t in tokens] if not hashes: return [0] * self.num_perm sig = [] for ai, bi in zip(self.a, self.b): sig.append(min(((ai * x + bi) & _MAX_HASH) for x in hashes)) return sig def fingerprint(self, text: str) -> str: """返回文本的 16 字符十六进制指纹。""" tokens = shingles(text) if not tokens: return "0" * 16 sig = self.signature(tokens) digest = hashlib.sha256() for v in sig: digest.update(struct.pack(">I", v)) return digest.hexdigest()[:16] class Deduplicator: """短窗口去重器。生产可替换为 Redis SETNX,测试用进程内实现。""" def __init__(self, window_seconds: int = 300, clock=None): self.window_seconds = window_seconds self._clock = clock or _time_now self._seen: Dict[str, float] = {} def is_duplicate(self, fingerprint: str) -> bool: now = self._clock() last = self._seen.get(fingerprint) if last is not None and (now - last) <= self.window_seconds: return True self._seen[fingerprint] = now return False def mark(self, fingerprint: str) -> None: self._seen[fingerprint] = self._clock() def clear(self) -> None: self._seen.clear() def _time_now() -> float: import time return time.time()