You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 17:57:16