从 XLSX 导出大量数据 - OutOfMemoryException
Export big amount of data from XLSX - OutOfMemoryException
我正准备以 Excel OpenXML 格式 (xlsx) 导出大量数据(115.000 行 x 30 列)。
我正在使用一些库,例如 DocumentFormat.OpenXML、ClosedXML、NPOI。
对于其中的每一个,都会抛出 OutOfMemoryException,因为 sheet 在内存中的表示会导致内存呈指数增长。
同样每1000行关闭文档文件(并释放内存),下次加载导致内存增加。
有没有更高效的方式导出xlsx数据而不占用大量内存?
看起来您正在使用必须使用数据库的电子表格。它有其局限性,这很容易成为其中之一。仅在您绝对需要坚持使用现有解决方案时才进一步阅读。但是,我不推荐它。因为还有一个问题:如果Excel无法保存这么大的文件,能不能打开这么大的文件?
因此,如果您不能切换到数据库平台,并且您上面提到的标准库在内部无法处理如此大量的数据,那么在创建大型 XLSX 时,您可能只能靠自己了。我的意思是例如这种方法:
- 批量导出您的数据(1,000 或 10,000 或任何有效的数据)以将每个批次的文件分开
创建一个工具(vb.net (this is closest to vba), c#, python, java,任何具有可靠 XML 库的工具)将单独的文件合并为一个。涉及:
- 从 XLSX 中提取 XML(通常是
file.xlsx\xl\worksheets\sheet1.xml
和 file.xlsx\xl\worksheets\sharedStrings.xml
)
- 通过 XML 操作库将这些部分粘合在一起(这不会因 OutOfMemoryException 而崩溃,因为您不再使用复杂的电子表格对象)
- 正在将结果文件重新打包回主XLSX(您可以将第一批输出文件作为主XLSX)
我已经向您展示了实现该结果的可能方法,但我会避免这种情况。 Excel从来都不是一个存储大量数据的平台。与上述任务相比,更容易说服管理层是时候更改此区域的 tools/processes。
Excel 可以打开相当大的文件,只要您的计算机有足够的内存。这是大多数时候的限制因素...
99% 的库都不是为处理大型数据集而构建的,如果你试图向它们扔太多数据,你最终会遇到内存不足的错误。
其中一些,例如我创建的 Spout,就是为了解决这个问题而创建的。诀窍是流式传输数据并避免将内容存储在内存中。我不确定您使用的是哪种语言(似乎不是 PHP),但可能有一个类似的库适用于您的语言。如果没有,您仍然可以查看 Spout - 它是开源的 - 并将其转换为您的语言。
OpenXML SDK 是完成这项工作的正确工具,但您在使用 SAX (Simple API for XML) approach rather than the DOM 方法时需要小心。来自 SAX 的链接维基百科文章:
Where the DOM operates on the document as a whole, SAX parsers operate on each piece of the XML document sequentially
这极大地 减少了处理大型 Excel 文件时消耗的内存量。
这里有一篇很好的文章 - http://polymathprogrammer.com/2012/08/06/how-to-properly-use-openxmlwriter-to-write-large-excel-files/
改编自那篇文章,这是一个输出 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;
}
我正准备以 Excel OpenXML 格式 (xlsx) 导出大量数据(115.000 行 x 30 列)。 我正在使用一些库,例如 DocumentFormat.OpenXML、ClosedXML、NPOI。
对于其中的每一个,都会抛出 OutOfMemoryException,因为 sheet 在内存中的表示会导致内存呈指数增长。
同样每1000行关闭文档文件(并释放内存),下次加载导致内存增加。
有没有更高效的方式导出xlsx数据而不占用大量内存?
看起来您正在使用必须使用数据库的电子表格。它有其局限性,这很容易成为其中之一。仅在您绝对需要坚持使用现有解决方案时才进一步阅读。但是,我不推荐它。因为还有一个问题:如果Excel无法保存这么大的文件,能不能打开这么大的文件?
因此,如果您不能切换到数据库平台,并且您上面提到的标准库在内部无法处理如此大量的数据,那么在创建大型 XLSX 时,您可能只能靠自己了。我的意思是例如这种方法:
- 批量导出您的数据(1,000 或 10,000 或任何有效的数据)以将每个批次的文件分开
创建一个工具(vb.net (this is closest to vba), c#, python, java,任何具有可靠 XML 库的工具)将单独的文件合并为一个。涉及:
- 从 XLSX 中提取 XML(通常是
file.xlsx\xl\worksheets\sheet1.xml
和file.xlsx\xl\worksheets\sharedStrings.xml
) - 通过 XML 操作库将这些部分粘合在一起(这不会因 OutOfMemoryException 而崩溃,因为您不再使用复杂的电子表格对象)
- 正在将结果文件重新打包回主XLSX(您可以将第一批输出文件作为主XLSX)
- 从 XLSX 中提取 XML(通常是
我已经向您展示了实现该结果的可能方法,但我会避免这种情况。 Excel从来都不是一个存储大量数据的平台。与上述任务相比,更容易说服管理层是时候更改此区域的 tools/processes。
Excel 可以打开相当大的文件,只要您的计算机有足够的内存。这是大多数时候的限制因素...
99% 的库都不是为处理大型数据集而构建的,如果你试图向它们扔太多数据,你最终会遇到内存不足的错误。
其中一些,例如我创建的 Spout,就是为了解决这个问题而创建的。诀窍是流式传输数据并避免将内容存储在内存中。我不确定您使用的是哪种语言(似乎不是 PHP),但可能有一个类似的库适用于您的语言。如果没有,您仍然可以查看 Spout - 它是开源的 - 并将其转换为您的语言。
OpenXML SDK 是完成这项工作的正确工具,但您在使用 SAX (Simple API for XML) approach rather than the DOM 方法时需要小心。来自 SAX 的链接维基百科文章:
Where the DOM operates on the document as a whole, SAX parsers operate on each piece of the XML document sequentially
这极大地 减少了处理大型 Excel 文件时消耗的内存量。
这里有一篇很好的文章 - http://polymathprogrammer.com/2012/08/06/how-to-properly-use-openxmlwriter-to-write-large-excel-files/
改编自那篇文章,这是一个输出 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;
}