刷新内存 OpenXML OpenXMLWriter 大型数据集

问题描述 投票:0回答:1

我有一个很大的数据集(100,000 行,33 列)。我正在使用 OpenXML 获取此数据并使用此处解释的方法将其写入 Excel 从 XLSX 导出大量数据 - OutOfMemoryException

由于 XMLWriter,它似乎在创建 Excel 文件时将所有数据存储在内存中,然后一次性写入。即使在上面没有我自己的数据的示例中,在最终写入数据之前,它似乎也使用了 700MB-1GB。

有没有办法定期清除缓冲区以限制内存使用,或者是否必须将其全部集成到 1 XMLWriter 中并遵循 OpenXML 的工作原理

c# excel openxml openxml-sdk
1个回答
0
投票

我有一个与你的数据集大小非常相似的数据集。令人困惑的是 OpenXML 使用 4.6+GB 内存将数据集写入 xlsx。而且整个过程比 Excel Interop 慢(使用 OpenXML 的全部目的是摆脱对 Excel 的依赖并在性能上击败它)。

我也遇到过您链接的类似文章,建议使用 OpenXmlWriter 来解决内存消耗问题,但我的第一次破解产生的结果可以忽略不计。我认为内存消耗仍然超过 3+ GB,考虑到直接写入流,这似乎很奇怪,因此内存中应该有更少的对象等。

运行更多的分析测试,我发现是什么在消耗内存。它是 OpenXML 中的

Cell
对象。即使在链接中提供的代码示例中,它们也会在 for 循环内生成
new Cell()
对象,从而最终导致内存中出现数百万个该死的错误。

我不太清楚为什么

Cell
对象如此糟糕。我的意思是在 DOM 模型中,当你在
Cells
中附加
Rows
时,它们都是链接的并且相互引用,因此在整个 DOM 失去引用之前无法从内存中收集。但在提供的 OpenXmlWriter 示例中,它们只是新建、使用并立即丢弃,没有链接到任何其他对象...猜测
Cell
只是一个胖对象,或者 OpenXML 将它们链接到后面的某个静态 DOM 对象场景。

无论如何,解决方案就是生成 ONE

Cell
对象并在 for 循环中重用它。

大致如下:

using (var writer = OpenXmlWriter.Create(workSheetPart))
{
    writer.WriteStartElement(new Worksheet());
    writer.WriteStartElement(new SheetData());

    var cell = new Cell(); // only one of these ever exists in memory
    var cv = new CellValue(); // just in case, this too

    foreach (var item in something.Rows)
    {
        writer.WriteStartElement(item.Row);

        foreach (var cellProxy in item.Cells)
        {
            // reset cell values inorder to reuse again
            cell.CellValue = null;
            cell.InlineString = null;
            cell.CellFormula = null;

            // set cell values to whatever needs to be written
            cell.DataType = cellProxy.DataType;
            if (cellProxy.DataType == CellValues.SharedString)
            {
                cv.Text = cellProxy.Text;
                cell.CellValue = cv;
            }
            else if (cellProxy.DataType == CellValues.String)
            {
                cell.CellFormula = cellProxy.Fomula;
            }
            ...
            writer.WriteElement(cell);
        }

        writer.WriteEndElement();
    }

    ...
    writer.WriteEndElement();
    writer.WriteEndElement();
}

在上面的代码示例中,

cellProxy
指的是我制作的自定义类。它与
Cell
类非常相似,只是没有肥胖,只存储我需要写入 xlsx 的值。

internal sealed class CellProxy
{
    internal short ColumnNumber { get; init; }
    internal string Text { get; init; } = String.Empty;
    internal CellValues DataType { get; init; } = CellValues.String;
    internal uint? StyleIndex { get; init; }
    internal CellFormula? Formula { get; init; }
}

something
指的是我的另一个类,我用它来收集传递到 OpenXmlWriter 所需的所有数据,以获得最终的 xlsx 文件。

internal sealed class Something
{
    internal List<Column> Columns { get; } = new List<Column>();
    internal List<(Row Row, List<CellProxy> Cells)> Rows { get; set; } // Row is the OpenXML Row class
    ... etc ...
}

因此,我首先从数据库中查询数据,迭代结果,生成存储在我的

Row
类中的
CellProxy
something
对象。然后我将
something
传递给 OpenXmlWriter 方法,该方法迭代我的自定义类并将它们写入 xlsx。

通过这种方法,我最终使用了约 700MB 来编写 xlsx,这与 Excel Interop 大致相当,但速度要快得多。也许可以通过创建一个

RowProxy
类来节省更多内存,但我已经对结果感到满意了。

我发现的另一个巧妙的技巧是,如果您生成的 xlsx 文件中碰巧有多个工作表。您可以多线程生成这些工作表,从而节省更多时间。不知何故,

OpenXmlWriter.Create()
方法创建了单独的流,这些流自动不会相互干扰以及内存中现有的 OpenXML DOM 模型,无需同步或锁定或任何其他操作。

因此,您从单线程开始,创建所有必需的

WorkSheetPart
对象。然后,您可以为每个工作表旋转单独的任务/线程 -> 从数据库查询数据,迭代结果,生成
Row
/
CellProxy
对象来表示数据,将它们传递给创建底层 xml。最后,你最终回到了起始的单线程,如果你调用
OpenXmlWriter
    

© www.soinside.com 2019 - 2024. All rights reserved.