摘要:"重复"的行并不总是完全相同的字节——尾部空格、大小写不同或 Unicode 归一化方式不同,都可能掩盖真正的重复,或制造出虚假的重复。理解比较的原理,能让清理更精准,而不是误删你需要的行。可搭配 Remove Duplicate LinesText DifferenceWord FrequencyRemove Extra Spaces 一起使用。

CharCount 的 Duplicate Finder 可从列表、日志、CSV 导出文件和关键词列表中删除重复行。真正棘手的部分不是删除本身——而是在删除任何内容之前先确定什么算作"同一行"。

技术背景:MDN 关于 JavaScript Set 的参考文档解释了大多数去重工具内部使用的数据结构,MDN 的 String.normalize() 指南介绍了 Unicode 归一化,而 Unicode 文本分段报告(UAX #29)则定义了文本如何被切分为有意义的单元。

重复检测的真正原理

在底层,重复行工具会遍历每一行,检查它是否已经出现过——通常使用 Set,这种结构存储唯一值,并以常数时间拒绝完全重复的项。这很快,但"完全"这个词在这句话里承担了大量工作:Set 比较的是字符串的 UTF-16 代码单元序列,所以 "苹果""苹果 "(尾部有空格)是不同的条目,大小写不同的英文单词同理。

这就是为什么对 CSV 导出文件或抓取来的关键词列表做简单去重经常"失败"——它并没有坏,它只是精确比较了你给它的内容,包括你没注意到的不可见空格和大小写差异。

保留顺序 vs. 先排序再去重

常见有两种策略。保留顺序的去重会保留每一行第一次出现的位置——这是日志、聊天记录导出,或任何顺序有意义的场合的正确选择。先排序再去重(如经典的 Unix 管道 sort | uniq)会先把所有内容按字母顺序重新排列,这在处理巨大文件时更快,但会破坏原始顺序——适合关键词列表,不适合变更日志。

只要"谁先出现"很重要,就选择保留顺序的去重;只有当列表原本的顺序就是任意的时,才选择先排序再去重。

归一化在何处改变去重结果

Unicode 允许同一个可见字符以不同的码位序列存在——一个带音调符号的字符,既可以是单个组合码位(NFC),也可以是基础字母加单独的组合变音符号(NFD)。两行在屏幕上看起来完全一样的文本,如果一行来自 Mac 导出、另一行来自 Windows 导出,就可能因为归一化形式不同而在精确匹配去重中失败。

String.normalize('NFC') 会在比较前把两种形式归约为同一种表示,这就是为什么一个好的去重工具会先对文本做归一化——否则它会悄悄漏掉那些视觉上相同、但 Unicode 序列不同的重复项。

更安全的清理流程

  • 先决定:这份列表的行顺序重要吗?这决定了你的策略。
  • 只有当列表本应不区分大小写时(关键词列表通常如此;密码列表绝不应如此),才去除尾部空格并统一大小写。
  • 执行去重,然后抽查被删除的行,确认它们确实是重复项。
  • 在验证清理后的版本之前,保留原始文件。

对于任何存疑的情况,用 Text Difference 比较前后版本,准确看看删除了什么,再用 Word Frequency 确认没有合理的重复词语被当作噪音处理。