Windows Form(C#):大Excel文件加载至DataTable的基础实现求助
基础实现:分批读取大Excel并加载到DataTable
我太懂这种想搞透底层逻辑的心情了——虽然现成的第三方库用着省心,但自己手动实现一遍分批读取,才真的能明白为什么内存会爆,以及怎么从根源解决。刚好我之前也踩过类似的坑,给你写一个完全基于底层逻辑的C#实现,不用那些封装好的批量工具,纯手动控制流式读取和分批解析!
核心思路先理清楚
Excel(尤其是xlsx格式)本质是压缩后的XML文件,不能直接按字节流硬切分,得按行/行批次来处理:
- 用
FileStream打开文件,全程流式读取,绝不把整个文件一次性塞进内存 - 每次只读取固定数量的行(比如1000行)作为一个批次
- 把每个批次的行解析成临时DataTable,再合并到最终的DataTable里
- 处理完一个批次就立刻释放该批次的内存,避免内存堆积
完整代码示例
这里用微软官方的Open XML SDK(这是底层操作Excel XML的库,不算封装工具,完全符合“基础实现”的要求),你可以通过NuGet安装DocumentFormat.OpenXml包。
using System.Data; using System.IO; using System.Linq; using System.Collections.Generic; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; public class LargeExcelStreamReader { // 自定义批次大小,根据你的内存情况调大或调小,比如2000、5000都可以 private const int BatchSize = 1000; public static DataTable LoadLargeExcelToDataTable(string excelFilePath) { var finalDataTable = new DataTable(); bool isFirstBatch = true; // 用FileStream流式打开文件,只读模式,避免独占文件 using (var fileStream = new FileStream(excelFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) using (var spreadsheetDoc = SpreadsheetDocument.Open(fileStream, isEditable: false)) { var workbookPart = spreadsheetDoc.WorkbookPart; // 这里默认取第一个工作表,你可以根据需求改成指定工作表 var worksheetPart = workbookPart.WorksheetParts.FirstOrDefault(); if (worksheetPart == null) return finalDataTable; var sheetData = worksheetPart.Worksheet.Elements<SheetData>().FirstOrDefault(); if (sheetData == null) return finalDataTable; var rowBatch = new List<Row>(); // 逐行遍历,攒够一批就处理一批 foreach (var row in sheetData.Elements<Row>()) { rowBatch.Add(row); // 要么达到批次大小,要么到了最后一行,就触发处理 if (rowBatch.Count >= BatchSize || row == sheetData.Elements<Row>().Last()) { var batchTable = ParseRowBatch(rowBatch, workbookPart, isFirstBatch); // 第一次处理要初始化最终DataTable的列结构 if (isFirstBatch) { finalDataTable = batchTable.Clone(); isFirstBatch = false; } // 把批次数据导入最终表 foreach (DataRow dr in batchTable.Rows) { finalDataTable.ImportRow(dr); } // 清空批次列表,让GC回收这部分内存 rowBatch.Clear(); } } } return finalDataTable; } // 把一批行解析成DataTable private static DataTable ParseRowBatch(List<Row> rows, WorkbookPart workbookPart, bool isHeader) { var batchTable = new DataTable(); // 获取Excel的共享字符串表(xlsx会把重复文本存在这里,节省空间) var sharedStringTable = workbookPart.GetPartsOfType<SharedStringTablePart>() .FirstOrDefault()?.SharedStringTable; foreach (var row in rows) { var dataRow = batchTable.NewRow(); int columnIndex = 0; foreach (var cell in row.Elements<Cell>()) { // 把Excel的列引用(比如A1、B2)转成0-based的索引 int cellColIndex = GetColumnIndex(cell.CellReference); // 如果是表头行,先创建列 if (isHeader) { // 处理空列的情况,确保列数足够 while (batchTable.Columns.Count <= cellColIndex) { batchTable.Columns.Add($"Column_{batchTable.Columns.Count}"); } batchTable.Columns[cellColIndex].ColumnName = GetCellActualValue(cell, sharedStringTable); } else { // 确保列数匹配,避免索引越界 while (batchTable.Columns.Count <= cellColIndex) { batchTable.Columns.Add($"Column_{batchTable.Columns.Count}"); } dataRow[cellColIndex] = GetCellActualValue(cell, sharedStringTable); } } // 表头行不需要加到数据里,只处理数据行 if (!isHeader) { batchTable.Rows.Add(dataRow); } } return batchTable; } // 从单元格引用中提取列索引(比如A→0,B→1,Z→25,AA→26) private static int GetColumnIndex(string cellReference) { if (string.IsNullOrWhiteSpace(cellReference)) return 0; // 提取引用中的字母部分(比如A1→A) var columnLetters = new string(cellReference.TakeWhile(char.IsLetter).ToArray()); int index = 0; foreach (var c in columnLetters) { index = index * 26 + (c - 'A' + 1); } return index - 1; // 转成0-based索引 } // 获取单元格的实际值,处理共享字符串、数字等情况 private static string GetCellActualValue(Cell cell, SharedStringTable sharedStringTable) { if (cell.CellValue == null) return string.Empty; string rawValue = cell.CellValue.InnerText; // 如果是共享字符串类型,去共享表中取真实文本 if (cell.DataType != null && cell.DataType.Value == CellValues.SharedString) { if (sharedStringTable != null && int.TryParse(rawValue, out int strIndex)) { var sharedItem = sharedStringTable.Elements<SharedStringItem>().ElementAtOrDefault(strIndex); rawValue = sharedItem?.InnerText ?? rawValue; } } return rawValue; } }
关键细节唠两句
- 流式读取:用
FileStream打开文件,SpreadsheetDocument.Open时设为isEditable: false,确保文件不会被一次性加载到内存,而是按需读取XML节点 - 批次控制:
BatchSize可以根据你的内存情况调整,比如内存够就设大一点,内存紧张就设小一点 - 内存回收:每批处理完就清空
rowBatch,让GC及时回收这部分内存,避免内存占用持续飙升 - 底层解析:手动处理Excel的列索引转换、共享字符串表,完全搞懂数据是怎么从Excel文件里读出来的,不是黑箱操作
怎么用?
很简单,直接调用方法就行:
string yourLargeExcelPath = @"D:\BigData.xlsx"; DataTable resultTable = LargeExcelStreamReader.LoadLargeExcelToDataTable(yourLargeExcelPath);
为什么这个方案能解决内存问题?
你之前用的NPOI、EPPlus默认是把整个工作表的所有行一次性加载到内存里,几十万行的话内存直接爆。而这个方案每次只加载BatchSize行,处理完就扔,内存占用会稳定在一个很小的范围内,不会随文件大小线性增长。
内容的提问来源于stack exchange,提问作者鍔夐幃鐟�,c#;excel;stream;out-of-memory;ram"
相关产品推荐
相关产品推荐

