资讯动态

C# Excel 读取保存与操作实例:从选型到避坑的完整指南

发布时间:2026/10/15 2:36:44 来源:尧图企业网站定制
简介这份资源面向具备一定C#基础、需要在.NET Framework环境下处理Excel数据的开发者聚焦Excel读取、写入、保存与资源释放等常见操作。实例基于Microsoft.Office.Interop.Excel库演示了创建Excel应用程序对象、打开工作簿、遍历工作表单元格读取数据、写入新数据、保存并正确释放COM对象等完整流程同时涉及ThreeDataReadFormExcle项目对多数据源工作表的整合处理并提示了Interop方式在效率与Office版本兼容性上的注意事项。压缩包共58个文件以cs源码、dll类库、xml配置、exe程序及sln解决方案为主另含xls示例表格与resx资源文件整体约1.55MB结构完整可直接运行调试。目前已有589人学习下载适合数据导入导出、报表生成等场景的开发者参考借鉴。1. 从一次数据导入翻车说起这套 C# Excel 实例到底能干什么上个月帮一个做生产报表的朋友处理数据他丢过来一个 200MB 的 xlsx说“就用 C# 读一下把合格率算出来”。我随手用 Interop 打开机器直接卡死内存飙到 2G 还没出结果。后来换成流式读取三分钟跑完。这件事让我意识到Excel 操作在 C# 里从来不是“能读就行”选错方案后面全是坑。这套 C# Excel 读取、保存与操作实例核心就是解决三件事把 Excel 当数据源高效读进来、把内存里的表结构稳定写出去、在读写之间做格式与公式的处理。它适合两类人一类是刚接触 C# 但需要处理报表、台账、导入导出的开发者另一类是用过 Interop 被 COM 释放和性能折磨过想找一套能直接抄作业的替代方案的人。下面我按“选型 → 读取 → 写入 → 避坑 → 进阶”的顺序把这份实例拆开讲。2. 选型先定生死Interop、OleDb、OpenXML 与第三方库怎么选2.1 四种主流方案的能力边界在 C# 里操作 Excel绕不开四条路。第一条是 Microsoft.Office.Interop.Excel调用本机安装的 Excel 进程功能最全公式、图表、透视表都能碰但代价是必须装 Office、COM 对象释放不干净就残留进程、大批量读写慢到怀疑人生。第二条是 OleDb把 Excel 当数据库查SELECT * FROM [Sheet1$]这种写法很直观适合只读、结构规整的表但它对 xlsx 的支持依赖 ACE 驱动驱动版本和位数对不上就直接报“未注册类”。第三条是 OpenXML SDK直接操作 xlsx 的 ZIP 包和 XML 部件不依赖 Office性能好缺点是 API 偏底层单元格引用、共享字符串表这些概念得先搞明白。第四条是第三方封装库比如 EPPlus、NPOI、ClosedXML在 OpenXML 之上做了对象化封装写起来接近 Interop 的手感又不用装 Office。选型没有绝对优劣看场景。我一般这样分需要保留复杂格式、图表、宏且机器确定装了 Office用 Interop只读、表头固定、数据量中等用 OleDb 最快出活服务端、无 Office 环境、数据量大用 OpenXML 或 NPOI想兼顾开发效率和性能用 EPPlus 或 ClosedXML。这份实例里主要覆盖的是后两条路线因为生产环境里“服务器不装 Office”几乎是默认前提。2.2 用 NuGet 把依赖装对不管选哪条路第一步都是把包引对。以 NPOI 和 EPPlus 为例在项目根目录执行# 添加 NPOI支持 xls 和 xlsx免费开源 dotnet add package NPOI # 添加 EPPlusAPI 更现代注意 5.x 之后是商业授权 dotnet add package EPPlus装完检查.csproj里的PackageReference确认版本号没有冲突。这里有个血泪经验NPOI 和 EPPlus 都依赖System.Drawing.Common的某些版本在 Linux 容器里跑会报 GDI 相关异常解决办法是显式引用System.Drawing.Common并配置EnableUnixSupport或者干脆换用不依赖绘图的读取路径。参数上NPOI 的XSSFWorkbook对应 xlsxHSSFWorkbook对应 xls别混用混用会直接抛类型转换异常。提示如果项目同时引了 Interop 和 NPOI编译时可能因为Application类名冲突报错用完全限定名或别名解决。3. 读取实战从十万行 xlsx 里把数据捞干净3.1 用 NPOI 流式读取大文件先看一段能直接跑的读取代码场景是读一个带表头的 xlsx把每行转成字典using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; public static ListDictionarystring, string ReadExcel(string filePath, string sheetName) { var result new ListDictionarystring, string(); // 用 FileStream 打开避免一次性把整个文件读进内存 using var fs new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); IWorkbook workbook new XSSFWorkbook(fs); ISheet sheet workbook.GetSheet(sheetName) ?? workbook.GetSheetAt(0); // 第一行当表头 IRow headerRow sheet.GetRow(0); if (headerRow null) return result; var headers new Liststring(); for (int c 0; c headerRow.LastCellNum; c) { headers.Add(headerRow.GetCell(c)?.ToString()?.Trim() ?? $Col{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) { ICell cell row.GetCell(c); dict[headers[c]] GetCellValue(cell); } result.Add(dict); } return result; } // 统一处理单元格类型避免数字变成科学计数法 private static string GetCellValue(ICell cell) { if (cell null) return string.Empty; return cell.CellType switch { CellType.Numeric DateUtil.IsCellDateFormatted(cell) ? cell.DateCellValue.ToString(yyyy-MM-dd HH:mm:ss) : cell.NumericCellValue.ToString(G), CellType.String cell.StringCellValue.Trim(), CellType.Boolean cell.BooleanCellValue.ToString(), CellType.Formula cell.CachedFormulaResultType switch { CellType.Numeric cell.NumericCellValue.ToString(G), CellType.String cell.StringCellValue, _ string.Empty }, _ string.Empty }; }逻辑上分三步打开流、定位 sheet、逐行逐列取值。参数说明几个关键点。FileShare.ReadWrite允许在文件被其他进程占用时仍能读适合日志类场景。LastRowNum是最后一行的索引注意它可能包含空行所以循环里要判row null。GetCellValue里对Numeric类型做了日期判断因为 Excel 里日期本质是数字不判断就会输出45123这种序列号。公式单元格读的是CachedFormulaResultType也就是上次 Excel 计算后缓存的值如果文件从没被 Excel 打开过缓存可能为空这时候要么用公式引擎重算要么接受空值。3.2 用 OleDb 快速查表与它的连接串陷阱如果只是只读、表头规整OleDb 写起来更短using System.Data.OleDb; string connStr ProviderMicrosoft.ACE.OLEDB.12.0;Data Sourcereport.xlsx; Extended PropertiesExcel 12.0 Xml;HDRYES;IMEX1;; using var conn new OleDbConnection(connStr); conn.Open(); using var cmd new OleDbCommand(SELECT 姓名, 部门, 金额 FROM [Sheet1$], conn); using var reader cmd.ExecuteReader(); while (reader.Read()) { Console.WriteLine(${reader[姓名]} | {reader[部门]} | {reader[金额]}); }连接串里三个参数决定成败。HDRYES表示第一行是列名设成 NO 就会用 F1、F2 代替。IMEX1是混合模式让列里既有数字又有文本时按文本读不加这个参数遇到“001”这种编号会被吞掉前导零。Excel 12.0 Xml对应 xlsxxls 要用Excel 8.0。最常见的翻车是报“Microsoft.ACE.OLEDB.12.0 未注册”原因是没装 Access Database Engine或者装了 64 位驱动但程序跑在 32 位模式位数必须一致。注意OleDb 读 xlsx 时如果某列前 8 行都是数字、第 9 行突然出现文本驱动会按数字类型推断导致文本行读出来是空。解决办法是IMEX1配合注册表TypeGuessRows0或者干脆换 NPOI。4. 写入与保存把内存表稳定落成 xlsx4.1 用 EPPlus 生成带格式的报表写入比读取更容易出细节问题比如列宽、日期格式、公式。下面这段用 EPPlus 生成一个带表头样式和合计行的报表using OfficeOpenXml; using OfficeOpenXml.Style; // EPPlus 5.x 之后需要设置许可证上下文 ExcelPackage.LicenseContext LicenseContext.NonCommercial; public static void WriteReport(string path, ListDictionarystring, object rows) { using var package new ExcelPackage(); var ws package.Workbook.Worksheets.Add(汇总); // 写表头 string[] headers { 姓名, 部门, 金额, 日期 }; for (int i 0; i headers.Length; i) { ws.Cells[1, i 1].Value headers[i]; ws.Cells[1, i 1].Style.Font.Bold true; ws.Cells[1, i 1].Style.Fill.PatternType ExcelFillStyle.Solid; ws.Cells[1, i 1].Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray); } // 写数据行 int row 2; foreach (var item in rows) { ws.Cells[row, 1].Value item[姓名]; ws.Cells[row, 2].Value item[部门]; ws.Cells[row, 3].Value Convert.ToDecimal(item[金额]); ws.Cells[row, 4].Value Convert.ToDateTime(item[日期]); ws.Cells[row, 4].Style.Numberformat.Format yyyy-mm-dd; row; } // 合计行用公式而不是算好再写死 ws.Cells[row, 2].Value 合计; ws.Cells[row, 3].Formula $SUM(C2:C{row - 1}); ws.Cells[row, 3].Style.Font.Bold true; // 自动列宽避免手动调 ws.Cells[ws.Dimension.Address].AutoFitColumns(); package.SaveAs(new FileInfo(path)); }关键参数逐个说。LicenseContext.NonCommercial是 EPPlus 5 之后的强制设置不设会抛异常商业项目要买 license。Numberformat.Format设成yyyy-mm-dd后单元格里存的是日期序列值显示才是日期别用字符串拼日期否则排序和公式都会错。合计行用Formula而不是先算好再写好处是用户改数据后合计自动更新代价是文件必须被 Excel 打开过才有缓存值程序再读时可能读到空所以如果后续还要程序读建议同时写一个计算好的值到旁边列。4.2 保存时的文件占用与覆盖问题保存最常见的报错是“文件正被另一进程使用”。原因通常是上一次打开没释放或者用FileStream打开后没关。用 EPPlus 的SaveAs(FileInfo)时如果目标文件已存在且被 Excel 打开会直接抛IOException。稳妥做法是先写临时文件再替换string tempPath path .tmp; package.SaveAs(new FileInfo(tempPath)); if (File.Exists(path)) File.Delete(path); File.Move(tempPath, path);这样即使保存中途失败原文件也不会被破坏。另外如果目标目录没有写权限SaveAs会抛UnauthorizedAccessException部署到 IIS 或容器里要提前确认目录权限别等上线才发现。5. 避坑与排查那些让我加班到凌晨的 Excel 异常5.1 现象读出来的数字变成科学计数法或丢精度原因Excel 单元格存的是 double超过 15 位有效数字会丢精度长数字如身份证号、订单号被当成数字读。解决读取时统一按字符串处理或者建表时就设成文本格式。NPOI 里用cell.ToString()而不是NumericCellValueOleDb 里加IMEX1。5.2 现象Interop 跑完 Excel 进程残留越跑越卡原因Application、Workbook、Worksheet这些 COM 对象没显式释放GC 回收不及时。解决每层对象用完就Marshal.ReleaseComObject并且把Application.Quit()放在 finally 里。更彻底的办法是别用 Interop换 OpenXML 系。5.3 现象NPOI 读 xls 报“Invalid header signature”原因文件其实是 xlsx但用了HSSFWorkbook去读或者文件被加密过。解决先读文件头判断格式xlsx 是PK开头ZIPxls 是D0 CF 11 E0开头。加密文件需要先解密或用支持加密的库。5.4 现象EPPlus 保存后公式单元格读出来是空原因公式没有缓存值程序读的是CachedFormulaResultType。解决保存前用package.Workbook.Calculate()触发一次计算或者写入时同时写值。5.5 现象Linux 容器里报 GDI 异常原因System.Drawing.Common在 .NET 6 之后默认不支持非 Windows。解决在.csproj里加RuntimeHostConfigurationOption IncludeSystem.Drawing.EnableUnixSupport Valuetrue /或者改用不依赖绘图的库。6. 进阶技巧用 DataTable 做中转与批量写入的取舍实际项目里Excel 数据往往要进数据库或者从数据库导出。我习惯用DataTable做中转因为 NPOI、EPPlus、OleDb 都能和它互转换库时业务代码不用大改。下面是把 NPOI 读出的结果转成DataTable再批量写库的片段public static DataTable ToDataTable(ListDictionarystring, string rows) { var dt new DataTable(); if (rows.Count 0) return dt; foreach (var key in rows[0].Keys) dt.Columns.Add(key, typeof(string)); foreach (var row in rows) { var dr dt.NewRow(); foreach (var key in row.Keys) dr[key] row[key]; dt.Rows.Add(dr); } return dt; }批量写库时用SqlBulkCopy比逐条 insert 快一个数量级但要注意列映射和类型转换字符串列写进数字列会整批失败。另一个技巧是分块读取大文件不要一次ToList()用yield return逐行吐内存占用能降一个量级。验证方法很简单拿一个 50MB 以上的文件跑一遍看内存曲线是不是平的如果一直涨说明某处还在攒列表。从那以后我每次接 Excel 相关的活都强制先问三个问题文件多大、跑在什么环境、要不要保留格式。这三个答案定了选型基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