详情

首页手游攻略 C#代码实现移除Excel重复行的两种实现做法完整指南

C#代码实现移除Excel重复行的两种实现做法完整指南

佚名 2026-10-24 17:20:01

平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“C#代码实现移除Excel重复行的两种实现做法”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。

实际处理时,数据清洗是日常开发中绕不开的环节,而“移除重复行”几乎是最高频的需求之一。在 C# 生态中,Spire.XLS for .NET 是一个不依赖 Office 安装的 Excel 操作库,提供了从原生 API 到手动控制的多种去重手段。在讨论具体实现方法前,先借助 NuGet 引入包。

Install-Package Spire.XLS

路径一:原生RemoveDuplicates()(建议优先)

在这个场景下,从 Spire.XLS 15.5.2 开始,Worksheet 上提供了原生的 RemoveDuplicates() 方法,语义与 Excel 界面中的“删除重复值”功能对齐。它有三个重载,覆盖了从全表去重到精细化控制的常用场景。

全表去重实际处理时,是最简形式,扫描工作表中所有已用区域,完全相同的行只保留首次出现的那一行:

using Spire.Xls;

using var workbook = new Workbook();
workbook.LoadFromFile("input.xlsx");
Worksheet sheet = workbook.Worksheets[0];

sheet.RemoveDuplicates();
workbook.SaveToFile("output.xlsx", ExcelVersion.Version2016);

指定区域去重允许你限定检测范围,避免表头、备注区或公式区域被误纳入判断:

// 参数依次为:起始行, 起始列, 结束行, 结束列(均从 1 开始)
sheet.RemoveDuplicates(2, 1, sheet.LastRow, sheet.LastColumn);

基于指定列 + 表头标记从实现思路看,是最贴近真实业务的一种。比如只根据“订单号”和“客户ID”判断重复,而忽略“更新时间”这类天然不同的列:

// hasHeaders: true 表示第一行是表头,不参与去重判断
// columnOffsets: 相对于起始列的偏移量,0 表示 A 列,1 表示 B 列
sheet.RemoveDuplicates(1, 1, sheet.LastRow, sheet.LastColumn, true, new[] { 0, 1 });

实际处理时,原生方法的优势在于将分组、比较和行位移全部交给库内部处理,代最小,且对行索引偏移问题无感知。需留意,该 API 依赖于较新的库版本;如果项目锁定在 15.5.2 之前的版本或者免费版,则需采用路径二。

路径二:LINQ 手动识别 +DeleteRow删除

在无法采用原生 RemoveDuplicates() 的版本约束下,或者需对“保留哪一条、删除哪一条”实施完全自定义的逻辑时,手动方案仍然可靠。其核心流程分三步:分组识别重复行号、确定待删除行号列表、从后往前(或动态修正索引)执行删除。

分组依据的选择至关重要。建议采用单元格的 DisplayedText 而非 Value 或 Text。DisplayedText 得到的是用户在 Excel 界面中看到的显示文本,会考虑数字格式、日期格式和公式计算结果;直接采用 Value 可能在“看起来相同”但底层存储不同的情况下漏判。

using Spire.Xls;
using System.Linq;

using var workbook = new Workbook();
workbook.LoadFromFile("input.xlsx");
Worksheet sheet = workbook.Worksheets[0];

// 假设数据从第 2 行开始(第 1 行为表头),仅根据 A 列判断重复
int startRow = 2;
var range = sheet.Range[startRow, 1, sheet.LastRow, sheet.LastColumn];

var duplicateRowIndexes = range.Rows
    .GroupBy(row => row.Columns[0].DisplayedText)
    .Where(g => g.Count() > 1)
    .SelectMany(g => g.Skip(1)) // 保留每组首行,其余标记为重复
    .Select(row => row.Columns[0].Row)
    .OrderBy(r => r)
    .ToList();

删除时的索引偏移从实现思路看,是手动方案最容易出错的地方。Excel 工作表的行号在删除后会实时变化:删除第 3 行后,原来的第 4 行变成新的第 3 行。如果按原始行号列表顺序执行 DeleteRow,后续行号会全部错位。常用做法是借助删除次数对索引进行动态修正:

for (int i = 0; i < duplicateRowIndexes.Count; i++)
{
    sheet.DeleteRow(duplicateRowIndexes[i] - i);
}

