.NET 平台操作 Execl 库对比
在 .NET
中处理 Excel
文件的常用库有以下几种,按功能和使用场景分类如下:
✅ 1. EPPlus(不推荐,商业用途需许可证)
- 特点:曾广泛用于读写
Excel
文件(.xlsx
),API
简洁易用。 - 缺点:社区版从
5.x
开始收费,且官方宣布不再维护免费版本。 - 安装包:
Install-Package EPPlus
- 适用格式:仅支持
.xlsx
- 官网:https://epplussoftware.com/
✅ 2. ClosedXML(推荐)
- 特点:基于
DocumentFormat.OpenXml
封装,简化了Excel
操作,适合中等复杂度的需求。 - 优点:
API
友好、文档丰富- 支持
LINQ
风格操作 - 易于导出数据到
Excel
- 安装包:
Install-Package ClosedXML
- 适用格式:
.xlsx
- 官网:https://closedxml.github.io/ClosedXML/
✅ 3. DocumentFormat.OpenXml(微软官方 SDK)
- 特点:微软提供的底层
SDK
,可直接操作Office Open XML
格式。 - 优点:
- 性能高,适合大批量数据处理
- 不依赖
Excel
应用程序
- 缺点:
- 学习曲线陡峭
- 需要熟悉
OpenXML
结构
- 安装包:
Install-Package DocumentFormat.OpenXml
- 适用格式:
.xlsx
,.docx
,.pptx
- 官方文档:https://learn.microsoft.com/en-us/office/open-xml/open-xml-sdk
✅ 4. NPOI(适用于旧版 .xls
和新版 .xlsx
)
- 特点:兼容
.xls
(Excel 2003
)和.xlsx
(Excel 2007+
),适合需要兼容旧格式的项目。 - 优点:
- 支持老格式
.xls
- 不依赖
Excel
安装
- 支持老格式
- 缺点:
API
较为繁琐
- 安装包:
Install-Package NPOI
✅ 5. ExcelDataReader.DataSet(读取专用)
- 特点:专注于读取
Excel
文件,速度快,内存占用低。 - 适用场景:数据导入、批量读取。
- 组合使用:
ExcelDataReader
+ExcelDataReader.DataSet
可将Excel
转换为DataSet
- 安装包:
Install-Package ExcelDataReader
Install-Package ExcelDataReader.DataSet
- 支持格式:
.xls
,.xlsx
✅ 6. Aspose.Cells(商业库,功能强大)
- 特点:
- 功能全面:图表、公式、样式、打印、转换
PDF
等 - 支持多种文件格式(
Excel、CSV、PDF、HTML
等)
- 功能全面:图表、公式、样式、打印、转换
- 优点:
- 性能强、稳定性高
- 文档齐全
- 缺点:
- 商业授权,费用较高(但提供免费试用)
- 安装包:
Install-Package Aspose.Cells
✅ 7. MiniExcel(轻量高性能开源库)
- 特点:专为高效读写
Excel
设计,适合数据导入导出场景。 - 优点:
- 轻量高效:零依赖、性能高、内存占用低,适合处理大数据量
- 支持格式:支持
.xlsx(Office Open XML)
和.xls(BIFF8)
格式 API
简洁:提供LINQ
风格API
,易于上手,提供模板导出功能- 不依赖
Office
:不依赖Microsoft Office
或Interop
组件 - 跨平台:支持
.NET Framework、.NET Core、.NET 5/6/7+
- 开源免费
MIT
协议,可商用
- 缺点:
- 不适合复杂样式或图表处理
- 安装包:
Install-Package MiniExcel
🚫 不推荐使用:
- Microsoft.Office.Interop.Excel:
- 依赖本地安装
Microsoft Office
- 不适用于服务器环境
- 不支持
.NET Core/.NET 5+
- 依赖本地安装
📝 总结对比表
库名 | 是否开源 | 开源协议 | 支持 .xls |
支持 .xlsx |
跨平台 | 依赖 Office | 推荐用途 |
---|---|---|---|---|---|---|---|
EPPlus | 是 | GNU GPL v3.0(旧版) / 商业(新版) | ❌ | ✅ | ❌ | ❌ | 已不推荐 |
ClosedXML | 是 | MIT | ❌ | ✅ | ✅ | ❌ | 快速开发,简单报表 |
DocumentFormat.OpenXml | 是 | MIT | ❌ | ✅ | ✅ | ❌ | 高性能、复杂格式处理 |
NPOI | 是 | Apache-2.0 | ✅ | ✅ | ✅ | ❌ | 兼容旧格式 .xls 文件 |
ExcelDataReader | 是 | MIT | ✅ | ✅ | ✅ | ❌ | 数据导入、只读场景 |
Aspose.Cells | 否 | 商业授权 | ✅ | ✅ | ✅ | ❌ | 企业级高级功能需求 |
MiniExcel | 是 | Apache-2.0 | ✅ | ✅ | ✅ | ❌ | 高性能数据导入导出 |
🔍 建议选择:
- 快速开发 & 导出报表:👉 ClosedXML
- 高性能 & 大数据量:👉 DocumentFormat.OpenXml 或 MiniExcel
- 需兼容
.xls
文件:👉 NPOI、ExcelDataReader 或 MiniExcel - 企业级完整功能:👉 Aspose.Cells
如你有具体使用场景(如导入、导出、大数据、样式处理等),可以进一步说明,我可以给出更具体的代码示例或推荐。
导入导出 Execl 数据性能测试
通过上面的对比分析,这里我们挑选几个开源的 .net
库进行性能测试,使用 BenchmarkDotNet
作为基准测试框架,并模拟 数据导入和导出
操作。
.net
开源库如下:
- 👉 ClosedXML
- 👉 DocumentFormat.OpenXml
- 👉 NPOI
- 👉 ExcelDataReader
- 👉 MiniExcel
✅ 测试目标
- 对比不同
Excel
库在 相同数据结构和不同数据量下 的 导入/导出性能 - 支持多种行数级别:100 行、1000 行、5000 行
- 使用统一模型类
Person
进行测试
🧪 测试环境要求
- 添加基准测试框架的
NuGet
包:
dotnet add package Datadog.Trace.BenchmarkDotNet
- 然后添加以下各库的
NuGet
包:
dotnet add package ClosedXML
dotnet add package DocumentFormat.OpenXml
dotnet add package NPOI
dotnet add package ExcelDataReader.DataSet
dotnet add package MiniExcel
项目实现
📦 统一模型类定义
public class Person
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
}
完整的项目实现如下:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="Datadog.Trace.BenchmarkDotNet" Version="2.61.0" />
<PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
<PackageReference Include="ExcelDataReader.DataSet" Version="3.7.0" />
<PackageReference Include="MiniExcel" Version="1.41.3" />
<PackageReference Include="NPOI" Version="2.7.4" />
</ItemGroup>
</Project>
⚙️ 基准测试配置类
ExcelLibraryBenchmark
//=================================================
// ExcelLibraryBenchmark 实现
//=================================================
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
namespace BenchmarkTest.examples.Execl;
[MemoryDiagnoser]
[RankColumn]
public class ExcelLibraryBenchmark
{
[Params(100, 1000, 5000)] // 不同数据量
public int RowCount;
private readonly List<Person> _data = [];
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < RowCount; i++)
{
_data.Add(new Person { Name = $"Name_{i}", Age = i });
}
}
[Benchmark(Baseline = true)]
public void ClosedXML_Export() => ClosedXLHelper.Export(_data);
[Benchmark]
public void OpenXML_Export() => OpenXMLHelper.Export(_data);
[Benchmark]
public void Npoi_Export() => NpoiHelper.Export(_data);
[Benchmark]
public void MiniExcel_Export() => MiniExcelHelper.Export(_data);
[Benchmark]
public List<Person> ClosedXML_Import() => ClosedXLHelper.Import();
[Benchmark]
public List<Person> OpenXML_Import() => OpenXMLHelper.Import();
[Benchmark]
public List<Person> Npoi_Import() => NpoiHelper.Import();
[Benchmark]
public List<Person> ExcelDataReader_Import() => ExcelDataReaderHelper.Import();
[Benchmark]
public List<Person> MiniExcel_Import() => MiniExcelHelper.Import();
public static void Run(IConfig config)
{
var summary = BenchmarkRunner.Run<ExcelLibraryBenchmark>(config);
Console.WriteLine(summary);
}
}
📚 各开源库实现类
✅ 1. ClosedXML 实现
//========================
// ClosedXML 实现
//========================
using ClosedXML.Excel;
namespace BenchmarkTest.examples.Execl;
public static class ClosedXLHelper
{
public static void Export(List<Person> data)
{
using var workbook = new XLWorkbook();
var ws = workbook.Worksheets.Add("People");
ws.Cell(1, 1).Value = "Name";
ws.Cell(1, 2).Value = "Age";
for (int i = 0; i < data.Count; i++)
{
ws.Cell(i + 2, 1).Value = data[i].Name;
ws.Cell(i + 2, 2).Value = data[i].Age;
}
workbook.SaveAs("ClosedXML.xlsx");
}
public static List<Person> Import()
{
using var workbook = new XLWorkbook("ClosedXML.xlsx");
var ws = workbook.Worksheet(1);
return ws.RowsUsed()
.Skip(1)
.Select(r => new Person
{
Name = r.Cell(1).GetString(),
Age = r.Cell(2).GetValue<int>()
}).ToList();
}
}
✅ 2. DocumentFormat.OpenXml 实现
//====================
// ClosedXML 实现
//====================
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
namespace BenchmarkTest.examples.Execl;
internal static class OpenXMLHelper
{
public static void Export(List<Person> data)
{
using var spreadsheetDocument = SpreadsheetDocument.Create("OpenXML.xlsx", SpreadsheetDocumentType.Workbook);
var workbookPart = spreadsheetDocument.AddWorkbookPart();
workbookPart.Workbook = new Workbook();
var worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
worksheetPart.Worksheet = new Worksheet(new SheetData());
var sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>();
var headerRow = new Row();
headerRow.Append(
ConstructCell("Name", CellValues.String),
ConstructCell("Age", CellValues.String));
sheetData.AppendChild(headerRow);
foreach (var item in data)
{
var row = new Row();
row.Append(
ConstructCell(item.Name, CellValues.String),
ConstructCell(item.Age.ToString(), CellValues.String));
sheetData.AppendChild(row);
}
workbookPart.Workbook.Append(new Sheets(new Sheet()
{
Id = workbookPart.GetIdOfPart(worksheetPart),
SheetId = 1,
Name = "People"
}));
workbookPart.Workbook.Save();
}
private static Cell ConstructCell(string value, CellValues dataType) => new()
{
CellValue = new CellValue(value),
DataType = new EnumValue<CellValues>(dataType)
};
public static List<Person> Import()
{
using var document = SpreadsheetDocument.Open("OpenXML.xlsx", false);
var workbookPart = document.WorkbookPart;
var sheet = workbookPart.Workbook.Descendants<Sheet>().First();
var worksheetPart = (WorksheetPart)workbookPart.GetPartById(sheet.Id);
var worksheet = worksheetPart.Worksheet;
var sheetData = worksheet.GetFirstChild<SheetData>();
var rows = sheetData.Descendants<Row>().Skip(1);
return rows.Select(row =>
{
var cells = row.Elements<Cell>().ToList();
return new Person
{
Name = GetCellValue(workbookPart, cells[0]),
Age = int.Parse(GetCellValue(workbookPart, cells[1]))
};
}).ToList();
}
private static string GetCellValue(WorkbookPart workbookPart, Cell cell)
{
if (cell.DataType.HasValue && cell.DataType == CellValues.SharedString)
{
var sstPart = workbookPart.SharedStringTablePart;
if (sstPart != null)
{
var sharedStringTable = sstPart.SharedStringTable;
return sharedStringTable.ElementAt(int.Parse(cell.InnerText)).InnerText;
}
}
return cell.InnerText;
}
}
✅ 3. NPOI 实现
//====================
// NpoiHelper 实现
//====================
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
namespace BenchmarkTest.examples.Execl;
internal static class NpoiHelper
{
public static void Export(List<Person> data)
{
using var fs = new FileStream("Npoi.xlsx", FileMode.Create, FileAccess.Write);
IWorkbook workbook = new XSSFWorkbook();
ISheet sheet = workbook.CreateSheet("People");
var header = sheet.CreateRow(0);
header.CreateCell(0).SetCellValue("Name");
header.CreateCell(1).SetCellValue("Age");
for (int i = 0; i < data.Count; i++)
{
var row = sheet.CreateRow(i + 1);
row.CreateCell(0).SetCellValue(data[i].Name);
row.CreateCell(1).SetCellValue(data[i].Age);
}
workbook.Write(fs);
}
public static List<Person> Import()
{
using var fs = new FileStream("Npoi.xlsx", FileMode.Open, FileAccess.Read);
IWorkbook workbook = WorkbookFactory.Create(fs);
ISheet sheet = workbook.GetSheetAt(0);
return sheet.Skip(1).Select(row =>
new Person
{
Name = row.GetCell(0).StringCellValue,
Age = (int)row.GetCell(1).NumericCellValue
}).ToList();
}
}
✅ 4. ExcelDataReader 实现
//================================
// ExcelDataReaderHelper 实现
//================================
using ExcelDataReader;
using System.Data;
namespace BenchmarkTest.examples.Execl;
internal static class ExcelDataReaderHelper
{
public static List<Person> Import()
{
using var stream = File.Open("Npoi.xlsx", FileMode.Open, FileAccess.Read);
using var reader = ExcelReaderFactory.CreateReader(stream);
var result = reader.AsDataSet().Tables[0];
return result.AsEnumerable().Skip(1).Select(row =>
new Person
{
Name = row["Name"]?.ToString(),
Age = int.Parse(row["Age"]?.ToString())
}).ToList();
}
}
✅ 5. MiniExcel 实现
//=========================
// MiniExcelHelper 实现
//=========================
using MiniExcelLibs;
namespace BenchmarkTest.examples.Execl;
internal static class MiniExcelHelper
{
public static void Export(List<Person> data)
{
MiniExcel.SaveAs("MiniExcel.xlsx", data);
}
public static List<Person> Import()
{
return MiniExcel.Query<Person>("MiniExcel.xlsx").ToList();
}
}
🏁 启动基准测试
- 在
Program.cs
中运行基准测试
using BenchmarkDotNet.Configs;
using BenchmarkTest.examples.Execl;
using Datadog.Trace.BenchmarkDotNet;
Console.WriteLine("Hello, BenchmarkDotNetTest!");
var config = DefaultConfig.Instance.WithDatadog();
ExcelLibraryBenchmark.Run(config);
- 运行测试
dotnet run -c Release
输出信息:
以下是对 BenchmarkDotNet
测试结果的详细分析:
📊 输出内容说明
以下是对 BenchmarkDotNet 测试报告 的详细解释,涵盖:
- 各库在不同数据量下的性能表现
- 内存分配情况
- 性能排名与对比
- 异常项说明(如
NA、Outliers
) - 推荐选型建议
📊 一、测试目标回顾
我们使用了 BenchmarkDotNet
对以下 Excel
操作库进行了基准测试:
库名 | 功能 |
---|---|
ClosedXML | 数据导出/导入 |
DocumentFormat.OpenXml (简称 OpenXML) | 数据导出/导入 |
NPOI | 数据导出/导入 |
MiniExcel | 数据导出/导入 |
ExcelDataReader | 数据导入 |
测试数据量级别:
100
行1000
行5000
行
测试指标包括:
- 平均执行时间(Mean)
- 标准差(StdDev)
- 内存分配(Allocated)
- GC 回收次数(Gen0, Gen1, Gen2)
📈 二、性能分析(按行数分类)
1. 小规模数据(100 行)
方法名 | Mean(us) | 内存分配(B) | Rank |
---|---|---|---|
MiniExcel_Import | 577.3 | 290,796 | 1 |
OpenXML_Export | 572.3 | 227,772 | 1 |
Npoi_Import | 669.6 | 826,139 | 2 |
OpenXML_Import | 809.7 | 363,145 | 3 |
ClosedXML_Export | 1,755.0 | 637,283 | 5 |
ClosedXML_Import | 1,731.1 | 1,127,449 | 5 |
📌 结论:
MiniExcel.Import()
和OpenXML.Export()
在小数据量下表现最优。ClosedXML
相对较慢,内存消耗也较高。
2. 中等规模数据(1000 行)
方法名 | Mean(us) | 内存分配(B) | Rank |
---|---|---|---|
MiniExcel_Import | 570.7 | 290,637 | 1 |
OpenXML_Export | 1,974.4 | 1,423,419 | 2 |
OpenXML_Import | 2,677.3 | 1,868,485 | 3 |
Npoi_Import | 2,862.5 | 4,446,786 | 4 |
ClosedXML_Export | 4,626.4 | 2,947,515 | 6 |
ClosedXML_Import | 6,401.7 | 6,359,064 | 7 |
📌 结论:
MiniExcel.Import()
依然最快,且内存消耗极低。OpenXML.Export()
性能稳定,适合中等规模导出。NPOI
和ClosedXML
随着数据量增加,性能下降明显,内存开销更大。
3. 大规模数据(5000 行)
方法名 | Mean(us) | 内存分配(B) | Rank |
---|---|---|---|
MiniExcel_Import | 574.6 | 290,797 | 1 |
OpenXML_Export | 9,810.3 | 7,380,015 | 2 |
OpenXML_Import | 12,492.4 | 8,559,265 | 3 |
Npoi_Import | 16,804.9 | 20,818,268 | 4 |
ClosedXML_Export | 20,664.3 | 12,961,790 | 5 |
ClosedXML_Import | 31,425.4 | 29,650,475 | 7 |
📌 结论:
MiniExcel.Import()
在大数据量下依然保持超快性能,几乎不随数据量增长而显著变慢。OpenXML.Export()
在导出时表现良好,适合需要大量导出的场景。ClosedXML
在大数据下表现最差,尤其在导入操作中。
📦 三、内存与 GC 分析
库名 | 内存分配趋势 | GC 压力 |
---|---|---|
MiniExcel | 极低 | 几乎无 |
OpenXML | 较低 | 轻度 |
ExcelDataReader | 适中 | 适中 |
NPOI | 高 | 高 |
ClosedXML | 最高 | 最严重 |
📌 总结:
MiniExcel
和OpenXML
内存占用最小,GC
压力最低。ClosedXML
和NPOI
内存分配频繁,GC
回收多,影响整体性能。
⚠️ 四、异常项说明
1. NA
值问题(未完成的测试)
方法名 | 问题描述 |
---|---|
MiniExcel_Export |
所有 RowCount 下都为 NA |
ExcelDataReader_Import |
所有 RowCount 下都为 NA |
✅ 可能原因:
MiniExcel.SaveAs(...)
方法调用失败或未实现。ExcelDataReader
未正确绑定文件路径或未加载数据源。
🔧 建议修复:
- 确保导出路径可写。
- 检查是否遗漏初始化逻辑或文件读取权限。
- 使用
.AsStream()
或.AsDataSet()
正确转换数据。
2. Outlier(异常值)
例如:
ExcelLibraryBenchmark.ClosedXML_Export: Default -> 3 outliers were removed (1.98 ms..1.99 ms)
📌 说明:
BenchmarkDotNet
自动识别并移除了部分“偏离正常范围”的极端值(可能是 GC 干扰、系统调度等导致)。- 这是正常行为,确保最终结果更准确。
🧠 五、综合排名与推荐选型
排名 | 方法名 | 适用场景 |
---|---|---|
1 | MiniExcel.Import() | 快速导入,无需复杂格式处理 |
2 | OpenXML.Export() | 高性能导出,支持样式和结构控制 |
3 | OpenXML.Import() | 导入 + 结构化操作 |
4 | ExcelDataReader.Import() | 只读导入,兼容性强 |
5 | NPOI.Export() | 兼容 .xls 格式 |
6 | ClosedXML.Export() | 快速开发、简单报表 |
7 | ClosedXML.Import() | 不推荐用于大规模数据 |
✅ 六、建议选择策略
场景 | 推荐库 |
---|---|
快速导入(只读) | MiniExcel / ExcelDataReader |
高性能导出(无复杂样式) | OpenXML / MiniExcel |
需要兼容 .xls 格式 |
NPOI |
快速原型开发 / 简单表格操作 | ClosedXML |
企业级功能(图表、公式、打印等) | Aspose.Cells(商业) |
📌 七、后续优化建议
- 增加模板导出、样式保留、多Sheet等高级功能测试
- 加入异常处理机制,防止导出失败中断测试流程
- 尝试异步方式提升 I/O 性能