用指纹算法给文章去重:mid_signature 实战

1. 引言

在内容平台、搜索引擎和推荐系统中,文章去重是一个基础且关键的任务。面对海量、高速产生的文本数据,如何高效、准确地识别出内容重复或高度相似的文章,直接影响到用户体验和系统资源分配。传统的基于字符串精确匹配或简单哈希的方法,在面对文本改写、语序调整、段落增删等"软重复"时往往力不从心。

指纹算法(Fingerprinting Algorithm)为此提供了一种优雅的解决方案。它通过提取文本的"指纹"(一种紧凑的数字签名),将复杂的文本相似度比较转化为指纹集合的相似度计算,大大提升了去重的效率和可扩展性。本文将深入探讨指纹算法的原理,并重点介绍一种名为 mid_signature 的实战算法,通过代码示例展示如何将其应用于文章去重场景。

2. 指纹算法核心思想

指纹算法的目标是为每篇文章生成一个唯一或近乎唯一的"指纹",使得内容相似的文章其指纹也相似,内容不同的文章其指纹差异显著。其核心流程通常包含以下步骤:

  1. 文本预处理:去除HTML标签、停用词、标点符号,统一大小写,进行词干化或分词(针对中文)。
  2. 特征提取:将预处理后的文本转换为一系列特征单元,如N-gram(字符或词的N元组)、关键词或语义向量。
  3. 指纹生成:对特征单元集合应用哈希函数(如SimHash, MinHash)或特定的选择策略(如选择特定位置的N-gram),生成一个固定长度或可变长度的数字签名(指纹)。
  4. 相似度判定:通过比较两篇文章指纹的汉明距离、Jaccard相似度等指标,判断它们是否重复或高度相似。

与全文比较相比,指纹比较的计算开销极低,且指纹本身尺寸小,非常适合构建倒排索引或布隆过滤器进行大规模快速检索。

3. mid_signature 算法详解

mid_signature 是一种基于文本局部最稳定特征的指纹算法。"mid"意指"中间",其核心思想是:在一篇文章经过排序的特征序列(如排序后的N-gram哈希值)中,选择中间区域的特征作为文章的指纹。这部分特征相对稳定,对文章开头或结尾的微小改动(如添加摘要、修改结尾)不敏感,但对核心内容的改变敏感。

3.1 算法步骤

  1. 生成N-gram:将预处理后的文本按字符或词滑动窗口切分成N-gram集合。
  2. 哈希化:对每个N-gram使用一个哈希函数(如MD5, MurmurHash)计算其哈希值,得到一个64位或128位的整数。
  3. 排序:将所有N-gram的哈希值进行排序,得到一个有序列表。
  4. 选取中段特征 :从排序后的列表中,按预设的规则选取中间一部分哈希值作为文章的指纹。例如,选取排序后列表的中间 K 个值,或者每隔固定间隔选取一个值直到取满 K 个。
  5. 指纹归一化 :将选取的 K 个哈希值拼接或再次哈希,生成一个最终的固定长度的签名(即 mid_signature)。

3.2 为何有效?

  • 抗局部扰动:文章开头加个标题,结尾加个"转载请注明",只会影响排序列表两端的一些哈希值,中间部分基本不变。
  • 对核心内容敏感:如果文章主体段落被重写或替换,排序列表中间区域的特征会发生显著变化,从而导致指纹不同。
  • 计算效率高:只需对N-gram哈希值进行一次排序和选择,复杂度可控。

4. 实战:用Python实现mid_signature文章去重

下面我们用一个完整的Python示例来演示 mid_signature 的实现和应用。

python 复制代码
import hashlib
import re
from typing import List, Set
class MidSignatureDeduplicator:
def init(self, ngram_n: int = 5, signature_len: int = 10, hash_bits: int = 64):
"""
初始化去重器。
:param ngram_n: N-gram的N值,默认为5(字符级)。
:param signature_len: 最终指纹的长度(选取的哈希值个数),默认为10。
:param hash_bits: 哈希值的位数,默认为64。
"""
self.ngram_n = ngram_n
self.signature_len = signature_len
self.hash_bits = hash_bits
self.hash_mask = (1 << hash_bits) - 1  # 用于限制哈希值位数
def _preprocess(self, text: str) -> str:
    """文本预处理:转小写,去除非字母数字字符,合并空白。"""
    text = text.lower()
    text = re.sub(r'[^a-z0-9\s]', '', text)  # 简单示例,实际可能需要更复杂的分词
    text = re.sub(r'\s+', ' ', text).strip()
    return text
