.net 从XLSX导出大量数据-OutOfMemoryException

.net 从XLSX导出大量数据-OutOfMemoryException,.net,openxml,xlsx,closedxml,vb.net,vba,c#,python,java,.net,Openxml,Xlsx,Closedxml,Vb.net,Vba,C#,Python,Java,我即将以Excel OpenXML格式(xlsx)导出大量数据(115000行x30列)。 我正在使用一些库,如DocumentFormat.OpenXML、ClosedXML、NPOI 每次执行此操作时,都会抛出OutOfMemoryException,因为内存中的工作表表示会导致内存呈指数级增长 另外,每1000行关闭一次文档文件(并释放内存),下一次加载会导致内存增加 有没有一种更高效的方法可以在不占用大量内存的情况下以xlsx格式导出数据?看起来您使用的是必须使用数据库的电子表格。它有其

我即将以Excel OpenXML格式(xlsx)导出大量数据(115000行x30列)。 我正在使用一些库,如DocumentFormat.OpenXML、ClosedXML、NPOI

每次执行此操作时,都会抛出OutOfMemoryException,因为内存中的工作表表示会导致内存呈指数级增长

另外,每1000行关闭一次文档文件(并释放内存),下一次加载会导致内存增加


有没有一种更高效的方法可以在不占用大量内存的情况下以xlsx格式导出数据?

看起来您使用的是必须使用数据库的电子表格。它有其局限性,这很容易成为其中之一。如果您绝对需要坚持现有解决方案,请进一步阅读。不过,我不建议这样做。因为还有一个问题:如果Excel无法保存这么大的文件,它能打开这样的文件吗?

