通过OpenXML读取大型Excel文件

时间:2014-07-18 16:27:58

标签: .net vb.net openxml

我正在开发一个项目,我正在使用VB.Net中的OpenXML SDK从excel文件中读取几个单元格并将其存储在DataTable中。 它适用于中型和小型文件 但是,当我尝试打开一个大小为107MB的大文件时,在阅读了几张文件后,我得到了一个OutOfMemory异常。我可以通过双击打开文件(虽然需要一段时间) 以下是我正在使用的代码。如果我可以通过减少内存消耗来改善阅读过程,请告诉我

Dim CellRage As String() = {"AG65", "AG281", "AG335", "AG389", "AG443", "AG497", "AG551", "AG800", "AG913", "AG1081", "AG1165", "AG1305"}
Dim CellValue(13) As String

Using myWorkbook As SpreadsheetDocument = SpreadsheetDocument.Open(stream, False)
     workbookPart = myWorkbook.WorkbookPart

     For Each worksheetpart As WorksheetPart In workbookPart.WorksheetParts
       For count As Integer = 0 To CellRage.GetUpperBound(0) -1
             CellValue(count) = CellValue(workbookPart, sheetName, CostCellRage(count - 2)) 
       Next
       'After few sheets throws OutofMemory Exception
     Next
End Using

Private Shared Function GetCellValue(workbookPart As WorkbookPart, sheetName As String, cellAddress As String) As String
        Dim theCell As Cell
        Dim wsPart As WorksheetPart
        Dim worksheet As Sheet
        Dim value As String
        Dim stringTablePart As SharedStringTablePart = workbookPart.SharedStringTablePart

        worksheet = workbookPart.Workbook.Descendants(Of Sheet).Where(Function(s) s.Name = sheetName).FirstOrDefault
        wsPart = CType(workbookPart.GetPartById(worksheet.Id), WorksheetPart)
        theCell = wsPart.Worksheet.Descendants(Of Cell).Where(Function(c) c.CellReference = cellAddress).FirstOrDefault

        If theCell.ChildElements.Count = 0 Then
            Return ""
        End If

        value = theCell.CellValue.Text

        If (theCell.DataType IsNot Nothing) AndAlso (theCell.DataType.ToString() = "s") Then
            value = stringTablePart.SharedStringTable.ChildElements(Int32.Parse(value)).InnerText
        End If

        Return value

    End Function

感谢您查看此内容

2 个答案:

答案 0 :(得分:2)

你遇到的问题是你的代码正在将每张表读入内存,这最终会导致你使用太多内存。

正如Jesper所指出的,您可以使用SAX方法,而不是当前使用的DOM方法。要使用带有SAX方法的OpenXML读取Excel文档,可以使用OpenXmlReader类。这将通过一次处理XML的各个部分而不是整个DOM来以更高内存效率的方式读取文件,这样可以允许您处理大文件。

OpenXmlReader以XML块的形式读取文件部分的XML内容;把它想象成使用Stream读取文件。我们无法跳转到任意单元格,因为我们还没有阅读整个文档。相反,我们需要做的是读取每一行并忽略我们不想要的行。一旦我们有了我们感兴趣的行,我们就可以获得整行XML,所以此时我们可以直接跳转到我们感兴趣的那一行中的单元格。

Dim desiredColumnNumber As Integer = 33
Dim cellRange As Integer() = New Integer() {65, 281, 335, 389, 443, 497, _
551, 800, 913, 1081, 1165, 1305}

Using reader As OpenXmlReader = OpenXmlReader.Create(worksheetPart)
    While reader.Read()
        'we want to find the first row
        If reader.ElementType = GetType(Row) Then
            Do
                If Not cellRange.Contains(Convert.ToInt32(CUInt(row.RowIndex))) Then
                    'we're not interested in this row so continue
                    Continue Do
                End If

                Dim row As Row = DirectCast(reader.LoadCurrentElement(), Row)

                If row.HasChildren Then
                    'get cell in column AG
                    Dim cell As Cell = DirectCast(row.ChildElements(desiredColumnNumber), Cell)
                    'do something with the cell...
                End If
                'move to the next row
            Loop While reader.ReadNextSibling()
        End If
    End While
End Using

在上面的代码中,我将您的单元格范围拆分为单元格编号(desiredCellNumber)和IntegercellRange)数组,这些数组存储了我们感兴趣的每一行的编号in(如果你不能在原始代码中将其拆分,则必须将每个单元格引用转换为正确的格式)。

然后我们检查每一行的RowIndex属性,看它是否存在于cellRange中。如果没有,那么我们继续前进到下一行,但如果它是我们感兴趣的行,我们可以通过访问Row.ChildElements属性来访问单元格。在上面的代码中,我们只对第33列感兴趣,因此我们使用row.ChildElements(desiredColumnNumber)读取该单元格值。

顺便说一句,当我处理这样的大文件时,我通常先将SharedStringTablePart读成Dictionary或类似的,然后在需要获取时从那里读取字符串值。显然,此消耗的内存量在很大程度上取决于Excel文件的内容,因此您可能希望做一些更聪明的事情来最小化存储共享字符串时使用的内存。读取共享字符串的代码(几乎)与上面的相同:

Dim sharedStrings As New Dictionary(Of Integer, String)()
If stringTablePart IsNot Nothing Then
    Using reader As OpenXmlReader = OpenXmlReader.Create(stringTablePart)
        Dim i As Integer = 0
        While reader.Read()
            If reader.ElementType = GetType(SharedStringItem) Then
                Dim sharedStringItem As SharedStringItem = DirectCast(reader.LoadCurrentElement(), SharedStringItem)
                sharedStrings.Add(i), If(sharedStringItem.Text IsNot Nothing, sharedStringItem.Text.Text, String.Empty))
                i = i + 1
            End If
        End While
    End Using
End If

答案 1 :(得分:1)

Openxml SDK使用' DOM方法'将文件加载到内存时。

为了处理大量数据,您应该考虑使用像XmlReader这样的SAX方法。

当然,XmlReader不是OpenXml SDK的一部分,但它可以为您提供更好的性能和更低的内存占用。