基于FileSystemWatcher的文本文件对比:处理行增减与偏移问题
优化文件变更对比方案(处理行增删移+忽略新增空行)
针对你的需求,核心问题是传统逐行对比无法处理行的增删导致的偏移,我们可以通过最长公共子序列(LCS)算法来匹配内容相同的行,从而准确识别行的新增、删除、修改和移动,同时实现忽略新增空行的日志规则。以下是具体实现方案:
1. 核心数据结构定义
先定义用于存储行信息和变更记录的类,方便追踪原始行号和变更类型:
// 存储文件行的原始信息 public class FileLine { public int OriginalLineNumber { get; set; } public string Content { get; set; } public bool IsEmpty => string.IsNullOrWhiteSpace(Content); } // 变更类型枚举 public enum ChangeType { Added, Deleted, Modified, Moved } // 变更日志条目 public class ChangeEntry { public ChangeType ChangeType { get; set; } public int? ReferenceLineNumber { get; set; } public int? MainLineNumber { get; set; } public string Content { get; set; } public string OldContent { get; set; } public string NewContent { get; set; } }
2. 文件读取与预处理
读取文件时保留每行的原始行号,为后续对比做准备:
private List<FileLine> ReadFileWithLineNumbers(string filePath) { return File.ReadAllLines(filePath) .Select((line, index) => new FileLine { OriginalLineNumber = index + 1, // 行号从1开始 Content = line }) .ToList(); }
3. 基于LCS的文件对比逻辑
通过LCS算法找出两个文件中匹配的行,再推导出行的增删改移,同时过滤新增的空行:
private List<ChangeEntry> CompareFiles(List<FileLine> referenceLines, List<FileLine> mainLines) { var changes = new List<ChangeEntry>(); int m = referenceLines.Count; int n = mainLines.Count; // 构建LCS动态规划表 int[,] dp = new int[m + 1, n + 1]; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (referenceLines[i - 1].Content.Equals(mainLines[j - 1].Content, StringComparison.Ordinal)) dp[i, j] = dp[i - 1, j - 1] + 1; else dp[i, j] = Math.Max(dp[i - 1, j], dp[i, j - 1]); } } // 回溯标记增删行,忽略主文件新增的空行 int iPtr = m, jPtr = n; var matchedRefIndices = new HashSet<int>(); var matchedMainIndices = new HashSet<int>(); while (iPtr > 0 && jPtr > 0) { if (referenceLines[i - 1].Content.Equals(mainLines[j - 1].Content, StringComparison.Ordinal)) { matchedRefIndices.Add(iPtr - 1); matchedMainIndices.Add(jPtr - 1); iPtr--; jPtr--; } else if (dp[iPtr - 1, jPtr] > dp[iPtr, jPtr - 1]) { // 参考文件存在、主文件缺失:标记为删除 var deletedLine = referenceLines[iPtr - 1]; changes.Add(new ChangeEntry { ChangeType = ChangeType.Deleted, ReferenceLineNumber = deletedLine.OriginalLineNumber, Content = deletedLine.Content }); iPtr--; } else { // 主文件存在、参考文件缺失:新增行,忽略空行 var addedLine = mainLines[jPtr - 1]; if (!addedLine.IsEmpty) { changes.Add(new ChangeEntry { ChangeType = ChangeType.Added, MainLineNumber = addedLine.OriginalLineNumber, Content = addedLine.Content }); } jPtr--; } } // 处理剩余未遍历的行 while (iPtr > 0) { var deletedLine = referenceLines[iPtr - 1]; changes.Add(new ChangeEntry { ChangeType = ChangeType.Deleted, ReferenceLineNumber = deletedLine.OriginalLineNumber, Content = deletedLine.Content }); iPtr--; } while (jPtr > 0) { var addedLine = mainLines[jPtr - 1]; if (!addedLine.IsEmpty) { changes.Add(new ChangeEntry { ChangeType = ChangeType.Added, MainLineNumber = addedLine.OriginalLineNumber, Content = addedLine.Content }); } jPtr--; } // 识别修改行:未匹配的行中,数量相等则判定为修改 var unmatchedRef = referenceLines.Where((l, idx) => !matchedRefIndices.Contains(idx)).ToList(); var unmatchedMain = mainLines.Where((l, idx) => !matchedMainIndices.Contains(idx) && !l.IsEmpty).ToList(); if (unmatchedRef.Count == unmatchedMain.Count) { for (int k = 0; k < unmatchedRef.Count; k++) { changes.Add(new ChangeEntry { ChangeType = ChangeType.Modified, ReferenceLineNumber = unmatchedRef[k].OriginalLineNumber, MainLineNumber = unmatchedMain[k].OriginalLineNumber, OldContent = unmatchedRef[k].Content, NewContent = unmatchedMain[k].Content }); } // 移除之前误判为增删的记录 changes.RemoveAll(c => c.ChangeType is ChangeType.Deleted && unmatchedRef.Any(r => r.OriginalLineNumber == c.ReferenceLineNumber)); changes.RemoveAll(c => c.ChangeType is ChangeType.Added && unmatchedMain.Any(m => m.OriginalLineNumber == c.MainLineNumber)); } // 识别移动行:匹配但行号变化的行 iPtr = m; jPtr = n; while (iPtr > 0 && jPtr > 0) { if (referenceLines[iPtr - 1].Content.Equals(mainLines[jPtr - 1].Content, StringComparison.Ordinal)) { var refLine = referenceLines[iPtr - 1]; var mainLine = mainLines[jPtr - 1]; if (refLine.OriginalLineNumber != mainLine.OriginalLineNumber) { changes.Add(new ChangeEntry { ChangeType = ChangeType.Moved, ReferenceLineNumber = refLine.OriginalLineNumber, MainLineNumber = mainLine.OriginalLineNumber, Content = refLine.Content }); } iPtr--; jPtr--; } else if (dp[iPtr - 1, jPtr] > dp[iPtr, jPtr - 1]) iPtr--; else jPtr--; } // 按行号排序日志 return changes.OrderBy(c => c.MainLineNumber ?? c.ReferenceLineNumber).ToList(); }
4. 日志记录实现
将变更记录格式化为可读日志:
private void LogChanges(List<ChangeEntry> changes, string mainFilePath, string referenceFilePath) { Console.WriteLine($"=== {Path.GetFileName(mainFilePath)} 变更日志 ==="); Console.WriteLine($"参考文件:{Path.GetFileName(referenceFilePath)}"); Console.WriteLine($"时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n"); foreach (var change in changes) { switch (change.ChangeType) { case ChangeType.Added: Console.WriteLine($"[新增] 主文件行{change.MainLineNumber}:{change.Content}"); break; case ChangeType.Deleted: Console.WriteLine($"[删除] 参考文件行{change.ReferenceLineNumber}:{change.Content}"); break; case ChangeType.Modified: Console.WriteLine($"[修改] 参考文件行{change.ReferenceLineNumber} → 主文件行{change.MainLineNumber}"); Console.WriteLine($" 原内容:{change.OldContent}"); Console.WriteLine($" 新内容:{change.NewContent}"); break; case ChangeType.Moved: Console.WriteLine($"[移动] 参考文件行{change.ReferenceLineNumber} → 主文件行{change.MainLineNumber}:{change.Content}"); break; } } if (!changes.Any()) Console.WriteLine("未检测到有效变更(已忽略新增空行)"); }
5. 整合到FileSystemWatcher触发方法
替换原有CompareAndLogChanges方法:
private void CompareAndLogChanges(string mainFilePath, string referenceFilePath) { if (!File.Exists(mainFilePath) || !File.Exists(referenceFilePath)) { Console.WriteLine("文件不存在,无法对比"); return; } var referenceLines = ReadFileWithLineNumbers(referenceFilePath); var mainLines = ReadFileWithLineNumbers(mainFilePath); var changes = CompareFiles(referenceLines, mainLines); LogChanges(changes, mainFilePath, referenceFilePath); // 可选:对比完成后更新参考文件为当前主文件 // File.Copy(mainFilePath, referenceFilePath, overwrite: true); }
关键特性说明
- 行偏移处理:通过LCS算法匹配内容相同的行,无论行位置如何变化,都能关联到参考文件的原始行号
- 空行过滤:仅忽略主文件新增的空行,原文件空行被修改或非空行被改为空的情况仍会记录
- 多变更类型识别:准确区分新增、删除、修改、移动四种变更场景,日志清晰关联原始行位置
内容的提问来源于stack exchange,提问作者Tudor Oprea
相关产品推荐
相关产品推荐

