用 EPPlus
读取 .xlsx
是目前最稳的选择
直接说结论:别用
Microsoft.Office.Interop.Excel,它依赖 Office 安装、线程不安全、服务器上基本跑不起来;也别碰已停更的
NPOI旧版(0.9x),对 .xlsx 支持弱且容易抛
Invalid header signature。推荐用现代、纯托管、NuGet 开箱即用的
EPPlus(v6+,注意需 .NET 5+ 或 .NET Framework 4.6.1+)。
安装命令:
dotnet add package EPPlus(若用 .NET Core/.NET 5+)或通过 NuGet 包管理器安装
EPPlusv6.x。
关键点:
EPPlus不需要 Excel 软件,也不调 COM,纯内存解析,适合后台服务 v6+ 默认启用「商业用途需授权」提示,若仅内网/非分发场景,可加一行代码关闭:
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;读取时默认不加载样式、公式结果(只读值),性能好;如需公式求值,得手动调用
worksheet.Calculate()
读取第一个 Sheet 的数据:跳过空行、处理标题行
常见需求是把 Excel 表格当二维数据源用,比如首行为列名,后面为记录。用
EPPlus可这样写:
using (var package = new ExcelPackage(new FileInfo("data.xlsx")))
{
var worksheet = package.Workbook.Worksheets[0]; // 第一个 sheet
var rowCount = worksheet.Dimension?.Rows ?? 0;
var colCount = worksheet.Dimension?.Columns ?? 0;
<pre class='brush:php;toolbar:false;'>var data = new List<Dictionary<string, object>>();
if (rowCount < 2) return data;
// 假设第1行是 header
var headers = Enumerable.Range(1, colCount)
.Select(c => worksheet.Cells[1, c].Text.Trim())
.ToArray();
for (int row = 2; row <= rowCount; row++)
{
var values = new Dictionary<string, object>();
bool allEmpty = true;
for (int col = 1; col <= colCount; col++)
{
var cell = worksheet.Cells[row, col];
var val = cell.Text; // 用 .Text 更安全(避免 null 引用),.Value 是 object 类型需判空转型
values[headers[col - 1]] = val;
if (!string.IsNullOrWhiteSpace(val)) allEmpty = false;
}
if (!allEmpty) data.Add(values);
}}
注意:
worksheet.Dimension可能为
null(空表),务必判空 别直接用
cell.Value——它可能是
double、
DateTime、
null,类型不统一;
cell.Text总是字符串,语义更接近「用户看到的内容」 Excel 行号/列号从 1 开始,不是 0,
Cells[row, col]别写反
遇到 System.IO.FileFormatException: File contains corrupted data
这通常不是文件真坏了,而是你用了错误的库或打开方式:
用FileStream打开后没设
FileShare.Read,被其他进程占用 → 改用
new FileInfo(path)构造
ExcelPackage,它内部会正确处理流 文件其实是
.xls(OLE 复合文档格式),但扩展名错写成
.xlsx→ 用十六进制编辑器看前 8 字节:真正的
.xlsx应以
50 4B 03 04(PK..)开头;
.xls是
D0 CF 11 E0(DOCFILE) 用低版本
EPPlus(v4/v5)打开由新版 Excel(如 Microsoft 365)保存的强加密或含新特性(如动态数组公式)的文件 → 升级到 v6.2+ 并确认
LicenseContext设置正确
读取指定列、跳过隐藏行、识别合并单元格
业务中常要按列名取值,或过滤掉人工隐藏的行:
按列名查值:先用headers数组找到列索引,再取对应
cell.Text,比硬编码列号更健壮 跳过隐藏行:
worksheet.Row(row).Hidden返回
true时
continue合并单元格:用
worksheet.MergedCells获取所有合并区域(如
"A1:C1"),再用
worksheet.Cells[row, col].Merged判断当前单元格是否属于合并区;若属合并区,只取左上角单元格的值即可,其余为空 日期列读出来是数字?那是 Excel 序列号(如 44197 = 2021-01-01),用
DateTime.FromOADate(cell.Value as double?)转换,但前提是
cell.Value确实是
double;更稳妥仍是先读
cell.Text再
DateTime.TryParse
合并单元格和隐藏行逻辑容易漏,尤其在报表类 Excel 中,不检查就会导致数据错位或重复。