这里 duplicateRowIndexes 已按升序排列,每删除一行,后续待删除行的实际位置就比原始行号小 i,所以用 duplicateRowIndexes[i] - i 得到正确的当前索引。如果待删除行号列表很大且内存敏感,也能够改为从后往前遍历删除,逻辑上等价,无需修正偏移。

两条路径的选择与注意事项

场景建议路径原因
库版本 ≥ 15.5.2,逻辑标准RemoveDuplicates()内置处理偏移,代码最简,与 Excel 行为对齐
库版本较旧,无法升级LINQ + DeleteRow唯一可行方案,逻辑可控
需自定义“保留哪一条”LINQ + DeleteRow结合项目来看,原生 API 固定保留首次出现,手动方案可在 Skip / Take 上灵活调整
只按部分列判断重复两者皆可原生用 columnOffsets,手动用 GroupBy 多字段组合

表头处理理解这一步时,在两路径中都需显式关注。原生方案的 hasHeaders 参数直接声明第一行是否为表头;手动方案则借助调整 startRow(从 2 开始)将表头排除在分组和删除范围之外。

资源释放容易被忽视。Workbook 实现了 IDisposable,处理大文件时应采用 using 声明或显式调用 Dispose(),否则文件句柄和内存不会及时释放,尤其在服务端长时间运行的进程中可能累积成问题。

方法补充

从实现思路看,“移除重复行”在 Excel 里有两种不同的含义,实现方法差别很大,先明确你要的是哪一种:

类型判断依据典型场景
整行去重所有列的值完全相同才算重复数据清洗、去除完全重复的记录
关键列去重只按指定的一列或多列判断重复,保留第一条订单号去重、用户 ID 去重、按“日期+产品”去重

另一个容易混淆的点是:“删除重复行”不等于“删除空行”,两者需分开处理。下面按库分别给出完整实现。

一、核心原则(所有方案通用)

删除行时必须从下往上遍历。

落到代码里,在遍历过程中删除行会导致行号整体上移,如果从上往下删除,会跳过部分行或抛出越界异常。正确写法:

for (int i = lastRow; i &gt;= firstRow; i--)
{
    // 判断第 i 行是否重复,是则删除
}

二、方案一:EPPlus(开源,非商业免费,建议)

EPPlus 适合直接操作 .xlsx,代码简洁,性能好。

安装:Install-Package EPPlus

1. 整行去重

using OfficeOpenXml;
using System.IO;

public static void RemoveDuplicateRows_EPPlus(string filePath)
{
    ExcelPackage.LicenseContext = LicenseContext.NonCommercial;

    using (var package = new ExcelPackage(new FileInfo(filePath)))
    {
        var ws = package.Workbook.Worksheets[0];
        int rowCount = ws.Dimension.End.Row;
        int colCount = ws.Dimension.End.Column;

        var seen = new HashSet&lt;string&gt;();

        // 从下往上遍历,保留靠上的第一条,删除后续重复
        for (int row = rowCount; row &gt;= 1; row--)
        {
            var values = new List&lt;string&gt;();
            for (int col = 1; col &lt;= colCount; col++)
            {
                values.Add(ws.Cells[row, col].Text?.Trim() ?? "");
            }

            string key = string.Join("u0001", values);

            if (!seen.Add(key))
            {
                ws.DeleteRow(row);
            }
        }

        package.Save();
    }
}

2. 按指定列去重

public static void RemoveDuplicateByColumn_EPPlus(string filePath, int keyColumnIndex, int headerRows = 1)
{
    ExcelPackage.LicenseContext = LicenseContext.NonCommercial;

    using (var package = new ExcelPackage(new FileInfo(filePath)))
    {
        var ws = package.Workbook.Worksheets[0];
        int rowCount = ws.Dimension.End.Row;

        var seen = new HashSet&lt;string&gt;();

        for (int row = rowCount; row &gt; headerRows; row--)
        {
            string key = ws.Cells[row, keyColumnIndex].Text?.Trim() ?? "";

            // 空值是否参与去重,按业务决定;这里跳过空值
            if (string.IsNullOrEmpty(key)) continue;

            if (!seen.Add(key))
            {
                ws.DeleteRow(row);
            }
        }

        package.Save();
    }
}

