前言
導出 Excel 是 .NET 的常見需求,開源社群、市場上,都提供了不少各式各樣的 Excel 操作相關包。 本文,我将使用 NPOI 、 EPPlus 、 OpenXML 、 Aspose.Cells 四個市面上常見的庫,各完成一個導出 Excel 示例。 然後對其代碼風格和 性能 做一個橫向比較。 最後我将說出我自己的感想。 文中所有的示例代碼可以在這裡下載下傳:https://github.com/sdcb/blog-data/tree/master/2019/20190824-dotnet-excel-compare
NPOI
NPOI源自于Java的Apache POI(https://poi.apache.org/),目前最新版本是2.4.1。NPOI是開源項目,作者是華人(https://github.com/tonyqus/),項目位址是:https://github.com/tonyqus/npoi。
幾年前大家導出Excel都使用COM,但COM不友善,這個元件的推出無疑彌補了.NET在Excel方面元件的空白,大家都說比COM好用。
NPOI還加入了.NET Core Community組織,項目位址是:https://github.com/dotnetcore/NPOI。
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是不開源,付費的庫,但提供無限期的試用,據[官方網站](https://docs.aspose.com/display/cellsnet/Licensing#Licensing-EvaluationVersionLimitations)顯示,試用版将
- 限制打開檔案數量100個
- 限制使用Aspose.Cells.GridWeb功能
- 生成的Excel将添加如下水印:
.net core EPPlus npoi_.NET導出Excel的四種方法及評測前言評測說明各個庫的使用和性能表現總結
但經過我的試用,無論是并行還是串行,都沒找到限制打開檔案數量100個的限制。是以,“試用版”對我們的實體限制,就隻有這個水印了(當然加了這個水印,客戶肯定也不會有好表情?)。
Excel-COM
COM是随着Excel安裝而自帶的庫,Excel的包名叫Microsoft.Office.Interop.Excel。本文不會深入解析,具體可以看[這篇文檔](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interop/how-to-access-office-onterop-objects)。
我想要多說兩句的是,COM的old-fashion(過時)不是沒有原因的,據我所知COM有以下缺點:
- 調用時會啟動一個程序外的excel.exe,可能因為它為是專門為Office設計的(不是為.NET內建設計的)
- 要求目标環境安裝相關軟體,沒安裝将無法運作
- 顯然也沒辦法跨平台
- 使用了大量動态/多參數接口,對開發不是很友好
- 不像托管記憶體,COM對資源釋放也有要求,具體參見[這篇文章](https://www.breezetree.com/blog/common-mistakes-programming-excel-with-c-sharp)
橫向比較
NPOI | EPPlus | OpenXML | Aspose | |
包依賴 | 有1個 | 無 | 無 | 無 |
封裝程度 | 正常 | 正常 | 低層 | 正常 |
支援格式 | 完善 | 僅xlsx | 僅xlsx | 完善 |
開源協定 | Apache-2.0 | LGPL | MIT | 不開源 |
收費類型 | 免費 | 免費 | 免費 | 收費 |
評測說明
版本與資料
所有代碼的版本号基于上文中提到的最新穩定版本:
包 | 最新穩定版本号 |
NPOI | 2.4.1 |
EPPlus | 4.5.3.2 |
OpenXML | 2.9.1 |
Aspose.Cells | 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(List 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;}
注意:
- 我有意使用了反射,這符合我們導出Excel代碼簡單、易學、好用、好擴充的願意;
- 我有意使用了泛型T,而不是實際類型,這也讓這些代碼容易擴充;
- 裡面的noCache用來規避編譯器優化删除代碼的行為
測試結果:
次數 | 配置設定記憶體 | 記憶體提高 | 耗時 |
1 | 9,863,520 | 8,712 | 156 |
2 | 9,852,592 | 138 | |
3 | 9,852,592 | 147 | |
4 | 9,873,096 | 9,240 | 136 |
5 | 9,853,936 | 776 | 133 |
可見,基于反射操作6萬/10列資料,每次需要配置設定約9MB記憶體,但這些記憶體都會被快速GC,最終記憶體提高較少。這些使用反射的代碼運作耗時在130ms-150ms左右。
各個庫的使用和性能表現
NPOI
void Export(List 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這個字首是從Java的POI庫傳過來的,全稱是XML SpreadSheet Format。
這種字首在NPOI包中很常見。
- XSSFWorkbook提供了bool Dispose()方法,但它未實作(是以千萬别調用它):
.net core EPPlus npoi_.NET導出Excel的四種方法及評測前言評測說明各個庫的使用和性能表現總結
性能測試結果:
次數 | 配置設定記憶體 | 記憶體提高 | 耗時 |
1 | 1,598,586,416 | 537,048 | 6590 |
2 | 1,589,239,728 | 7,712 | 10155 |
3 | 1,589,232,056 | -5,368 | 10309 |
4 | 1,589,237,064 | 7,144 | 10355 |
5 | 1,589,245,000 | 9,560 | 10594 |
配置設定記憶體穩定在1.48GB的樣子,首次記憶體會提高524KB左右,後面趨于穩定。首次耗時6秒多,後面穩定在10秒多。
EPPlus
void Export(List 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.Cells,EPPlus的下标是基于1的(而不是0)。
次數 | 配置設定記憶體 | 記憶體提高 | 耗時 |
1 | 534,970,328 | 156,048 | 3248 |
2 | 533,610,232 | 14,896 | 2807 |
3 | 533,595,936 | 7,648 | 2853 |
4 | 533,590,776 | 4,408 | 2742 |
5 | 533,598,440 | 11,280 | 2759 |
配置設定記憶體約508MB,耗時首次稍長,約3.2秒,後面穩定在2.7-2.8秒。
OpenXML
void Export(List data, string path){ using SpreadsheetDocument excel = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook); WorkbookPart workbookPart = excel.AddWorkbookPart(); workbookPart.Workbook = new Workbook(); WorksheetPart worksheetPart = workbookPart.AddNewPart(); worksheetPart.Worksheet = new Worksheet(new SheetData()); Sheets sheets = excel.WorkbookPart.Workbook.AppendChild(new Sheets()); Sheet sheet = new Sheet { Id = excel.WorkbookPart.GetIdOfPart(worksheetPart), SheetId = 1, Name = "Sheet1" }; sheets.Append(sheet); SheetData sheetData = worksheetPart.Worksheet.GetFirstChild(); 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.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.String), })); return row; })); excel.Save();}
注意,因為`OpenXML`比較偏低層,東西比較複雜,是以我們慢慢說:
- 對于一些對象,它需要建立相應的Part,如WorksheetPart;
-
Excel可以使用SharedStringTable來共享變量值,适合相同字元串非常多的場景。
但此示例共享變量值收益很低,但會極大地增加代碼複雜性(普通使用者可能很難寫出),是以本示例未使用SharedStringTable;
- 它基于單元格位置辨別,如B3(第三行第二列),是以索引方式比EPPlus/NPOI都要複雜;
- 代碼示例中使用'A' + i - 1來計算位置辨別,是以這個示例不能用于超過26列(字母數)的資料;
-
代碼使用LINQ(而不是循環)來枚舉所有行/列,可以讓代碼在已經非常複雜的情況下,更簡潔一點;
經測試,将LINQ改成for循環對性能結果變化影響極其微小。
測試結果如下:
次數 | 配置設定記憶體 | 記憶體提高 | 耗時 |
1 | 556,937,896 | 145,832 | 4009 |
2 | 555,981,216 | 312 | 3783 |
3 | 555,985,936 | 2,760 | 3884 |
4 | 555,984,384 | 1,872 | 3869 |
5 | 555,989,120 | 3,880 | 3704 |
記憶體占用約530MB左右,第一次比後面多1MB的樣子,耗時3.7-4.0秒之間。
Aspose.Cells
void Export(List 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.Cells像Excel軟體一樣,提供了Sheet1/Sheet2/Sheet3三個預設的工作表,是以取這三個工作表時,不要建立,而是取出來。
性能測試結果如下:
次數 | 配置設定記憶體 | 記憶體提高 | 耗時 |
1 | 404,004,944 | 3,619,520 | 3316 |
2 | 357,931,648 | 6,048 | 2078 |
3 | 357,934,744 | 7,216 | 2007 |
4 | 357,933,376 | 6,280 | 2017 |
5 | 357,933,360 | 6,424 | 2007 |
Aspose.Cells首次占用記憶體385MB,用于3.3秒,後面每次降低為記憶體341MB,用時2.0秒。
總結
四種導出Excel庫的橫向評測資料如下,資料取5次數值的記憶體消耗中位數
,百分比以EPPlus的測試資料為100%基準:
次數 | 配置設定記憶體 | 記憶體占比 | 耗時 | 耗時占比 |
基準(僅反射) | 9,853,936 | 1.85% | 133 | 4.82% |
NPOI | 1,589,237,064 | 297.83% | 10355 | 375.32% |
EPPlus | 533,598,440 | 100% | 2759 | 100% |
OpenXML | 555,985,936 | 104.19% | 3884 | 140.78% |
Aspose | 357,933,360 | 67% | 2007 | 72.74% |
可以得出以下結論:
- Demo基于反射,但反射總損耗的性能不高,記憶體、耗時均不超過5%;
- NPOI的性能表現是所有項目中最差的,每次需要配置設定1.5GB的記憶體和超過10秒的耗時;
- EPPlus表現不錯,記憶體和耗時在開源組中表現最佳;
- 收費的Aspose.Cells表現最佳,記憶體占用最低,用時也最短;
- 較為底層的OpenXML表現非常一般,比EPPlus要差,更不能與收費的Aspose相提并論;
我的感想
在真的願意嘗試一下之前,人們很容易相信自己的直覺。底層庫,通常能帶來更大的可擴充性,能做出上層庫很難做的事來。底層庫有時性能會更快,就像更底層的C/C++比上層的JavaScript更快一樣。但事情也不都如此,如
- 更高層的React.js能在性能上将較底層的DOM操作比下去
- 資料庫基于集合的操作也比基于遊标的操作要快得多
在導出Excel這個例子中,我了解到Excel的xlsx格式是非常複雜的、多個xml的集合。如果基于xml做抽象——也是很正常的做法,拼出6萬/10列的資料,需要至少60萬個xml标簽做拼接,很顯然這需要配置設定/浪費大量記憶體,是以性能上不來。
我基于以下幾點無責任猜測:Aspose内部可能沒xml做抽象,而是純資料做抽象(就像React.js那樣),然後再統一寫入到Excel檔案。是以性能可以達到其它庫達不到的目标:
- Aspose.Cells對xml等實作相關技術隻字未提(可能因為要支援多種檔案格式);
- Aspose.Cells是先在記憶體中建立,再寫入檔案/流(NPOI也是);
- Aspose.Cells建立Excel時要求客戶直接使用Workbook類(NPOI也是);
- Aspose.Cells完全隐藏了Excel的位置(如B3)資訊,下标從0開始(NPOI也是)
比較這幾點,NPOI也與Aspose.Cells有幾分相似,但導出不到6MB的`Excel`它記憶體配置設定居然高達1.5GB,是後者的444%!畢竟疊代更新了這麼多年了,代碼品質我相信應該沒問題。是以我再次無責任推測:這可能因為它是從Java那邊移植過來的。
我的選擇/推薦
在我做這個性能評測前,我一直使用的是EPPlus,因為我不喜歡NPOI有第三方依賴,也不喜歡NPOI那些“XSSF”之類的字首命名,也顯然不會去費心思寫那麼多費力不讨好的OpenXML代碼。
更别提這次評測發現EPPlus的性能确實不錯,唯一的缺點就是它單元格下标從1開始的設計。即便如此,我還是首選推薦EPPlus。
近期也經常使用Aspose.Cells這種商業庫,它的功能強大,API清晰好用,這個評測也證明它的性能卓越。除了高昂(https://purchase.aspose.com/pricing/cells/net)的價格,沒别的缺點了。乃有錢客戶/老闆的不二之選!
出處:微信公衆号【DotNet騷操作】
微信不能留言,請點選原文連結去部落格園留言。原文連結:https://www.cnblogs.com/sdflysha/p/20190824-dotnet-excel-compare.html
覺得好看,請點這裡↓↓↓