使用Open XML SDK处理大Excel内存过高,求高效行级处理方案
我之前在处理超大型Excel文件的时候遇到过完全一样的问题——DocumentFormat.OpenXml SDK默认的Descendants<T>遍历方式会悄悄构建整个工作表的对象树,而Cell和Row的_next强引用链会把所有对象都拴在内存里,GC根本收不走,内存直接飙到几个G都很正常。
要解决这个问题,核心思路是绕开SDK的对象树构建,直接用流式XML解析,这样每次只处理当前节点,不会把整个工作表都留在内存里。下面是具体的实现方案:
第一步:提前加载共享字符串表(SharedStringTable)
因为大部分字符串值都存在共享字符串表里,提前把它加载到一个字典里缓存起来,后续解析单元格的时候直接查表就行,不用每次去读XML,效率更高:
private Dictionary<int, string> LoadSharedStrings(WorkbookPart workbookPart) { var sharedStringPart = workbookPart.GetPartsOfType<SharedStringTablePart>().FirstOrDefault(); if (sharedStringPart?.SharedStringTable == null) { return new Dictionary<int, string>(); } return sharedStringPart.SharedStringTable.Elements<SharedStringItem>() .Select((item, index) => new { Index = index, Text = item.InnerText }) .ToDictionary(x => x.Index, x => x.Text); }
第二步:用XmlReader流式遍历工作表
不要用worksheetPart.Worksheet.Descendants<Row>(),而是直接打开工作表的XML流,用XmlReader逐节点解析,这样只会把当前处理的节点留在内存里:
var workbookPart = _document.WorkbookPart; var sharedStrings = LoadSharedStrings(workbookPart); foreach (var sheet in workbookPart.Workbook.Descendants<Sheet>()) { var worksheetPart = (WorksheetPart)workbookPart.GetPartById(sheet.Id); // 直接读取工作表的XML流,避免构建完整对象树 using (var reader = XmlReader.Create(worksheetPart.GetStream())) { while (reader.Read()) { // 找到<row>节点,开始处理一行 if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "row") { ProcessRow(reader, sharedStrings); } } } // 处理完当前工作表后,手动删除Part,强制释放资源 workbookPart.DeletePart(worksheetPart); }
第三步:实现行和单元格的解析逻辑
在ProcessRow方法里,继续用XmlReader遍历当前行下的<c>(单元格)节点,解析每个单元格的值:
private void ProcessRow(XmlReader reader, Dictionary<int, string> sharedStrings) { // 遍历当前row节点的子节点 while (reader.Read()) { // 遇到row的结束节点,退出当前行的处理 if (reader.NodeType == XmlNodeType.EndElement && reader.LocalName == "row") { break; } // 找到cell节点 if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "c") { var cellType = reader.GetAttribute("t"); // 获取单元格类型(s=共享字符串,n=数字等) var cellValue = string.Empty; // 读取单元格的<v>节点(值) if (reader.ReadToDescendant("v")) { cellValue = reader.ReadElementContentAsString(); } // 解析最终值 var finalValue = ParseCellValue(cellType, cellValue, sharedStrings); // 这里写你的业务逻辑,比如把值写入数据库、生成报表等 // ... } } } private string ParseCellValue(string cellType, string cellValue, Dictionary<int, string> sharedStrings) { if (string.IsNullOrEmpty(cellValue)) { return string.Empty; } switch (cellType) { case "s": // 共享字符串 if (int.TryParse(cellValue, out var stringIndex) && sharedStrings.TryGetValue(stringIndex, out var sharedText)) { return sharedText; } break; case "n": // 数字 // 可以根据需求转换成int/double,或者直接返回字符串 return cellValue; case "b": // 布尔值 return cellValue == "1" ? "TRUE" : "FALSE"; case "d": // 日期 if (double.TryParse(cellValue, out var dateNum)) { // 注意:OpenXml的日期是OLE自动化日期,从1900-01-01开始计算天数,存在闰年bug return DateTime.FromOADate(dateNum).ToString("yyyy-MM-dd HH:mm:ss"); } break; } return cellValue; }
为什么这个方案能解决内存问题?
- 流式解析:XmlReader是向前读取的,每次只加载当前节点到内存,处理完就可以被GC回收,不会构建完整的Row/Cell对象树,也就不会产生那个导致内存泄漏的强引用链。
- 手动释放资源:处理完每个工作表后调用
DeletePart,强制SDK释放该工作表的所有资源,避免内存占用累积。 - 共享字符串缓存:提前加载SharedStringTable,既提升了解析效率,又避免了反复读取XML的开销。
额外注意事项
- 如果你的Excel有大量格式或者注释,XmlReader会忽略这些非数据节点,只处理行和单元格,进一步减少内存占用。
- 对于极端大的文件(比如几十万行),可以考虑在处理行的时候批量写入数据(比如每处理1000行就批量插入数据库),避免业务逻辑本身带来的内存压力。
- 确保在使用完
XmlReader后及时释放(用using语句),避免流泄漏。
内容的提问来源于stack exchange,提问作者vcsjones
相关产品推荐
相关产品推荐

