You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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"

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:26:01