因此,如果您无法切换到数据库平台,并且上面提到的标准库在内部无法处理如此大量的数据,那么在创建大型XLSX时,您可能需要自己处理。我的意思是,例如,这种方法:

  • 将数据分批(1000或10000或任何有效的数据)导出到每个批次的单独文件中
  • 创建一个工具((这是最接近的),任何有坚实的XML库的工具)将不同的文件连接成一个文件。它涉及:

  • 从XLSX提取XML(通常是
    file.XLSX\xl\worksheets\sheet1.XML
    file.XLSX\xl\worksheets\sharedStrings.XML
  • 通过XML操纵库将这些部分粘合在一起(这不会在OutOfMemoryException上崩溃,因为您不再使用复杂的电子表格对象)
  • 将结果文件重新打包回主XLSX(您可以将第一批输出文件作为主XLSX)

  • 我已经向您展示了实现结果的可能方法,但我会避免这样做。Excel从来不是存储大量数据的平台。与上述任务相比,可以更容易地说服管理层,是时候更改该领域的工具/流程了。

    只要计算机内存充足,Excel就能够打开相当大的文件。大多数情况下,这是限制因素

    99%的库都不是为处理大型数据集而构建的,如果您试图向它们抛出太多数据,那么最终会出现内存不足错误


    其中一些,就像我创造的,是为了解决这个问题而创造的。诀窍是流式传输数据,避免将内容存储在内存中。我不确定您使用的是哪种语言(似乎不是PHP),但您的语言可能有一个类似的库。如果没有,您仍然可以查看Spout—它是开源的—并将其转换为您的语言。

    OpenXML SDK是这项工作的合适工具,但您需要小心使用(XML的简单API)方法,而不是转换方法。从SAX的维基百科链接文章:

    当DOM作为一个整体对文档进行操作时,SAX解析器按顺序对XML文档的每一部分进行操作

    这大大减少了处理大型Excel文件时所消耗的内存量

    这里有一篇关于它的好文章-

    根据那篇文章改编,下面是一个输出115k行和30列的示例:

    public static void LargeExport(string filename)
    {
        using (SpreadsheetDocument document = SpreadsheetDocument.Create(filename, SpreadsheetDocumentType.Workbook))
        {
            //this list of attributes will be used when writing a start element
            List<OpenXmlAttribute> attributes;
            OpenXmlWriter writer;
    
            document.AddWorkbookPart();
            WorksheetPart workSheetPart = document.WorkbookPart.AddNewPart<WorksheetPart>();
    
            writer = OpenXmlWriter.Create(workSheetPart);            
            writer.WriteStartElement(new Worksheet());
            writer.WriteStartElement(new SheetData());
    
            for (int rowNum = 1; rowNum <= 115000; ++rowNum)
            {
                //create a new list of attributes
                attributes = new List<OpenXmlAttribute>();
                // add the row index attribute to the list
                attributes.Add(new OpenXmlAttribute("r", null, rowNum.ToString()));
    
                //write the row start element with the row index attribute
                writer.WriteStartElement(new Row(), attributes);
    
                for (int columnNum = 1; columnNum <= 30; ++columnNum)
                {
                    //reset the list of attributes
                    attributes = new List<OpenXmlAttribute>();
                    // add data type attribute - in this case inline string (you might want to look at the shared strings table)
                    attributes.Add(new OpenXmlAttribute("t", null, "str"));
                    //add the cell reference attribute
                    attributes.Add(new OpenXmlAttribute("r", "", string.Format("{0}{1}", GetColumnName(columnNum), rowNum)));
    
                    //write the cell start element with the type and reference attributes
                    writer.WriteStartElement(new Cell(), attributes);
                    //write the cell value
                    writer.WriteElement(new CellValue(string.Format("This is Row {0}, Cell {1}", rowNum, columnNum)));
    
                    // write the end cell element
                    writer.WriteEndElement();
                }
    
                // write the end row element
                writer.WriteEndElement();
            }
    
            // write the end SheetData element
            writer.WriteEndElement();
            // write the end Worksheet element
            writer.WriteEndElement();
            writer.Close();
    
            writer = OpenXmlWriter.Create(document.WorkbookPart);
            writer.WriteStartElement(new Workbook());
            writer.WriteStartElement(new Sheets());
    
            writer.WriteElement(new Sheet()
            {
                Name = "Large Sheet",
                SheetId = 1,
                Id = document.WorkbookPart.GetIdOfPart(workSheetPart)
            });
    
            // End Sheets
            writer.WriteEndElement();
            // End Workbook
            writer.WriteEndElement();
    
            writer.Close();
    
            document.Close();
        }
    }
    
    //A simple helper to get the column name from the column index. This is not well tested!
    private static string GetColumnName(int columnIndex)
    {
        int dividend = columnIndex;
        string columnName = String.Empty;
        int modifier;
    
        while (dividend > 0)
        {
            modifier = (dividend - 1) % 26;
            columnName = Convert.ToChar(65 + modifier).ToString() + columnName;
            dividend = (int)((dividend - modifier) / 26);
        }
    
        return columnName;
    }
    
    公共静态void LargeExport(字符串文件名)
    {
    使用(电子表格文档=电子表格文档.Create(文件名,电子表格文档类型.工作簿))
    {
    //写入开始元素时将使用此属性列表
    列出属性;
    OpenXmlWriter;
    document.AddWorkbookPart();
    WorksheetPart WorksheetPart=document.WorkbookPart.AddNewPart();
    writer=OpenXmlWriter.Create(工作表部分);
    writer.writeStart元素(新工作表());
    writer.writeStarElement(新的SheetData());
    
    对于(int rowNum=1;rowNum可能是另存为.xls的HTML表?不,我需要纯xlsx文件!我同意您的观点,问题是流数据或指向文件(避免在内存中加载完整的工作表表示).你的喷口真的很有趣,但不幸的是,我使用的语言是C#,移植对我来说太过膨胀了me@GianluigiLiguori–也许有一种方法可以安装PHP并直接使用该库。显然,我正在寻找本机的.NET解决方案。您是否尝试过这种方法:?它是由Microsoft构建的,似乎支持大型电子表格是的,它会导致同样的问题:ClosedXML本身在后台使用OpenXML这是有史以来最好的解决方案!!!!导出500.000行x 400列的文件占用60MB的平均内存我很高兴我能提供帮助@GianluigiLiguori@petelids已尝试您的代码并运行!像EPPlus、CsvHelper+CsvHelper.Excel这样的库失败或内存泄漏。您是否引用了这些信息对于方法GetColumnName(为什么使用这些数字?…)还是正确测试的方法?非常感谢@Riga,我很高兴它对您有效。我很确定
    GetColumnName
    是正确的,但我还没有编写一系列测试来确认它。@Riga-
    26
    只是字母表中的字母数;当我们到达
    Z
    时,下一列变成
    AA
    ,然后从
    AZ
    我们转到
    BA
    等。
    65
    A
    的ASCII值……我没有任何其他答案可以解释该逻辑,但我有一个答案可以从另一个角度解释它(即,将单元格引用转换为列索引)这可能有助于解释更多的事情。这是可以找到的。看起来这个答案是反对票最喜欢的目标。