def _ngrams(self, text: str) -> List[str]:
"""生成字符级N-gram列表。"""
if len(text) < self.ngram_n:
return [text]
return [text[i:i+self.ngram_n] for i in range(len(text) - self.ngram_n + 1)]
def _hash_ngram(self, ngram: str) -> int:
"""计算N-gram的哈希值(模拟64位)。"""
# 使用MD5并取前64位
md5 = hashlib.md5(ngram.encode('utf-8')).hexdigest()
# 取前16个十六进制字符,转换为64位整数
hash_int = int(md5[:16], 16) & self.hash_mask
return hash_int
def generate_signature(self, text: str) -> List[int]:
"""为给定文本生成mid_signature指纹。"""
# 1. 预处理
cleaned = self._preprocess(text)
if not cleaned:
return []
# 2. 生成N-gram并哈希
ngrams = self._ngrams(cleaned)
hashes = [self._hash_ngram(gram) for gram in ngrams]
if not hashes:
return []
3. 排序
sorted_hashes = sorted(hashes)
4. 选取中段特征
total = len(sorted_hashes)
策略:从1/4处开始,选取signature_len个哈希值
start_idx = total // 4
end_idx = start_idx + self.signature_len
如果文本太短,则取全部哈希值(补0或重复)
if start_idx &gt;= total:
selected = sorted_hashes
else:
selected = sorted_hashes[start_idx:end_idx]
5. 如果选取的数量不足,用0填充
while len(selected) &lt; self.signature_len:
selected.append(0)
return selected[:self.signature_len]
def similarity(self, sig1: List[int], sig2: List[int]) -> float:
"""计算两个指纹的相似度(基于Jaccard)。"""
set1, set2 = set(sig1), set(sig2)
if not set1 and not set2:
return 1.0
intersection = len(set1 & set2)
union = len(set1 | set2)
return intersection / union if union > 0 else 0.0
def is_duplicate(self, text1: str, text2: str, threshold: float = 0.7) -> bool:
"""判断两篇文章是否重复(相似度超过阈值)。"""
sig1 = self.generate_signature(text1)
sig2 = self.generate_signature(text2)
sim = self.similarity(sig1, sig2)
return sim >= threshold
========== 使用示例 ==========
if name == "main":
deduper = MidSignatureDeduplicator(ngram_n=5, signature_len=10)
示例文章
article1 = "用指纹算法给文章去重:mid_signature实战。本文介绍了指纹算法的原理和mid_signature的实现。"
article2 = "实战mid_signature:用指纹算法进行文章去重。这篇文章讲解了mid_signature算法以及如何用Python实现。"
article3 = "完全不同的主题:今天天气很好,适合去公园散步。"
print("文章1指纹:", deduper.generate_signature(article1))
print("文章2指纹:", deduper.generate_signature(article2))
print("文章3指纹:", deduper.generate_signature(article3))
print(f"\n文章1 vs 文章2 相似度: {deduper.similarity(deduper.generate_signature(article1), deduper.generate_signature(article2)):.3f}")
print(f"文章1 vs 文章3 相似度: {deduper.similarity(deduper.generate_signature(article1), deduper.generate_signature(article3)):.3f}")
threshold = 0.6
print(f"\n阈值 {threshold} 下去重判断:")
print(f"文章1与文章2是否重复? {deduper.is_duplicate(article1, article2, threshold)}")
print(f"文章1与文章3是否重复? {deduper.is_duplicate(article1, article3, threshold)}")</code></pre>
代码解读:
MidSignatureDeduplicator 类封装了整个去重流程。
generate_signature 是核心方法,实现了前述的5个步骤。
相似度计算使用了Jaccard相似度(基于指纹集合),你也可以改用汉明距离(如果指纹是位向量)。
示例显示,语义相近的 article1 和 article2 指纹相似度较高,而与 article3 相似度极低。
5. 优化与扩展
上述基础实现可以针对实际场景进行优化:
中文分词:对于中文文本,应使用分词工具(如jieba)获得词序列,再构建词级别的N-gram,效果通常优于字符级。
哈希函数:MurmurHash3等非加密哈希速度更快,更适合此场景。
特征选择策略:除了取中段,还可以考虑取哈希值模某个质数后为特定值的特征,以进一步降维。
大规模检索:将指纹存入数据库(如Elasticsearch)或构建内存索引(如字典),通过倒排索引快速找到候选相似文章,再进行精细比对。
动态阈值:根据文章长度、领域等因素动态调整相似度阈值。
6. 总结
mid_signature 算法通过捕捉文本中相对稳定的"中段特征"来生成指纹,在保证对核心内容改动敏感的同时,有效抵抗了开头/结尾的局部扰动。它实现简单、计算高效,是入门指纹算法和文章去重的一个优秀实践案例。
在实际生产系统中,可能需要结合多种指纹算法(如SimHash用于近重复检测,MinHash用于大规模集合相似度计算)以及更复杂的文本预处理和特征工程,以达到最佳的去重效果和性能平衡。
相关推荐
xiaohua10093 小时前
Linux-JVM线程查询
linux·jvm
Super 含3 小时前
Android 启动优化(三):Perfetto 实战——启动时间到底花在哪里?
jvm·算法
会编程的吕洞宾4 小时前
Full GC频繁接口卡顿?JVM排查到调优实战
jvm
不会就选b4 小时前
Linux之线程进阶(一)
jvm
互联网中的一颗神经元5 小时前
01. Go 内存管理全景架构
java·jvm·golang
互联网中的一颗神经元6 小时前
02. 核心概念与术语表
java·jvm·spring
名字还没想好☜19 小时前
Java 线上内存泄漏排查实战:jmap 导堆、MAT 找 GC Roots 与四类常见泄漏
java·开发语言·jvm·内存泄漏
淡海水1 天前
01-07-运行时-GC深度剖析-内存分配回收与结构选择
jvm·windows·unity·gc
counting money1 天前
SpringBoot 项目创建(IDEA不全,还需要修改)
java·spring boot·spring·maven