HashSet流式去重需逐行读取、显式编码、换行标准化、长度防护、缓冲写入;超10GB时分块哈希+外部排序,忽略大小写用StringComparer.OrdinalIgnoreCase而非ToLower()。

用 HashSet 流式去重,避免内存爆炸
直接把几 GB 的文件全读进 List 再用 Distinct(),基本等于触发 OutOfMemoryException。关键不是“去重逻辑”,而是“不加载全文到内存”。HashSet 是唯一能兼顾查重速度(O(1))和内存可控的结构,但必须配合逐行读取。
- 用
File.ReadLines(path)(不是File.ReadAllLines),它返回IEnumerable,真正按需读取,不缓存整文件 -
HashSet只存已见行的哈希值,字符串本身仍只在当前行生命周期内存在;重复行直接跳过,不进集合 - 若文件含 BOM 或混合换行符(
\r\n/\n),先用line.TrimEnd('\r', '\n')统一处理,否则"abc\r\n"和"abc"被视为不同行
处理超长行或特殊编码时,别硬扛默认编码
默认 File.ReadLines 用 UTF-8,但遇到 GBK 编码的中文日志、或某行末尾有未闭合引号导致解析错位,就会乱码或截断——此时去重结果全错。必须显式指定编码,且对单行长度设防。
- 改用
new StreamReader(path, Encoding.GetEncoding("GBK"))+ReadLine()循环,比File.ReadLines更可控 - 加长度检查:若
line?.Length > 10_000_000,记录警告并跳过(防恶意超长行拖垮哈希计算) - 若需忽略大小写去重,初始化
HashSet时传StringComparer.OrdinalIgnoreCase,别自己调ToLower()——后者会额外分配字符串对象
写入结果时用 StreamWriter 批量刷盘,别每行 Flush()
去重后写新文件,如果对每一行都调 sw.WriteLine(line); sw.Flush();,磁盘 I/O 次数翻几十万倍,速度暴跌。缓冲区大小和刷盘时机得手动管。
- 构造
StreamWriter时指定缓冲区:new StreamWriter(outputPath, false, Encoding.UTF8, 64 * 1024)(64KB 缓冲) - 完全写完再
Close()或Dispose(),让底层自动刷盘;除非中途崩溃风险高,否则别主动Flush() - 若输出需保持原文件编码,从输入流读取时记下
streamReader.CurrentEncoding,传给StreamWriter构造函数
真遇到 10GB+ 文件,考虑分块哈希 + 外部排序
当单机内存不足(比如只有 4GB RAM 却要处理 12GB 日志),HashSet 仍可能因哈希碰撞或字符串驻留膨胀而 OOM。这时得放弃“一行一判”,改用确定性分片。
- 先按首字母或哈希前缀把原文件拆成多个小文件:
line.GetHashCode() % 100→ 分到 00–99 个临时文件 - 每个小文件单独用
HashSet去重,生成中间去重文件 - 最后合并所有中间文件,再跑一次去重(此时数据量已大幅下降,可全载入内存)
- 注意:此法不保原始顺序;若需稳定序,改用
SortedSet替代HashSet,但性能降约 30%
实际最常被忽略的是换行符标准化和编码探测——很多“去重无效”问题,根源是 "abc" 和 "abc "(带空格)或 "abc\uFEFF"(带 BOM)被当成不同行,而不是算法本身慢。










