ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TXT重复字查找与清理:Word正则Python实战指南

TXT重复字查找与清理:Word正则Python实战指南 1. 先分清四种“重复”方案才不会用错前阵子我帮人整理一本网络小说TXT全文上百万字里面光是“的的”“了了”这种连续重复字就出现二百多处。用眼睛一行行看看到凌晨也看不完。后来我写了个小脚本把连续重复字、叠字和重复行一次性扫出来不到一分钟就出了完整清单。这篇就把整套从手工到脚本的玩法摊开讲包括每个方案的适用范围和容易踩的坑。但动手之前一定要先分清你说的“重复”到底是哪一种。因为“txt查找连续重复的字”“删除重复的文字”“查找叠字”这三件事对应的工具和正则写法差别很大用错了不是删不干净就是把正常内容误删。重复类型典型例子常见处理方向连续重复字“的的”“了了”“哈啰哈啰”压缩成单字、保留N个字叠字/叠词“青青”“悠悠”“高高兴兴”通常是修辞只查不改连续重复词语“快乐快乐”“重要重要”删成单个词语连续重复行整段章节重复、日志连续相同行按行去重、合并流水一个很容易被忽略的点是叠字本身不一定是错别字。“青青子衿悠悠我心”里的“青青”“悠悠”是正常修辞不该被一键删掉。所以正规做法应该是“先扫描定位再人工确认哪些要处理”而不是上来就全局替换。另外文件大小直接决定工具路线。几百KB的文本用Word或Notepad手工处理完全够用几十MB甚至几百MB的TXT用记事本打开都费劲更别说全局查找替换。按我自己的经验通常这样选小于几百KB、只是想处理几个已知错误字用 Word/WPS 查找替换。会一点正则、文件在几十MB以内用 Notepad最快最直观。文件大、需要批量处理多个txt直接用Python脚本一次性读入扫描、输出清单、生成清洗后的文件。2. 用Word/WPS做小范围替换适合“已知错误”而不是“未知扫描”很多人第一步想到的是把TXT内容复制进Word用查找替换。这个方法不是不行但只适合你已经知道要改哪些字的情况。2.1 已知“的的”“了了”用普通查找替换就够了打开Word或WPS按CtrlH进入替换界面查找内容输入“的的”替换为“的”然后逐次点击“全部替换”。注意在替换前先点一次“查找全部”通过左下角的“找到 XX 项”确认实际数量避免文档里恰好有“目的的士”这类不该动的内容。实际操作中我建议把常见错误字列一个清单像“的的、了了、是是、就就、能能、哈哈、呵呵”一次性查完。但这里有一个Word处理中文叠字的硬伤它没法用通配符表达“任意连续两个相同汉字”。Word通配符里的?代表任意一个字符但??匹配的是任意两个字符不保证两个字符相等。所以你想搜索所有叠字Word做不到至少做不到像Notepad那样一个正则全扫出来。2.2 WPS和Word的小技巧用“查找全部”当扫描器虽然Word不能匹配“任意叠字”但你可以利用它做分类统计。先在查找框输入“的的”点击“查找全部”底部会列出所有命中位置点击任意一条就能跳到上下文快速判断是否要改。这个方法在文本不大时很好用因为人工判断的成本很高Word列表能帮你一屏看几十处。如果文件有上百万字Word打开就卡而且“全部替换”一旦点错撤销栈跟不上容易把源文件弄乱。所以我处理大TXT时最多用Word处理前几千字的样本确定替换规则后还是转到脚本或正则编辑器。2.3 为什么我不建议用Word处理整本小说Word的查找替换本质是面向文档编辑不是面向纯文本批量清洗。TXT文件超过10MBWord打开就很吃力超过50MB很容易假死。更重要的是普通TXT没有Word那种段落样式约束你替换错一个正则很难通过“撤消”找回大文件被批量改动的内容。如果只是偶尔改几个已知重复字Word是零学习成本的选择但如果目标是“把整个目录下的txt全部扫描一遍”Word的效率和组织性都不够。我的结论很简单Word适合你已经有明确目标文本时做定点修改不适合探索式扫描。3. Notepad正则匹配一次找出所有连续重复字如果你还没装Notepad建议先装一个。它是免费软件打开大TXT比记事本流畅得多而且自带PCRE正则查找替换处理“连续重复字”这类问题非常顺手。3.1 核心正则(.)\1在Notepad里按CtrlF打开搜索勾选“正则表达式”。查找内容(.)\1这段正则的意思(.)先捕获任意一个字符\1表示“和刚才捕获的那个字符相同”表示至少再重复一次。点“全部查找”结果面板会列出所有命中项。比如文本里有“哈哈”“的的”“111”它都会找出来。注意这里的.默认不会跨行所以不会把行尾和下一行开头匹配到一起这是好事。如果你只想找中文叠字不想误伤英文里的ll、数字里的11可以稍微改一下查找内容([一-龥])\1这个正则的[一-龥]表示从汉字“一”到“龥”的Unicode范围基本覆盖常用汉字。这样will里的ll不会被命中中文叠字照常能查出来。3.2 替换规则压缩成1个还是保留2个扫描之后如果决定替换按CtrlH查找模式选“正则表达式”。如果想把所有连续重复字压缩成一个比如“的的”变“的”、“哈哈哈哈”变“哈”查找内容(.)\1替换为\1如果想把连续重复字保留两个比如“哈哈哈哈”变“哈哈”、“的的的”变“的的”查找内容(.)\1{2,}替换为\1\1这里要特别解释一下(.)\1{2,}它匹配的是“同一个字符连续出现3次及以上”所以只有两字叠词不会被碰。很多人在这一步栽跟头以为用(.)\1然后替换成\1\1就能保留两个实际不行——那是把重复串全部替换成两个字符但如果原文只有两个重复字也会被替换成两个看着没差别。真正干净的写法是上面这种。3.3 匹配中文重复词语或短句连续重复的词语通常不是单字而是两个或更多字组成的词比如“快乐快乐”“好的好的”“你来了你来了”。Notepad可以用下面这个正则查找内容([一-龥]{2,6})\1替换为\1这个正则捕获2到6个汉字然后要求后面再出现一遍同样内容。点“全部查找”时你会发现“快乐快乐”被整段命中替换后就只剩下一个“快乐”。但要注意这个写法很容易误匹配到“东南西北西北风”这种恰好包含重复片段的句子。所以中文重复词我从来不做全局替换都是先“全部查找”看一遍命中列表再决定是否单独替换。还有如果重复词中间有标点比如“快乐快乐”上面的正则就匹配不到因为中间多了标点。这时候可以换成查找内容([一-龥]{2,6})[。!?,.\s]?\1这个写法允许中间夹一个标点或空格但不要贪心把[。!?,.\s]?改成*否则它可能跨过很长一段文本去匹配极易误伤。4. Python脚本批量扫描、定向删除、整目录处理如果文件超过几十MB或者你有一整个文件夹的TXT要处理Notepad手工点击效率就太低了。改用Python脚本几分钟就能跑完。这里的思路依然是“先扫描、后清洗”避免脚本自动删掉不该删的内容。4.1 先扫描叠字和连续重复字把下面代码保存成scan_duplicates.py和你要处理的demo.txt放在同一目录然后在命令行运行import re def scan_duplicate_chars(text, min_repeat2, max_hits300): # min_repeat2 表示查找连续重复2次及以上的字符 pattern re.compile(r(.)\1{%d,} % (min_repeat - 1)) hits [] for m in pattern.finditer(text): start max(0, m.start() - 10) end min(len(text), m.end() 10) context text[start:end].replace(\n, \\n) hits.append((m.group(), m.start(), context)) return hits[:max_hits] if __name__ __main__: with open(demo.txt, encodingutf-8) as f: text f.read() for dup, pos, ctx in scan_duplicate_chars(text): print(f{pos:8d} {dup!r:10} ...{ctx}...)运行后输出每一条命中的位置、重复内容和上下文。我一般只打印前300条避免控制台被刷爆。如果命中的是“的的”你可以继续往下看如果命中“哈哈”这种语气词你心里就有数知道不能全局删。4.2 清洗脚本连续重复字、重复词、重复行确定规则后再用清洗脚本。以下这个clean.py会依次做三件事把连续汉字重复压缩为单字删除连续重复词语删除相邻重复行。import re def reduce_hanzi_repeats(text, keep1): # keep1: 连续重复的汉字只保留1个 # keep2: 连续重复的汉字最多保留2个 if keep 0: return text pattern re.compile(r([\u4e00-\u9fa5])\1{%d,} % keep) return pattern.sub(lambda m: m.group(1) * keep, text) def remove_consecutive_phrases(text, min_len2, max_len6): # 中文词语或短句连续重复只保留第一段 pattern re.compile( r([\u4e00-\u9fa5]{%d,%d})(?:[\s。!?,.]*\1) % (min_len, max_len) ) return pattern.sub(r\1, text) def remove_adjacent_duplicate_lines(text): # 相邻两行内容相同只保留第一行 lines text.splitlines() out [] prev None for line in lines: key line.strip() if key ! prev: out.append(line) prev key return \n.join(out) def read_text_auto(path): # 自动尝试常见编码优先处理Windows下常见的UTF-8/GBK for enc in (utf-8-sig, utf-8, gb18030): try: with open(path, r, encodingenc) as f: return f.read() except UnicodeDecodeError: continue with open(path, r, encodingutf-8, errorsignore) as f: return f.read() if __name__ __main__: text read_text_auto(demo.txt) # 按需开关不是每一个都要执行 # 把两个以上重复汉字缩成一个 text reduce_hanzi_repeats(text, keep1) # 把“快乐快乐”这类连续重复词删成单个 text remove_consecutive_phrases(text) # 删除相邻重复行 text remove_adjacent_duplicate_lines(text) with open(demo_clean.txt, w, encodingutf-8-sig) as f: f.write(text)read_text_auto这个函数解决了很多Windows用户最常见的编码问题。老式TXT很多是ANSI/GBK编码直接用UTF-8读会抛错或产生乱码。这里按utf-8-sig、utf-8、gb18030的顺序尝试基本能覆盖绝大多数中文TXT文件。写入时用utf-8-sig也就是带BOM的UTF-8这样Windows记事本打开不会乱码。4.3 批量处理整个目录下的TXT如果只有一个文件运行脚本就够了。但我实际用到最多的场景是“某文件夹里十几个小说txt全都要清理”。这时把上面的处理函数和下面的批量逻辑合到一起from pathlib import Path def clean_file(src): text read_text_auto(src) text reduce_hanzi_repeats(text, keep1) text remove_consecutive_phrases(text) text remove_adjacent_duplicate_lines(text) out src.with_name(src.stem _clean src.suffix) with open(out, w, encodingutf-8-sig) as f: f.write(text) return out if __name__ __main__: for path in Path(.).glob(*.txt): if path.name.endswith(_clean.txt): continue print(正在处理:, path.name) clean_file(path)这里有个细节批量处理时输出文件名加_clean后缀保留原始文件不动。我第一次写脚本时直接原地覆盖结果正则考虑不周整个文件被改坏原始数据又没备份只能重新下载。从那以后我的所有文本清洗脚本默认都是“写新文件”。5. 编码、备份、正则误伤最容易拖垮进度的三个坑这一节不是废话而是我踩过最多的地方。很多人不是不会找叠字是前脚搜出来后脚就把整个文件搞得乱码或者误删。5.1 编码是TXT处理的第一道坎TXT没有任何格式信息同一个文件可能是UTF-8、UTF-16 LE、ANSI/GBK。用Notepad打开时如果中文显示成乱码先别急着替换在“编码”菜单里切换“转为ANSI”“转为UTF-8”看效果。有一个容易混淆的概念菜单里有“以UTF-8编码打开”和“转为UTF-8编码”两种选项。前者是重新读文件后者是把当前内存内容转换编码后保存。如果你文件已乱码直接“转为UTF-8”只会把乱码状态固定下来。正确做法是先尝试不同编码打开看到正常中文后再做后续操作。在Python里gb18030比gbk更通用它能兼容几乎所有GBK字符还覆盖生僻字。所以我脚本里面的编码尝试顺序是固定的utf-8-sig在前gb18030兜底。5.2 备份比多写十行代码更有用做任何批量替换前先复制一份备份。最简单的做法copy demo.txt demo.txt.bak或者把整个文件夹打包。用Notepad时虽然可以CtrlZ撤销但如果文件几十MB撤销一次要等很久而且Notepad不一定能完整恢复。用脚本清洗时脚本也没有可交互的撤销机制。所以我自己养成了一个习惯凡是正则替换和代码清洗永远先扫后改凡是批量修改永远先备份。5.3 正则误伤到底有多常见(.)\1看着简单实际误伤率极高。英文文本里will会被它匹配成ll重复替换后变成wil数字文本里119911会被层层匹配标点符号里连续感叹号也会被匹配。还有一类隐蔽误伤是中文本身。比如“明明”是正常词但如果你在处理“明明明明白白我的爱”这类歌词或口语文本正则很可能把三个字以后的内容压缩掉直接破坏原文。这就是为什么我不建议任何人跳过“扫描清单”直接“全部替换”。用([一-龥])\1这类汉字限定正则能规避一部分英文和数字误伤但语义层面的误伤只能靠人眼确认。另一个常见错误是替换方向搞反。很多人想删除重复文字结果把查找内容写成\1替换成(.)\1等于把所有单字变成重复越换越多。检查一下方向确保“查找内容”是正则“替换为”是反向引用这个低级错误通常就不会犯。6. 扩展用法从叠字词频到小说水字清理这套方案不只用于改正错别字我后来把它扩展到了几个实际工作场景效果都不错顺手分享出来。6.1 叠字词频统计如果你需要统计一部作品里所有叠字出现的频率可以用这段代码import re from collections import Counter text read_text_auto(demo.txt) counter Counter(m.group() for m in re.finditer(r([\u4e00-\u9fa5])\1, text)) for word, count in counter.most_common(30): print(word, count)注意([一-龥])\1匹配的是两个相同汉字连在一起像“青青”“悠悠”“明明”都会命中然后按出现次数排序。做诗词或语料分析时这个列表能直接告诉你叠字风格集中在哪几个字上。6.2 小说TXT的“水字”清理组合拳网络小说下载文件里常见两类污染一是同义章节标题重复出现二是整段重复的“水字”。对章节标题我会先用Notepad扫描第.{1,6}章之类的重复行对整段重复我会用remove_adjacent_duplicate_lines做一次相邻去重。这样做的原因是整段重复通常都是紧挨着的不会误删全书其他位置同样会出现的正常句子。6.3 日志文件压缩如果你处理的不是中文TXT而是服务器日志重复行会连续刷屏。remove_adjacent_duplicate_lines同样适用但更聪明的做法是把连续重复的次数记下来而不是直接删除。改一行代码就能实现在判断key prev时给计数器加一不输出重复内容等遇到新行时再把上一行的重复次数附在行尾。这样日志从几十万行压缩到几千行信息量一点不少。最后分享一个我个人坚持了很多年的习惯所有文本清洗任务第一次运行永远是在副本上做。等输出文件确认无误再把原文件覆盖掉。这个习惯一开始看起来有点笨但只要你手滑误删过一次就会明白它有多值钱。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进