资讯动态

C# Excel读写实战:NPOI选型、避坑与性能优化指南

发布时间:2026/10/18 3:37:02 来源:尧图企业网站定制
简介这份资源面向具备一定C#基础、需要在.NET Framework环境下处理Excel数据的开发者聚焦Excel读取、写入、保存与资源释放等常见操作。内容基于Microsoft.Office.Interop.Excel库涵盖创建Excel应用程序对象、打开工作簿、遍历工作表单元格读取数据、写入新数据、保存工作簿以及通过ReleaseComObject释放COM资源等关键环节并附带ThreeDataReadFormExcle示例项目演示如何整合读取、处理与保存三个数据源的完整流程。压缩包共58个文件以dll依赖库、cs源码、xml配置、exe可执行文件及sln解决方案为主另含xls示例数据与config配置文件整体约1.55MB结构清晰便于直接运行调试。目前已有589人学习下载适合需要快速掌握C#操作Excel、规避内存泄漏与版本兼容问题的开发者参考借鉴。1. 从一份 C# Excel 读写实例说起为什么我建议你先跑通再谈选型如果你正在用 C# 做数据处理大概率绕不开 Excel。不管是把数据库里的订单导成报表还是把业务方发来的对账表读进系统Excel 读写几乎是每个 .NET 从业者都会撞上的场景。这份资源是一套 C# 操作 Excel 的完整实例覆盖读取、保存、单元格操作、批量写入等常见动作适合刚接手数据导入导出模块的新手也适合想找一份能直接改改就用的参考代码的老手。它解决的核心问题很具体让你不用从零翻文档直接看到一份能跑通的读写流程长什么样。我见过太多人卡在“库选哪个、连接字符串怎么写、大文件怎么不崩”这三步上这份实例的价值就在于把这三步串起来了。2. 选型先定调C# 读写 Excel 的几条技术路线怎么挑2.1 三种主流方案的真实差异在动手之前得先想清楚用哪条路。C# 操作 Excel 常见的有三类一是基于 COM 的Microsoft.Office.Interop.Excel二是 OLEDB 把 Excel 当数据库查三是第三方库比如 EPPlus、NPOI、ClosedXML。这三条路不是谁替代谁的关系而是各有各的适用边界。Interop 的好处是功能最全Excel 能干的它基本都能干公式、图表、透视表都不在话下。但它的代价也很明显机器上必须装 Office进程容易残留服务器端跑起来经常出现“玄学”崩溃。我一般只在桌面端、且确定目标机器有 Office 的场景下才考虑它。OLEDB 的思路是把 Excel 文件当成一个数据源用 SQL 去查。它的优点是读取快、代码短适合只读不写的场景。缺点是它依赖 Access Database Engine32 位和 64 位还经常打架写入能力也弱格式控制基本没有。第三方库是目前最推荐的方向。EPPlus 从 5.0 开始改成了商业授权商用要注意NPOI 是 Apache 协议免费且跨平台对 xls 和 xlsx 都支持ClosedXML 基于 OpenXMLAPI 写起来最舒服但只支持 xlsx。选哪个取决于你的授权要求、是否需要兼容老式 xls、以及团队对 API 的熟悉程度。2.2 一份可落地的选型对照方案依赖读写能力跨平台授权注意Interop必须装 Office读写全否随 OfficeOLEDBAccess Engine读强写弱否随系统NPOI无读写全是Apache 2.0ClosedXML无读写全是MITEPPlus无读写全是5.0 起商业这张表不是让你背而是让你在动手前先对号入座。比如你要部署到 Linux 容器里那 Interop 和 OLEDB 直接排除如果业务方还在发.xls老文件ClosedXML 就不行得用 NPOI。2.3 用 NPOI 搭一个最小可跑环境下面以 NPOI 为例把环境搭起来。选它是因为免费、跨平台、xls/xlsx 通吃适合大多数团队。# 新建一个控制台项目命名随意 dotnet new console -n ExcelDemo cd ExcelDemo # 安装 NPOI版本按当前稳定版走 dotnet add package NPOI装完之后Program.cs里先写一个最简单的读取确认环境没问题using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; // 打开一个 xlsx 文件路径按实际改 using var fs new FileStream(demo.xlsx, FileMode.Open, FileAccess.Read); IWorkbook workbook new XSSFWorkbook(fs); // 取第一个工作表 ISheet sheet workbook.GetSheetAt(0); // 遍历前 5 行打印每行第一列 for (int i 0; i sheet.LastRowNum i 5; i) { IRow row sheet.GetRow(i); if (row null) continue; ICell cell row.GetCell(0); Console.WriteLine(cell?.ToString() ?? ); }这段代码的逻辑很直白XSSFWorkbook对应 xlsx如果是 xls 就换成HSSFWorkbook。GetSheetAt(0)按索引取表LastRowNum是最后一行的索引而不是行数这点新手经常搞混。GetRow可能返回 null因为空行不会被创建所以必须判空。GetCell同理取不到就是 null直接ToString()会抛异常。提示LastRowNum返回的是从 0 开始的索引实际行数是它加一。如果表里中间有大量空行遍历时判空不能省。3. 读取实战从单元格取值到整表映射对象3.1 单元格类型判断是绕不过去的坎Excel 的单元格是有类型的数字、字符串、日期、布尔、公式取法都不一样。很多人图省事直接cell.ToString()结果日期变成一串数字金额丢了小数位。正确的做法是先判断CellType再分情况取值。// 根据单元格类型安全取值 static string GetCellValue(ICell cell) { if (cell null) return ; switch (cell.CellType) { case CellType.String: return cell.StringCellValue; case CellType.Numeric: // 判断是不是日期格式 if (DateUtil.IsCellDateFormatted(cell)) return cell.DateCellValue?.ToString(yyyy-MM-dd) ?? ; return cell.NumericCellValue.ToString(); case CellType.Boolean: return cell.BooleanCellValue.ToString(); case CellType.Formula: // 公式取缓存值避免拿到公式串 return cell.CachedFormulaResultType switch { CellType.Numeric cell.NumericCellValue.ToString(), CellType.String cell.StringCellValue, _ }; default: return ; } }这里的关键点有三个。第一DateUtil.IsCellDateFormatted用来区分“数字”和“日期”因为日期在底层也是数字不判断就会输出 45000 这种序列号。第二公式单元格的CellType是Formula直接取StringCellValue可能拿到公式本身要用CachedFormulaResultType判断缓存结果的类型。第三所有分支都要有兜底返回空串比抛异常友好。3.2 把整张表映射成对象列表实际项目里读出来的数据一般不会停在字符串层面而是要转成实体对象。下面这段把表头按列名定位再把每行映射成字典最后转对象。// 读取整表返回字典列表key 是表头 static ListDictionarystring, string ReadSheet(string path, string sheetName) { var result new ListDictionarystring, string(); using var fs new FileStream(path, FileMode.Open, FileAccess.Read); IWorkbook workbook new XSSFWorkbook(fs); ISheet sheet workbook.GetSheet(sheetName); if (sheet null) return result; // 第一行当表头 IRow headerRow sheet.GetRow(0); if (headerRow null) return result; var headers new Liststring(); for (int c 0; c headerRow.LastCellNum; c) { headers.Add(GetCellValue(headerRow.GetCell(c))); } // 从第二行开始读数据 for (int r 1; r sheet.LastRowNum; r) { IRow row sheet.GetRow(r); if (row null) continue; var dict new Dictionarystring, string(); for (int c 0; c headers.Count; c) { dict[headers[c]] GetCellValue(row.GetCell(c)); } result.Add(dict); } return result; }逻辑上先固定表头行用LastCellNum拿到列数注意它返回的是最后一列的索引加一和LastRowNum的语义不同。然后逐行读每行按表头顺序填字典。这样即使列顺序变了只要表头名字对得上映射就不会错。参数上sheetName用名字取表比索引用更稳因为业务方插一列删一列索引就乱了。注意LastCellNum返回的是列数索引加一LastRowNum返回的是行索引。这两个 API 的语义不一致是 NPOI 里最容易踩的坑之一。3.3 大文件读取的内存控制如果文件有几万行一次性new XSSFWorkbook会把整个文件加载进内存容易 OOM。NPOI 提供了SXSSFWorkbook用于写入时的流式处理读取侧则可以用事件模型XSSFReader但代码复杂度高。折中做法是控制单次读取的行数分批处理。// 分批读取每批 1000 行降低峰值内存 const int batchSize 1000; int startRow 1; while (true) { var batch new ListDictionarystring, string(); // 这里复用上面的读取逻辑只取 [startRow, startRowbatchSize) 区间 // 实际项目中把区间参数传进读取方法即可 if (batch.Count 0) break; // 处理这一批 startRow batchSize; }分批的核心是不要在一次循环里持有整表数据处理完一批就释放。参数batchSize我一般设 1000 到 5000太小频繁 IO太大内存还是压不住。如果确实要读超大文件建议直接上ExcelDataReader这类只读流式库它专为读取设计内存占用低得多。4. 写入与保存从新建工作簿到格式化输出4.1 新建工作簿并写入数据写入的流程和读取对称建工作簿、建表、建行、建单元格、设值、保存。下面这段生成一个带表头的 xlsx。using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; // 新建工作簿和工作表 IWorkbook workbook new XSSFWorkbook(); ISheet sheet workbook.CreateSheet(导出结果); // 写表头 IRow header sheet.CreateRow(0); string[] columns { 订单号, 客户, 金额, 日期 }; for (int i 0; i columns.Length; i) { header.CreateCell(i).SetCellValue(columns[i]); } // 写数据行 var data new[] { new object[] { A001, 某客户, 1280.50, DateTime.Now }, new object[] { A002, 另一客户, 340.00, DateTime.Now } }; for (int r 0; r data.Length; r) { IRow row sheet.CreateRow(r 1); for (int c 0; c data[r].Length; c) { ICell cell row.CreateCell(c); switch (data[r][c]) { case double d: cell.SetCellValue(d); break; case DateTime dt: cell.SetCellValue(dt); // 设置日期格式否则显示为数字 ICellStyle dateStyle workbook.CreateCellStyle(); dateStyle.DataFormat workbook.CreateDataFormat().GetFormat(yyyy-MM-dd); cell.CellStyle dateStyle; break; default: cell.SetCellValue(data[r][c]?.ToString() ?? ); break; } } } // 保存到文件 using var fs new FileStream(output.xlsx, FileMode.Create, FileAccess.Write); workbook.Write(fs);逻辑说明CreateSheet建表CreateRow建行CreateCell建单元格三步缺一不可。写日期时必须设CellStyle的DataFormat否则 Excel 里看到的是一串数字。FileMode.Create会覆盖同名文件如果要追加得先读再写。参数上GetFormat的字符串遵循 Excel 的格式代码yyyy-MM-dd是最常用的。4.2 列宽、样式与合并单元格导出报表时列宽和样式直接影响可用性。默认列宽很窄中文会显示成####。下面把列宽和表头样式补上。// 自动调整列宽对中文支持一般建议手动设 for (int i 0; i columns.Length; i) { sheet.AutoSizeColumn(i); // 手动兜底避免中文被截断 int width sheet.GetColumnWidth(i); sheet.SetColumnWidth(i, (int)(width * 1.3)); } // 表头加粗居中 ICellStyle headerStyle workbook.CreateCellStyle(); IFont font workbook.CreateFont(); font.IsBold true; headerStyle.SetFont(font); headerStyle.Alignment HorizontalAlignment.Center; for (int i 0; i columns.Length; i) { header.GetCell(i).CellStyle headerStyle; } // 合并单元格第 0 行第 0 到 2 列 sheet.AddMergedRegion(new NPOI.SS.Util.CellRangeAddress(0, 0, 0, 2));AutoSizeColumn对纯英文还行中文经常算不准所以我在后面乘了 1.3 倍做兜底。CellRangeAddress的四个参数依次是起始行、结束行、起始列、结束列合并后只有左上角单元格的值有效。样式对象要复用不要在每个单元格里new一个否则大文件会生成大量重复样式文件体积暴涨。4.3 保存时的几个关键参数保存这一步看似简单但有几个点值得说。第一workbook.Write(fs)之后不要立刻fs.Close()用using让它自动释放。第二如果目标文件正被 Excel 打开写入会抛IOException这是最常见的运行时错误要么提示用户关闭要么写到临时文件再替换。第三xls 和 xlsx 的写入类不同HSSFWorkbook对应 xlsXSSFWorkbook对应 xlsx混用会报格式错误。// 写入临时文件再替换避免文件被占用 string tempPath output.tmp.xlsx; using (var fs new FileStream(tempPath, FileMode.Create, FileAccess.Write)) { workbook.Write(fs); } File.Copy(tempPath, output.xlsx, overwrite: true); File.Delete(tempPath);这个“先写临时文件再替换”的习惯是我处理文件占用问题的常用手法。它不能解决所有并发问题但能避开大部分“文件正被另一进程使用”的报错。5. 避坑与排查那些让我加班到深夜的 Excel 问题5.1 现象读取时报“文件格式无效”原因通常是文件扩展名和实际格式不符。业务方把 xls 改名成 xlsx 发过来或者从系统导出的其实是 CSV 但后缀写成了 xlsx。NPOI 按扩展名选 Workbook 类选错了就抛异常。解决不要靠扩展名判断读文件头。xlsx 是 zip 格式前两字节是PKxls 是 OLE 复合文档前八字节是D0 CF 11 E0。根据文件头选XSSFWorkbook还是HSSFWorkbook比看后缀靠谱得多。5.2 现象日期读出来是 45000 这样的数字原因是没有判断单元格是否为日期格式。Excel 内部用序列号存日期1900 年 1 月 1 日是 1之后每天加一。不判断格式取到的就是数字。解决用DateUtil.IsCellDateFormatted(cell)判断是日期就取DateCellValue。注意这个判断依赖单元格的格式设置如果业务方把日期单元格设成了“常规”那也判断不出来只能靠列名或约定来兜底。5.3 现象大文件读取时内存飙升甚至崩溃原因是XSSFWorkbook会把整个文件解析成 DOM 树放在内存里几万行就能吃掉几百兆。解决优先用流式读取库或者分批处理。如果必须用 NPOI控制单次加载的数据量处理完及时置空引用。写入侧用SXSSFWorkbook它会把超出一定行数的数据刷到磁盘内存占用可控。5.4 现象写入后文件打不开提示“内容有问题”原因多半是流没有正确关闭或者workbook.Write之后又对 workbook 做了修改。OpenXML 格式对写入顺序敏感写完再改会导致内部结构不一致。解决确保Write是最后一步写完立即释放流。用using包住FileStream不要手动Close后再Dispose。如果用了SXSSFWorkbook记得调用Dispose清理临时文件。5.5 现象服务器上跑 Interop 报“检索 COM 类工厂失败”原因是服务器没装 Office或者 IIS 的运行账户没有权限访问 Office 组件。这是 Interop 方案在服务端的经典问题。解决服务端不要用 Interop换 NPOI 或 ClosedXML。如果非用不可得装 Office 并配置 DCOM 权限但即便如此进程残留和并发问题依然存在。我的经验是服务端场景直接排除 Interop省下的排查时间够写好几个模块了。6. 进阶技巧用 DataTable 批量导出和读取校验6.1 从 DataTable 一键导出 Excel实际项目里数据往往先落在DataTable或List里再导出。下面这段把DataTable直接写成 Excel列名当表头类型自动判断。// 把 DataTable 导出为 xlsx static void ExportDataTable(DataTable table, string path) { IWorkbook workbook new XSSFWorkbook(); ISheet sheet workbook.CreateSheet(Sheet1); // 写表头 IRow header sheet.CreateRow(0); for (int c 0; c table.Columns.Count; c) { header.CreateCell(c).SetCellValue(table.Columns[c].ColumnName); } // 写数据按列类型分派 for (int r 0; r table.Rows.Count; r) { IRow row sheet.CreateRow(r 1); for (int c 0; c table.Columns.Count; c) { object value table.Rows[r][c]; ICell cell row.CreateCell(c); switch (value) { case null: case DBNull: cell.SetCellValue(); break; case double d: cell.SetCellValue(d); break; case DateTime dt: cell.SetCellValue(dt); break; case bool b: cell.SetCellValue(b); break; default: cell.SetCellValue(value.ToString()); break; } } } using var fs new FileStream(path, FileMode.Create, FileAccess.Write); workbook.Write(fs); }这段的关键是DBNull的处理。数据库里取出来的空值不是null而是DBNull.Value不单独判断会走到ToString()分支输出一个空字符串看似没问题但如果列是数字类型空字符串会让 Excel 把它当文本后续公式计算会出错。所以空值统一写空串保持列类型一致。6.2 读取后的数据校验读进来的数据不能直接用得先校验。常见的是必填项、格式、范围三类。下面是一个校验函数的骨架。// 校验一行数据返回错误信息列表 static Liststring ValidateRow(Dictionarystring, string row) { var errors new Liststring(); // 必填校验 if (string.IsNullOrWhiteSpace(row.GetValueOrDefault(订单号))) errors.Add(订单号不能为空); // 格式校验金额必须是数字 if (row.TryGetValue(金额, out string amount) !decimal.TryParse(amount, out _)) errors.Add($金额格式错误{amount}); // 范围校验日期不能晚于今天 if (row.TryGetValue(日期, out string dateStr) DateTime.TryParse(dateStr, out DateTime date) date DateTime.Today) errors.Add($日期不能晚于今天{dateStr}); return errors; }校验的逻辑是逐项检查把错误收集起来一次性返回而不是遇到第一个错就抛异常。这样业务方能一次看到所有问题改起来效率高。参数上GetValueOrDefault避免键不存在时抛异常TryParse避免格式错误时崩溃。6.3 一个我常用的验证习惯每次写完导出功能我都会做一件事把导出的文件用 Excel 打开检查三样东西——日期列是不是日期格式、数字列能不能求和、中文有没有乱码。这三项过了基本就没大问题。这个习惯帮我拦下过好几次“日期变数字”“金额变文本”的低级错误。从那以后我每次交付 Excel 相关功能前都强制自己走一遍“打开、求和、看日期”这三步。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