.net npoi xssfclientanchor设置图片缩放大小_.NET导出Excel的四种方法及评测
前言
導(dǎo)出Excel是.NET的常見(jiàn)需求,開(kāi)源社區(qū)、市場(chǎng)上,都提供了不少各式各樣的Excel操作相關(guān)包。本文,我將使用NPOI、EPPlus、OpenXML、Aspose.Cells四個(gè)市面上常見(jiàn)的庫(kù),各完成一個(gè)導(dǎo)出Excel示例。然后對(duì)其代碼風(fēng)格和性能做一個(gè)橫向比較。最后我將說(shuō)出我自己的感想。文中所有的示例代碼可以在這里下載:https://github.com/sdcb/blog-data/tree/master/2019/20190824-dotnet-excel-compareNPOI
NPOI源自于Java的Apache POI(https://poi.apache.org/),目前最新版本是2.4.1。NPOI是開(kāi)源項(xiàng)目,作者是華人(https://github.com/tonyqus/),項(xiàng)目地址是:https://github.com/tonyqus/npoi。
幾年前大家導(dǎo)出Excel都使用COM,但COM不方便,這個(gè)組件的推出無(wú)疑彌補(bǔ)了.NET在Excel方面組件的空白,大家都說(shuō)比COM好用。
NPOI還加入了.NET Core Community組織,項(xiàng)目地址是:https://github.com/dotnetcore/NPOI。
EPPlus
EPPlus是另一個(gè)開(kāi)源的Excel操作庫(kù),目前最新版本是4.5.3.2。Github地址是:https://github.com/JanKallman/EPPlus。
EPPlus僅依賴基礎(chǔ)類庫(kù)BCL,完全沒(méi)有第三方包依賴,也是.NET原生庫(kù)。
EPPlus只支持導(dǎo)出Office 2007之后的格式,也就是xlsx。這已經(jīng)是存在12年的格式了,但如果有客戶想要導(dǎo)出xls,EPPlus將不支持。
OpenXML
OpenXML的NuGet包全稱是DocumentFormat.OpenXml:是微軟推出的較為低層的Excel操作庫(kù),最新穩(wěn)定版本是2.9.1。OpenXML也是開(kāi)源項(xiàng)目,地址是:https://github.com/OfficeDev/Open-XML-SDK。
從該項(xiàng)目的名字可以看出,OpenXML比較涉及底層,因此很容易令人浮想聯(lián)翩,感覺(jué)它的性能、速度很可能是最快的,但真的如此嗎?
Aspose.Cells
這是Aspose Pty Ltd公司推出的Excel操作庫(kù)。它是眾多Aspose File Format API產(chǎn)品其中之一。目前最新版本是19.8.0(基于年/月)。Aspose提供了應(yīng)有盡有的文件格式支持,除了.NET外,Aspose還提供了C++和Java的包。
據(jù)我所知Aspose的客戶支持服務(wù)也不錯(cuò),客戶提出的問(wèn)題經(jīng)常可以在下一次發(fā)布時(shí)解決。
Aspose.Cells是不開(kāi)源,付費(fèi)的庫(kù),但提供無(wú)限期的試用,據(jù)[官方網(wǎng)站](https://docs.aspose.com/display/cellsnet/Licensing#Licensing-EvaluationVersionLimitations)顯示,試用版將
限制打開(kāi)文件數(shù)量100個(gè)
限制使用Aspose.Cells.GridWeb功能
生成的Excel將添加如下水印:
但經(jīng)過(guò)我的試用,無(wú)論是并行還是串行,都沒(méi)找到限制打開(kāi)文件數(shù)量100個(gè)的限制。因此,“試用版”對(duì)我們的物理限制,就只有這個(gè)水印了(當(dāng)然加了這個(gè)水印,客戶肯定也不會(huì)有好表情?)。
Excel-COM
COM是隨著Excel安裝而自帶的庫(kù),Excel的包名叫Microsoft.Office.Interop.Excel。本文不會(huì)深入解析,具體可以看[這篇文檔](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interop/how-to-access-office-onterop-objects)。
我想要多說(shuō)兩句的是,COM的old-fashion(過(guò)時(shí))不是沒(méi)有原因的,據(jù)我所知COM有以下缺點(diǎn):
調(diào)用時(shí)會(huì)啟動(dòng)一個(gè)進(jìn)程外的excel.exe,可能因?yàn)樗鼮槭菍iT(mén)為Office設(shè)計(jì)的(不是為.NET集成設(shè)計(jì)的)
要求目標(biāo)環(huán)境安裝相關(guān)軟件,沒(méi)安裝將無(wú)法運(yùn)行
顯然也沒(méi)辦法跨平臺(tái)
使用了大量動(dòng)態(tài)/多參數(shù)接口,對(duì)開(kāi)發(fā)不是很友好
不像托管內(nèi)存,COM對(duì)資源釋放也有要求,具體參見(jiàn)[這篇文章](https://www.breezetree.com/blog/common-mistakes-programming-excel-with-c-sharp)
橫向比較
| NPOI | EPPlus | OpenXML | Aspose | |
| 包依賴 | 有1個(gè) | 無(wú) | 無(wú) | 無(wú) |
| 封裝程度 | 正常 | 正常 | 低層 | 正常 |
| 支持格式 | 完善 | 僅xlsx | 僅xlsx | 完善 |
| 開(kāi)源協(xié)議 | Apache-2.0 | LGPL | MIT | 不開(kāi)源? |
| 收費(fèi)類型 | 免費(fèi) | 免費(fèi) | 免費(fèi) | 收費(fèi) |
評(píng)測(cè)說(shuō)明
版本與數(shù)據(jù)
所有代碼的版本號(hào)基于上文中提到的最新穩(wěn)定版本:
| 包 | 最新穩(wěn)定版本號(hào) |
| NPOI | 2.4.1 |
| EPPlus | 4.5.3.2 |
| OpenXML | 2.9.1 |
| Aspose.Cells | 19.8.0 |
數(shù)據(jù)全部基于我上篇文章使用的6萬(wàn)條/10列的數(shù)據(jù),總共數(shù)據(jù)量19,166 KB。所有數(shù)據(jù)可以從這里下載:https://github.com/sdcb/blog-data/tree/master/2019/20190821-generate-lorem-data
環(huán)境
| 項(xiàng)目 | 值 |
| CPU | E3-1230 v3 @ 3.30GHz |
| 內(nèi)存 | 24GB DDR3-1600 MHz (8GBx3)? |
| ?操作系統(tǒng) | Windows 10 1903 64位 |
| 電源選項(xiàng) | 已設(shè)置為“高性能”? |
| 軟件 | LINQPad 6.0.18 |
| 運(yùn)行時(shí)環(huán)境 | .NET Core 3.0-preview8-28405-07 |
注意,LINQPad設(shè)置了optimize+,代碼都是優(yōu)化后執(zhí)行的;代碼都指定了Util.NewProcess = true;,確保每次運(yùn)行都會(huì)在新進(jìn)程中運(yùn)行,不會(huì)互相影響。
我的性能測(cè)試函數(shù)介紹
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 { 次數(shù) = i, 分配內(nèi)存 = (allocate2 - allocate1).ToString("N0"), 內(nèi)存提高 = (memory2 - memory1).ToString("N0"), 耗時(shí) = sw.ElapsedMilliseconds, }; });}除了時(shí)間,內(nèi)存占用實(shí)際也是非常非常重要、但容易被人忽略的性能指標(biāo)。大家都以為“內(nèi)存不值錢(qián)”,但——
一旦訪問(wèn)量大,內(nèi)存就會(huì)瞬間上漲,導(dǎo)致頻繁GC,導(dǎo)致性能下降;
內(nèi)存高也會(huì)導(dǎo)致服務(wù)器分頁(yè),這時(shí)性能就會(huì)急劇下降;
吞吐量下降會(huì)導(dǎo)致隊(duì)列排滿,此時(shí)服務(wù)器就會(huì)報(bào)503等錯(cuò)誤,客戶就發(fā)現(xiàn)服務(wù)器“宕機(jī)了”。
(提示:除非你的客戶真的愿意多花錢(qián)再升級(jí)一下服務(wù)器,否則不要提“內(nèi)存不值錢(qián)”。)
在我的性能測(cè)試函數(shù)中,使用了如下兩個(gè)函數(shù)來(lái)測(cè)試內(nèi)存占用:
GC.GetTotalAllocatedBytes(true) 獲取分配內(nèi)存大小
GC.GetTotalMemory(true) 獲取占用內(nèi)存大小
占用內(nèi)存可能會(huì)比分配內(nèi)存小,因?yàn)榇嬖诶厥?GC),但GC會(huì)影響性能。
通過(guò)調(diào)用Measure函數(shù),可以測(cè)得傳入的action的耗時(shí)和內(nèi)存占用。默認(rèn)會(huì)調(diào)用5次,可以從5次測(cè)試結(jié)果中取出能反映性能的值。
測(cè)試基準(zhǔn)
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;}注意:
我有意使用了反射,這符合我們導(dǎo)出Excel代碼簡(jiǎn)單、易學(xué)、好用、好擴(kuò)展的愿意;
我有意使用了泛型T,而不是實(shí)際類型,這也讓這些代碼容易擴(kuò)展;
里面的noCache用來(lái)規(guī)避編譯器優(yōu)化刪除代碼的行為
測(cè)試結(jié)果:
| 次數(shù) | 分配內(nèi)存 | 內(nèi)存提高 | 耗時(shí) |
| 1 | 9,863,520 | 8,712 | 156 |
| 2 | 9,852,592 | 0 | 138 |
| 3 | 9,852,592 | 0 | 147 |
| 4 | 9,873,096 | 9,240 | 136 |
| 5 | 9,853,936 | 776 | 133 |
可見(jiàn),基于反射操作6萬(wàn)/10列數(shù)據(jù),每次需要分配約9MB內(nèi)存,但這些內(nèi)存都會(huì)被快速GC,最終內(nèi)存提高較少。這些使用反射的代碼運(yùn)行耗時(shí)在130ms-150ms左右。
各個(gè)庫(kù)的使用和性能表現(xiàn)
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這個(gè)前綴是從Java的POI庫(kù)傳過(guò)來(lái)的,全稱是XML SpreadSheet Format。
這種前綴在NPOI包中很常見(jiàn)。
XSSFWorkbook提供了bool Dispose()方法,但它未實(shí)現(xiàn)(因此千萬(wàn)別調(diào)用它):
性能測(cè)試結(jié)果:
| 次數(shù) | 分配內(nèi)存 | 內(nèi)存提高 | 耗時(shí) |
| 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 |
分配內(nèi)存穩(wěn)定在1.48GB的樣子,首次內(nèi)存會(huì)提高524KB左右,后面趨于穩(wěn)定。首次耗時(shí)6秒多,后面穩(wěn)定在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的下標(biāo)是基于1的(而不是0)。
| 次數(shù) | 分配內(nèi)存 | 內(nèi)存提高 | 耗時(shí) |
| 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 |
分配內(nèi)存約508MB,耗時(shí)首次稍長(zhǎng),約3.2秒,后面穩(wěn)定在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();}注意,因?yàn)?#96;OpenXML`比較偏低層,東西比較復(fù)雜,所以我們慢慢說(shuō):
對(duì)于一些對(duì)象,它需要?jiǎng)?chuàng)建相應(yīng)的Part,如WorksheetPart;
Excel可以使用SharedStringTable來(lái)共享變量值,適合相同字符串非常多的場(chǎng)景。
但此示例共享變量值收益很低,但會(huì)極大地增加代碼復(fù)雜性(普通用戶可能很難寫(xiě)出),因此本示例未使用SharedStringTable;
它基于單元格位置標(biāo)識(shí),如B3(第三行第二列),因此索引方式比EPPlus/NPOI都要復(fù)雜;
代碼示例中使用'A' + i - 1來(lái)計(jì)算位置標(biāo)識(shí),因此這個(gè)示例不能用于超過(guò)26列(字母數(shù))的數(shù)據(jù);
代碼使用LINQ(而不是循環(huán))來(lái)枚舉所有行/列,可以讓代碼在已經(jīng)非常復(fù)雜的情況下,更簡(jiǎn)潔一點(diǎn);
經(jīng)測(cè)試,將LINQ改成for循環(huán)對(duì)性能結(jié)果變化影響極其微小。
測(cè)試結(jié)果如下:
| 次數(shù) | 分配內(nèi)存 | 內(nèi)存提高 | 耗時(shí) |
| 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 |
內(nèi)存占用約530MB左右,第一次比后面多1MB的樣子,耗時(shí)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三個(gè)默認(rèn)的工作表,因此取這三個(gè)工作表時(shí),不要?jiǎng)?chuàng)建,而是取出來(lái)。
性能測(cè)試結(jié)果如下:
| 次數(shù) | 分配內(nèi)存 | 內(nèi)存提高 | 耗時(shí) |
| 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首次占用內(nèi)存385MB,用于3.3秒,后面每次降低為內(nèi)存341MB,用時(shí)2.0秒。
總結(jié)
四種導(dǎo)出Excel庫(kù)的橫向評(píng)測(cè)數(shù)據(jù)如下,數(shù)據(jù)取5次數(shù)值的內(nèi)存消耗中位數(shù)
,百分比以EPPlus的測(cè)試數(shù)據(jù)為100%基準(zhǔn):
| 次數(shù) | 分配內(nèi)存 | 內(nèi)存占比 | 耗時(shí)? | 耗時(shí)占比 |
| 基準(zhǔn)(僅反射) | 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% |
可以得出以下結(jié)論:
Demo基于反射,但反射總損耗的性能不高,內(nèi)存、耗時(shí)均不超過(guò)5%;
NPOI的性能表現(xiàn)是所有項(xiàng)目中最差的,每次需要分配1.5GB的內(nèi)存和超過(guò)10秒的耗時(shí);
EPPlus表現(xiàn)不錯(cuò),內(nèi)存和耗時(shí)在開(kāi)源組中表現(xiàn)最佳;
收費(fèi)的Aspose.Cells表現(xiàn)最佳,內(nèi)存占用最低,用時(shí)也最短;
較為底層的OpenXML表現(xiàn)非常一般,比EPPlus要差,更不能與收費(fèi)的Aspose相提并論;
我的感想
在真的愿意嘗試一下之前,人們很容易相信自己的直覺(jué)。底層庫(kù),通常能帶來(lái)更大的可擴(kuò)展性,能做出上層庫(kù)很難做的事來(lái)。底層庫(kù)有時(shí)性能會(huì)更快,就像更底層的C/C++比上層的JavaScript更快一樣。但事情也不都如此,如
更高層的React.js能在性能上將較底層的DOM操作比下去
數(shù)據(jù)庫(kù)基于集合的操作也比基于游標(biāo)的操作要快得多
在導(dǎo)出Excel這個(gè)例子中,我了解到Excel的xlsx格式是非常復(fù)雜的、多個(gè)xml的集合。如果基于xml做抽象——也是很正常的做法,拼出6萬(wàn)/10列的數(shù)據(jù),需要至少60萬(wàn)個(gè)xml標(biāo)簽做拼接,很顯然這需要分配/浪費(fèi)大量?jī)?nèi)存,因此性能上不來(lái)。
我基于以下幾點(diǎn)無(wú)責(zé)任猜測(cè):Aspose內(nèi)部可能沒(méi)xml做抽象,而是純數(shù)據(jù)做抽象(就像React.js那樣),然后再統(tǒng)一寫(xiě)入到Excel文件。因此性能可以達(dá)到其它庫(kù)達(dá)不到的目標(biāo):
Aspose.Cells對(duì)xml等實(shí)現(xiàn)相關(guān)技術(shù)只字未提(可能因?yàn)橐С侄喾N文件格式);
Aspose.Cells是先在內(nèi)存中創(chuàng)建,再寫(xiě)入文件/流(NPOI也是);
Aspose.Cells創(chuàng)建Excel時(shí)要求客戶直接使用Workbook類(NPOI也是);
Aspose.Cells完全隱藏了Excel的位置(如B3)信息,下標(biāo)從0開(kāi)始(NPOI也是)
比較這幾點(diǎn),NPOI也與Aspose.Cells有幾分相似,但導(dǎo)出不到6MB的`Excel`它內(nèi)存分配居然高達(dá)1.5GB,是后者的444%!畢竟迭代更新了這么多年了,代碼質(zhì)量我相信應(yīng)該沒(méi)問(wèn)題。因此我再次無(wú)責(zé)任推測(cè):這可能因?yàn)樗菑腏ava那邊移植過(guò)來(lái)的。
我的選擇/推薦
在我做這個(gè)性能評(píng)測(cè)前,我一直使用的是EPPlus,因?yàn)槲也幌矚gNPOI有第三方依賴,也不喜歡NPOI那些“XSSF”之類的前綴命名,也顯然不會(huì)去費(fèi)心思寫(xiě)那么多費(fèi)力不討好的OpenXML代碼。
更別提這次評(píng)測(cè)發(fā)現(xiàn)EPPlus的性能確實(shí)不錯(cuò),唯一的缺點(diǎn)就是它單元格下標(biāo)從1開(kāi)始的設(shè)計(jì)。即便如此,我還是首選推薦EPPlus。
近期也經(jīng)常使用Aspose.Cells這種商業(yè)庫(kù),它的功能強(qiáng)大,API清晰好用,這個(gè)評(píng)測(cè)也證明它的性能卓越。除了高昂(https://purchase.aspose.com/pricing/cells/net)的價(jià)格,沒(méi)別的缺點(diǎn)了。乃有錢(qián)客戶/老板的不二之選!
出處:微信公眾號(hào)【DotNet騷操作】
微信不能留言,請(qǐng)點(diǎn)擊原文鏈接去博客園留言。原文鏈接:https://www.cnblogs.com/sdflysha/p/20190824-dotnet-excel-compare.html
覺(jué)得好看,請(qǐng)點(diǎn)這里↓↓↓
總結(jié)
以上是生活随笔為你收集整理的.net npoi xssfclientanchor设置图片缩放大小_.NET导出Excel的四种方法及评测的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: golang 的交叉编译
- 下一篇: 造句简单_造句游戏的心理投射:被现代心理