
简介这份资源面向具备一定C#基础、需要在项目中处理Excel数据的开发者围绕Excel读取、保存与常见操作展开采用Microsoft.Office.Interop.Excel库实现与Excel的进程内交互。内容涵盖引入Interop库、创建Application对象、打开工作簿、遍历单元格读取数据、写入单元格、保存工作簿以及关闭并释放COM对象等完整流程并附带一个可运行的项目示例演示如何读取、处理和保存三个数据源的工作表内容便于直接参考或按需修改。压缩包共58个文件以dll动态库、cs源码、xml配置、exe可执行文件及sln解决方案等为主整体约1.55MB结构紧凑适合快速导入Visual Studio调试。目前已有589人学习下载。对于数据导入导出、报表生成等场景读者可借此掌握Interop操作Excel的核心思路同时了解大数据量下改用EPPlus等库的取舍与Office版本兼容性等注意事项。1. 从一次数据导入翻车说起C# 读写 Excel 到底该用什么某公司内部有个小工具需求很朴素把业务同事发来的 Excel 台账读进数据库再把处理结果写回一个新的 Excel 文件。第一版用Microsoft.Office.Interop.Excel硬写开发机上跑得好好的部署到一台没装 Office 的服务器上直接抛异常进程还残留了一堆看不见的 Excel 后台进程内存一路涨。这就是典型的「开发环境玄学生产环境翻车」。C# 操作 Excel 这件事难点从来不在「怎么读一个单元格」而在于选对库、处理好格式差异、管住资源释放。标题里的三个动作——读取、保存、操作实例——其实对应三类真实诉求把外部数据吃进来、把结果吐出去、在内存里对表格做增删改查。适合谁看写过一点 C#、被 Excel 导入导出需求反复折磨的后端或工具开发者。下面按「选型 → 读 → 写 → 避坑 → 进阶」的顺序把能直接抄的代码和参数讲清楚。2. 选型先定生死四种主流方案的能力边界2.1 四种方案的横向对比在动手写第一行代码前先把可选路线摆出来。C# 生态里读写 Excel 常见有四条路能力差异很大选错了后面全是返工。方案是否依赖 Office支持 .xlsx支持 .xls读写能力典型适用场景InteropCOM 互操作是是是读写全能调格式本机装了 Office 的桌面工具OleDbACE 驱动否是是偏读写弱快速把表当数据库查EPPlus否是否读写全API 友好服务端生成报表NPOI否是是读写全格式支持细需要兼容老 .xls 的场景选型的第一原则服务端不要碰 Interop。它本质是启动一个 Excel 进程再通过 COM 通信并发差、易残留进程、对运行账户权限敏感。我一般只在「用户本机、明确装了 Office、且需要调用 Excel 自身公式计算」时才考虑它。2.2 为什么服务端优先选 NPOI 或 EPPlusNPOI 是 Apache 基金会下的项目源自 Java 的 POI纯托管代码不依赖任何 Office 组件同时支持 .xls 和 .xlsx。EPPlus 的 API 更现代链式调用写起来舒服但较新版本改成了商业授权商用前要确认许可证。如果你的输入里可能混着老式 .xlsNPOI 是更稳的选择如果只处理 .xlsx 且团队能接受其授权条款EPPlus 的开发体验更好。这里给一个判断口诀要兼容 .xls 选 NPOI要极致简洁选 EPPlus要本机公式计算才考虑 Interop临时查询用 OleDb。下面所有实例以 NPOI 为主线因为它覆盖面最广学会它再切 EPPlus 只是 API 名字的差异。2.3 环境准备与依赖引入新建一个 .NET 控制台或类库项目通过包管理器引入 NPOI。命令行方式如下# 在项目目录下执行安装 NPOI 主包 dotnet add package NPOI # 如果项目是 .NET Framework用 PackageReference 或 packages.config 均可装完后确认引用里出现NPOI及其依赖。注意 NPOI 不同大版本对 .NET 目标框架要求不同较新版本要求 .NET Standard 2.0 以上老项目如果还停在 .NET Framework 4.0需要挑对应历史版本。这一步没有玄学装不上通常是目标框架太低或包源不通。3. 读取 Excel从打开工作簿到安全拿到单元格值3.1 用 NPOI 打开工作簿的最小代码读取的第一步是把文件流交给 NPOI让它判断是 .xls 还是 .xlsx。下面这段是能直接跑的最小实例using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using NPOI.HSSF.UserModel; public static ISheet OpenFirstSheet(string filePath) { // 用 FileStream 打开FileShare.ReadWrite 允许其他进程同时读避免文件被占用报错 using var fs new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); IWorkbook workbook; // 根据扩展名选择具体实现.xlsx 用 XSSF.xls 用 HSSF if (Path.GetExtension(filePath).Equals(.xlsx, StringComparison.OrdinalIgnoreCase)) workbook new XSSFWorkbook(fs); else workbook new HSSFWorkbook(fs); // 返回第一个工作表索引从 0 开始 return workbook.GetSheetAt(0); }逻辑说明FileShare.ReadWrite是关键参数很多「文件正被另一进程使用」的异常就是没设它。XSSFWorkbook对应 OOXML 格式HSSFWorkbook对应老的二进制格式选错会直接抛异常。返回ISheet后就能按行遍历。3.2 遍历行与单元格处理空行和类型转换拿到ISheet后遍历要小心两件事空行和单元格类型。Excel 里一个「空」单元格可能是 null、空字符串或 Blank 类型直接ToString()会得到意外结果。public static void ReadRows(ISheet sheet) { // LastRowNum 是最后一行的索引从 0 开始不是行数 for (int i 0; i sheet.LastRowNum; i) { IRow row sheet.GetRow(i); if (row null) continue; // 整行为空跳过 for (int j 0; j row.LastCellNum; j) { ICell cell row.GetCell(j); string value GetCellValue(cell); Console.WriteLine($第{i 1}行 第{j 1}列: {value}); } } } // 统一把单元格转成字符串按类型分别处理 private static string GetCellValue(ICell cell) { if (cell null) return string.Empty; switch (cell.CellType) { case CellType.String: return cell.StringCellValue; case CellType.Numeric: // 日期在 Excel 里是数值需要判断格式 if (DateUtil.IsCellDateFormatted(cell)) return cell.DateCellValue?.ToString(yyyy-MM-dd HH:mm:ss) ?? ; return cell.NumericCellValue.ToString(); case CellType.Boolean: return cell.BooleanCellValue.ToString(); case CellType.Formula: // 公式单元格取缓存的计算结果避免自己解析公式 return cell.CachedFormulaResultType CellType.Numeric ? cell.NumericCellValue.ToString() : cell.StringCellValue; default: return string.Empty; } }参数说明LastRowNum和LastCellNum都是索引语义循环边界用和要分清。DateUtil.IsCellDateFormatted用来区分「数字」和「日期」这是读取时最常见的坑——不判断的话日期会变成一串 45000 之类的序列号。公式单元格取CachedFormulaResultType拿的是上次 Excel 保存时缓存的结果如果文件从没被 Excel 打开过缓存可能为空。3.3 大文件读取用流式 API 控制内存普通XSSFWorkbook会把整个文件加载进内存几万行还行几十万行就会 OOM。NPOI 提供了事件驱动的流式读取接口XSSFReader适合只读大文件的场景。using NPOI.XSSF.EventUserModel; using NPOI.OpenXml4Net.OPC; // 流式读取不把整个工作簿载入内存 public static void ReadLargeFile(string filePath) { using var fs new FileStream(filePath, FileMode.Open, FileAccess.Read); var pkg OPCPackage.Open(fs); var reader new XSSFReader(pkg); var styles (XSSFReader.SheetIterator)reader.GetStylesTable(); // 实际使用需配合 SheetContentsHandler 逐行回调此处仅示意入口 }逻辑说明流式读取的核心是「逐行回调」你实现一个SheetContentsHandlerNPOI 每解析一行就调你一次内存占用与文件大小基本无关。代价是不能随机访问、不能回退适合「读一遍就入库」的 ETL 场景。如果既要读又要改还是得用普通模式但要评估数据量。4. 保存与写入新建工作簿、写回文件和格式控制4.1 新建工作簿并写入数据写出的流程是建工作簿 → 建工作表 → 建行 → 建单元格 → 写值 → 落盘。下面是一个完整的导出实例using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; public static void WriteExcel(string outputPath) { IWorkbook workbook new XSSFWorkbook(); ISheet sheet workbook.CreateSheet(结果); // 表头 IRow header sheet.CreateRow(0); header.CreateCell(0).SetCellValue(编号); header.CreateCell(1).SetCellValue(名称); header.CreateCell(2).SetCellValue(金额); // 数据行 for (int i 1; i 100; i) { IRow row sheet.CreateRow(i); row.CreateCell(0).SetCellValue(i); row.CreateCell(1).SetCellValue($项目{i}); row.CreateCell(2).SetCellValue(i * 12.5); } // 写文件用 FileMode.Create 覆盖已有文件 using var fs new FileStream(outputPath, FileMode.Create, FileAccess.Write); workbook.Write(fs); // 关闭工作簿释放资源 workbook.Close(); }参数说明CreateRow和CreateCell都是「不存在才创建」重复调用同一索引会拿到已有对象。SetCellValue有多个重载传 int、double、string、bool 都行NPOI 会自动设置单元格类型。workbook.Write之后一定要Close否则在部分场景下文件句柄不释放。4.2 单元格样式字体、边框和数字格式导出报表绕不开样式。NPOI 的样式对象是工作簿级别的同一个样式可以复用到多个单元格但不要在每个单元格里都 Create 一个新样式那样会撑爆样式表。// 创建一次复用多次 ICellStyle moneyStyle workbook.CreateCellStyle(); moneyStyle.DataFormat workbook.CreateDataFormat().GetFormat(#,##0.00); IFont boldFont workbook.CreateFont(); boldFont.IsBold true; ICellStyle headerStyle workbook.CreateCellStyle(); headerStyle.SetFont(boldFont); headerStyle.Alignment HorizontalAlignment.Center; // 应用到单元格 header.GetCell(0).CellStyle headerStyle; dataRow.GetCell(2).CellStyle moneyStyle;逻辑说明DataFormat用格式字符串控制显示#,##0.00是千分位加两位小数。字体通过CreateFont创建后SetFont绑定到样式。样式数量有上限.xlsx 约 64000 个循环里反复创建新样式是典型翻车点正确做法是在循环外建好、循环内复用。4.3 保存时的常见异常与处理保存阶段最常见的两个异常一是目标文件被 Excel 打开着FileMode.Create会抛IOException二是路径目录不存在。前者要么提示用户关闭文件要么先写临时文件再替换后者用Directory.CreateDirectory兜底。string dir Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(dir) !Directory.Exists(dir)) Directory.CreateDirectory(dir); // 先写临时文件成功后再替换降低写一半失败导致文件损坏的风险 string tempPath outputPath .tmp; using (var fs new FileStream(tempPath, FileMode.Create, FileAccess.Write)) { workbook.Write(fs); } File.Copy(tempPath, outputPath, overwrite: true); File.Delete(tempPath);这种「临时文件 替换」的写法在生成大报表时尤其值得避免用户拿到一个半截文件还以为数据就这么多。5. 避坑与排查那些让导入导出反复返工的问题5.1 日期变成数字序列号现象读出来的日期是 45000 这种整数。原因Excel 内部用序列号存日期NPOI 默认按数值返回。解决用DateUtil.IsCellDateFormatted(cell)判断后再取DateCellValue如 3.2 节代码所示。注意有些文件日期列没设日期格式这时只能靠业务规则判断比如数值落在合理日期区间就按日期处理。5.2 文件被占用导致读写失败现象抛「文件正被另一进程使用」。原因文件被 Excel 打开或上一次的流没释放。解决读取时用FileShare.ReadWrite写入时先检测能否独占打开不能就提示用户关闭所有FileStream和IWorkbook都用using或显式Close。Interop 方案还要额外杀掉残留进程这也是不推荐它的原因之一。5.3 大文件内存溢出现象几十万行文件读取时进程内存飙升甚至崩溃。原因XSSFWorkbook全量加载。解决只读场景改用XSSFReader流式接口必须全量处理时分批读取、处理完一批释放一批或改用数据库中转。判断阈值一般超过 5 万行、列数较多时就该考虑流式。5.4 公式单元格取不到值现象公式列读出来是空或 0。原因公式结果依赖 Excel 计算并缓存文件若由程序生成且未经过 Excel缓存为空。解决读取时判断CellType.Formula并取CachedFormulaResultType如果缓存确实为空只能自己实现公式逻辑或用 Interop 让 Excel 算一遍后者又回到依赖 Office 的老问题需权衡。5.5 中文乱码与编码问题现象导出的 CSV 用 Excel 打开中文乱码。原因CSV 默认编码与 Excel 预期不符。解决如果最终产物是 CSV写文件时用带 BOM 的 UTF-8如果是 .xlsxNPOI 内部按 Unicode 处理一般不会乱码出现乱码多半是中间经过了错误的编码转换。这条严格说不属于 Excel 本身但导入导出链路里极常见。6. 进阶技巧把读写封装成可复用的工具方法6.1 泛型映射把行直接转成对象实际项目里不会满足于逐格取值更希望「给一个类型自动把表头映射成属性」。核心思路是读第一行做表头建立「列名 → 属性」的字典再逐行反射赋值。public static ListT ToListT(ISheet sheet) where T : new() { var result new ListT(); IRow header sheet.GetRow(0); if (header null) return result; // 建立列索引到属性的映射 var map new Dictionaryint, PropertyInfo(); var props typeof(T).GetProperties(); for (int j 0; j header.LastCellNum; j) { string name GetCellValue(header.GetCell(j)); var prop props.FirstOrDefault(p p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (prop ! null) map[j] prop; } for (int i 1; i sheet.LastRowNum; i) { IRow row sheet.GetRow(i); if (row null) continue; var obj new T(); foreach (var kv in map) { string val GetCellValue(row.GetCell(kv.Key)); // 按属性类型做转换这里省略复杂类型处理 kv.Value.SetValue(obj, Convert.ChangeType(val, kv.Value.PropertyType)); } result.Add(obj); } return result; }逻辑说明表头名与属性名做不区分大小写匹配匹配不上的列直接忽略这样表格多几列也不影响。Convert.ChangeType对可空类型和枚举会失败生产代码里要加类型判断和异常兜底。这个封装能把「读表」从几十行样板代码压缩成一行调用。6.2 用 DataTable 做中转的取舍另一种常见做法是先读进DataTable再统一处理或绑定控件。优点是通用、和数据库操作风格一致缺点是内存占用翻倍、类型信息丢失。我的习惯是一次性导入用泛型映射需要动态列或对接旧代码时才用DataTable。两者不要混用否则类型转换的坑会叠加。6.3 一个我踩过的教训早年做导出时我在循环里给每个单元格都CreateCellStyle小数据量测试完全正常上线后数据量一大就抛「样式数量超出限制」排查了半天才定位到。从那以后我养成了一个习惯凡是工作簿级别的对象样式、字体、数据格式一律在循环外创建、循环内复用。这个习惯帮我省掉了后面无数次返工。读写 Excel 本身不难难的是把这些边界条件提前想到。希望帮到你。本文还有配套的精品资源点击获取