简介:这份资源面向需要在.NET环境中处理Excel的C#开发者,尤其是从事数据导入导出、报表生成等任务的程序员。内容围绕EPPlus这一开源库展开,覆盖XLSX格式文件的写入、读取与修改三大核心操作,并延伸至样式设置、公式计算、图表创建等进阶用法,适合具备一定C#基础、希望提升办公自动化效率的中级开发者参考实践。压缩包共41个文件,约2.55MB,以dll动态库、cs源码、xml与config配置、resx资源文件及xlsx示例表格为主,另含sln解决方案与exe可执行程序,构成一套可直接运行的完整工程。目前已有3725人学习下载,说明其在同类教程中具备一定认可度。通过该实例,读者可掌握ExcelPackage与FileStream的配合方式、工作表与单元格的读写技巧、批量数据加载及保存流程,并了解资源管理与内存泄漏的规避思路,便于快速迁移到实际项目中。
1. 报表导出又崩了:为什么我最终把 Excel 操作从 Interop 换成了 EPPlus
做过企业后台的同行大概都遇到过这种场景:运营要一份带格式的月度对账表,财务要能直接在上面改数字再回传,导出量一上来,服务器上还装着 Office 的机器就开始抽风,进程残留、句柄泄漏、偶发 COM 异常,排查起来像开盲盒。这类需求的核心其实就三件事——写入、读取、修改 Excel,而 C# 里能干净利落把这三件事做完的库,EPPlus 是我这些年用得最顺手的之一。
它不依赖本机安装 Office,纯托管代码跑在 .NET 里,服务端、桌面端、批处理任务都能用。这篇笔记就围绕「c#利用EPPlus写入、读取、修改excel实例」这个主题,把选型理由、最小可跑代码、参数怎么调、坑在哪,一层层拆开。适合正在做报表导出、数据导入、模板回填的 .NET 开发者,新手能照着复现,熟手能对一下边界。
2. 先把 EPPlus 跑起来:环境、授权与最小写入实例
2.1 为什么是 EPPlus,而不是 Interop 或别的库
选型这件事,先看约束再看功能。Interop 的问题在于它本质是去驱动本机 Excel 进程,服务端部署要装 Office,并发一高就容易出「服务器意外关闭连接」这类玄学问题,而且资源释放稍有不慎就留下僵尸进程。NPOI 是另一个常见选择,功能也全,但 API 偏底层,写个带样式的表要堆不少代码。EPPlus 的定位刚好卡在中间:API 直观、样式和公式支持完整、不依赖 Office,对「写入 + 读取 + 修改」这种典型报表场景覆盖得很到位。
代价是授权。EPPlus 从 5.0 起改成了 Polyform Noncommercial 许可,商业闭源项目需要购买商业授权,个人学习、开源、非商业用途可以免费。这一点必须先确认清楚,别代码写完上线了才发现合规问题。选型时我一般会问三个问题:部署环境有没有 Office、并发量多大、是否需要读写复杂样式。前两个决定要不要用 Interop,第三个决定用 EPPlus 还是 NPOI。
2.2 装包与最小写入:五行代码落一个文件
先通过 NuGet 引入。命令行方式:
# 引入 EPPlus 包,版本按项目实际需要锁定 dotnet add package EPPlus最小写入实例,把一张两列的表写进新文件:
using OfficeOpenXml; // EPPlus 命名空间 using System.IO; // 5.0 之后需要显式声明许可上下文,非商业用途用 NonCommercial ExcelPackage.LicenseContext = LicenseContext.NonCommercial; // 用 using 保证 package 被正确释放,这是避免文件被占用的关键 using (var package = new ExcelPackage()) { // 新增一个工作表,名字会显示在底部标签上 var sheet = package.Workbook.Worksheets.Add("对账明细"); // 表头:Cells[行, 列],行列都从 1 开始,不是 0 sheet.Cells[1, 1].Value = "订单号"; sheet.Cells[1, 2].Value = "金额"; // 数据行 sheet.Cells[2, 1].Value = "A1001"; sheet.Cells[2, 2].Value = 1280.50m; // 保存到指定路径,文件不存在会新建,存在会覆盖 package.SaveAs(new FileInfo(@"D:\reports\demo.xlsx")); }逻辑说明:ExcelPackage是整个工作簿的内存模型,Worksheets.Add建表,Cells[行,列]定位单元格,SaveAs落盘。参数上要注意两点,一是LicenseContext必须在创建 package 之前设置,否则会抛异常;二是SaveAs接收的是FileInfo,路径目录必须已存在,EPPlus 不会帮你建目录,目录不存在会直接报错。
2.3 批量写入与样式:别一行一行赋值
真实报表动辄几千行,逐格赋值性能很差。正确姿势是用LoadFromCollection或LoadFromArrays一次性灌数据,再统一设样式:
// 假设有一个订单列表 var orders = new[] { new { OrderNo = "A1001", Amount = 1280.50m, Date = new DateTime(2024, 5, 1) }, new { OrderNo = "A1002", Amount = 860.00m, Date = new DateTime(2024, 5, 2) } }; var sheet = package.Workbook.Worksheets.Add("批量"); // 从 A1 开始写入,true 表示首行作为表头 sheet.Cells["A1"].LoadFromCollection(orders, true); // 表头加粗、加背景色 using (var header = sheet.Cells["A1:C1"]) { header.Style.Font.Bold = true; header.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid; header.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray); } // 金额列格式化为两位小数 sheet.Column(2).Style.Numberformat.Format = "#,##0.00"; // 按内容自动撑开列宽 sheet.Cells[sheet.Dimension.Address].AutoFitColumns();LoadFromCollection的第二个参数控制是否输出表头,匿名类型的属性名会直接变成列名。AutoFitColumns在大数据量下会明显变慢,几万行以上建议手动设Column(i).Width。Numberformat.Format用的是 Excel 自己的格式串,和 C# 的格式化语法不是一回事,别混用。
3. 读取与修改:把已有 Excel 当数据源和模板用
3.1 读取已有文件:定位、遍历与类型转换的坑
读取比写入更容易翻车,因为文件是别人给的,格式不可控。基本读法:
var file = new FileInfo(@"D:\reports\demo.xlsx"); using (var package = new ExcelPackage(file)) { // 按名字取表,名字错了会返回 null,必须判空 var sheet = package.Workbook.Worksheets["对账明细"]; if (sheet?.Dimension == null) return; // 空表直接退出 int rows = sheet.Dimension.End.Row; int cols = sheet.Dimension.End.Column; // 从第 2 行开始读,跳过表头 for (int r = 2; r <= rows; r++) { // GetValue<T> 泛型读取,类型不匹配会抛异常 string orderNo = sheet.Cells[r, 1].GetValue<string>(); decimal amount = sheet.Cells[r, 2].GetValue<decimal>(); // 处理业务…… } }Dimension表示已使用区域,空表时为 null,这是最常见的空引用来源。GetValue<T>在类型对不上时会抛InvalidCastException,比如单元格里是文本「待定」你却按 decimal 读。稳妥做法是先取sheet.Cells[r, c].Value(返回 object),再自己判断转换,或者用GetValue<object>()配合TryParse。
3.2 修改已有文件:模板回填与保留原格式
「修改」的典型场景是拿一份带格式的模板,往指定格子里填数,再另存。关键是打开模板、改值、另存为新文件,不要在原文件上直接覆盖,避免模板被污染:
var template = new FileInfo(@"D:\templates\monthly.xlsx"); var output = new FileInfo(@"D:\reports\2024-05.xlsx"); using (var package = new ExcelPackage(template)) { var sheet = package.Workbook.Worksheets[0]; // 回填指定单元格,原有样式、合并、边框都会保留 sheet.Cells["B3"].Value = DateTime.Now.ToString("yyyy-MM"); sheet.Cells["C5"].Value = 1280.50m; // 在已有数据末尾追加一行 int nextRow = (sheet.Dimension?.End.Row ?? 1) + 1; sheet.Cells[nextRow, 1].Value = "A1003"; sheet.Cells[nextRow, 2].Value = 500m; // 另存为新文件,模板本身不动 package.SaveAs(output); }用new ExcelPackage(template)打开时,EPPlus 会把整个工作簿读进内存,你改的是内存副本,SaveAs到新路径不会影响原模板。如果确实要原地保存,用package.Save(),但要注意文件被其他进程占用时会抛IOException,这是「文件正在被使用」报错的头号原因。
3.3 公式、合并单元格与日期:三个高频细节
公式直接赋字符串即可,EPPlus 会识别并计算:
sheet.Cells["D2"].Formula = "=B2*C2"; // 写入公式 package.Workbook.Calculate(); // 触发重算,否则读值时可能拿到旧结果合并单元格用Cells["A1:C1"].Merge = true,合并后只有左上角那个格能写值,往其他格写会被忽略,这是很多人「写了没反应」的原因。日期要特别注意:Excel 内部用序列号存日期,EPPlus 读出来可能是 double,建议写入时直接赋DateTime对象,读取时用GetValue<DateTime>(),并给单元格设Numberformat.Format = "yyyy-mm-dd"保证显示正确。
4. 避坑与排查:EPPlus 实战里最容易翻车的五件事
4.1 报「文件正在被使用」或保存后文件损坏
现象:SaveAs抛IOException,或者保存出来的文件用 Excel 打开提示需要修复。原因通常是 package 没释放,或者多个线程同时操作同一个文件。解决:所有ExcelPackage一律用using包住,确保Dispose被调用;并发场景下每个线程用独立的 package 和独立文件,不要共享实例。EPPlus 的ExcelPackage不是线程安全的,这点没有后悔药,只能从设计上避开。
4.2 读到的数字变成科学计数法或精度丢失
现象:长订单号读出来变成1.23E+15,或者金额小数位对不上。原因:单元格被 Excel 当成数值类型,超过 15 位有效数字就丢精度。解决:订单号这类「看起来是数字其实是文本」的字段,写入时显式设成文本格式sheet.Cells[r,c].Style.Numberformat.Format = "@",或者写入时前面加单引号语义(EPPlus 里直接赋 string 并设文本格式即可)。金额用decimal而不是double。
4.3 大数据量导出内存暴涨甚至 OOM
现象:导出十万行时进程内存飙到几个 G。原因:EPPlus 把整个工作簿放在内存里,行数越多占用越大。解决:控制单次导出行数,超过阈值就分文件;关闭不必要的AutoFitColumns;及时Dispose。如果确实要处理超大表,考虑流式方案或换更适合大数据的库,EPPlus 的强项是中小规模带样式的报表,不是海量数据管道。
4.4 中文列名或表名读取失败
现象:Worksheets["对账明细"]返回 null。原因:表名里有空格、特殊字符,或者实际名字和你以为的不一样。解决:先用package.Workbook.Worksheets遍历打印所有表名确认,或者用索引Worksheets[0]取第一张表。表名匹配是精确匹配,大小写和空格都敏感。
4.5 授权上下文没设导致启动即崩
现象:程序一创建ExcelPackage就抛LicenseException。原因:5.0 之后必须设置ExcelPackage.LicenseContext。解决:在程序入口或首次使用前设置一次即可,全局生效。商业项目记得确认授权类型,别用NonCommercial蒙混商业用途,合规风险自己扛。
5. 进阶技巧:用 EPPlus 做一份能直接交付的带样式报表
前面讲的都是零件,这一章把它们拼成一份能直接发给业务方的成品。我平时做月度报表的习惯是:模板固定、数据动态、样式统一、公式自算,最后加一层校验,确保导出的文件打开就能用,不用业务方再手动调格式。
先看一个完整的导出方法,把表头样式、斑马纹、冻结首行、条件格式一次配齐:
public void ExportReport(string path, List<OrderRow> data) { ExcelPackage.LicenseContext = LicenseContext.NonCommercial; using (var package = new ExcelPackage()) { var sheet = package.Workbook.Worksheets.Add("月报"); // 1. 写表头 sheet.Cells[1, 1].Value = "订单号"; sheet.Cells[1, 2].Value = "数量"; sheet.Cells[1, 3].Value = "单价"; sheet.Cells[1, 4].Value = "金额"; // 2. 写数据,金额列用公式而不是算好再写,方便业务方改数量后自动重算 for (int i = 0; i < data.Count; i++) { int r = i + 2; sheet.Cells[r, 1].Value = data[i].OrderNo; sheet.Cells[r, 2].Value = data[i].Qty; sheet.Cells[r, 3].Value = data[i].Price; sheet.Cells[r, 4].Formula = $"=B{r}*C{r}"; } int lastRow = data.Count + 1; // 3. 表头样式 using (var head = sheet.Cells[1, 1, 1, 4]) { head.Style.Font.Bold = true; head.Style.Font.Color.SetColor(System.Drawing.Color.White); head.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid; head.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.SteelBlue); head.Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Center; } // 4. 斑马纹:偶数行浅灰底 for (int r = 2; r <= lastRow; r += 2) { sheet.Cells[r, 1, r, 4].Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid; sheet.Cells[r, 1, r, 4].Style.Fill.BackgroundColor.SetColor( System.Drawing.Color.FromArgb(245, 245, 245)); } // 5. 金额列格式 + 合计行 sheet.Column(3).Style.Numberformat.Format = "#,##0.00"; sheet.Column(4).Style.Numberformat.Format = "#,##0.00"; sheet.Cells[lastRow + 1, 3].Value = "合计"; sheet.Cells[lastRow + 1, 4].Formula = $"=SUM(D2:D{lastRow})"; // 6. 冻结首行,滚动时表头不动 sheet.View.FreezePanes(2, 1); // 7. 自动筛选 sheet.Cells[1, 1, lastRow, 4].AutoFilter = true; // 8. 触发公式计算,保证打开文件时合计已算好 package.Workbook.Calculate(); package.SaveAs(new FileInfo(path)); } }这段代码里有几个值得单独说的点。金额列用公式=B{r}*C{r}而不是在 C# 里算好再写,好处是业务方改了数量或单价,金额会自动重算,报表从「死数据」变成「活表格」。FreezePanes(2, 1)表示冻结第 1 行、第 1 列左侧不动,参数是「冻结到哪个单元格的左上角」。AutoFilter让业务方能自己筛选排序,交付体验直接上一个档次。Workbook.Calculate()必须在保存前调用,否则某些阅读器打开时公式列是空的。
再补一个校验环节。导出后我一般会立刻用 EPPlus 把文件读回来,核对行数和合计,避免「导出成功但内容是空的」这种低级事故:
using (var check = new ExcelPackage(new FileInfo(path))) { var s = check.Workbook.Worksheets[0]; int dataRows = (s.Dimension?.End.Row ?? 1) - 1; if (dataRows != data.Count) throw new Exception($"导出行数不符:期望 {data.Count},实际 {dataRows}"); }这个「写完读回来验一遍」的习惯,帮我拦下过好几次因为数据源为空、路径写错、权限不足导致的静默失败。EPPlus 本身不会告诉你「你导了个空表」,它只会老老实实保存一个只有表头的文件,校验得自己加。
最后说一个性能上的取舍。带样式的报表,样式操作本身比写数据还耗时,尤其是逐格设样式。我的做法是能批量就批量,Cells[起始行,起始列,结束行,结束列]一次性拿到区域再设样式,比循环里一格一格设快得多。几万行以内,这套写法在普通服务器上跑完全没问题;真到了几十万行,就该考虑分页导出或者换数据管道了,别硬刚 EPPlus 的内存模型。
这些年踩下来,我最大的教训是:Excel 操作的坑,八成不在库本身,而在文件的生命周期管理——谁打开、谁释放、谁在并发。把using写扎实,把授权设对,把类型转换判空,剩下的就是调样式和格式的体力活。希望帮到你。
本文还有配套的精品资源,点击获取