添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

.NET导出Excel的四种方法及评测

导出 Excel .NET 的常见需求,开源社区、市场上,都提供了不少各式各样的 Excel 操作相关包。本文,我将使用 NPOI EPPlus OpenXML Aspose.Cells 四个市面上常见的库,各完成一个导出 Excel 示例。然后对其代码风格和 性能 做一个横向比较。最后我将说出我自己的感想。

文中所有的示例代码可以在这里下载:
https://github.com/sdcb/blog-data/tree/master/2019/20190824-dotnet-excel-compare

NPOI 源自于 Java 写的 Apache POI ,目前最新版本是 2.4.1 NPOI 是开源项目,作者是 华人 ,项目地址是: https://github.com/tonyqus/npoi

几年前大家导出 Excel 都使用 COM ,但 COM 不方便,这个组件的推出无疑弥补了 .NET Excel 方面组件的空白,大家都说比 COM 好用。

NPOI 还加入了 .NET Core Community组织

EPPlus

EPPlus 是另一个开源的 Excel 操作库,目前最新版本是 4.5.3.2 Github 地址如下: https://github.com/JanKallman/EPPlus

EPPlus 仅依赖基础类库( BCL ),完全没有第三方包依赖,也是 .NET 原生库。

EPPlus 只支持导出 Office 2007 之后的格式,也就是 xlsx 。这已经是存在12年的格式了,但如果有客户想要导出 xls EPPlus 将不支持。

OpenXML

OpenXML NuGet 包全称是 DocumentFormat.OpenXml :是微软推出的较为低层的 Excel 操作库,最新稳定版本是 2.9.1 OpenXML 也是开源项目,地址是:https://github.com/OfficeDev/Open-XML-SDK。

从该项目的名字可以看出, OpenXML 比较涉及底层,因此很容易令人浮想联翩,感觉它的性能、速度很可能是最快的, 但真的如此吗

Aspose.Cells

这是 Aspose Pty Ltd 公司推出的 Excel 操作库。它是众多 Aspose File Format API 产品其中之一。目前最新版本是 19.8.0 (基于年/月)。 Aspose 提供了应有尽有的文件格式支持,除了 .NET 外, Aspose 还提供了 C++ Java 的包。

据我所知 Aspose 的客户支持服务也不错,客户提出的问题经常可以在下一次发布时解决。

Aspose.Cells 是不开源,付费的库,但提供 无限期 的试用,据 官方网站 显示,试用版将:

  • 限制打开文件数量100个
  • 限制使用Aspose.Cells.GridWeb功能
  • 生成的 Excel 将添加如下水印:

但经过我的试用,无论是并行还是串行,都没找到 限制打开文件数量100个 的限制。因此,“试用版”对我们的物理限制,就只有这个水印了(当然加了这个水印客户肯定也不会有好表情?)。

Excel-COM

COM是随着Excel安装而自带的库, Excel 的包名叫 Microsoft.Office.Interop.Excel 。本文不会深入解析,具体可以看 这篇文档

我想要多说两句的是, COM old-fashion (过时)不是没有原因的,据我所知 COM 有以下缺点:

  • 调用时会启动一个进程外的 excel.exe ,可能因为它为是专门为Office设计的(不是为 .NET 集成设计的)
  • 要求目标环境安装相关软件,没安装将无法运行
  • 显然也没办法跨平台
  • 使用了大量动态/多参数接口,对开发不是很友好
  • 不像托管内存, COM 对资源释放也有要求,具体参见 这篇文章
NPOI EPPlus OpenXML Aspose.Cells
包依赖 有1个
封装程度 正常 正常 低层 正常
支持格式 完善 仅xlsx 仅xlsx 完善
开源协议 Apache-2.0 LGPL MIT 不开源
收费类型 开源免费 开源免费 开源免费 试用/付费

版本与数据

所有代码的版本号基于上文中提到的最新稳定版本:

NPOI EPPlus OpenXML Aspose.Cells
最新版本 2.4.1 4.5.3.2 2.9.1 19.8.0

