简介这份资源面向具备一定C#基础、需要在数据处理与报表生成场景中操作Excel的开发者围绕Excel读取、写入、保存与资源释放等核心环节给出可运行的实例代码。压缩包共58个文件以cs源码、dll类库、xml与config配置、exe可执行程序及sln解决方案为主另含xls示例表格与resx资源文件整体约1.55MB结构完整便于直接打开调试。内容基于Microsoft.Office.Interop.Excel库展开涵盖创建Application对象、打开工作簿、遍历单元格读取数据、写入新值、保存并正确释放COM对象等关键步骤同时涉及NPOI相关类库的引用并提示大数据量下Interop效率偏低、需匹配Office版本等注意事项。项目示例展示了多数据源工作表的读取与处理流程读者可据此快速搭建导入导出、数据分析等实用功能。目前已有589人学习下载适合作为入门与排错参考。1. 从一份 C# Excel 操作实例说起为什么我建议你手头常备一套接手一个数据导入模块时最怕的不是算法难而是业务方甩来一个几十兆的.xlsx里面合并单元格、隐藏列、公式引用、日期格式混着来你打开一看连表头在第几行都不确定。这份 C# Excel 读取与保存操作实例解决的就是这类“脏活”——它不教你 Excel 有多强大而是把 C# 读写 Excel 的几条主流路径、参数配置和常见翻车点用可运行的代码片段串起来。适合两类人一是刚接触 C# 但需要快速交付数据导入导出功能的开发者二是用过 NPOI 或 EPPlus 但总在格式兼容、内存占用上踩坑的老手。下面按“选型 → 读取 → 写入 → 避坑 → 进阶”的顺序拆开讲每一步都落到能直接抄的代码和参数上。2. 选型先定死NPOI、EPPlus、ClosedXML 到底怎么选2.1 三个库的定位差异在 C# 里操作 Excel绕不开三个名字NPOI、EPPlus、ClosedXML。它们不是替代关系而是各自卡在不同场景里。NPOI 是 Apache 基金会的项目最早从 Java 的 POI 移植过来同时支持.xlsHSSF和.xlsxXSSF。它的优势是“全”——老格式、公式、图表、甚至 VBA 宏都能碰一碰而且不依赖 Office 安装。缺点是 API 偏底层写一个带样式的单元格要十几行新手容易写出一堆ICellStyle重复创建导致文件膨胀。EPPlus 专注.xlsxAPI 设计更现代using语法糖用起来顺手对 DataTable 和集合的导出支持很好。但要注意它的许可证从 5.0 开始商业用途需要付费授权内部工具或开源项目用旧版 4.x 没问题商用前务必确认合规。ClosedXML 建立在 OpenXML SDK 之上定位是“让操作 Excel 像操作对象一样简单”。它读写.xlsx的代码量最少适合快速做报表导出。但遇到超大文件或复杂公式链时性能会明显掉下来而且不支持.xls。我一般这样选需要兼容.xls或处理遗留文件直接上 NPOI纯.xlsx且追求开发效率ClosedXML 优先如果项目已经买了 EPPlus 授权或者需要精细控制样式和公式计算用 EPPlus。2.2 环境准备与依赖引入以 .NET 6 控制台项目为例用 NuGet 装包。NPOI 当前稳定版是 2.7.xClosedXML 是 0.102.xEPPlus 商用版 7.x。不要混装多个库的旧版本容易出现System.Drawing.Common冲突。# 创建项目 dotnet new console -n ExcelDemo cd ExcelDemo # 方案一NPOI支持 xls 和 xlsx dotnet add package NPOI --version 2.7.1 # 方案二ClosedXML仅 xlsx代码最简 dotnet add package ClosedXML --version 0.102.2 # 方案三EPPlus仅 xlsx注意许可证 dotnet add package EPPlus --version 7.1.3装完后检查.csproj里的TargetFramework如果是netstandard2.0或net48NPOI 需要额外加System.Text.Encoding.CodePages包否则读中文会乱码。这个坑后面避坑章节还会展开。提示三个库不要同时引入同一个项目除非你明确知道自己在做适配层。依赖冲突时优先保留 NPOI因为它的依赖树最干净。3. 读取实战从文件流到 DataTable 的完整链路3.1 NPOI 读取 xlsx 并转 DataTable先看最常用的场景用户上传一个.xlsx后台解析成DataTable再入库。下面这段代码处理了表头行、空行跳过、单元格类型判断三个关键点。using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using System.Data; public DataTable ReadExcelWithNpoi(string filePath) { var dt new DataTable(); using var fs new FileStream(filePath, FileMode.Open, FileAccess.Read); IWorkbook workbook new XSSFWorkbook(fs); // .xls 用 HSSFWorkbook ISheet sheet workbook.GetSheetAt(0); // 默认读第一个 Sheet // 1. 定位表头行跳过前 N 行空行或说明行 int headerRowIndex 0; for (int i sheet.FirstRowNum; i sheet.LastRowNum; i) { var row sheet.GetRow(i); if (row ! null row.Cells.Any(c c.CellType ! CellType.Blank)) { headerRowIndex i; break; } } // 2. 构建 DataTable 列 IRow headerRow sheet.GetRow(headerRowIndex); for (int c 0; c headerRow.LastCellNum; c) { var cell headerRow.GetCell(c); string colName cell?.ToString()?.Trim() ?? $Col{c}; if (string.IsNullOrEmpty(colName)) colName $Col{c}; // 防止重复列名导致 DataTable 报错 while (dt.Columns.Contains(colName)) colName _1; dt.Columns.Add(colName); } // 3. 逐行填充数据 for (int r headerRowIndex 1; r sheet.LastRowNum; r) { IRow row sheet.GetRow(r); if (row null) continue; var dr dt.NewRow(); bool hasValue false; for (int c 0; c dt.Columns.Count; c) { ICell cell row.GetCell(c); if (cell null) continue; switch (cell.CellType) { case CellType.String: dr[c] cell.StringCellValue; break; case CellType.Numeric: // 判断是否为日期格式 if (DateUtil.IsCellDateFormatted(cell)) dr[c] cell.DateCellValue; else dr[c] cell.NumericCellValue; break; case CellType.Boolean: dr[c] cell.BooleanCellValue; break; case CellType.Formula: // 公式取缓存值避免重复计算 dr[c] cell.CachedFormulaResultType CellType.Numeric ? cell.NumericCellValue : cell.StringCellValue; break; default: dr[c] cell.ToString(); break; } hasValue true; } if (hasValue) dt.Rows.Add(dr); } return dt; }逻辑说明XSSFWorkbook对应.xlsxHSSFWorkbook对应.xls用错会直接抛InvalidFormatException。表头定位那段是为了应对“前几行是标题说明”的常见情况headerRowIndex找到第一个非空行。列名去重是因为DataTable不允许同名列而 Excel 里经常出现两个“备注”。单元格类型判断里DateUtil.IsCellDateFormatted是 NPOI 提供的日期识别方法不加这个判断日期会变成一串数字比如 45123。参数怎么改如果 Sheet 名固定把GetSheetAt(0)换成GetSheet(Sheet1)如果数据从第二行开始但第一行是合并单元格headerRowIndex的循环条件要相应调整。3.2 ClosedXML 读取代码量减半的写法同样的需求ClosedXML 写起来更短适合快速验证。using ClosedXML.Excel; using System.Data; public DataTable ReadExcelWithClosedXml(string filePath) { var dt new DataTable(); using var workbook new XLWorkbook(filePath); var ws workbook.Worksheet(1); // 第一个工作表 // 找到第一个非空行作为表头 var headerRow ws.RowsUsed().First(); foreach (var cell in headerRow.CellsUsed()) { string colName cell.GetString().Trim(); if (string.IsNullOrEmpty(colName)) colName $Col{cell.Address.ColumnNumber}; while (dt.Columns.Contains(colName)) colName _1; dt.Columns.Add(colName); } // 从表头下一行开始读 foreach (var row in ws.RowsUsed().Skip(1)) { var dr dt.NewRow(); for (int c 1; c dt.Columns.Count; c) { var cell row.Cell(c); dr[c - 1] cell.IsEmpty() ? DBNull.Value : cell.Value; } dt.Rows.Add(dr); } return dt; }逻辑说明RowsUsed()自动跳过完全空行First()拿到第一个有数据的行。cell.Value返回object日期、数字、文本都能直接塞进DataRow不用手动判断类型。但要注意ClosedXML 对公式单元格默认返回计算后的值如果公式没被 Excel 计算过比如文件是程序生成的可能拿到空值。参数调整Worksheet(1)里的数字是工作表位置不是名称如果表头不在第一行把First()换成Skip(n).First()。3.3 读取时的内存与流处理大文件读取时XSSFWorkbook会把整个文件加载进内存几百兆的.xlsx能吃掉几个 G。NPOI 提供了SXSSFWorkbook用于写入但读取侧没有真正的流式 API。常见做法是先用FileStream打开限制并发读取数量或者把大文件拆成多个小 Sheet 分批处理。// 限制 NPOI 读取时的临时文件策略针对 xlsx var fs new FileStream(filePath, FileMode.Open, FileAccess.Read); IWorkbook workbook new XSSFWorkbook(fs); // 读取完成后及时释放 fs.Dispose(); workbook.Close();如果只是统计行数或抽样不要用LastRowNum遍历全部改用sheet.GetRowEnumerator()逐行读读到满足条件就 break。4. 写入与保存样式、公式、大数据量导出4.1 NPOI 写入带样式的 xlsx导出报表时表头加粗、列宽自适应、日期格式化是基本要求。下面这段代码创建一个新工作簿写入表头和数据并设置列宽。using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using NPOI.SS.Util; public void WriteExcelWithNpoi(string filePath, DataTable dt) { IWorkbook workbook new XSSFWorkbook(); ISheet sheet workbook.CreateSheet(数据); // 表头样式 ICellStyle headerStyle workbook.CreateCellStyle(); IFont headerFont workbook.CreateFont(); headerFont.IsBold true; headerFont.FontHeightInPoints 11; headerStyle.SetFont(headerFont); headerStyle.FillForegroundColor IndexedColors.Grey25Percent.Index; headerStyle.FillPattern FillPattern.SolidForeground; headerStyle.BorderBottom BorderStyle.Thin; // 写表头 IRow headerRow sheet.CreateRow(0); for (int c 0; c dt.Columns.Count; c) { ICell cell headerRow.CreateCell(c); cell.SetCellValue(dt.Columns[c].ColumnName); cell.CellStyle headerStyle; } // 日期样式 ICellStyle dateStyle workbook.CreateCellStyle(); dateStyle.DataFormat workbook.CreateDataFormat().GetFormat(yyyy-mm-dd); // 写数据 for (int r 0; r dt.Rows.Count; r) { IRow row sheet.CreateRow(r 1); for (int c 0; c dt.Columns.Count; c) { ICell cell row.CreateCell(c); object val dt.Rows[r][c]; if (val is DateTime dateVal) { cell.SetCellValue(dateVal); cell.CellStyle dateStyle; } else if (val is double dblVal) cell.SetCellValue(dblVal); else if (val is bool boolVal) cell.SetCellValue(boolVal); else cell.SetCellValue(val?.ToString() ?? ); } } // 列宽自适应NPOI 需要手动计算 for (int c 0; c dt.Columns.Count; c) { sheet.AutoSizeColumn(c); int width sheet.GetColumnWidth(c); sheet.SetColumnWidth(c, Math.Min(width 512, 255 * 256)); // 最大 255 字符 } using var fs new FileStream(filePath, FileMode.Create, FileAccess.Write); workbook.Write(fs); workbook.Close(); }逻辑说明CreateCellStyle和CreateFont必须从workbook创建不能跨工作簿复用。AutoSizeColumn在 NPOI 里对中文字符支持一般算出来的宽度可能偏窄所以加 512 的余量。SetColumnWidth的单位是 1/256 字符宽上限 255*256。参数调整如果数据量超过 10 万行用SXSSFWorkbook替代XSSFWorkbook它会把行刷到磁盘临时文件内存占用稳定在几十兆。// 大数据量写入保留 100 行在内存其余刷盘 IWorkbook workbook new SXSSFWorkbook(100); // ... 写入逻辑相同 ((SXSSFWorkbook)workbook).Dispose(); // 清理临时文件4.2 ClosedXML 写入与公式设置ClosedXML 写公式很直观直接给单元格赋字符串SUM(A1:A10)即可。using ClosedXML.Excel; public void WriteExcelWithClosedXml(string filePath, DataTable dt) { using var workbook new XLWorkbook(); var ws workbook.Worksheets.Add(报表); // 写表头 for (int c 0; c dt.Columns.Count; c) { var cell ws.Cell(1, c 1); cell.Value dt.Columns[c].ColumnName; cell.Style.Font.Bold true; cell.Style.Fill.BackgroundColor XLColor.LightGray; } // 写数据 for (int r 0; r dt.Rows.Count; r) { for (int c 0; c dt.Columns.Count; c) { ws.Cell(r 2, c 1).Value dt.Rows[r][c] switch { DateTime d d, double dbl dbl, bool b b, _ dt.Rows[r][c]?.ToString() ?? }; } } // 添加汇总行公式 int lastDataRow dt.Rows.Count 1; int sumCol dt.Columns.Count 1; ws.Cell(lastDataRow 1, 1).Value 合计; ws.Cell(lastDataRow 1, sumCol).FormulaA1 $SUM({ws.Cell(2, sumCol).Address}:{ws.Cell(lastDataRow, sumCol).Address}); ws.Columns().AdjustToContents(); // 自动列宽 workbook.SaveAs(filePath); }逻辑说明AdjustToContents比 NPOI 的AutoSizeColumn对中文支持更好但数据量大时耗时明显建议只在列数少时用。FormulaA1设置公式后ClosedXML 不会立即计算公式值打开 Excel 时才会算如果需要程序内取值得用cell.Value触发重算或调用workbook.RecalculateAllFormulas()。4.3 保存时的文件占用与覆盖问题写入时最常见的翻车是“文件被另一个进程占用”。原因通常是FileStream没释放或者 Excel 还开着目标文件。解决方式用using包裹所有流写入前检查文件是否存在并尝试删除。string path C:\temp\output.xlsx; if (File.Exists(path)) { try { File.Delete(path); } catch (IOException) { throw new Exception(文件被占用请关闭 Excel 后重试); } } using var fs new FileStream(path, FileMode.CreateNew, FileAccess.Write); workbook.Write(fs);注意FileMode.Create会覆盖但不清空原文件句柄如果之前有未释放的读流仍可能报错。用CreateNew配合显式删除更稳。5. 避坑与排查这五个问题我几乎每次都能遇到5.1 中文乱码现象是读出来全是问号或方块原因NPOI 在 .NET Core 下默认编码不含 GBK读取.xls或某些带中文的.xlsx时cell.StringCellValue返回乱码。解决在程序启动时注册编码提供程序。System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);这行代码放在Main方法第一行或者任何读取操作之前。只加一次全局生效。5.2 日期变成数字45123 这种值原因Excel 内部用双精度浮点数存日期1900 年 1 月 1 日是 1。NPOI 的NumericCellValue直接返回这个数字。解决用DateUtil.IsCellDateFormatted(cell)判断再用cell.DateCellValue取DateTime。ClosedXML 的cell.Value会自动转换但前提是单元格格式被识别为日期。5.3 公式单元格读到空值原因程序生成的 Excel 文件里公式没有缓存计算结果CachedFormulaResultType是Blank。解决读取时判断cell.CellType CellType.Formula如果缓存为空用workbook.GetCreationHelper().CreateFormulaEvaluator().Evaluate(cell)强制计算。但注意复杂公式或外部引用可能算不出来这时只能提示用户手动打开保存一次。5.4 大文件 OutOfMemoryException现象读 50MB 的.xlsx直接崩内存飙到 2G 以上。原因XSSFWorkbook全量加载。解决换SXSSFWorkbook写入读取侧用FileStream配合XSSFWorkbook但限制并发或者把文件拆成多个小 Sheet 分批读。如果只是抽取部分列用sheet.GetRow(r).GetCell(c)按需读不要先转DataTable。5.5 列宽自适应对中文无效现象AutoSizeColumn后中文列还是显示####。原因NPOI 的宽度计算基于字体像素中文字符宽度是英文的两倍但算法没考虑。解决手动设置宽度或者用SetColumnWidth(c, 20 * 256)给固定值。ClosedXML 的AdjustToContents稍好但遇到合并单元格也会失准。6. 进阶技巧用 DataTable 做中间层统一读写接口如果你项目里同时存在 NPOI 和 ClosedXML或者未来可能换库建议抽一层IExcelService接口入参和出参都用DataTable。这样业务代码不依赖具体库换实现只改一个类。public interface IExcelService { DataTable Read(string filePath, int sheetIndex 0); void Write(string filePath, DataTable data, string sheetName Sheet1); } // NPOI 实现 public class NpoiExcelService : IExcelService { /* 前面章节的代码 */ } // ClosedXML 实现 public class ClosedXmlExcelService : IExcelService { /* 前面章节的代码 */ }调用时用工厂或依赖注入决定用哪个。我一般会在配置文件里加一个ExcelProvider节点值为Npoi或ClosedXml运行时反射创建实例。这样测试环境用 ClosedXML 快速跑生产环境切 NPOI 保兼容。另一个技巧是读取时先做一次“预检”用workbook.NumberOfSheets确认 Sheet 数量用sheet.GetRow(0)确认表头行是否存在避免直接GetRow(0).GetCell(0)空引用。这个习惯帮我省过很多次半夜排查。从那以后我每次接 Excel 相关的需求都强制先跑一遍“预检 小样本读取”确认格式和编码没问题再写全量逻辑。希望帮到你。本文还有配套的精品资源点击获取