.NET9 实现处理 Execl 数据操作的性能测试

发布于:2025-07-10 ⋅ 阅读:(18) ⋅ 点赞:(0)

.NET 平台操作 Execl 库对比

.NET 中处理 Excel 文件的常用库有以下几种,按功能和使用场景分类如下:

✅ 1. EPPlus(不推荐,商业用途需许可证)

  • 特点:曾广泛用于读写 Excel 文件(.xlsx),API 简洁易用。
  • 缺点:社区版从 5.x 开始收费,且官方宣布不再维护免费版本。
  • 安装包
Install-Package EPPlus

✅ 2. ClosedXML(推荐)

  • 特点:基于 DocumentFormat.OpenXml 封装,简化了 Excel 操作,适合中等复杂度的需求。
  • 优点
    • API 友好、文档丰富
    • 支持 LINQ 风格操作
    • 易于导出数据到 Excel
  • 安装包
Install-Package ClosedXML

3. DocumentFormat.OpenXml(微软官方 SDK)

  • 特点:微软提供的底层 SDK,可直接操作 Office Open XML 格式。
  • 优点
    • 性能高,适合大批量数据处理
    • 不依赖 Excel 应用程序
  • 缺点
    • 学习曲线陡峭
    • 需要熟悉 OpenXML 结构
  • 安装包
Install-Package DocumentFormat.OpenXml

✅ 4. NPOI(适用于旧版 .xls 和新版 .xlsx

  • 特点:兼容 .xlsExcel 2003)和 .xlsxExcel 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 OfficeInterop 组件
    • 跨平台:支持 .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

输出信息:

ExcelLibraryBenchmark

以下是对 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() 性能稳定,适合中等规模导出。
  • NPOIClosedXML 随着数据量增加,性能下降明显,内存开销更大。

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 最高 最严重

📌 总结

  • MiniExcelOpenXML 内存占用最小,GC 压力最低。
  • ClosedXMLNPOI 内存分配频繁,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(商业)

📌 七、后续优化建议

  1. 增加模板导出、样式保留、多Sheet等高级功能测试
  2. 加入异常处理机制,防止导出失败中断测试流程
  3. 尝试异步方式提升 I/O 性能

网站公告

今日签到

点亮在社区的每一天
去签到