ExcelDataReader是.NET中读取Excel最轻量跨平台方案,支持.xls/.xlsx,需用Stream构造、手动处理类型与日期、流式读取防OOM,注意列名大小写匹配。

用 ExcelDataReader 读取 Excel 文件最省心
直接上结论:别碰 OleDbConnection(尤其在 .NET Core/.NET 5+),也别为简单读取去装整个 Microsoft.Office.Interop.Excel(需本机装 Office、线程模型敏感、极易卡死)。ExcelDataReader 是目前轻量、跨平台、无依赖、性能够用的首选。
它支持 .xls(BIFF8)和 .xlsx(OpenXML),纯托管实现,NuGet 安装即用:
dotnet add package ExcelDataReader dotnet add package ExcelDataReader.DataSet // 如需转 DataTable
关键实操点:
- 必须用
File.OpenRead()或Stream构造IExcelDataReader,不能传文件路径字符串 -
.xlsx默认读取第一个 sheet,如需指定 sheet 名,用reader.SelectWorksheet("Sheet1") - 调用
reader.Read()前,务必先调用reader.IsFirstRowAsColumnNames = true(若首行为标题),否则列名会是F1,F2… - 读完必须调用
reader.Close()或用using,否则底层ZipArchive流未释放,后续再打开同一文件会报IOException: The process cannot access the file
DataTable 转换看似方便,但列类型常不准
ExcelDataReader.DataSet 提供 AsDataSet() 方法,一行转 DataTable,很诱人。但 Excel 本身无强类型,该方法仅靠前几行采样推断列类型——一旦某列前 10 行都是数字,第 11 行出现文本,整列可能被识别为 int,导致该行数据变 DBNull 或抛异常。
更稳的做法是手动遍历 IExcelDataReader:
using var stream = File.OpenRead("data.xlsx");
using var reader = ExcelReaderFactory.CreateReader(stream);
reader.IsFirstRowAsColumnNames = true;
while (reader.Read())
{
string name = reader.GetString("Name"); // 按列名取,安全
decimal? price = reader.GetDecimal("Price"); // 返回 null 如果为空或非数字
}
优势明显:
- 按需读字段,不加载整表到内存
- 用
GetString/GetInt32等带类型的方法,失败时可 fallback(如reader.GetValue("Price")?.ToString()) - 避免因采样偏差导致的列类型误判
日期读出来是数字?那是 Excel 的序列值
Excel 内部把日期存为从 1900-01-01 起的天数(double),比如 44197.5 表示 2021-01-01 12:00。当你用 reader.GetDouble("Date") 拿到这个数,直接 ToString 就是乱码。
正确做法:
- 优先用
reader.GetDateTime("Date")——ExcelDataReader内置了转换逻辑,能自动识别并转成DateTime - 如果该单元格实际存的是文本型日期(如 "2021/01/01"),
GetDateTime会失败,此时改用reader.GetString("Date")再DateTime.TryParse - 绝对不要自己写
DateTime.FromOADate(value)来硬转 —— 这只适用于 Excel 的 OLE Automation 日期格式,而.xlsx实际用的是 ISO 标准,ExcelDataReader已封装差异
大文件卡顿、OOM?别一次性全读进内存
一个 10MB 的 .xlsx,解压后 XML 内容可能超 100MB;若用 AsDataSet() 或循环 reader.AsDataSet().Tables[0].Rows,很容易触发 GC 压力甚至 OutOfMemoryException。
真实业务中,多数场景只需逐行处理、校验、写库或生成报告。这时必须流式读取:
- 保持
IExcelDataReader实例,反复调用Read(),每行处理完立刻丢弃对象引用 - 避免创建临时
List<t></t>缓存全部数据,除非明确需要随机访问 - 如需分批(如每 1000 行提交一次数据库),用计数器 +
yield return封装成IEnumerable<t></t>,下游按需消费
真正容易被忽略的,是流的生命周期和列名大小写的隐式匹配——Excel 表头可能是 “CustomerID”,代码里写 reader.GetString("customerid") 会返回 null,不报错也不提示,静默失败。建议统一转小写比较,或提前用 reader.FieldCount 和 reader.GetName(i) 校验列存在性。


















