平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“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 >= 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<string>();
// 从下往上遍历,保留靠上的第一条,删除后续重复
for (int row = rowCount; row >= 1; row--)
{
var values = new List<string>();
for (int col = 1; col <= 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<string>();
for (int row = rowCount; row > 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<string>();
for (int row = lastRow; row >= 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重复行适合结合实际项目边做边理解。先抓住核心思路,再逐步补上细节和边界处理,最后效果会更稳定,也更容易复用。