相信很多人在做项目的都碰到过Excel数据导出的需求,我从最开始使用最原始的HTML拼接(将需要导出的数据拼接成TABLE标签)到后来happy的使用开源的NPOI,EPPlus等开源组件导出EXCEL。 但不久前,我在一个项目碰到一个需求:要将几个分别有近60多万的数据源导出到Excel中,我们先不要讨论这个需求本身是否合理,客户就是要这样。我先后用NPOI和EPPlus,都发现同一个问题:OutOfMemoryException,我电脑12G内存居然不够用? 的确内存溢出了,但内存还剩下好几个G的,就会溢出,我用。NET做的网站,开发的时候Host应该是VisualStudio安装的IISExpress,应该是VS本身的限制,不过在网上查阅资料也没发现这的确也是困扰一些人的,也没查到什么结果。 好在还有Google,跃过墙外,在StackOverflow上查到资料:OpenXML,这不是什么新技:Office2007在设计的时候,为了更好的和其它应用程序交互,使用了XMLZIP技术来实现excel,world,PPT等组件的本地保存,我们所使用xlsx,dox,pptx文件本质上就一个ZIP压缩包,包内是组织好的XML文件,也就是说,我们可以通过生成,修改,生成合规的XML文件,再压缩成ZIP包,这就是一个可以被Office识别的文件了。 用图说话: 在园子里其实也有不少人介绍过OpenXML,我想就多一个视角来介绍OpenXML吧,好像也有很长时间没人写关于这个博文。 什么是OfficeOpenXML? 我们来看下维基百科的定义: OfficeOpenXML(alsoinformallyknownasOOXMLorMicrosoftOpenXML(MOX)〔2)isazipped,XMLbasedfileformatdevelopedbyMicrosoft〔3〕forrepresentingspreadsheets,charts,presentationsandwordprocessingdocuments。TheformatwasinitiallystandardizedbyEcma(asECMA376),andbytheISOandIEC(asISOIEC29500)inlaterversions。 StartingwithMicrosoftOffice2007,theOfficeOpenXMLfileformatshavebecomethedefault〔4〕targetfileformatofMicrosoftOffice。〔5〕〔6〕MicrosoftOffice2010providesreadsupportforECMA376,readwritesupportforISOIEC29500Transitional,andreadsupportforISOIEC29500Strict。〔7〕MicrosoftOffice2013andMicrosoftOffice2016additionallysupportbothreadingandwritingofISOIEC29500Strict。〔8〕re refer:https:en。wikipedia。orgwikiOfficeOpenXML 从Office2007开始,就开始使用XML文件格式作为MicrosoftOffice的默认保存方式,其实我们通常用的NPOIoffice2007部分和EPPlus就是使用OpenXML来开发的。 为什么同是使用OpenXML,NPOI和EPPLus会出现内存溢出的问题? 这两个开源组件有对Office套件有着很全面的支持,它们会把数据加载到内存中一次性处理,如果碰到数据量过大,就很可能遇到这个问题,网上EPPlus在20多万条数据的就溢出了,NPOI在11多万的时候就会溢出,这个是和数据的列数和内容有关系,不管怎样,我们以后可能是会碰到这种大量数据的EXCEL导出,我们不需要很复杂的功能,就是想要导出一个EXCEL列表,这其实是可以做到的。 OpenXML怎样做不会内存溢出? NPOI和EPPlus在导出大量数据的Excel列表时可能会发生内存溢出的问题,原因是它们都把数据保存在内存中,因为它们支持各种复杂的功能,那么简单的列表,就是数量超大,我们把它通过文件流写入磁盘,这个问题就解决了。 如何使用OPENXML? 我们需要去微软官网下载OFFICEOPENXML的SDK,链接:https:www。microsoft。comenhkdownloaddetails。aspx?id30425,推荐使用NuGet在VISULALSTUDIO直接将引用添加到Project。 GitHub示例代码:https:github。comOfficeDevOpenXMLSDK 代码实现 说了这么多废话,我们看如何用OPENXML实现一个EXCEL列表的导出: 从原理上讲就是用OpenXML一个一个把标签写入本地磁盘。 我截取我写的导出类的几个方法来来解释: 指定磁盘路径初始化OpenWorkDoucment summary param privatevoidOpenWorkDocument(stringfileName) { documentSpreadsheetDocument。Create(fileName,SpreadsheetDocumentType。Workbook); } 用datatable作为数据源,实际情况可以根据需要调整 summary publicvoidAddSheet(DataTabledt,stringsheetName) { if(dtnulldt。Rows。Count0) { thrownewArgumentNullException(nameof(dt),datasourcecannotbenull); } if(documentnull) { thrownewArgumentNullException(nameof(document),pleaseinitdocumentfirst); } thislistofattributeswillbeusedwhenwritingastartelement Listattributes; 这是我们为什么不会溢出的关键点,使用XmlWriter写入磁盘 OpenXmlWriterwriter; WorksheetPartworkSheetPartdocument。WorkbookPart。AddNewPart(); writerOpenXmlWriter。Create(workSheetPart); 使用OpenXML麻烦的地方就是我们要用SDK去拼接XML内容 writer。WriteStartElement(newWorksheet()); writer。WriteStartElement(newSheetViews());sheetViews writer。WriteStartElement(newSheetView()sheetView { TabSelectedtrue, WorkbookViewId0U这里的下标是从0开始的 }); 这里是冻结列头,别问为什么是A2,我试了A1不行 PanepanenewPane() { StatenewEnumValue(PaneStateValues。Frozen), VerticalSplitnewDoubleValue((double)1), TopLeftCellnewStringValue(A2), ActivePanenewEnumValue(PaneValues。BottomLeft) }; 对于一些文档本身的结构的描述,我们可以直接把准备属性设置正确,直接写入,因为描述实例很占用资源小,当然我们也可以把描述结点的子节点,子子节点都通过WriteStartElememt写入,不过很麻烦,容易出错 writer。WriteStartElement(pane);Pane writer。WriteEndElement();Pane writer。WriteStartElement(newSelection() { PanenewEnumValue(PaneValues。BottomLeft) }); writer。WriteEndElement();Selection关闭标签 writer。WriteEndElement();sheetView关闭标签 writer。WriteEndElement();sheetViews关闭标签 writer。WriteStartElement(newSheetData()); varrowIndex0; foreach(DataRowrowindt。Rows) { buildheader if(rowIndex0) { createanewlistofattributes attributesnewList(); addtherowindexattributetothelist attributes。Add(newOpenXmlAttribute(r,null,(rowIndex1)。ToString())); headerstart writer。WriteStartElement(newRow(),attributes); foreach(DataColumncolindt。Columns) { attributesnewList(); 这里注意,在Excel在处理字符串的时候,会将所有的字符串保存到sharedStrings。xml,cell内写入在sharedString。XML的索引,属性t(type)设置为s(str)我们在导出excel的时候把sharedString。mxl考虑进来会加大复杂程度,所以将t设置为str,一个不存在的type,excel会直接解析cell内的字串值 attributes。Add(newOpenXmlAttribute(t,null,str)); 通过s指定style样式的下标 attributes。Add(newOpenXmlAttribute(s,null,FORMATINDEXHEADER。ToString())); 能过r指定单元格位置,好像不是必需,注意这里下标位置是从1开始的 attributes。Add(newOpenXmlAttribute(r,,string。Format({0}{1},GetColumnName(col。Ordinal1),rowIndex1))); writer。WriteStartElement(newCell(),attributes); writer。WriteElement(newCellValue(col。ColumnName)); writer。WriteEndElement(); } headerend writer。WriteEndElement(); rowIndex; } 数据写入,我们通过xmlWriter不会触发异常createanewlistofattributes attributesnewList(); addtherowindexattributetothelist attributes。Add(newOpenXmlAttribute(r,null,(rowIndex1)。ToString())); headerstart writer。WriteStartElement(newRow(),attributes); foreach(DataColumncolindt。Columns) { attributesnewList(); switch(col。DataType。ToString()) { caseSystem。Int32: attributes。Add(newOpenXmlAttribute(s,null,FORMATINDEXINT。ToString())); attributes。Add(newOpenXmlAttribute(t,null,n));number break; caseSystem。Double: caseSystem。Decimal: caseSystem。Float: attributes。Add(newOpenXmlAttribute(s,null,FORMATINDEXDEC。ToString()));headerstyle attributes。Add(newOpenXmlAttribute(t,null,n));number break; default: attributes。Add(newOpenXmlAttribute(s,null,FORMATINDEXSTR。ToString()));headerstyle attributes。Add(newOpenXmlAttribute(t,null,str));string break; } addthecellreferenceattribute attributes。Add(newOpenXmlAttribute(r,null,string。Format({0}{1},GetColumnName(col。Ordinal1),rowIndex1))); writer。WriteStartElement(newCell(),attributes); writer。WriteElement(newCellValue(row〔col。Ordinal〕。ToString())); writer。WriteEndElement(); } headerend writer。WriteEndElement(); rowIndex; } EndSheetData writer。WriteEndElement(); EndWorksheet writer。WriteEndElement(); writer。Close(); if(document。WorkbookPart。Workbooknull) { document。WorkbookPart。WorkbooknewWorkbook(); document。WorkbookPart。Workbook。Append(newSheets()); } 数据写入完成后,注册一个sheet引用到workbook。xml,也就是在excel最下面的sheetname varsheetnewSheet() { Name!String。IsNullOrWhiteSpace(sheetName)?sheetName:(SheetDateTime。Now。ToString(ms)), SheetIdUInt32Value。FromUInt32((uint)msheetIndex), Iddocument。WorkbookPart。GetIdOfPart(workSheetPart) }; document。WorkbookPart。Workbook。Sheets。Append(sheet); } 生成Style样式,注意下标从0开始,依次加1,如果有跳过1直接设置3这样情况,可能无法正常解析到样式 privateStylesheetGenerateStylesheet() { StylesheetstyleSheetnull; FontsfontsnewFonts( newFont(Index0default newFontSize(){Val11} ), newFont(Index1header newFontSize(){Val11}, newBold(), newColor(){RgbFFFFFF} )); FillsfillsnewFills( newFill(newPatternFill(){PatternTypePatternValues。None}),Index0default newFill(newPatternFill(){PatternTypePatternValues。Gray125}),Index1default newFill(newPatternFill(newForegroundColor{RgbnewHexBinaryValue(){Value0070c0}}){PatternTypePatternValues。Solid}) ); BordersbordersnewBorders( newBorder(),index0default newBorder(index1blackborder newLeftBorder(newColor(){Autotrue}){StyleBorderStyleValues。Thin}, newRightBorder(newColor(){Autotrue}){StyleBorderStyleValues。Thin}, newTopBorder(newColor(){Autotrue}){StyleBorderStyleValues。Thin}, newBottomBorder(newColor(){Autotrue}){StyleBorderStyleValues。Thin}, newDiagonalBorder()) ); NumberingFormatsnumbersnewNumberingFormats( newNumberingFormat(){NumberFormatId0,FormatCodenewStringValue(,0。00)}, newNumberingFormat(){NumberFormatId1,FormatCodenewStringValue(0)} ); CellFormatscellFormatsnewCellFormats( default newCellFormat(){FormatIdFORMATINDEXDEFUALT}, bodystring newCellFormat{FormatIdFORMATINDEXSTR,FontId0,FillId0,BorderId1,ApplyBordertrue}, bodydecimal newCellFormat{FormatIdFORMATINDEXDEC,FontId0,FillId0,BorderId1,NumberFormatId0,ApplyBordertrue}, header newCellFormat{FormatIdFORMATINDEXHEADER,FontId1,FillId2,BorderId1,ApplyFilltrue},header bodyint newCellFormat{FormatIdFORMATINDEXINT,FontId0,FillId0,BorderId1,NumberFormatId1,ApplyBordertrue} ); styleSheetnewStylesheet(numbers,fonts,fills,borders,cellFormats); returnstyleSheet; } privatevoidWriteWorkbookStyle() { if(document!null) { WorkbookStylesPartstylePartdocument。WorkbookPart。AddNewPart(); varstyleSheetGenerateStylesheet(); styleSheet。Save(stylePart); } } 设置样式,冻结首行,这些都可以简单完成,如果需要添加图表什么的,还是建议用NPOI,EPPlus等开源方案,有图表的excel不会太大。 对于OpenXML的介绍就到这里了,有什么错误的地方,请指正。来源:RHINOWU cnblogs。comrhinop8283219。html