ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#用NPOI操作Excel全指南:读写样式、大数据量导出与性能优化

C#用NPOI操作Excel全指南:读写样式、大数据量导出与性能优化 简介面向C#开发者的NPOI操作Excel实例压缩包覆盖旧版.xls与新版.xlsx两类文件的创建、读取、写入、样式设置及保存等常见场景适合刚接触NPOI或需要在.NET项目中快速集成Excel导入导出的初中级开发者。压缩包共15个文件以DLL动态库、C#源码文件和XML说明为主整体大小约2.19MB目录按2012Version与201607Version分版整理并分别提供NPOIExcelHelper.cs与Helper1.cs工具类便于对照不同NPOI版本选择使用。资源已吸引6347人学习下载内容包括NPOI核心程序集、OpenXml4Net、SharpZipLib等依赖项以及封装好的Excel操作辅助类可帮助读者减少环境配置成本直接理解HSSFWorkbook与XSSFWorkbook的差异并快速把读写逻辑复用到实际项目中。 C#项目里折腾Excel读写绕不开NPOI这个库。它是开源的专门解决.NET环境下操作Excel的问题最实在的一点是完全不用装Office服务器上没装办公软件也照样能跑。我最早接触NPOI是因为要做上位机数据报表现场工控机不可能装OfficeCom组件也被系统服务调用搞得头疼换成NPOI后这些问题统统消失。这篇文章我把常用场景和踩坑经验整理出来涉及.xls和.xlsx两种格式的读写、样式设置、大数据量写入附带模板方法与问题排查适合正在做桌面工具、上位机数据导出、批量导入功能的C#开发者参考。1. 项目核心思路与方案选型1.1 为什么选NPOI而不是其他方案先花点时间说清楚选型问题这个决定会影响后续所有开发体验。目前.NET生态下操作Excel的主流方案有几条路线微软官方提供的COM组件Microsoft.Office.Interop.Excel、开源库OpenXML SDK、商业库EPPlus以及本文要讲的NPOI。COM组件是最老牌的方案缺点也很明显——依赖本机安装Office服务器上没装就直接报错。而且COM对象在Web应用或Windows服务里频繁创建回收容易造成进程卡死。我在维护一个旧项目时被它坑过服务器上装的是精简版WPSExcel对象怎么都创建不成功。OpenXML SDK走的是纯XML解析路线不依赖环境但对象模型特别繁琐操作一个单元格要写一大堆代码对快速开发不友好。EPPlus功能强大但从4.5版本开始商用收费个人和小公司用起来有授权顾虑。NPOI的优势在于纯托管代码、无外部依赖、完全免费、兼容.NET Framework和.NET Core/.NET 5、同时支持老的.xls和新的.xlsx格式。底层实现借鉴了Java领域的POI项目稳定性和社区活跃度都不错。做上位机和工业软件的朋友特别适合这套方案因为部署环境往往很固定不适合引入太多依赖而NPOI就一个DLL的事。1.2 理清xls与xlsx背后的底层差异不少新手会在.xls和.xlsx格式之间栽跟头本质原因是它们底层存储机制完全不同。.xls是Excel 97-2003时代的二进制格式专业说法叫BIFFBinary Interchange File Format用字节流直接存储单元格数据和样式信息。.xlsx是Excel 2007之后采用的Office Open XML格式本质是一个ZIP压缩包内部包含多个XML文件分别存储工作表数据、样式、共享字符串等内容。这个差异直接决定了NPOI中的类选择处理.xls文件用HSSFWorkbook类Horrible SpreadSheet Format处理.xlsx文件用XSSFWorkbook类XML SpreadSheet Format。很多初学者把这两个类搞混拿HSSFWorkbook去读.xlsx文件程序直接抛异常。如果希望代码同时兼容两种格式可以写一个简单的判断逻辑根据文件名后缀或文件头魔数D0 CF 11 E0是OLE2格式50 4B 03 04是ZIP格式来决定实例化哪个类后面我会给出具体代码。这里还要提一个实际场景的注意点.xls格式单个工作表最多65536行.xlsx支持1048576行。如果你的数据量超过6万行建议直接生成.xlsx否则数据写入时会报“Row number must not be greater than 65535”之类的问题。我在做设备历史数据导出时刚开始没注意这个限制数据量一大就被坑了。2. 环境准备与基础读写操作2.1 NuGet安装与命名空间引入新建一个.NET Framework或.NET Core项目后在NuGet包管理器中搜索“NPOI”安装最新稳定版即可。我用的是2.6.x版本截至写这篇文章稳定版本在2.7.x左右安装命令如下Install-Package NPOI如果你的项目目标框架是.NET Framework 4.5需要注意版本兼容性2.5.x之后的版本基本都要求4.6.1或更高。还有一点值得留意NPOI依赖一个叫“NPOI.OOXML”的底层层NuGet会自动解析不需要单独处理。引入关键命名空间using NPOI.HSSF.UserModel; // 处理xls using NPOI.XSSF.UserModel; // 处理xlsx using NPOI.SS.UserModel; // 公共接口Workbook、Sheet、Row、Cell等 using NPOI.SS.Util; // 单元格范围工具类合并区域用 using NPOI.XSSF.Streaming; // SXSSFWorkbook大数据量导出用 using System.IO;2.2 最基础的两个操作创建文件与读取文件先看一个最简单的创建Excel并写入数据的过程。这里我推荐用接口类型IWorkbook、ISheet、IRow、ICell来编写业务代码而不是直接用HSSFWorkbook或XSSFWorkbook的具体类型。这样上层代码完全不关心最终生成的是xls还是xlsx切换格式只需要改一行实例化代码。IWorkbook workbook; // 根据需要的格式创建不同实例 if (isXlsx) { workbook new XSSFWorkbook(); } else { workbook new HSSFWorkbook(); } ISheet sheet workbook.CreateSheet(测试表); IRow row sheet.CreateRow(0); ICell cell row.CreateCell(0); cell.SetCellValue(你好NPOI); // 设置单元格值为数字 row.CreateCell(1).SetCellValue(3.14); // 保存到文件流 using (FileStream fs new FileStream(D:\test.xlsx, FileMode.Create, FileAccess.ReadWrite)) { workbook.Write(fs); }这个例子基本展示了写Excel的最小闭环创建工作簿、创建工作表、创建行、创建单元格、填值、保存。有几个细节需要提醒行和列的索引都是从0开始CreateRow(0)表示第一行CreateCell(0)表示第一列。单元格有两种值写入方式一种是SetCellValue传入string或doubleNPOI会自动识别类型另一种是直接给cell设置CellType两种结果有细微差别后面在类型处理部分详细讲。保存用FileStream没问题但注意Write完之后workbook对象不一定能重复写最好一次性完成。读取操作核心思路和写入对称先加载文件流再获取sheet、遍历行列IWorkbook workbook; using (FileStream fs new FileStream(D:\test.xlsx, FileMode.Open, FileAccess.Read)) { workbook new XSSFWorkbook(fs); } ISheet sheet workbook.GetSheetAt(0); // 按索引取第一个工作表 // 或者workbook.GetSheet(工作表名); for (int rowIdx 0; rowIdx sheet.LastRowNum; rowIdx) { IRow row sheet.GetRow(rowIdx); if (row null) continue; // 注意空行GetRow可能返回null for (int colIdx 0; colIdx row.LastCellNum; colIdx) { ICell cell row.GetCell(colIdx); if (cell null) continue; Console.Write(cell.ToString() \t); } Console.WriteLine(); }这段代码逻辑很简单但实际用的时候有几个隐藏陷阱我在后面专门用一节来展开。这里先提一个最重要的循环里不要写评论啊不是循环里务必判断row和cell是否为null尤其是从外部系统导出的Excel经常会有“看起来是空行但实际有格式”的脏数据不判断的话很容易抛NullReferenceException。3. 核心实操细节单元格类型、样式与公式处理3.1 单元格类型分类与正确取值方式Excel单元格可以存储字符串、数字、日期、布尔值、公式等多种类型。用NPOI读取时必须区分类型否则取出来的值可能是错的或者类型转换直接抛异常。NPOI中的CellType枚举定义了这些基本类型枚举值说明对应判断String字符串类型cell.CellType CellType.StringNumeric数字或日期类型cell.CellType CellType.NumericBoolean布尔值cell.CellType CellType.BooleanFormula公式类型cell.CellType CellType.FormulaBlank空白cell.CellType CellType.BlankError错误值cell.CellType CellType.Error读取单元格的通用方法我认为最稳定的写法是这样private static object GetCellValue(ICell cell) { if (cell null) return null; switch (cell.CellType) { case CellType.String: return cell.StringCellValue; case CellType.Numeric: if (DateUtil.IsCellDateFormatted(cell)) // 判断是否为日期格式 { return cell.DateCellValue; // 返回DateTime类型 } else { return cell.NumericCellValue; // 返回double类型 } case CellType.Boolean: return cell.BooleanCellValue; case CellType.Formula: return GetFormulaCellValue(cell); case CellType.Blank: return string.Empty; case CellType.Error: return cell.ErrorCellValue; default: return cell.ToString(); } }重点说说Numeric分支。Excel底层存储日期其实就是一个序列数比如“2024-01-25”在内部就是数字“45222”这样的值。NPOI读取日期单元格时会根据单元格的数字格式DataFormat判断它是不是日期。直接用cell.ToString()获取日期单元格的值得到的是序列数比如45300这种数字而不是日期字符串。解决方法是先调用DateUtil.IsCellDateFormatted(cell)判断是日期就取DateCellValue得到DateTime对象之后怎么格式化就是.NET框架里的事。3.2 关于公式单元格的两套处理方案读取公式单元格是另一个高频问题。当我用NPOI读取一个带SUM函数、IF函数之类公式的单元格时默认拿到的其实是公式本身字符串。比如我读到的可能是“SUM(A1:A10)”而不是计算结果“150”。如果业务只需要结果这里有两条路可以走。第一种是使用NPOI的FormulaEvaluator让NPOI自己计算一次公式IFormulaEvaluator evaluator workbook.GetCreationHelper().CreateFormulaEvaluator(); ICell cell row.GetCell(0); CellValue evaluatedValue evaluator.Evaluate(cell); double result evaluatedValue.NumericValue; // 或根据预期类型取 StringValue、BooleanValue这种方式不依赖Excel环境NPOI自带计算引擎。缺点是如果文件里的公式引用了其他文件的外部链接可能计算不出来。第二种是直接用cell.NumericCellValue或者cell.StringCellValue但必须在打开工作簿时就设置公式求值模式NPOI中可以用// 方式一加载工作簿后立即设置 XSSFWorkbook xssfWorkbook new XSSFWorkbook(fs); xssfWorkbook.SetForceFormulaRecalculation(true); // 强制重新算公式不过实测下来这个设置对某些版本的效果不稳定我一般直接用FormulaEvaluator逻辑明确、结果可控。3.3 样式设置字体、背景色、边框与合并单元格用NPOI生成带样式的报表很多新手觉得代码量大其实核心套路就几个对象。先看一个完整的例子然后逐步解释ICellStyle style workbook.CreateCellStyle(); // 设置背景色 style.FillForegroundColor IndexedColors.LightBlue.Index; style.FillPattern FillPattern.SolidForeground; // 设置边框 style.BorderTop BorderStyle.Thin; style.BorderBottom BorderStyle.Thin; style.BorderLeft BorderStyle.Thin; style.BorderRight BorderStyle.Thin; // 设置字体 IFont font workbook.CreateFont(); font.FontName 微软雅黑; font.FontHeightInPoints 11; font.IsBold true; style.SetFont(font); // 居中 style.Alignment HorizontalAlignment.Center; style.VerticalAlignment VerticalAlignment.Center; // 对单元格应用样式 cell.CellStyle style;有几个易错点值得单独说明。创建样式必须通过workbook.CreateCellStyle()不能直接new ICellStyle因为样式要挂在workbook内部样式表上每个workbook有独立的样式管理机制。字体也必须通过workbook.CreateFont()创建不能表白地复用其他workbook的字体。另外如果循环给几千个单元格设置相同样式不要每次循环都CreateCellStyle这样会在workbook里产生海量重复样式文件体积增大性能也受影响。正确做法是把style创建到循环外面循环里直接给cell.CellStyle赋值同一个样式对象。合并单元格也是报表常用的操作NPOI用CellRangeAddress类表示合并区域配合Sheet的AddMergedRegion方法使用// 合并第一行第一列到第一行第三列即A1:C1 sheet.AddMergedRegion(new CellRangeAddress(0, 0, 0, 2)); // 合并多行多列区域 sheet.AddMergedRegion(new CellRangeAddress(2, 4, 1, 1));这里有个细节合并区域之后只要左上角的单元格有值其他区域的值会被丢弃。而且把整个区域的值都写满再合并并不会保留所有值只保留区域左上角的值。所以推荐先合并再给左上角单元格赋值。3.4 从DataTable快速导入导出——通用模板方法实际项目中把DataTable导成Excel是非常高频的需求比如把数据库查询结果导出为报表。这里贴一个我项目里常用的模板方法支持xls/xlsx自动转换public static byte[] ExportDataTableToExcel(DataTable dt, string sheetName Sheet1, bool isXlsx true) { IWorkbook workbook; if (isXlsx) { workbook new XSSFWorkbook(); } else { workbook new HSSFWorkbook(); } ISheet sheet workbook.CreateSheet(sheetName); // 写入列头 IRow headerRow sheet.CreateRow(0); for (int i 0; i dt.Columns.Count; i) { ICell cell headerRow.CreateCell(i); cell.SetCellValue(dt.Columns[i].ColumnName); // 给列头设置样式 ICellStyle headerStyle workbook.CreateCellStyle(); IFont font workbook.CreateFont(); font.IsBold true; headerStyle.SetFont(font); headerStyle.Alignment HorizontalAlignment.Center; headerStyle.FillForegroundColor IndexedColors.LightGray.Index; headerStyle.FillPattern FillPattern.SolidForeground; cell.CellStyle headerStyle; } // 写入数据行 for (int r 0; r dt.Rows.Count; r) { IRow row sheet.CreateRow(r 1); for (int c 0; c dt.Columns.Count; c) { object val dt.Rows[r][c]; ICell cell row.CreateCell(c); if (val null || val DBNull.Value) { cell.SetCellValue(string.Empty); } else if (val is int || val is long || val is double || val is decimal) { cell.SetCellValue(Convert.ToDouble(val)); } else if (val is DateTime) { cell.SetCellValue((DateTime)val); // 可选设置日期格式 } else { cell.SetCellValue(val.ToString()); } } } // 自动调整列宽 for (int c 0; c dt.Columns.Count; c) { sheet.AutoSizeColumn(c); } using (MemoryStream ms new MemoryStream()) { workbook.Write(ms); return ms.ToArray(); } }调用时直接byte[] data ExportDataTableToExcel(dt); File.WriteAllBytes(D:\report.xlsx, data);即可。有几个实用小技巧DataTable里如果是布尔值直接ToString会得到“True/False”如果业务需要“是/否”在else分支里单独判一下类型再处理。列头样式如果在循环里重复创建不好可以用一个全局的headerStyle对象在循环外创建好再复用上面的代码为了演示便于理解放在里面实际量产建议提出来。4. 大数据量写入与性能优化4.1 为什么XSSFWorkbook写大数据会卡死内存很多开发说“我用NPOI导5万行数据内存直接飙到2GB”这个现象在XSSFWorkbook中很典型。原因是XSSFWorkbook把整个工作簿数据都放在内存对象模型里每写一个单元格就生成一个XSSFCell对象5万行×20列就是100万个单元格对象每个对象还有丰富属性GC压力极大。NPOI参考POI的方案提供了SXSSFWorkbook类Streaming Usermodel API它的名字叫Streaming核心设计思路是滑动窗口缓存。字面上的意思是数据写到一定行数后就刷新到磁盘临时文件内存中只保留最近N行从而用极小的内存写出超大Excel文件。我个人的经验是要导超过1万行的数据就直接用SXSSFWorkbook别犹豫。下面是具体用法using NPOI.XSSF.Streaming; // 第二个参数是窗口大小表示内存中保留的行数 SXSSFWorkbook workbook new SXSSFWorkbook(100); ISheet sheet workbook.CreateSheet(大数据导出); for (int r 0; r 200000; r) { IRow row sheet.CreateRow(r); for (int c 0; c 30; c) { row.CreateCell(c).SetCellValue(测试数据_ r _ c); } // 每1000行主动刷新一次到磁盘 if (r % 1000 0) { // SXSSF没有公开的flush方法可通过WindowSize机制自动滑动 // 或调用 ((SXSSFSheet)sheet).FlushRows(); } }注意窗口大小100表示内存中只保留最后100行。这个值不是越大越好也不是越小越好。窗口太小频繁写临时文件导致IO频繁太大内存释放效果不明显。一般100-500之间是比较合理的区间具体取决于数据行数和服务器配置。4.2 大数据场景下的几个性能习惯除了换SXSSFWorkbook性能优化还有几个很实用的习惯不要用AutoSizeColumn。sheet.AutoSizeColumn()会遍历整列所有单元格来计算最佳列宽在大数据量下这个操作等于双重遍历极其耗时。我的做法是手动估算列宽根据表头字符串长度乘以一个系数用sheet.SetColumnWidth(c, width)设置固定值。公式尽量少用。上千行的SUM或IF公式会让Excel打开时计算量暴增生成时NPOI也要处理公式解析。如果只是导出数据把计算在C#里做完直接把数值写进单元格。循环体里避免new样式。这个前面强调过大数据量下问题更明显几万行如果每行都创建一个样式对象不仅仅内存爆最终生成的Excel文件也会很大因为样式表里记录了每一行的每一个重复样式。写完务必调用workbook.Dispose()或释放临时文件。SXSSFWorkbook在写入过程中会在系统临时目录生成文件如果不释放会残留大量临时文件占磁盘。正确做法try { // 写入逻辑 } finally { workbook.Dispose(); }SXSSFWorkbook实现了IDisposableDispose方法会清理临时文件。用完之后记得调用。5. 常见问题与排查技巧实录5.1 读取与写入的典型疑难杂症我把项目中实际遇到的坑整理成了一张速查表方便按图索骥排查问题现象根本原因解决方案用HSSFWorkbook读取.xlsx文件抛异常类与格式不匹配前文提过的二进制格式与XML格式差异改写为根据文件头自动判断格式或统一使用XSSFWorkbook处理.xlsx日期单元格读出来是数字前面提到Excel内部日期存序列数NPOI默认没有识别为日期格式使用DateUtil.IsCellDateFormatted判断然后取DateCellValue读取带公式的单元格拿不到结果CellType为Formula时NPOI默认返回公式文本使用FormulaEvaluator或设置SetForceFormulaRecalculation空单元格直接被跳过循环中GetCell返回null直接访问属性抛异常判空后再处理合并单元格后取不到值合并区域只有左上角单元格有值其他格为null或Blank定位到合并的第一行第一列的cell去取值写入的字符串显示为“########”列宽太窄容纳不下内容手动SetColumnWidth不要依赖AutoSizeColumn数据超6万行写入.xls报错xls格式硬性限制为65536行改用.xlsx格式XSSFWorkbook/SXSSFWorkbook大文件导出内存暴涨XSSFWorkbook将所有数据驻留内存改用SXSSFWorkbook并设置合理窗口服务器上打开生成的Excel提示“文件已损坏”简单场景下可能是生成过程中文件流未正确关闭导致文件不完整确保using释放FileStream/MemoryStream写完再关闭Web项目下载文件时中文文件名乱码响应头的Content-Disposition未做URL编码使用UrlEncoded或等价方式处理文件名5.2 格式自动识别兼容方案与模板复用心得最后一个实操建议如果你的工具类需要同时支持xls和xlsx的读取我强烈建议用一个自动识别方法按文件头魔数判断避免要用户手动选择格式。文件的前4个字节就能区分public static IWorkbook CreateWorkbook(string filePath) { using (FileStream fs new FileStream(filePath, FileMode.Open, FileAccess.Read)) { return CreateWorkbook(fs); } } public static IWorkbook CreateWorkbook(Stream stream) { byte[] header new byte[8]; stream.Position 0; stream.Read(header, 0, 8); stream.Position 0; // xls文件的OLE2压缩头固定为 D0 CF 11 E0 A1 B1 1A E1 if (header[0] 0xD0 header[1] 0xCF header[2] 0x11 header[3] 0xE0) { return new HSSFWorkbook(stream); } else { return new XSSFWorkbook(stream); } }这样输入文件的后续处理逻辑完全不用关心格式都用IWorkbook接口操作非常省心。这个工具方法我放在自己的C#公共类库中互操作工具类、上位机报表功能全部复用一次写好所有项目都能用。再说说模板复用。在工厂报表、导出对账单这类场景中我推荐制作一个“Excel模板文件”提前设置好列宽、列头样式、数字格式等然后通过NPOI打开模板把数据填入指定区域而不是每次从零创建所有样式。这样生成的报表观感统一、代码量也更少。操作中只需要注意的一点是模板中不要包含复杂的公式或图表NPOI对图表支持程度有限可能会丢失或错乱。6. 我把这个项目落地后的整体感受几个项目做下来NPOI给我的整体印象是学习曲线中等底层能力扎实功能覆盖满足90%以上的Excel读写需求。真正的困难不在于API本身而在于对Excel文件格式的理解——无论是xls和xlsx的底层差异、日期序列值的转换规则还是FormulaEvaluator的求值机制把背后的逻辑吃透了使用起来自然顺手。如果手头正打算用C#处理Excel的任务我的建议是先从简单的一列一列读写开始再逐步叠加样式、公式、大数据量写入遇到问题先检查格式是否匹配、再检查类型是否正确剩下的交给经验积累。文中涉及的代码可以直接复制到项目中测试遇到报错欢迎对照“常见问题”表格排查。取数逻辑和性能优化的思路彼此独立可以按需使用。本文还有配套的精品资源点击获取
返回列表