数据全部基于我上篇文章使用的6万条/10列的数据,总共数据量 19,166 KB 。所有数据可以从这里下载:
https://github.com/sdcb/blog-data/tree/master/2019/20190821-generate-lorem-data

项目
CPU E3-1230 v3 @ 3.30GHz
内存 24GB DDR3-1600 MHz (8GBx3)
操作系统 Windows 10 1903 64位
电源选项 已设置为高性能
软件 LINQPad 6.0.18
运行时 .NET Core 3.0-preview8-28405-07

注意,LINQPad设置了 optimize+ ,代码都是优化后执行的;代码都指定了 Util.NewProcess = true; ,确保每次运行都会在新进程中运行,不会互相影响。

我的性能测试函数介绍

IEnumerable<object> Measure(Action action, int times = 5)
    return Enumerable.Range(1, times).Select(i =>
        var sw = Stopwatch.StartNew();
        long memory1 = GC.GetTotalMemory(true);
        long allocate1 = GC.GetTotalAllocatedBytes(true);
            action();
        long allocate2 = GC.GetTotalAllocatedBytes(true);
        long memory2 = GC.GetTotalMemory(true);
        sw.Stop();
        return new
            次数 = i, 
            分配内存 = (allocate2 - allocate1).ToString("N0"),
            内存提高 = (memory2 - memory1).ToString("N0"), 
            耗时 = sw.ElapsedMilliseconds,

除了时间,内存占用实际也是非常非常重要、但容易被人忽略的性能指标。大家都以为“内存不值钱”,但——

  • 一旦访问量大,内存就会瞬间上涨,导致频繁GC,导致性能下降;
  • 内存高也会导致服务器分页,这时性能就会急剧下降;
  • 吞吐量下降会导致队列排满,此时服务器就会报503等错误,客户就发现服务器“宕机了”。

(提示:除非你的客户的愿意花钱升级一下服务器,否则不要提“内存不值钱”。)

在我的性能测试函数中,使用了如下两个函数来测试内存占用:

  • GC.GetTotalAllocatedBytes(true) 获取分配内存大小
  • GC.GetTotalMemory(true) 获取占用内存大小

占用内存可能会比分配内存小,因为存在垃圾回收(GC),但GC会影响性能。

通过调用Measure函数,可以测得传入的action的耗时和内存占用。默认会调用5次,可以从5次测试结果中取出能反映性能的值。

string Export<T>(List<T> data, string path)
    PropertyInfo[] props = typeof(User).GetProperties();
    string noCache = null;
    for (var i = 0; i < props.Length; ++i)
        noCache = props[i].Name;
    for (var i = 0; i < data.Count; ++i)
        for (var j = 0; j < props.Length; ++j)
            noCache = props[j].GetValue(data[i]).ToString();
    return noCache;
  1. 我有意使用了反射,这符合我们导出Excel代码简单、易学、好用、好扩展的愿意;
  2. 我有意使用了泛型T,而不是实际类型,这也让这些代码容易扩展;
  3. 里面的noCache用来规避编译器优化删除代码的行为

测试结果:

次数分配内存内存提高耗时
19,863,5208,712156
29,852,5920138
39,852,5920147
49,873,0969,240136
59,853,936776133

可见,基于反射操作6万/10列数据,每次需要分配约9MB内存,但这些内存都会被快速GC,最终内存提高较少。这些使用反射的代码运行耗时在130ms-150ms左右。

各个库的使用和性能表现​

void Export<T>(List<T> data, string path)
    IWorkbook workbook = new XSSFWorkbook();
    ISheet sheet = workbook.CreateSheet("Sheet1");
    var headRow = sheet.CreateRow(0);
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
        headRow.CreateCell(i).SetCellValue(props[i].Name);
    for (var i = 0; i < data.Count; ++i)
        var row = sheet.CreateRow(i + 1);
        for (var j = 0; j < props.Length; ++j)
            row.CreateCell(j).SetCellValue(props[j].GetValue(data[i]).ToString());
    using var file = File.Create(path);
    workbook.Write(file);
  • 里面用到了XSSFWorkBook,其中XSSF这个前缀是从JavaPOI库传过来的,全称是XML SpreadSheet Format

    这种前缀在NPOI包中很常见。
  • XSSFWorkbook提供了bool Dispose()方法,但它未实现(因此千万别调用它):

性能测试结果:

次数分配内存内存提高耗时
11,598,586,416537,0486590
21,589,239,7287,71210155
31,589,232,056-5,36810309
41,589,237,0647,14410355
51,589,245,0009,56010594

分配内存稳定在1.48GB的样子,首次内存会提高524KB左右,后面趋于稳定。首次耗时6秒多,后面稳定在10秒多。

EPPlus

void Export<T>(List<T> data, string path)
    using var stream = File.Create(path);
    using var excel = new ExcelPackage(stream);
    ExcelWorksheet sheet = excel.Workbook.Worksheets.Add("Sheet1");
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
        sheet.Cells[1, i + 1].Value = props[i].Name;
    for (var i = 0; i < data.Count; ++i)
        for (var j = 0; j < props.Length; ++j)
            sheet.Cells[i + 2, j + 1].Value = props[j].GetValue(data[i]);
    excel.Save();

注意,不同于NPOI/Aspose.CellsEPPlus的下标是基于1的(而不是0)。

次数分配内存内存提高耗时
1534,970,328156,0483248
2533,610,23214,8962807
3533,595,9367,6482853
4533,590,7764,4082742
5533,598,44011,2802759

分配内存约508MB,耗时首次稍长,约3.2秒,后面稳定在2.7-2.8秒。

OpenXML

void Export<T>(List<T> data, string path)
    using SpreadsheetDocument excel = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook);
    WorkbookPart workbookPart = excel.AddWorkbookPart();
    workbookPart.Workbook = new Workbook();
    WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
    worksheetPart.Worksheet = new Worksheet(new SheetData());
    Sheets sheets = excel.WorkbookPart.Workbook.AppendChild<Sheets>(new Sheets());
    Sheet sheet = new Sheet
        Id = excel.WorkbookPart.GetIdOfPart(worksheetPart),
        SheetId = 1,
        Name = "Sheet1"
    sheets.Append(sheet);
    SheetData sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>();
    PropertyInfo[] props = typeof(User).GetProperties();
    {    // header
        var row = new Row() { RowIndex = 1 };
        sheetData.Append(row);
        row.Append(props.Select((prop, i) => new Cell
            CellReference = ('A' + i - 1) + row.RowIndex.Value.ToString(),
            CellValue = new CellValue(props[i].Name),
            DataType = new EnumValue<CellValues>(CellValues.String),
    sheetData.Append(data.Select((item, i) => 
        var row = new Row { RowIndex = (uint)(i + 2) };
        row.Append(props.Select((prop, j) => new Cell
            CellReference = ('A' + j - 1) + row.RowIndex.Value.ToString(),
            CellValue = new CellValue(props[j].GetValue(data[i]).ToString()),
            DataType = new EnumValue<CellValues>(CellValues.String),
        return row;
    excel.Save();

注意,因为OpenXML比较偏低层,东西比较复杂,所以我们慢慢说:

  • 对于一些对象,它需要创建相应的Part,如WorksheetPart
  • Excel可以使用SharedStringTable来共享变量值,适合相同字符串非常多的场景。

    但此示例共享变量值收益很低,但会极大地增加代码复杂性(普通用户可能很难写出),因此本示例未使用SharedStringTable
  • 它基于单元格位置标识,如B3(第三行第二列),因此索引方式比EPPlus/NPOI都要复杂;
  • 代码示例中使用('A' + i - 1)来计算位置标识,因此这个示例不能用于超过26列(字母数)的数据;
  • 代码使用LINQ(而不是循环)来枚举所有行/列,可以让代码更简洁(在已经非常复杂的代码情况下)

    经测试,将LINQ改成for循环对性能结果变化影响极其微小

测试结果如下:

次数分配内存内存提高耗时
1556,937,896145,8324009
2555,981,2163123783
3555,985,9362,7603884
4555,984,3841,8723869
5555,989,1203,8803704

内存占用约530MB左右,第一次比后面多1MB的样子,耗时3.7-4.0秒之间。

Aspose.Cells

void Export<T>(List<T> data, string path)
    using var excel = new Workbook();
    Worksheet sheet = excel.Worksheets["Sheet1"];
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
        sheet.Cells[0, i].Value = props[i].Name;
    for (var i = 0; i < data.Count; ++i)
        for (var j = 0; j < props.Length; ++j)
            sheet.Cells[i + 1, j].Value = props[j].GetValue(data[i]);
    excel.Save(path);

注意,Aspose.CellsExcel软件一样,提供了Sheet1/Sheet2/Sheet3三个默认的工作表,因此取这三个工作表时,不要创建,而是取出来。

性能测试结果如下:

次数分配内存内存提高耗时
1404,004,9443,619,5203316
2357,931,6486,0482078
3357,934,7447,2162007
4357,933,3766,2802017
5357,933,3606,4242007

Aspose.Cells首次占用内存385MB,用于3.3秒,后面每次降低为内存341MB,用时2.0秒。

四种导出Excel库的横向评测数据如下,数据取5次数值的内存消耗中位数,百分比以EPPlus的测试数据为100%基准:

 分配内存内存占比耗时耗时占比
基线(仅反射)9,853,9361.85%1334.82%
NPOI1,589,237,064297.83%10355375.32%
EPPlus533,598,440100%2759100%
OpenXML555,985,936104.19%3884140.78%
Aspose.Cells357,933,36067%200772.74%

可以得出以下结论:

  1. Demo基于反射,但反射总损耗的性能不高,内存、耗时均不超过5%;
  2. NPOI的性能表现是所有项目中最差的,每次需要分配1.5GB的内存和超过10秒的耗时;
  3. EPPlus表现不错,内存和耗时在开源组中表现最佳;
  4. 收费的Aspose.Cells表现最佳,内存占用最低,用时也最短;
  5. 较为底层的OpenXML表现非常一般,比EPPlus要差,更不能与收费的Aspose相提并论;

在真的愿意尝试一下之前,人们很容易相信自己的直觉。底层库,通常能带来更大的可扩展性,能做出上层库很难做的事来。底层库有时性能会更快,就像更底层的C/C++比上层的JavaScript更快一样。但事情也不都如此,如

  • 更高层的React.js能在性能上将较底层的DOM操作比下去
  • 数据库基于集合的操作也比基于游标的操作要快得多

在导出Excel这个例子中,我了解到Excelxlsx格式是非常复杂的、多个xml的集合。如果基于xml做抽象——也是很正常的做法,拼出6万/10列的数据,需要至少60万个xml标签做拼接,很显然这需要分配/浪费大量内存,因此性能上不来。

我基于以下几点无责任猜测:Aspose内部可能没xml做抽象,而是纯数据做抽象(就像React.js那样),然后再统一写入到Excel文件。因此性能可以达到其它库达不到的目标:

  1. Aspose.Cellsxml等实现相关技术只字未提(可能因为要支持多种文件格式);
  2. Aspose.Cells是先在内存中创建,再写入文件/流(NPOI也是);
  3. Aspose.Cells创建Excel时要求客户直接使用Workbook类(NPOI也是);
  4. Aspose.Cells完全隐藏了Excel的位置(如B3)信息,下标从0开始(NPOI也是)

比较这几点,NPOI也与Aspose.Cells有几分相似,但导出不到6MBExcel它内存分配居然高达1.5GB,是后者的444%!毕竟迭代更新了这么多年了,代码质量我相信应该没问题。因此我再次无责任推测:这可能因为它是从Java那边移植过来的。

我的选择/推荐

在我做这个性能评测前,我一直使用的是EPPlus,因为我不喜欢NPOI有第三方依赖,也不喜欢NPOI那些“XSSF”之类的前缀命名,也显然不会去费心思写那么多费力不讨好的OpenXML代码。

更别提这次评测发现EPPlus的性能确实不错,唯一的缺点就是它单元格下标从1开始的设计。即便如此,我还是首选推荐EPPlus

近期也经常使用Aspose.Cells这种商业库,它的功能强大,API清晰好用,这个评测也证明它的性能卓越。除了高昂的价格,没别的缺点了。乃有钱客户/老板的不二之选!

.NET导出Excel的四种方法及评测导出Excel是.NET的常见需求,开源社区、市场上,都提供了不少各式各样的Excel操作相关包。本文,我将使用NPOI、EPPlus、OpenXML、Aspose.Cells四个市面上常见的库,各完成一个导出Excel示例。然后对其代码风格和性能做一个横向比较。最后我将说出我自己的感想。文中所有的示例代码可以在这里下载:https://github....
代码如下: protected void Excel_Click(object sender, EventArgs e) { Response.Charset = “UTF-8”; Response.ClearContent(); Response.Clear(); Response.ContentEncoding = System.Text.Encoding.UTF8; Response.HeaderEncoding = System.Text.Encoding.UTF8; Response.AddHeader(“content-disposition”, “attachment; file
开源强大的操作EXCEL文档的第三方控件EPPlus(源码) 第三方控件EPPLUS 是完全免费的,而且是开源的,而且对一般的操作功能足够 不过从官网下载的源码不知道什么原因有些乱码,下面附件中的源码是我整理过后的源码,版本4.1.1 VS2010下绝对可以运行 官网地址:https://github.com/JanKallman/EPPlus
.NET导出Excel四种方法评测 导出Excel.NET的常见需求,开源社区、市场上,都提供了不少各式各样的Excel操作相关包。本文,我将使用NPOI、EPPlus、OpenXML、Aspose.Cells四个市面上常见的库,各完成一个导出Excel示例。然后对其代码风格和性能做一个横向比较。最后我将说出我自己的感想。 文中所有的示例代码可以在这里下载: https://git...
先来简单介绍下市面上最广泛常见的三种操作excel库的优缺点 1.NPOI 优点:免费开源,无需装Office即可操作excel, 支持处理的文件格式包括xls, xlsx, docx.格式 缺点:不支持大数据量以及多sheet的导出 2.Aspose.Cells 优点:支持大数据量以及多sheet的导出,提供了应有尽有的文件格式支持,速度快性能佳 缺点:除了收费几乎没有缺点,试用版 限制打开文件数量100个,限制使用Aspose.Cells.GridWeb功能,生成的Excel会有水印 3.EPPlus
RDLC报表的使用 机房收费系统这是第三遍重构了,但每当到报表这块都是让我很纠结,这次合作开发亦是如此。在学习vb6时,使用的报表是第三方的Grid++Report;VB.Net个人版以及合作版使用的是在VS .NET自带的报表设计器,Microsoft 将这种报表的后缀定为RDLC. RDLC(Report Definition Language Client)是微软基于RDL在.NET上继
好的,我可以给你一些关于在 ASP.NET导出 Excel 文件时添加“正在加载”界面的指导。 首先,我们需要在界面上添加一个显示“正在加载”的元素,通常是一个 div 或者 span 元素,可以使用 CSS 来设置它的样式。 然后,在点击导出 Excel 按钮时,可以使用 JavaScript 代码来显示“正在加载”元素。 接下来,我们可以使用 ASP.NET 的 Response 对象来导出 Excel 文件。例如: Response.Clear(); Response.AddHeader("content-disposition", "attachment;filename=MyExcelFile.xls"); Response.ContentType = "application/vnd.ms-excel"; // 这里可以写代码来生成 Excel 文件的内容 Response.End(); 最后,在导出完成后,我们可以再次使用 JavaScript 代码来隐藏“正在加载”元素。 总的来说,我们需要在界面上添加一个显示“正在加载”的元素,在点击导出按钮时显示该元素,然后使用 ASP.NET 的 Response 对象导出 Excel 文件,最后隐藏“正在加载”元素。