在采集用户留言、论坛帖子或接口返回文本时,我见过不少类似这样的样本:你是凑企鹅你是凑企鹅你是凑企鹅。第一次看像乱码,复制到编辑器里搜索,才发现是同一句话被连续拼了三次。它不是人写的正常表达,也不是普通口语,而是一段带有明显重复噪音的数据。如果直接把这类文本送去分词、聚类或做关键词统计,轻则污染词频,重则让相似度模型把两条完全无关的文本误判成同一类。更隐蔽的一点是,这类重复文本很难用“完整行去重”一次清掉,因为它在行内已经重复了,而字典、集合类工具只能处理“行与行之间的完全重复”。
这篇文章围绕这条脏数据展开,讲清楚重复文本为什么会出现、怎么识别、怎么清洗、怎么验证,以及过程中有哪些容易踩的坑。我会给出一个可运行的最小 Python 清洗项目,同时介绍 Linux 下的sort、uniq、awk、perl组合用法。读完以后,你可以把这套方法用到日志清洗、评论去重、舆情数据处理和 NLP 训练集预处理中。
1. 先弄清楚重复拼接文本算哪种脏数据
1.1 无语义重复与自然重复并不是一回事
你是凑企鹅你是凑企鹅你是凑企鹅是一个无语义重复的典型样本。整段文字没有新增信息,只有同一句话的机械循环。人一眼能看出来的原因是,它完整复现了同一个子串,并且重复边界非常整齐。
自然语言中的“重复”则完全不同。比如“我说我说我说你还不信”,虽然也出现了连续的“我说”,但它在表达情绪、语气和停顿,并不是单纯的数据冗余。如果清洗逻辑把所有连续出现的短语都压缩成一个,就会把这类正常表达也破坏掉。
因此在设计清洗规则前,必须先区分三种情况:
| 类型 | 示例 | 是否该清理 | 处理难点 |
|---|---|---|---|
| 整行完全重复 | 同一行内容出现 100 次 | 应该清理 | 简单,用去重集合即可 |
| 行内连续重复 | 你是凑企鹅你是凑企鹅你是凑企鹅 | 通常应该压缩为单个短语 | 需要识别重复边界 |
| 语义级重复 | “这台手机很好用”和“这手机使用体验不错” | 需要结合业务判断 | 依赖语义模型或人工规则 |
| 语气重复 | “好好好,我知道了” | 不能一律压缩 | 需要看重复单元长度和上下文 |
一般做文本预处理时,首先处理的是前两类。因为它们可以通过规则和字符串算法稳定识别,误判风险较低。第三类属于语义去重,通常放在检索、聚类或模型训练阶段处理,不能直接使用简单的文本替换。
1.2 这类脏样本通常从哪里来
我在实际日志和数据仓库任务里见过的重复拼接文本,来源主要有四类。
第一类是前端或客户端在采集时做了循环拼接。比如一个字段值被循环写入,写入次数来自错误的上游参数,最后形成一整段无意义的字符串。
第二类是爬虫或数据同步任务在失败重试时,把上一次的部分结果继续追加到新结果后面。如果两条记录本身内容相似,拼接后的文本会出现前后重复段。
第三类是日志框架的堆栈字段被重复输出。比如某些监控系统把异常信息聚合到一起,每聚合一次就把原文本追加一遍,最终在消息队列里留下大量重复行。
第四类是用户刷屏或恶意灌水。匿名评论、问答平台经常有人把一句话连续发布多次,或者通过脚本生成大量近似文本,目的是提高曝光量或干扰自动审核。
这些来源意味着:你很难用一句“数据采集方已经做过清洗”来替自己开脱,数据处理链路的每一层都可能有责任做一次防御性清洗。
1.3 不处理会对后续环节造成什么污染
如果带着这类文本进入下游,影响会逐层放大。
分词阶段,像 jieba 这类基于词频和前缀的模型会把重复片段识别成更长的未知词,导致词边界错误。比如你是凑企鹅你是凑企鹅你是凑企鹅按字符被切成若干个不稳定片段,词频统计结果会明显偏向重复词。
关键词提取阶段,TF-IDF 会把出现次数多的组成部分判断为关键词。一段重复文本本身占了几百字,分词后的字符数量虚高,会让“凑”、“企鹅”这类片段在关键词权重里异常靠前。
文本聚类和相似度检索阶段,重复文本会造成假相似。如果两条消息都包含同一段被重复拼接的内容,即使主体信息完全不同,字面特征也会让它们表现得很相似,从而被错误归入同一个类。
模型训练阶段,重复样本会放慢收敛速度,并造成训练集和验证集之间泄漏。若同一文本以不同重复形式同时出现在训练集、测试集,评估指标就会虚高,线上表现却明显偏离。
所以,清洗重复文本不是洁癖,而是数据质量建设的一环。处理优先级可以放在格式归一化和缺失值填充之后,但要放在特征提取和建模之前。
2. 环境准备:搭建一个最小可运行的清洗实验目录
2.1 建立目录并准备样例数据
这一节我们先在本地跑通一个最小闭环。实验环境只需要 Python 3.8 以上版本,不需要安装第三方依赖。目录结构建议如下:
text_cleaner/ ├── data/ │ ├── raw.txt │ └── normalized.txt ├── scripts/ │ ├── inspect.py │ └── clean.py └── output/ ├── dedup_lines.txt └── repeat_compressed.txt先创建目录并准备一个包含重复噪音的文本文件。raw.txt里我故意混入几种类型的脏数据,方便观察不同规则的清洗效果。
mkdir -p text_cleaner/data text_cleaner/scripts text_cleaner/output样本内容如下:
你是凑企鹅你是凑企鹅你是凑企鹅 今天天气不错,适合跑步 今天天气不错,适合跑步 你是凑企鹅你是凑企鹅你是凑企鹅 这个方案需要再评审一次 这个方案需要再评审一次 这个方案需要再评审一次 收到,明天下午见把上面内容保存为text_cleaner/data/raw.txt。这里有一行是长度为 6 的重复短语拼接,有三行是整行重复,还有一行是正常文本。这样构造可以同时验证“行间去重”和“行内压缩”两个逻辑。
2.2 先做数据体检,不要急着写清洗函数
写清洗脚本之前,先统计原始文件的行数、空行数、重复占比和平均长度。这样能知道当前数据的脏程度,也能在清洗后对比处理效果。下面是一个小脚本scripts/inspect.py,只使用标准库。
from collections import Counter from pathlib import Path def inspect_text(path: str) -> None: text = Path(path).read_text(encoding="utf-8") lines = text.splitlines() non_empty_lines = [line.strip() for line in lines if line.strip()] line_counter = Counter(non_empty_lines) duplicate_lines = { line: count for line, count in line_counter.items() if count > 1 } total_chars = sum(len(line) for line in non_empty_lines) print(f"文件路径: {path}") print(f"非空行数: {len(non_empty_lines)}") print(f"总字符数: {total_chars}") print(f"平均行长度: {total_chars / max(1, len(non_empty_lines)):.2f}") print("重复行统计:") for line, count in duplicate_lines.items(): print(f" {count} 次 -> {line}") if __name__ == "__main__": inspect_text("data/raw.txt")运行命令:
cd text_cleaner python scripts/inspect.py预期的输出会显示非空行数为 8,有两行内容重复出现 2 次,有一行内容重复出现 3 次。这段输出先帮你确认基础重复规模,后续清洗是“减少多少重复行”就有了参照物。
2.3 确定清洗目标和边界
数据处理脚本不能只考虑当前这一个样本。真正投入项目前,需要明确几个边界条件:
第一,清洗对象是字符级别完全重复,还是允许中间有空格、标点、大小写差异。如果要清洗你是凑企鹅 你是凑企鹅这类带空格的版本,就要在匹配前做空白字符归一化。
第二,重复单元的最小长度。若阈值设为 1,那么“哈哈”会被识别成“哈”的两个重复,造成误删。若阈值设得太大,又无法覆盖短文本场景。通常中文环境下,重复片段最小长度可以从 2 开始,例如“你好你好”。
第三,压缩结果保留哪个部分。连续重复时保留第一次出现的片段,还是把两次出现的片段合并成一次?这会直接影响下游对文本语义的判读。
学习环境里,我们可以把这些条件写成脚本参数,方便反复调试。生产环境里,则要先把参数与业务规则固化,再进入自动化流程。
3. 从完全重复到相似重复:逐层去重的实现思路
3.1 第一道防线:去掉整行完全重复
最基础的操作是整行去重。如果数据来自日志中同一时间点的重复上报,或者来自数据库中的冗余读取,整行完全重复出现的概率很高。
使用 Python 的set可以快速去重,但不保留原有顺序。若希望保留第一次出现的顺序,推荐使用dict.fromkeys:
from pathlib import Path def dedup_lines_in_file(input_path: str, output_path: str) -> None: lines = Path(input_path).read_text(encoding="utf-8").splitlines() cleaned_lines = list(dict.fromkeys(lines)) Path(output_path).write_text( "\n".join(cleaned_lines) + "\n", encoding="utf-8", ) print(f"清理前: {len(lines)} 行") print(f"清理后: {len(cleaned_lines)} 行")这里使用dict.fromkeys而不是set,原因是从 Python 3.7 开始 dict 会保留插入顺序,能稳定保留第一次出现的行。对于千万级数据量,内存占用会偏高,更适合用下面的 Linux 命令或者分块文件处理。
3.2 第二道防线:识别行内连续重复并压缩
整行去重处理不了你是凑企鹅你是凑企鹅你是凑企鹅,因为这一行只有一个样本。它的问题不是多行相同,而是单行内重复了同一个子串三次。
下面用“最小重复单元”的思路来检测:若整串文本等于某个子串重复 N 次,就认为它是一段连续重复文本,可以压缩成一次。代码故意从短到长枚举子串长度,并且只允许子串完整覆盖全文本。
def find_repeat_unit(text: str) -> tuple[str, int] | None: """如果 text 由某个子串重复而成,返回 (子串, 重复次数),否则返回 None。""" n = len(text) if n < 2: return None for unit_len in range(1, n // 2 + 1): if n % unit_len != 0: continue unit = text[:unit_len] repeat_count = n // unit_len if unit * repeat_count == text: return unit, repeat_count return None def compress_line(text: str) -> str: unit_info = find_repeat_unit(text) if unit_info: unit, _ = unit_info return unit return text用这个函数处理标题样本:
sample = "你是凑企鹅你是凑企鹅你是凑企鹅" print(find_repeat_unit(sample)) # ('你是凑企鹅', 3) print(compress_line(sample)) # 你是凑企鹅这个做法的核心是,从长度为 1 的候选单元开始尝试,直到长度超过文本一半。只要某个长度能被整除,且按该长度重复后与原文完全相等,就找到了完整重复单元。它比正则表达式更可控,因为正则很难表达“整串必须由同一个短片段重复 N 次组成”这种约束。
实际日志中,一行文本可能并不是“整行重复”,而是中间夹杂正常内容。此时可以先拆分句子,或者把问题弱化为“连续重复区域的识别”。比如某些日志格式为:
用户提交失败,原因:超时,超时,超时,请重试若要把中间的连续重复“超时,超时,超时”压缩成“超时”,就要定位重复片段的位置。一个较稳妥的做法是使用正则:
import re def compress_consecutive_repeats(text: str, min_unit: int = 2) -> str: """把文本中连续重复 2 次以上的片段压缩为 1 次。""" pattern = re.compile(r"(.{%d,}?)\1+" % min_unit) while True: new_text = pattern.sub(r"\1", text, count=1) if new_text == text: break text = new_text return text需要注意,这个正则能处理嵌入在句子里的连续重复,但也可能误伤“我跑跑步跑跑步”这类情况。实际使用时要先看清洗目标,再决定是否对全文本做这种替换。
3.3 用字符熵初步判断文本是否冗余
字符熵可以衡量一段文本的信息量。如果文本只有少数几个字符反复出现,那么熵值会很低。以随机中文字符组成的句子为例,字符种类多且分布均匀,熵值会较高。
计算文本字符熵的脚本:
from collections import Counter import math def char_entropy(text: str) -> float: if not text: return 0.0 counter = Counter(text) total = len(text) entropy = 0.0 for count in counter.values(): p = count / total entropy -= p * math.log2(p) return entropy text_repeat = "你是凑企鹅你是凑企鹅你是凑企鹅" text_normal = "今天天气不错,适合户外跑步" print(char_entropy(text_repeat)) print(char_entropy(text_normal))这里有个有趣的坑:你是凑企鹅只是把同一个短语重复三次,字符频次并没有变化,所以整行熵值与单次短语一样。单独看文本字符熵实际上并不总能识别这种“完整短语重复”。因此建议把熵作为辅助信号,而不是主判断依据。如果你的样本是AAAAAAAA这种字符级重复,熵会明显偏低;若是短语级重复,还是要结合重复单元算法。
3.4 相似文本去重:什么时候需要 SimHash 和向量嵌入
如果清洗对象不只是完全重复或行内连续重复,而是两行文本表达的意思相同但措辞略有不同,就应该使用相似度算法。
常见做法如下表:
| 方法 | 适用规模 | 样本要求 | 优点 | 缺点 |
|---|---|---|---|---|
| 精确集合去重 | 十万级至千万级 | 整行完全一致 | 简单、快、可解释 | 无法处理近似文本 |
| 正则压缩 | 任意文本量 | 行内连续重复 | 能压缩行内噪声 | 规则维护成本高 |
| SimHash | 百万级以上 | 文本特征词可提取 | 分桶后可支持大规模去重 | 中文短文本效果不稳定 |
| MinHash | 文档去重 | 集合式文本 | 高召回、可并行 | 需要 Jaccard 相似度阈值 |
| 编辑距离 | 千条以内 | 精确比较 | 结果准确 | 计算复杂度高 |
| 向量嵌入 | 任意规模 | 需要模型资源 | 能捕捉语义相似 | 需要 GPU 或向量库 |
学习阶段不需要一上来就上 SimHash。先观察数据,如果 90% 的重复都来自“整行完全重复”和“连续拼接”,用前两步方法即可清理大部分问题。只有当你发现重复文本中大量夹杂同义词、口语变体和语气词时,才考虑引入语义去重。
4. 在 Linux 输出端顺手清洗:sort、uniq、awk、perl 的组合用法
4.1 统计重复次数并删除完全重复行
当数据量达到几百万行时,Python 的read_text()会把整个文件加载到内存,可能触发内存不足。Linux 命令更适合做流式排序和去重。
先看每一行的重复次数:
cd text_cleaner/data sort raw.txt | uniq -c | head -n 20输出示例中,计数在最左边,代表该行重复次数。sort会改变行的原始顺序,所以如果只需要“统计频次”,这样最快。
如果需要删除重复行并保留首次出现的顺序,使用awk数组:
awk '!seen[$0]++ {print}' raw.txt > ../output/dedup_lines.txt这个命令的核心是seen[$0]++。当遇到第一行时,seen[$0]为 0,取反后为真,于是打印;第二次遇到同一行时,seen[$0]已经为 1,!1为假,不再打印。对大量文本而言,awk 的处理速度比 Python 脚本快,因为它是 C 语言实现的文本处理工具。
4.2 处理行内连续重复文本
Linux 命令同样可以压缩行内重复。使用 Perl 兼容正则,把连续重复的短片段替换成一次:
perl -CSD -Mutf8 -pe 's/(. {2,}?)\1+/\1/g' ../data/raw.txt > ../output/repeat_compressed.txt需要说明的是,这类正则对 Unicode 中文支持依赖-CSD和-Mutf8参数,而不同平台的 Perl 编译选项可能不同。直接在生产环境执行前,先在小样本上测试。
如果你更习惯 Python,可以把范围控制在脚本里,解析速度也足够。命令行方式的优势是无需编写基础设施,适合日志出现异常时的快速止血。
4.3 用管道一次完成多条规则
实际处理时,多个规则可以串成管道。下面的例子按顺序完成行内连续重复压缩、去除完全重复行、计算剩余行数:
perl -CSD -Mutf8 -pe 's/(.{2,}?)\1+/\1/g' raw.txt | awk '!seen[$0]++' | wc -l使用管道的风险在于:前面步骤的误伤无法在管道内看到。因此本地临时验证可以这样用,但在正式数据清洗任务里,每一步的输出都应当保存成中间文件,方便检查。
5. 怎样验证清洗结果,而不是只数删了多少行
5.1 对比清洗前后的逐行结果
运行清洗脚本后,不能只看剩下多少行,还要打开文件逐段查看。你可以使用diff命令观察被修改的具体文本:
diff data/raw.txt output/cleaned.txt对于你是凑企鹅你是凑企鹅你是凑企鹅这行,预期 diff 显示的旧版是 18 个字符,新版是 6 个字符。这个变化意味着压缩成功。对于本来就正常的文本,diff 不应出现任何差异。
如果清洗批量很大,人工逐行查看不现实,需要抽样。写脚本随机抽取清洗前和清洗后的行对,让业务同学抽查 100 到 200 条,确认没有明显误删。
5.2 统计压缩比和误删率
在文本清洗任务里,有两个指标很有价值。
指标一:文本压缩率。按行计算清洗后的总字符数除以清洗前的总字符数,看整体文本缩减了多少。若压缩率明显低于预期,可能是重复内容过多;若接近 100%,说明当前数据处理步骤几乎没起作用。
指标二:误删率。人工标记一份 100 条的小样本,明确哪些行不应该被修改、哪些短语不应该被压缩。用清洗脚本处理后,统计被错误修改的样本比例。
from pathlib import Path def compression_ratio(original: str, cleaned: str) -> float: len_before = len(Path(original).read_text(encoding="utf-8")) len_after = len(Path(cleaned).read_text(encoding="utf-8")) return len_after / max(1, len_before) * 100 print(compression_ratio("data/raw.txt", "output/cleaned.txt"))这份 100 条小标注集同时也是回归测试集。以后每次修改清洗规则,都要重新跑一遍,看误删率是否升高。
5.3 检查下游分词结果是否恢复正常
如果文本最终要进入分词或词频统计,可以清洗前后分别跑一遍相同的 jieba 分词代码,比较关键词列表。清洗之前,你是凑企鹅会以完整片段或异常片段高频出现;清洗之后,它应当只出现一次,不再干扰其他正常词。
import jieba from collections import Counter def top_keywords(text: str, top_n: int = 10): words = [w.strip() for w in jieba.cut(text) if w.strip()] return Counter(words).most_common(top_n) original = Path("data/raw.txt").read_text(encoding="utf-8") cleaned = Path("output/cleaned.txt").read_text(encoding="utf-8") print(top_keywords(original)) print(top_keywords(cleaned))如果清洗后关键词分布更接近业务直觉,说明去重方向正确。如果某些正常短语被误删,则在词频中会看到明显缺失。这也是文本清洗效果最直观的验证方式。
6. 常见坑:为什么清不干净,为什么误删正常文本
6.1 只做整行去重,漏掉行内重复
很多初学者拿到你是凑企鹅你是凑企鹅你是凑企鹅,第一反应是交给set或uniq。做完发现数据量没减少,因为文件中只有这一行,它并不会被整行去重捕获。
定位方式:打印每条文本的长度,观察是否存在长度异常偏长的行。再看是否可以通过“最小重复单元检测”被压缩。
处理建议:在整行去重之前,先做一层行内连续重复压缩。两者是不同粒度的操作,不能互相替代。
6.2 正则重复阈值太小,导致误伤正常表达
把正则写成(.{1,}?)\1+,会把“好好休息”中重复的“好”字识别为连续重复,压缩成“好休息”,破坏语义。
推荐将最小重复单元长度设为 2 或者 3。中文常见叠词“慢慢走”“哈哈笑”很多是单字或双字重复,业务中是否需要保留要提前确定。宁可漏过少量脏数据,也比把正常语句改错要安全,因为漏过的数据还能通过上游策略再处理。
6.3 大小写、全半角和不可见字符导致去重失败
你是凑企鹅 你是凑企鹅与你是凑企鹅你是凑企鹅从肉眼看很像,但一个中间有空格,一个没有,字符串并不相同。全角逗号和半角逗号、换行符、制表符、零宽空格都可能成为匹配失败的元凶。
检查办法是打印文本的repr()或十六进制表示:
line = "你是凑企鹅\u200b你是凑企鹅" print(repr(line)) # '你是凑企鹅\u200b你是凑企鹅'处理办法:先做 Unicode 归一化,将全角字符转为半角、去掉零宽空格,再清洗。例如使用标准库unicodedata.normalize("NFKC", text)。
import unicodedata def normalize_text(text: str) -> str: text = unicodedata.normalize("NFKC", text) text = text.replace("\u200b", "") # 去掉零宽空格 return re.sub(r"\s+", "", text.strip())6.4 把口语化正常重复也压缩掉了
像“是是是,你说得对”“好好好,我马上看”这类文本,连续重复并不是拼写错误,而是表达情绪。
处理方式有几种。第一种是看重复片段是否构成完整语义单元。“是是是”由单字重复构成,通常不算脏数据,如果强制压缩成“是”会改变语气。第二种是结合业务语境,比如在客服工单中,用户重复敲字可能表示强调,不一定需要清理。第三种是使用白名单,在脚本中保留“好好好”“是是是”“哈哈哈”等常见口语模式。白名单需要持续积累,不能一劳永逸。
6.5 内存不足或无脑加载整个文件
处理超大文本时,避免一次性读取整个文件到内存。推荐用生成器逐行处理:每读一行、清洗一行、写出到临时文件,最后再合并。
from pathlib import Path def stream_clean(input_path: str, output_path: str) -> None: input_file = Path(input_path) output_file = Path(output_path) with input_file.open("r", encoding="utf-8") as fin, \ output_file.open("w", encoding="utf-8") as fout: for line in fin: line = line.rstrip("\n") compressed = compress_line(line) fout.write(compressed + "\n")这种方式只需要保存当前行和输出句柄,不会一次性占用大量内存。对于 GB 级文件,还能结合fileinput或awk分担压力。
6.6 清洗结果没有回归验证,规则越写越乱
清洗规则会随着需求增长而增多。如果每新增一条规则都不重新跑历史样本,很容易出现“解决了新问题,破坏了旧样本”的情况。常见现象是:某天业务方反馈大量正常文本被改成单字,而你很难定位是哪条规则导致的。
建议维护一个最小回归集,包含所有同类问题的代表样本,例如:整行重复、带空格的连续重复、口语化叠词、URL 参数拼接、日志正常文本。每次修改脚本后运行:
python scripts/clean.py --input tests/sample_raw.txt --output tests/sample_cleaned.txt diff tests/expected.txt tests/sample_cleaned.txtexpected 文件由人工审核确认,只要 diff 为空,就可以继续往更大范围的数据上运行。
7. 从学习脚本到生产流程:可复用清单与扩展方向
7.1 清洗规则执行优先级
清洗规则不能随机堆叠,建议按以下顺序执行,能降低误删风险:
- 用
unicodedata.normalize("NFKC", text)做 Unicode 归一化,并去掉零宽空格。 - 将换行、制表符导致的多行文本按字段处理后拆回单行。
- 去掉 URL、HTML 标签、邮箱等明显格式噪声时,使用白名单式正则。
- 做整行完全去重,保留第一次出现的顺序。
- 做行内连续重复压缩,最小重复单元长度建议为 2。
- 根据业务需要加入语气白名单,避免误删正常叠词。
- 对长度异常长、重复单元明显的文本单独抽样审核。
- 最后再进入语义级相似度去重,并保留原始文本用于审计。
这八步并不是严格顺序,而是风险从低到高、粒度从不精确到精确的递进。风险越高的步骤越靠后,越需要人工抽样检查。
7.2 学习环境与生产环境的差别
在小数据集上写脚本,可以通过read_text()直接完成,因为数据不大,方便调试。进入生产环境后,需要考虑几个因素:
| 维度 | 学习/实验环境 | 生产环境 |
|---|---|---|
| 数据量 | 几百行到几十万行 | 每天上亿条日志或评论 |
| 处理方式 | 单机 Python 脚本 | Spark、Flink、Kafka Streams 等分布式任务 |
| 规则维护 | 写在 demo 脚本里 | 配置中心或在表结构里保存清洗版本 |
| 可观测性 | 打印删了多少行 | 输出 metrics、报警、审计日志 |
| 回滚 | 重新跑一次脚本 | 保留原文件、清洗后文件、规则版本 |
| 样本验证 | 人工抽查 | 定期运行回归集,设置误删率阈值 |
生产环境里,文本清洗不只要“删掉重复”,还要记录每条文本是在哪个环节、哪条规则下被修改的。比较稳妥的做法是增加一个清洗事件表,字段包括原始文本哈希、清洗后文本哈希、规则 ID、处理时间。这样出了问题能回查,不至于在黑盒流程中改坏数据。
7.3 开发验收前后可以对照的检查清单
每写完一版清洗流程,可以按这个清单逐项打勾:
- 是否保存了原始文本备份,防止误删后不可恢复。
- 是否清理了全角、半角、零宽空格的差异。
- 整行完全重复是否已按首次出现顺序去重。
- 是否验证过“行内连续重复”场景,而不是只做了整行去重。
- 最小重复单元长度是否设置合理。
- 是否加入口语叠词白名单,比如“好好好”“是是是”。
- 是否使用抽样 diff 或人工检查确认没有误删正常文本。
- 是否记录了规则版本和处理数据版本。
- 是否有回归测试集,能在规则变更后自动发现破坏。
- 是否确认内存、CPU、输出文件落盘方式满足生产要求。
这份清单同样适用于你接手的任何一个历史清洗脚本。先按它逐项检查,再决定能不能直接上线,能避免很多隐性风险。
7.4 从单条样本识别到语义质量监控
回到开头的你是凑企鹅你是凑企鹅你是凑企鹅,它其实是一个很好的数据质量信号。如果在业务数据里突然大量出现这类“同一短语连续拼接”文本,说明上游采集或生成逻辑可能出问题了,而不只是需要在清洗阶段花力气修正。
更完整的做法是:把这段文本的重复度做成一个监控指标。例如抽取每天落库文本的 1%,计算“最小重复单元长度”和“压缩比”,当异常比例超过设定阈值时触发告警。这会促使团队去排查上游生成逻辑,而不是永远在下游打补丁。
真正有价值的文本清洗,不应该只满足于把脏数据修掉。它应该帮你发现脏数据从哪来,并推动上游修复。掌握了重复文本识别、压缩和验证方法后,你可以进一步研究文本指纹、SimHash、MinHash 或向量化检索,把“字符串去重”升级为“语义级去重”。但无论使用哪种高级方案,规则清洗和回归验证依然是不可省略的基础层。