在 Interop.Excel 中使用 excel.range 时如何避免 OutOfMemory 错误

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

我正在 vb.net (.Net Framework >= 4.5) 中编写一个应用程序,该应用程序通过 Web 服务(作为数据表对象)从数据库检索数据,并通过以下方式将其解析为特定格式的 Excel 文件(不幸的是):互操作库。

应用程序运行良好,但最近我开始收到此 OutOfMemory 错误:

HRESULT 异常:0x8007000E (E_OUTOFMEMORY)

当我使用以下方法将数据从数据表解析为 excel.range 对象(rng)时,会发生错误:

rng.value = ConvertDataTableTo2DArray(DT)

函数 ConvertDataTableTo2DArray 接受一个数据表对象并返回一个包含信息的二维数组。我这样做已经有一段时间了,因为这似乎是进行此类操作的最快方法(根据我在网上的研究)。

        Public Shared Function ConvertDataTableTo2DArray(dataTable As DataTable) As Object(,)
            Dim rows As Integer = dataTable.Rows.Count
            Dim columns As Integer = dataTable.Columns.Count
            Dim dataArray(rows - 1, columns - 1) As Object

            For i As Integer = 0 To rows - 1
                For j As Integer = 0 To columns - 1
                    dataArray(i, j) = dataTable.Rows(i)(j)
                Next
            Next

            Return dataArray
        End Function

咨询到处发布的其他问题,我发现 Excel.Range 对象的大小有限,因此我解决该问题的最初方法是将数据表按 x 行数的批次进行划分,并再次声明一个新范围每批次。这根本不能解决问题,所以要么我没有清除以前正确使用的数据,要么错误的原因并不完全是我想象的那样。

一些附加信息:

  • 我正在处理 22 列的行。
  • 错误似乎发生在第 120 行左右。
  • 我已经尝试过 100、50 和 20 行的批次,在同一组行中出现相同的错误(即,对于 20 行/批次,它发生在第 6 批次)。
  • 我使用的是配备 I7 和 32GB RAM 的戴尔笔记本电脑。我已经检查过,由于未使用的 RAM 超过 10 GB,该过程失败。

我的代码(不幸的是名称是西班牙语):

        Public Shared Sub CopiarDataTableAExcel(dataTable As DataTable, excelHoja As Excel.Worksheet, inicioCelda As String)
            ' Obtener el rango de inicio en base a la celda especificada
            Dim rangoInicio As Excel.Range = excelHoja.Range(inicioCelda)

            ' Obtener el rango final en base al tamaño de la DataTable
            Dim rangoFin As Excel.Range = excelHoja.Cells(rangoInicio.Row + dataTable.Rows.Count - 1, rangoInicio.Column + dataTable.Columns.Count - 1)

            ' Obtener el rango completo que abarca desde el inicio hasta el fin
            Dim rangoCompleto As Excel.Range = excelHoja.Range(rangoInicio, rangoFin)
            Try
                ' Copiar los datos de la DataTable al rango completo en Excel
                rangoCompleto.Value = ConvertDataTableTo2DArray(dataTable)
            Catch ex As Exception
                If ex.Message.Contains("E_OUTOFMEMORY") Then
                    Dim filas_por_particion As Int16 = 20
                    Dim particiones As Int16 = Math.Ceiling(dataTable.Rows.Count / filas_por_particion)
                    For prt = 1 To particiones
                        Dim num_filas As Integer = filas_por_particion
                        If prt = particiones Then
                            num_filas = dataTable.Rows.Count - (prt * filas_por_particion) ' numero de filas menos las filas ya escritas
                        End If
                        'Dim nuevo_rango_inicio As Excel.Range = excelHoja.Range("A" & ((prt - 1) * num_filas + 2).ToString)
                        Dim nuevo_rango_inicio As Excel.Range = excelHoja.Cells((prt - 1) * num_filas + 2, 1)
                        Dim nuevo_rango_fin As Excel.Range = excelHoja.Cells(nuevo_rango_inicio.Row + num_filas, nuevo_rango_inicio.Column + dataTable.Columns.Count - 1)

                        Dim nuevo_rango As Excel.Range = excelHoja.Range(nuevo_rango_inicio, nuevo_rango_fin)
                        Dim auxDt As DataTable = dataTable.Clone

                        For fila = (prt - 1) * num_filas To prt * num_filas - 1
                            auxDt.ImportRow(dataTable.Rows(fila))
                        Next
                        nuevo_rango.Value = ConvertDataTableTo2DArray(auxDt)
                    Next
                Else
                    MsgBox("Se ha producido el siguiente error: " & Chr(13) & ex.Message)
                End If
            End Try
        End Sub

在代码的第一部分到

rangoCompleto.Value = ConvertDataTableTo2DArray(dataTable)
,你可以找到我最初的方法。在catch语句中,我引入了这个仍然失败的“批量”副本。根据我的理解,nuevo_rango* 变量的每个声明都应该清除以前使用的内存,因此新的 excel.range nuevo_rango 应该使用只有 x (num_filas) 行和 22 列的新范围。在调试时,我检查了我的批处理数据是否达到了预期的大小,这就是为什么我不理解 OutOfMemory 错误。


更新:

