用 HashSet<string></string>
流式去重,避免内存爆炸
直接把几 GB 的文件全读进
List<string></string>再用
Distinct(),基本等于触发
OutOfMemoryException。关键不是“去重逻辑”,而是“不加载全文到内存”。
HashSet<string></string>是唯一能兼顾查重速度(O(1))和内存可控的结构,但必须配合逐行读取。 用
File.ReadLines(path)(不是
File.ReadAllLines),它返回
IEnumerable<string></string>,真正按需读取,不缓存整文件
HashSet<string></string>只存已见行的哈希值,字符串本身仍只在当前行生命周期内存在;重复行直接跳过,不进集合 若文件含 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<string></string>时传
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<string></string>替代
HashSet,但性能降约 30%
实际最常被忽略的是换行符标准化和编码探测——很多“去重无效”问题,根源是
"abc"和
"abc "(带空格)或
"abc\uFEFF"(带 BOM)被当成不同行,而不是算法本身慢。
