使用 Open XML SDK 流式传输 Excel 数据
Streaming through Excel data with Open XML SDK
我们有一个相当大的 Excel 工作簿。大约 3,300 列和数千行。
我们发现尝试对数据执行任何操作都会导致内存使用量过高,大约 3 GB。
DocumentFormat.OpenXml
包似乎在迭代时在内存中保留了作品sheet 的完整对象结构。一般来说,我们是这样做的:
var workbookPart = _document.WorkbookPart;
var worksheets = workbookPart.Workbook.Descendants<Sheet>();
foreach(var worksheet in worksheets)
{
var worksheetPart = (WorksheetPart) workbookPart.GetPartById(worksheet.Id);
foreach(var row in worksheetPart.Worksheet.Descendants<Row>())
{
foreach(var cell in row.Descendants<Cell>())
{
var (_, value) = ParseCell(cell);
}
}
}
ParseCell
只是通过在工作簿上查找 SharedStringTable
中的字符串值来获取 Cell
的内容,或者如果它是一个数字,则解析该数字。
只是 运行 这段代码对 ParseCell
的结果什么都不做,仍然使用相当大的内存。
当我们分析这段代码时,我们注意到 sheet 中的每个单元格在堆上都有一个 Cell
,尽管我们尽最大努力使用 IEnumerable<T>
API 来避免有内存中的大量集合。
这非常接近此 Nuget 包的推荐用法。
从分析中,出现问题是每个 Cell
都对下一个 Cell
有很强的引用,同样 Row
.
每个 Cell
都有一个名为 _next
的字段,这就是使每个 Cell 具有强根的原因。单元格 A 对单元格 B、B 到 C、C 到 D 具有强引用。
Row
具有类似的结构,其中第 0 行有一个 _next
字段到第 1 行,依此类推,因此对于我们经历的每个 Row
,它保持对下一个 Row
.
的强烈引用
所以一切都联系在一起。当我在 WinDbg 处理完最后一个 Row
后查看它时,!dumpheap -stat
堆上的 Cell
正好与工作簿中包含的数量相同。
我们使用此 SDK 的方式不会扩展到更多行。有没有一种方法可以更有效地使用此包并逐行处理 Worksheet 而无需在内存中保留整个 worksheet 的对象图?
这里一个合适的解决方案是使用OpenXmlReader
XML reader。另一个关键是使用 Elements
而不是 Decendents
以避免在 XML 结构中看得太深。
using (var reader = OpenXmlReader.Create(worksheetPart))
{
while (reader.Read())
{
if (typeof(Row).IsAssignableFrom(reader.ElementType))
{
var row = (Row)reader.LoadCurrentElement();
foreach (var cell in row.Elements<Cell>())
{
var (_, value) = ParseCell(cell);
}
}
}
}
这确实 "stream" 元素和内存使用最少。
我们有一个相当大的 Excel 工作簿。大约 3,300 列和数千行。
我们发现尝试对数据执行任何操作都会导致内存使用量过高,大约 3 GB。
DocumentFormat.OpenXml
包似乎在迭代时在内存中保留了作品sheet 的完整对象结构。一般来说,我们是这样做的:
var workbookPart = _document.WorkbookPart;
var worksheets = workbookPart.Workbook.Descendants<Sheet>();
foreach(var worksheet in worksheets)
{
var worksheetPart = (WorksheetPart) workbookPart.GetPartById(worksheet.Id);
foreach(var row in worksheetPart.Worksheet.Descendants<Row>())
{
foreach(var cell in row.Descendants<Cell>())
{
var (_, value) = ParseCell(cell);
}
}
}
ParseCell
只是通过在工作簿上查找 SharedStringTable
中的字符串值来获取 Cell
的内容,或者如果它是一个数字,则解析该数字。
只是 运行 这段代码对 ParseCell
的结果什么都不做,仍然使用相当大的内存。
当我们分析这段代码时,我们注意到 sheet 中的每个单元格在堆上都有一个 Cell
,尽管我们尽最大努力使用 IEnumerable<T>
API 来避免有内存中的大量集合。
这非常接近此 Nuget 包的推荐用法。
从分析中,出现问题是每个 Cell
都对下一个 Cell
有很强的引用,同样 Row
.
每个 Cell
都有一个名为 _next
的字段,这就是使每个 Cell 具有强根的原因。单元格 A 对单元格 B、B 到 C、C 到 D 具有强引用。
Row
具有类似的结构,其中第 0 行有一个 _next
字段到第 1 行,依此类推,因此对于我们经历的每个 Row
,它保持对下一个 Row
.
所以一切都联系在一起。当我在 WinDbg 处理完最后一个 Row
后查看它时,!dumpheap -stat
堆上的 Cell
正好与工作簿中包含的数量相同。
我们使用此 SDK 的方式不会扩展到更多行。有没有一种方法可以更有效地使用此包并逐行处理 Worksheet 而无需在内存中保留整个 worksheet 的对象图?
这里一个合适的解决方案是使用OpenXmlReader
XML reader。另一个关键是使用 Elements
而不是 Decendents
以避免在 XML 结构中看得太深。
using (var reader = OpenXmlReader.Create(worksheetPart))
{
while (reader.Read())
{
if (typeof(Row).IsAssignableFrom(reader.ElementType))
{
var row = (Row)reader.LoadCurrentElement();
foreach (var cell in row.Elements<Cell>())
{
var (_, value) = ParseCell(cell);
}
}
}
}
这确实 "stream" 元素和内存使用最少。