按照@djv的建议和他提供的链接中的信息,我尝试了这个:

        Public Shared Sub CopiarDataTableAExcel(dataTable As DataTable, excelHoja As Excel.Worksheet, inicioCelda As String)
            ' Obtener el rango de inicio en base a la celda especificada
            Dim rangoInicio As Excel.Range = excelHoja.Range(inicioCelda)

            ' Obtener el rango final en base al tamaño de la DataTable
            Dim rangoFin As Excel.Range = excelHoja.Cells(rangoInicio.Row + dataTable.Rows.Count - 1, rangoInicio.Column + dataTable.Columns.Count - 1)

            ' Obtener el rango completo que abarca desde el inicio hasta el fin
            Dim rangoCompleto As Excel.Range = excelHoja.Range(rangoInicio, rangoFin)
            Try
                ' Copiar los datos de la DataTable al rango completo en Excel
                rangoCompleto.Value = ConvertDataTableTo2DArray(dataTable)
            Catch ex As Exception
                If ex.Message.Contains("E_OUTOFMEMORY") Then
                    Dim filas_por_particion As Int16 = 100
                    Dim particiones As Int16 = Math.Ceiling(dataTable.Rows.Count / filas_por_particion)
                    For prt = 1 To particiones
                        Dim num_filas As Integer = filas_por_particion
                        If prt = particiones Then
                            num_filas = (prt * filas_por_particion) - dataTable.Rows.Count ' numero de filas menos las filas ya escritas
                        End If
                        DT2Excel(excelHoja, num_filas, prt, dataTable)
                        GC.Collect()
                        GC.WaitForPendingFinalizers()
                    Next
                Else
                    MsgBox("Se ha producido el siguiente error: " & Chr(13) & ex.Message)
                End If
            End Try
        End Sub


        Public Shared Function DT2Excel(exl As Excel.Worksheet, num_filas As Int16, num_iter As Int16, dt As DataTable) As Boolean

            Try
                Dim rng_inicio As New Tuple(Of Int16, Int16)((num_iter - 1) * num_filas + 2, 1)
                Dim nuevo_rango_inicio As Excel.Range = exl.Cells(rng_inicio.Item1, rng_inicio.Item2)
                Dim rng_fin As New Tuple(Of Int16, Int16)(nuevo_rango_inicio.Row + num_filas, nuevo_rango_inicio.Column + dt.Columns.Count - 1)
                Dim nuevo_rango_fin As Excel.Range = exl.Cells(rng_fin.Item1, rng_fin.Item2)

                Dim nuevo_rango As Excel.Range = exl.Range(nuevo_rango_inicio, nuevo_rango_fin)
                Dim auxDt As DataTable = dt.Clone

                For fila = (num_iter - 1) * num_filas To num_iter * num_filas - 1
                    auxDt.ImportRow(dt.Rows(fila))
                Next
                nuevo_rango.Value = ConvertDataTableTo2DArray(auxDt)
                nuevo_rango = Nothing
                Return True
            Catch ex As Exception
                Return False
            End Try

        End Function

它不再抛出错误,但在第 104 行之后仍然没有复制。我什至使用另一个函数中的范围封装了该部分,以避免出现注意到的调试错误,但我觉得只有当我封装all Excel 时,这才有效对象,这会违背函数的目的。

看起来 Excel.range 对象仍然没有释放内存...我想我可以实例化工作簿并在每次迭代中打开文件,但这违背了快速执行操作的目的。

.net excel vb.net excel-interop
2个回答
0
投票

尝试释放函数内的范围

Public Shared Function DT2Excel(exl As Excel.Worksheet, num_filas As Int16, num_iter As Int16, dt As DataTable) As Boolean
    Dim nuevo_rango_inicio As Excel.Range
    Dim nuevo_rango_fin As Excel.Range
    Dim nuevo_rango As Excel.Range
    Try
        Dim rng_inicio As New Tuple(Of Int16, Int16)((num_iter - 1) * num_filas + 2, 1)
        nuevo_rango_inicio = exl.Cells(rng_inicio.Item1, rng_inicio.Item2)

        Dim rng_fin As New Tuple(Of Int16, Int16)(nuevo_rango_inicio.Row + num_filas, nuevo_rango_inicio.Column + dt.Columns.Count - 1)
        nuevo_rango_fin = exl.Cells(rng_fin.Item1, rng_fin.Item2)

        nuevo_rango = exl.Range(nuevo_rango_inicio, nuevo_rango_fin)

        Dim auxDt As DataTable = dt.Clone

        For fila = (num_iter - 1) * num_filas To num_iter * num_filas - 1
            auxDt.ImportRow(dt.Rows(fila))
        Next
        nuevo_rango.Value = ConvertDataTableTo2DArray(auxDt)
        
        Return True
    Catch ex As Exception
        Return False
    Finally
        nuevo_rango = Nothing
        nuevo_rango_inicio = Nothing
        nuevo_rango_fin = Nothing
        GC.Collect()
        GC.WaitForPendingFinalizers()
    End Try

End Function

这可能并不重要。退出函数应该释放引用,GC 应该正确处理它,但值得一试。

也在 For 循环中的方法

CopiarDataTableAExcel
中执行相同操作。使用
Finally
块清除引用和 GC。不过,有点奇怪的是,您似乎在内存不足异常之后在异常处理程序中执行实际工作。


0
投票

我也有类似的情况和同样的错误。事实证明,某些数据元素以

=
字符开头,这导致了内存不足错误。在将它们写入 Excel 之前,我通过在这些字符串的开头添加撇号
'
来修复此问题。例如。
=sample_text
变为
'=sample_text
。这样,Excel 不会尝试将它们解释为公式,而是解释为普通文本字符串。

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