多列组合键:把 key 换成多个列值拼接即可,比如 $"{cell1.Text}|{cell2.Text}"。

三、方案二:ClosedXML(开源,MIT 免费)

落到代码里,ClosedXML 的 API 更接近 Excel 对象模型,链式调用友好。

安装:Install-Package ClosedXML

using ClosedXML.Excel;
using System.Collections.Generic;

public static void RemoveDuplicateRows_ClosedXML(string filePath, int keyColumnIndex)
{
    using (var workbook = new XLWorkbook(filePath))
    {
        var ws = workbook.Worksheet(1);

        int lastRow = ws.LastRowUsed().RowNumber();
        int firstDataRow = 2; // 假设第 1 行是表头

        var seen = new HashSet&lt;string&gt;();

        for (int row = lastRow; row &gt;= firstDataRow; row--)
        {
            string key = ws.Cell(row, keyColumnIndex).GetString().Trim();

            if (string.IsNullOrEmpty(key)) continue;

            if (!seen.Add(key))
            {
                ws.Row(row).Delete();
            }
        }

        workbook.Save();
    }
}

四、方案三:Aspose.Cells(商业库,内置 RemoveDuplicates,最省事)

落到代码里,Aspose.Cells 直接提供了与 Excel“删除重复项”功能对应的 API,无需手写遍历逻辑,还兼容指定多列作为判断依据。

安装:Install-Package Aspose.Cells

using Aspose.Cells;

public static void RemoveDuplicateRows_Aspose(string filePath)
{
    Workbook workbook = new Workbook(filePath);
    Worksheet worksheet = workbook.Worksheets[0];

    // 指定数据区域:从 A1 开始,到已用区域结束
    // startRow, startColumn, endRow, endColumn
    int startRow = 1; // 0 表示第一行;如果第一行是表头,从 1 开始
    int startCol = 0;
    int endRow = worksheet.Cells.MaxDataRow;
    int endCol = worksheet.Cells.MaxDataColumn;

    // hasHeaders = true 表示首行为表头,不参与去重
    // cellArea 可传多个区域,按区域判断重复
    worksheet.Cells.RemoveDuplicates(startRow, startCol, endRow, endCol);

    workbook.Save(filePath);
}

按指定多列去重:RemoveDuplicates 有一个重载接收 CellArea[],能够只把关键列传入:

// 只按 A 列和 C 列判断重复
CellArea area1 = CellArea.CreateCellArea(1, 0, endRow, 0); // A 列
CellArea area2 = CellArea.CreateCellArea(1, 2, endRow, 2); // C 列
worksheet.Cells.RemoveDuplicates(true, new CellArea[] { area1, area2 });

理解这一步时,Aspose.Cells 的 RemoveDuplicates 内部已正确处理行删除的索引问题,比手写循环更稳,也更快。

五、方案四:Spire.XLS(商业库,有免费版)

理解这一步时,Spire.XLS 同样提供了 RemoveDuplicates 方法。

安装:Install-Package Spire.XLS

using Spire.Xls;
public static void RemoveDuplicateRows_Spire(string filePath)
{
    Workbook workbook = new Workbook();
    workbook.LoadFromFile(filePath);
    Worksheet sheet = workbook.Worksheets[0];
    // 从第 2 行开始(跳过表头),到最后一个有数据的行
    // 参数:startRowIndex, startColumnIndex, endRowIndex, endColumnIndex
    sheet.RemoveDuplicates(1, 0, sheet.LastRow, sheet.LastColumn);
    workbook.SaveToFile(filePath, ExcelVersion.Version2016);
}

Spire.XLS 免费版有页数限制,可申请临时许可证去除水印。

结语

实际处理时,Spire.XLS 在移除重复行这件事上提供了层次分明的能力:原生 RemoveDuplicates() 适合版本允许、逻辑标准的场景,将偏移处理封装在库内;LINQ 手动方案则在版本受限或需精细控制时提供了足够的灵活性。

理解这一步时,总的来说,C#移除Excel重复行适合结合实际项目边做边理解。先抓住核心思路,再逐步补上细节和边界处理,最后效果会更稳定,也更容易复用。

点击查看更多
推荐专题
热门阅读