我相信很多人在做项目的都碰到过Excel数据导出的需求,我从最开始使用最原始的HTML拼接(将需要导出的数据拼接成TABLE标签)到后来happy的使用开源的NPOI, EPPlus等开源组件导出EXCEL,但不久前,我在一个项目碰到一个需求:要将几个分别有近60多万的数据源导出到Excel中,我们先不要讨论这个需求本身是否合理,客户就是要这样。我先后用NPOI和EPPlus,都发现同一个问题:OutOfMemoryException,我电脑12G内存居然不够用?

的确内存溢出了,但内存还剩下好几个G的,就会溢出,我用 .NET做的网站,开发的时候Host应该是Visual Studio安装的IIS Express, 应该是VS本身的限制,不过在网上查阅资料也没发现这的确也是困扰一些人的,也没查到什么结果,好在还有Google, 跃过墙外,在Stack Overflow上查到资料: OpenXML , 这不是什么新技:  Office 2007在设计的时候, 为了更好的和其它应用程序交互,使用了XML + ZIP技术来实现excel, world, PPT等组件的本地保存, 我们所使用xlsx, dox, pptx文件本质上就一个ZIP压缩包,包内是组织好的XML文件,也就是说,我们可以通过生成, 修改, 生成合规的XML文件,再压缩成ZIP包,这就是一个可以被Office识别的文件了。

用图说话:

在园子里其实也有不少人介绍过 Open XML, 我想就多一个视角来介绍Open XML吧,好像也有很长时间没人写关于这个博文。

什么是Office Open XML?

我们来看下维基百科的定义:

Office Open XML (also informally known as OOXML or Microsoft Open XML (MOX)[2) is a zipped, XML-based file format developed by Microsoft[3] for representing spreadsheets, charts, presentations and word processing documents. The format was initially standardized by Ecma (as ECMA-376), and by the ISO and IEC (as ISO/IEC 29500) in later versions.

Starting with Microsoft Office 2007, the Office Open XML file formats have become the default[4] target file format of Microsoft Office.[5][6] Microsoft Office 2010 provides read support for ECMA-376, read/write support for ISO/IEC 29500 Transitional, and read support for ISO/IEC 29500 Strict.[7] Microsoft Office 2013 and Microsoft Office 2016 additionally support both reading and writing of ISO/IEC 29500 Strict.[8]re

refer: https://en.wikipedia.org/wiki/Office_Open_XML

从Office 2007开始,就开始使用XML文件格式作为Microsoft Office的默认保存方式,其实我们通常用的NPOI  office 2007部分和EPPlus就是使用Open XML来开发的。

为什么同是使用Open XML, NPOI和EPPLus会出现内存溢出的问题?

这两个开源组件有对Office套件有着很全面的支持,它们会把数据加载到内存中一次性处理,如果碰到数据量过大,就很可能 遇到这个问题,网上EPPlus在20多万条数据的就溢出了,NPOI在11多万的时候就会溢出, 这个是和数据的列数和内容有关系,不管怎样,我们以后可能是会碰到这种大量数据的EXCEL导出,我们不需要很复杂的功能,就是想要导出一个EXCEL列表,这其实是可以做到的。

Open XML怎样做不会内存溢出?

NPOI和EPPlus在导出大量数据 的Excel列表时可能 会发生内存溢出的问题,原因是它们都把数据保存在内存中,因为它们支持各种复杂的功能,那么简单的列表,就是数量超大,我们把它通过文件流写入磁盘,这个问题就解决了。

如何使用OPEN XML?

我们需要去微软官网下载OFFICE OPEN XML的SDK,链接: https://www.microsoft.com/en-hk/download/details.aspx?id=30425,推荐使用NuGet在VISULAL STUDIO直接将引用添加到Project。

在GitHub还有一些示例代码:https://github.com/OfficeDev/Open-XML-SDK

代码实现

说了这么多废话,我们看如何用OPEN XML实现一个EXCEL列表的导出:

从原理上讲就是用OpenXML一个一个把标签写入本地磁盘。

我截取我写的导出类的几个方法来来解释:

  1. /// <summary>
  2. /// 指定磁盘路径初始化OpenWorkDoucment
  3. /// </summary>
  4. /// <param name="fileName"></param>
  5. private void OpenWorkDocument(string fileName)
  6. {
  7. document = SpreadsheetDocument.Create(fileName, SpreadsheetDocumentType.Workbook);
  8. }
  1. ///<summary>
    ///用datatable作为数据源,实际情况可以根据需要调整
    ///</summary>
    public void AddSheet(DataTable dt, string sheetName)
  1. {
  2. if (dt == null || dt.Rows.Count == )
  3. {
  4. throw new ArgumentNullException(nameof(dt), "data source can not be null");
  5. }
  6.  
  7. if (document == null)
  8. {
  9. throw new ArgumentNullException(nameof(document), "please init document first");
  10. }
  11.  
  12. //this list of attributes will be used when writing a start element
  13. List<OpenXmlAttribute> attributes;
           //这是我们为什么不会溢出的关键点, 使用XmlWriter写入磁盘
  14. OpenXmlWriter writer;
  15.  
  16. WorksheetPart workSheetPart = document.WorkbookPart.AddNewPart<WorksheetPart>();
  17. writer = OpenXmlWriter.Create(workSheetPart);
        //使用OpenXML麻烦的地方就是我们要用SDK去拼接XML内容
  18. writer.WriteStartElement(new Worksheet());
  19. writer.WriteStartElement(new SheetViews()); //sheetViews
  20. writer.WriteStartElement(new SheetView() //sheetView
  21. {
  22. TabSelected = true,
  23. WorkbookViewId = 0U //这里的下标是从0开始的
  24. });
  25.         //这里是冻结列头,别问为什么是A2,我试了A1不行
  26. Pane pane = new Pane()
  27. {
  28. State = new EnumValue<PaneStateValues>(PaneStateValues.Frozen),
  29. VerticalSplit = new DoubleValue((double)),
  30. TopLeftCell = new StringValue("A2"),
  31. ActivePane = new EnumValue<PaneValues>(PaneValues.BottomLeft)
  32. };
  33.        //对于一些文档本身的结构的描述,我们可以直接把准备属性设置正确,直接写入,因为描述实例很占用资源小,当然我们也可以把描述结点的子节点,子子节点都通过WriteStartElememt写入,不过很麻烦,容易出错
  34. writer.WriteStartElement(pane); //Pane
  35. writer.WriteEndElement(); //Pane
  36. writer.WriteStartElement(new Selection()
  37. {
  38. Pane = new EnumValue<PaneValues>(PaneValues.BottomLeft)
  39. });
  40. writer.WriteEndElement(); //Selection 关闭标签
  41. writer.WriteEndElement(); //sheetView 关闭标签
  42. writer.WriteEndElement(); //sheetViews 关闭标签
  43.  
  44. writer.WriteStartElement(new SheetData());
  45. var rowIndex = ;
  46. foreach (DataRow row in dt.Rows)
  47. {
  48. //build header
  49. if (rowIndex == )
  50. {
  51. //create a new list of attributes
  52. attributes = new List<OpenXmlAttribute>();
  53. // add the row index attribute to the list
  54. attributes.Add(new OpenXmlAttribute("r", null, (rowIndex + ).ToString()));
  55. //header start
  56. writer.WriteStartElement(new Row(), attributes);
  57. foreach (DataColumn col in dt.Columns)
  58. {
  59. attributes = new List<OpenXmlAttribute>();
  60. //这里注意,在Excel在处理字符串的时候,会将所有的字符串保存到sharedStrings.xml, cell内写入在sharedString.XML的索引, 属性t(type)设置为s(str)//我们在导出excel的时候把sharedString.mxl考虑进来会加大复杂程度,所以将t设置为str, 一个不存在的type, excel会直接解析cell内的字串值
  61. attributes.Add(new OpenXmlAttribute("t", null, "str"));
  62. //通过s指定style样式的下标
  63. attributes.Add(new OpenXmlAttribute("s", null, FORMAT_INDEX_HEADER.ToString()));
    //能过r指定单元格位置,好像不是必需, 注意这里下标位置是从1开始的
  64. attributes.Add(new OpenXmlAttribute("r", "", string.Format("{0}{1}", GetColumnName(col.Ordinal + ), rowIndex + )));
  65. writer.WriteStartElement(new Cell(), attributes);
  66. writer.WriteElement(new CellValue(col.ColumnName));
  67. writer.WriteEndElement();
  68.  
  69. }
  70. //header end
  71. writer.WriteEndElement();
  72. rowIndex++;
  73. }
  74.  
  75. //数据写入,我们通过xmlWriter不会触发异常//create a new list of attributes
  76. attributes = new List<OpenXmlAttribute>();
  77. // add the row index attribute to the list
  78. attributes.Add(new OpenXmlAttribute("r", null, (rowIndex + ).ToString()));
  79. //header start
  80. writer.WriteStartElement(new Row(), attributes);
  81. foreach (DataColumn col in dt.Columns)
  82. {
  83. attributes = new List<OpenXmlAttribute>();
  84.  
  85. switch (col.DataType.ToString())
  86. {
  87. case "System.Int32":
  88. attributes.Add(new OpenXmlAttribute("s", null, FORMAT_INDEX_INT.ToString()));
  89. attributes.Add(new OpenXmlAttribute("t", null, "n")); //number
  90. break;
  91. case "System.Double":
  92. case "System.Decimal":
  93. case "System.Float":
  94. attributes.Add(new OpenXmlAttribute("s", null, FORMAT_INDEX_DEC.ToString())); //header style
  95. attributes.Add(new OpenXmlAttribute("t", null, "n")); //number
  96. break;
  97. default:
  98. attributes.Add(new OpenXmlAttribute("s", null, FORMAT_INDEX_STR.ToString())); //header style
  99. attributes.Add(new OpenXmlAttribute("t", null, "str")); //string
  100. break;
  101. }
  102. //add the cell reference attribute
  103. attributes.Add(new OpenXmlAttribute("r", null, string.Format("{0}{1}", GetColumnName(col.Ordinal + ), rowIndex + )));
  104. writer.WriteStartElement(new Cell(), attributes);
  105. writer.WriteElement(new CellValue(row[col.Ordinal].ToString()));
  106. writer.WriteEndElement();
  107. }
  108. //header end
  109. writer.WriteEndElement();
  110.  
  111. rowIndex++;
  112. }
  113.  
  114. // End SheetData
  115. writer.WriteEndElement();
  116. // End Worksheet
  117. writer.WriteEndElement();
  118. writer.Close();
  119.  
  120. if (document.WorkbookPart.Workbook == null)
  121. {
  122. document.WorkbookPart.Workbook = new Workbook();
  123. document.WorkbookPart.Workbook.Append(new Sheets());
  124. }
  125. //数据写入完成后,注册一个sheet引用到workbook.xml, 也就是在excel最下面的sheet name
  126. var sheet = new Sheet()
  127. {
  128. Name = !String.IsNullOrWhiteSpace(sheetName) ? sheetName : ("Sheet " + DateTime.Now.ToString("ms")),
  129. SheetId = UInt32Value.FromUInt32((uint)m_sheetIndex++),
  130. Id = document.WorkbookPart.GetIdOfPart(workSheetPart)
  131. };
  132. document.WorkbookPart.Workbook.Sheets.Append(sheet);
  133. }
  1. //生成Style样式, 注意下标从0开始, 依次加1, 如果有跳过1直接设置3这样情况, 可能无法正常解析到样式
  2. private Stylesheet GenerateStylesheet()
  3. {
  4. Stylesheet styleSheet = null;
  5.  
  6. Fonts fonts = new Fonts(
  7. new Font( // Index 0 - default
  8. new FontSize() { Val = }
  9.  
  10. ),
  11. new Font( // Index 1 - header
  12. new FontSize() { Val = },
  13. new Bold(),
  14. new Color() { Rgb = "FFFFFF" }
  15.  
  16. ));
  17.  
  18. Fills fills = new Fills(
  19. new Fill(new PatternFill() { PatternType = PatternValues.None }), // Index 0 - default
  20. new Fill(new PatternFill() { PatternType = PatternValues.Gray125 }), // Index 1 - default
  21. new Fill(new PatternFill(new ForegroundColor { Rgb = new HexBinaryValue() { Value = "0070c0" } }) { PatternType = PatternValues.Solid })
  22. );
  23.  
  24. Borders borders = new Borders(
  25. new Border(), // index 0 default
  26. new Border( // index 1 black border
  27. new LeftBorder(new Color() { Auto = true }) { Style = BorderStyleValues.Thin },
  28. new RightBorder(new Color() { Auto = true }) { Style = BorderStyleValues.Thin },
  29. new TopBorder(new Color() { Auto = true }) { Style = BorderStyleValues.Thin },
  30. new BottomBorder(new Color() { Auto = true }) { Style = BorderStyleValues.Thin },
  31. new DiagonalBorder())
  32. );
  33.  
  34. NumberingFormats numbers = new NumberingFormats(
  35. new NumberingFormat() { NumberFormatId = , FormatCode = new StringValue("#,##0.00") },
  36. new NumberingFormat() { NumberFormatId = , FormatCode = new StringValue("") }
  37. );
  38.  
  39. CellFormats cellFormats = new CellFormats(
  40. // default
  41. new CellFormat() { FormatId = FORMAT_INDEX_DEFUALT },
  42. // body string
  43. new CellFormat { FormatId = FORMAT_INDEX_STR, FontId = , FillId = , BorderId = , ApplyBorder = true },
  44. // body decimal
  45. new CellFormat { FormatId = FORMAT_INDEX_DEC, FontId = , FillId = , BorderId = , NumberFormatId = , ApplyBorder = true },
  46. //header
  47. new CellFormat { FormatId = FORMAT_INDEX_HEADER, FontId = , FillId = , BorderId = , ApplyFill = true }, // header
  48. // body int
  49. new CellFormat { FormatId = FORMAT_INDEX_INT, FontId = , FillId = , BorderId = , NumberFormatId = , ApplyBorder = true }
  50. );
  51.  
  52. styleSheet = new Stylesheet(numbers, fonts, fills, borders, cellFormats);
  53. return styleSheet;  
  54. }  
  1. private void WriteWorkbookStyle()
  2. {
  3. if (document != null)
  4. {
  5. WorkbookStylesPart stylePart = document.WorkbookPart.AddNewPart<WorkbookStylesPart>();
  6. var styleSheet = GenerateStylesheet();
  7. styleSheet.Save(stylePart);
  8. }
  9.  
  10. }

设置样式,冻结首行,这些都可以简单完成,如果需要添加图表什么的,还是建议用NPOI, EPPlus等开源方案,有图表的excel不会太大。

对于Open XML的介绍就到这里了,有什么错误的地方,请指正。

.NET使用Office Open XML导出超大数量数据到 Excel的更多相关文章

  1. Office Open XML导出大数据

    Office Open XML导出大量数据到 Excel .NET使用Office Open XML导出大量数据到 Excel我相信很多人在做项目的都碰到过Excel数据导出的需求,我从最开始使用最原 ...

  2. 使用POI导出百万级数据到excel的解决方案

    1.HSSFWorkbook 和SXSSFWorkbook区别 HSSFWorkbook:是操作Excel2003以前(包括2003)的版本,扩展名是.xls,一张表最大支持65536行数据,256列 ...

  3. .NET使用Office Open XML导出大量数据到 Excel

    我相信很多人在做项目的都碰到过Excel数据导出的需求,我从最开始使用最原始的HTML拼接(将需要导出的数据拼接成TABLE标签)到后来happy的使用开源的NPOI, EPPlus等开源组件导出EX ...

  4. 关于Java导出100万行数据到Excel的优化方案

    1>场景 项目中需要从数据库中导出100万行数据,以excel形式下载并且只要一张sheet(打开这么大文件有多慢另说,呵呵). ps:xlsx最大容纳1048576行 ,csv最大容纳1048 ...

  5. java 分页导出百万级数据到excel

    最近修改了一个导出员工培训课程的历史记录(一年数据),导出功能本来就有的,不过前台做了时间限制(只能选择一个月时间内的),还有一些必选条件, 导出的数据非常有局限性.心想:为什么要做出这么多条件限制呢 ...

  6. 导出jqgrid表格数据为EXCEL文件,通过tableExport.js插件。

    今天公司项目需要做个导出功能,将jqgrid查询出的数据导出为EXCEL表格文件,期间遇到两个问题: 1.导出报错 uncaught exception: INVALID_CHARACTER_ERR: ...

  7. 问问题_Java一次导出百万条数据生成excel(web操作)

    需求:在web页面操作,一次导出百万条数据并生成excel 分析: 1.异步生成Excel,非实时,完成后使用某种方式通知用户 2.生成多个excel文件,并打包成zip文件,因为一个excel容纳不 ...

  8. 导出网页表格数据为Excel文件的前端解决方案

    在工作中,我们有时会遇到这样的需求,比如:要把页面的表格数据导出为Excel文件.在此记录下自己用的解决方法.代码如下: function tableToExcel(data){ //要导出的数据,t ...

  9. 导出html table 数据到Excel

    其实只需要复制  粘贴.... <script type="text/javascript" src="http://code.jquery.com/jquery- ...

随机推荐

  1. iOS有关图片处理的总结 (四)------图片的饱和度,亮度,对照度。

    在做图片处理的时候.会遇到调节图片的饱和度的问题,这里就要用到Core Image这个框架,Core Image是一个非常强大的框架. 它能够让你简单地应用各种滤镜来处理图像,比方改动鲜艳程度, 色泽 ...

  2. 豌豆夹Redis解决方式Codis源代码剖析:Proxy代理

    豌豆夹Redis解决方式Codis源代码剖析:Proxy代理 1.预备知识 1.1 Codis Codis就不详细说了,摘抄一下GitHub上的一些项目描写叙述: Codis is a proxy b ...

  3. RecyclerView高速通用适配Adapter

    RecyclerView Adapter 为RecyclerView提供更简单的适配器实现方式,不断更新完好中. Demo视频演示 GitHub地址 博客 使用 BaseViewHolder 的使用 ...

  4. 我的Java开发学习之旅------&gt;Base64的编码思想以及Java实现

    Base64是一种用64个字符来表示随意二进制数据的方法. 用记事本打开exe.jpg.pdf这些文件时,我们都会看到一大堆乱码,由于二进制文件包括非常多无法显示和打印的字符.所以,假设要让记事本这种 ...

  5. Javaproject集成log4j 2.x

    log4j2和log4j在配置文件和引入jar包上出现了不同.这里做个备忘,这里使用的版本号为apache-log4j-2.3-bin.zip. 1.apache-log4j-2.3-bin.zip下 ...

  6. 入门vue----(介绍)

    声明式渲染 Vue.js 的核心是一个允许采用简洁的模板语法来声明式的将数据渲染进 DOM 的系统: <div id="app"> {{ message }} < ...

  7. 源生js惯性滚动与回弹效果

    在写移动端的APP或者页面时,经常会遇到惯性滚动与回弹效果.用插件iscroll可以轻松解决这个问题,大多数的移动框架也能轻松解决这个问题,它们内部都封装了这个效果. 一直好奇这个效果原生JS是怎么实 ...

  8. 《On Writing Well 30th Anniversa》【PDF】下载

    <On Writing Well 30th Anniversa>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230382210 内容简 ...

  9. C# 委托与事件详解(三)

    今天我接着上面的3篇文章来讲一下,为什么我们在日常的编程活动中遇到这么多sender,EventArgs e 参数:protected void Page_Load(object sender, Ev ...

  10. iOS 本地项目上传github,github管理项目配置

    一.注册github账号   首先需要注册一个github账号,注册地址:https://github.com 接着会来到这 然后会收到一封github发的邮件,进入邮箱验证 二.创建个人的githu ...