NPOI v2.2.1实战:.NET中Excel导入导出完整指南 📅 发布时间:2026/9/10 0:23:06 👁 浏览次数: 简介面向.NETC#/VB.NET开发者的NPOI v2.2.1二进制发行包专注于在应用程序中读写Word与Excel文档可帮助解决Excel报表导出、Word文档生成、邮件合并等场景下的文件操作难题。压缩包共19个文件总大小约3.52MB其中10个DLL为核心类库覆盖HSSF/XSSF等Excel操作API及Word处理模块同时包含XML配置、TXT说明、LICENSE许可、3张JPG预览图与1张PNG图可辅助开发者直观了解包内文件构成并正确配置许可。已有2499人学习下载特别适合在WPF或后台服务中需要导出数据、填充模板的.NET工程师。程序集提供了Net20/Net40等多个目标框架版本附带Read Me与Release Notes说明便于快速核对运行时兼容性并减少装配错误是集成Office文档处理能力时直接可用的组件资源。 上周五下班前运营拿着一份 Excel 模板找到我说要导出一张带合并单元格、带表头样式、还要能直接发出去给客户看的月度对账表。在 .NET 项目里遇到这种需求我的第一反应永远是 NPOI。这些年处理 Office 文件NPOI v2.2.1 几乎是我所有项目的默认选项写这篇文章权当给自己做个备忘也把踩过的坑一次性说清楚。NPOI 是 Apache POI 的 .NET 移植版本核心价值一句话就能讲明白服务端不需要安装 Office纯托管代码就能读写 xls、xlsx、docx、pptx。对大多数业务系统来说Excel 的导入导出是最常见的需求v2.2.1 则是 NPOI 在 NuGet 上长期稳定存在的一个版本兼容 .NET Standard 2.0。这意味着 .NET Framework 4.6.1 以上的老项目能用.NET Core / .NET 5 的新项目也能直接引。如果你正在纠结选哪个 Excel 操作库或者已经被 EPPlus 的许可问题卡住这篇文章应该能给你一个比较完整的落地方案。1. 版本选型的现实逻辑为什么我盯上的是 v2.2.11.1 NPOI 在 .NET 生态里的位置先说一个事实NPOI 并不是 .NET 里唯一能操作 Excel 的库。EPPlus 功能覆盖很全但 4.5 版本之后采用了 Polyform Noncommercial 许可证商业项目要买授权。对很多内部管理系统来说这个成本立刻变成了一道需要法务和技术一起决策的坎。OpenXML SDK 是微软官方方案性能好、可控性强但它本质上是把 OpenXML 规范封装了一层写一张带样式的表要反复和设备 XML 部件打交道代码量明显偏大普通业务开发维护起来并不轻松。ClosedXML 是后起之秀API 设计更现代但我实际对比下来在处理超大数据量时它的内存表现和 NPOI 没有拉开决定性差距而且社区里积累的现成案例数量还是不如 NPOI 多。NPOI 真正打动我的有两点。第一是完全开源免费Apache 2.0 许可公司层面没有任何授权风险也不需要每次升级都重新过一遍许可证条款。第二是 API 直接继承了 Apache POI 的设计Java 生态里能搜到的大量 POI 示例稍微改改命名空间就能翻译成 C#。这一点在团队协作时特别值钱很多后端同学之前写过 Java看 NPOI 的代码几乎不需要适应期。1.2 与 EPPlus、OpenXML SDK、ClosedXML 的横向对比库许可证上手成本大文件表现适用场景NPOI v2.2.1Apache 2.0低API 贴近 POI常规够用超大用 SXSSF免费、长期维护的项目EPPlusPolyform Noncommercial低好能接受商业授权的项目OpenXML SDKMIT中高好需要深度控制 XML 结构的场景ClosedXMLMIT低中中小文件、追求现代 API这张表不是想说 NPOI 碾压其他库而是想说明选型要回到项目约束本身。如果公司不允许引入商业授权组件又不想花钱NPOI 几乎是唯一能同时满足“免费”“功能够全”“社区资料多”三个条件的选项。我这些年也试过在这几个库之间来回切最后还是回到 NPOI原因也很朴素它的问题我基本都踩过了知道怎么绕。1.3 v2.2.1 的兼容性边界回到版本本身。NPOI 的版本节奏不算快v2.2.1 发布后很多线上项目长期停留在这一版原因就一个字稳。它的 API 风格对应 Apache POI 3.x 时代命名空间稳定网上大量现成代码都能对上号。用 NPOI 之前要记住三条硬边界HSSFWorkbook 对应 xls 格式最大 65536 行、256 列XSSFWorkbook 对应 xlsx 格式最大 1048576 行、16384 列SXSSFWorkbook 是 XSSF 的流式版本用临时文件换内存空间适合超大数据量导出。我刚用 NPOI 时犯过一个低级错误用 XSSFWorkbook 生成内容却把文件后缀写成 .xls结果 Excel 一直提示格式和扩展名不匹配。后来我把导入导出逻辑统一封装成工具类根据扩展名自动选择对应的 Workbook 类型才彻底把这类问题挡在门外。2. 创建一份带样式的 xlsx写入链路的骨架代码2.1 写入前的第一道选择题HSSF 还是 XSSF写文件之前先想清楚一件事目标文件是老格式 xls 还是新格式 xlsx这决定了你用 HSSFWorkbook 还是 XSSFWorkbook。我的建议是能选 xlsx 就选 xlsx除非客户有非常老的 Office 兼容要求。xls 的 65536 行限制在报表场景里太容易踩雷而 xlsx 在 Excel 2007 以上都能正常打开没必要给自己留隐患。XSSFWorkbook 的使用方式也简单new 一个实例CreateSheet 创建工作表CreateRow 创建行CreateCell 创建单元格SetCellValue 填值最后 Write 到流。整个过程就是一层层往上建对象很像在内存里搭积木。2.2 从空工作簿到完整报表的代码骨架这里用一个最常见的“订单报表”场景做示例标题行加粗灰底数据行包含字符串、数值、日期三类单元格最后设置列宽、冻结首行输出为 byte[] 方便接口返回下载。using System.IO; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using NPOI.SS.Util; public byte[] BuildReport(ListOrderDto orders) { IWorkbook workbook new XSSFWorkbook(); ISheet sheet workbook.CreateSheet(订单报表); // 标题行样式 ICellStyle headerStyle workbook.CreateCellStyle(); headerStyle.FillForegroundColor IndexedColors.Grey25Percent.Index; headerStyle.FillPattern FillPattern.SolidForeground; IFont headerFont workbook.CreateFont(); headerFont.Boldweight (short)FontBoldWeight.Bold; headerStyle.SetFont(headerFont); headerStyle.BorderBottom BorderStyle.Thin; headerStyle.BorderTop BorderStyle.Thin; headerStyle.BorderLeft BorderStyle.Thin; headerStyle.BorderRight BorderStyle.Thin; headerStyle.VerticalAlignment VerticalAlignment.Center; IRow headerRow sheet.CreateRow(0); string[] titles { 订单号, 商品名称, 金额, 下单时间 }; for (int i 0; i titles.Length; i) { ICell cell headerRow.CreateCell(i); cell.SetCellValue(titles[i]); cell.CellStyle headerStyle; } // 日期样式 ICellStyle dateStyle workbook.CreateCellStyle(); IDataFormat dataFormat workbook.CreateDataFormat(); dateStyle.DataFormat dataFormat.GetFormat(yyyy-MM-dd HH:mm:ss); for (int i 0; i orders.Count; i) { IRow row sheet.CreateRow(i 1); row.CreateCell(0).SetCellValue(orders[i].OrderNo); row.CreateCell(1).SetCellValue(orders[i].ProductName); row.CreateCell(2).SetCellValue((double)orders[i].Amount); ICell dateCell row.CreateCell(3); dateCell.SetCellValue(orders[i].OrderTime); dateCell.CellStyle dateStyle; } sheet.SetColumnWidth(0, 20 * 256); sheet.SetColumnWidth(1, 30 * 256); sheet.SetColumnWidth(2, 15 * 256); sheet.SetColumnWidth(3, 22 * 256); sheet.CreateFreezePane(0, 1); using (MemoryStream ms new MemoryStream()) { workbook.Write(ms); workbook.Close(); return ms.ToArray(); } }这段代码建议直接保存它是最常见的模板。有几个点要解释workbook.CreateCellStyle() 创建的样式对象不要放到循环里反复创建SetCellValue(DateTime) 是重载方法内部会转成日期序列值但不给单元格设置 DataFormat 的话Excel 里显示出来就是一串数字。2.3 样式、日期、列宽与合并单元格报表“能看”的关键日期格式是 NPOI 初学者最容易懵的地方。日期在单元格内部就是 double显示成什么样完全由 CellStyle 的 DataFormat 决定。代码里用 dataFormat.GetFormat(yyyy-MM-dd HH:mm:ss) 拿到格式编号再赋给样式Excel 才会按预期显示成“2025-01-15 14:30:00”。如果你看到导出的日期是一串 40000 多的数字不用怀疑就是 DataFormat 没设。合并单元格也是一个高频需求用 CellRangeAddress 实现。比如把第一行从第 0 列合并到第 3 列sheet.AddMergedRegion(new CellRangeAddress(0, 0, 0, 3));注意四个参数顺序起始行、结束行、起始列、结束列。我见过不止一次把行列顺序写反导致合并区域错乱的 bug。合并之后还要给区域内所有单元格都设置样式否则合并区域看起来只有左上角那一格有边框很丑。列宽和行高的单位也容易搞混。SetColumnWidth 的第二个参数是以“字符宽度的 1/256”为单位想要 20 个字符宽就得写成 20 * 256Row.Height 的单位是“1/20 磅”想设成 25 磅就写 25 * 20。这些细节文档里写得很含蓄实测几次就记住了。2.4 输出到流时最容易翻车的三个细节项目里出现“生成的 Excel 打开就提示文件已损坏”90% 是输出流处理出了问题workbook.Write 之前就把底层流提前关掉Write 之后没有调用 workbook.Close()句柄没有释放用 FileStream 写入后没有 Flush 或 Dispose数据还留在缓冲区。我的习惯是使用 using 包住 MemoryStream让流在 Write 完成之后再关闭。如果要把结果通过接口返回记得把 MemoryStream 的 Position 复位到 0再 Copy 到输出流。using (MemoryStream ms new MemoryStream()) { workbook.Write(ms); workbook.Close(); ms.Flush(); ms.Position 0; return ms.ToArray(); }这套写法我在多个项目里复用基本没再出现过“文件损坏”的投诉。3. 读取 Excel 的三个隐形地雷类型、公式与合并单元格3.1 读取链路的固定动作读取和写入相反是从流加载 IWorkbook然后取 Sheet、Row、Cell。NPOI 在工作簿识别上也做了统一接口WorkbookFactory.Create 可以根据文件流自动判断 xls 还是 xlsx省去自己判断扩展名的麻烦。using (FileStream fs new FileStream(input.xlsx, FileMode.Open, FileAccess.Read)) { IWorkbook workbook WorkbookFactory.Create(fs); ISheet sheet workbook.GetSheetAt(0); DataFormatter formatter new DataFormatter(); FormulaEvaluator evaluator workbook.GetCreationHelper().CreateFormulaEvaluator(); for (int rowIdx 0; rowIdx sheet.LastRowNum; rowIdx) { IRow row sheet.GetRow(rowIdx); if (row null) continue; foreach (ICell cell in row.Cells) { switch (cell.CellType) { case CellType.String: Console.WriteLine(cell.StringCellValue); break; case CellType.Numeric: if (DateUtil.IsCellDateFormatted(cell)) { Console.WriteLine(cell.DateCellValue); } else { Console.WriteLine(cell.NumericCellValue); } break; case CellType.Formula: CellValue value evaluator.Evaluate(cell); Console.WriteLine(value.StringValue ?? value.NumberValue.ToString()); break; default: Console.WriteLine(formatter.FormatCellValue(cell)); break; } } } }这段代码像一把瑞士军刀遇到绝大多数文件都能把值取出来。但真实世界的 Excel 远比模板里的要野下面几个坑是一个一个踩出来的。3.2 单元格类型判断为什么 GetNumericCellValue 会抛异常读取时最容易崩的操作就是“想当然地认为这个单元格是数字”。一份系统导出过又被人工编辑过的 Excel单元格类型完全不按原模板走数字可能被存成文本日期可能变成字符串公式结果可能只有缓存值。NPOI 的 ICell 会根据实际类型提供不同的取值方法GetNumericCellValue 遇到字符串单元格会直接抛异常。正确姿势永远是先用 cell.CellType 判断再走对应的读取分支。我实际遇到过一个极端案例上游系统导出的 xlsx 里所有“看起来是数字”的单元格实际都是字符串因为它的导出代码用的是 SetCellValue(string)数值的显示靠数字文本对齐。如果我不做类型判断直接读第一行就崩了。3.3 公式单元格拿到的是缓存值不是结果公式单元格是另一个深坑。ICell.CellType 会返回 Formula但值其实在两个地方一是公式计算结果缓存二是公式本身。如果你把 Formula 归进 default 分支处理很可能什么都拿不到。NPOI 提供了 FormulaEvaluator可以按公式重新计算结果。但要注意Evaluate 返回的是 CellValue 对象不是直接塞回原来的 ICell。它内部区分了 StringValue 和 NumberValue取值前还要判断一下当前结果的类型。如果你确实想把公式替换成计算结果可以调用 EvaluateInCell但这样会改掉原始文件里的公式慎用。还有一种情况是“程序生成但从未打开过的 Excel”公式可能没有被 Excel 引擎计算过缓存值根本不存在。这种文件最吃 FormulaEvaluator否则你读出来的是一个空值。3.4 合并单元格与日期判断的两个实战处理合并单元格在处理导入数据时经常被忽略。表面上你看表格里是一块区域但底层只有左上角那个单元格有值其余都是空。遍历行时合并区域内的“空值”会被正常读到但业务上你可能希望这几个单元格都取同一个值。处理方式并不复杂遍历 sheet.NumMergedRegions拿到所有 CellRangeAddress判断当前坐标是否落在某个合并区域内。如果在就回退去读取区域左上角单元格的值。我通常会把这层判断封装进读取工具类让上层代码无感。日期判断推荐 DateUtil.IsCellDateFormatted(cell)。它是启发式判断会检查 Numeric 单元格的 DataFormat 是不是日期格式。但要注意如果单元格被存成文本“2024-01-01”这个判断不会生效你拿到的是一个字符串需要自己用 DateTime.TryParse 做转换NPOI 不会帮你做这种隐式解析。4. 大数据量导出SXSSFWorkbook 的滑动窗口与内存拐点4.1 当 10 万行数据让 XSSFWorkbook 吃满内存XSSFWorkbook 的本质是把整个工作簿都构建在内存里。导出 1 万行没感觉5 万行还凑合一旦超过 10 万行再加上多列、多样式内存占用就是几百 MB 到上 GB 的量级增长。在 WebAPI 场景里来两个并发导出请求应用基本就告警了。我当时接手过一个历史数据导出功能单表 80 万行用 XSSFWorkbook 直接内存溢出进程反复重启。后来把核心逻辑切到 SXSSFWorkbook问题才真正解决。如果你也遇到类似的“导出必挂”大概率不是代码 Bug而是选错了 Workbook 实现。4.2 滑动窗口在做什么SXSSFWorkbook 的原理一句话就能讲明白滑动窗口。构造时可以传入一个 windowSize比如 100表示内存里最多保留最近 100 行超过的行会被写入磁盘上的临时文件对用户不可见。最后 Write 时框架再把临时文件统一合并成最终的 xlsx。它本质是用磁盘 IO 换内存空间所以导出耗时通常比 XSSFWorkbook 略慢但换来了稳定性。对纯数据导出场景这是最实用的解药。需要留意的是SXSSFWorkbook 对某些高级特性支持不完整比如部分自动筛选、图表、图片相关功能在流式模式下受限。如果你导出的就是纯行列数据那直接切 SXSSF 就好不用犹豫。4.3 切换 SXSSFWorkbook 的代码与注意点SXSSFWorkbook 位于 NPOI.XSSF.Streaming 命名空间使用方式跟 XSSFWorkbook 很像using NPOI.XSSF.Streaming; SXSSFWorkbook workbook new SXSSFWorkbook(100); try { ISheet sheet workbook.CreateSheet(大表); for (int i 0; i 500000; i) { IRow row sheet.CreateRow(i); row.CreateCell(0).SetCellValue(i); row.CreateCell(1).SetCellValue(数据行 i); } using (FileStream fs new FileStream(big.xlsx, FileMode.Create)) { workbook.Write(fs); } } finally { workbook.Dispose(); }三个注意点使用后一定要 Dispose它占用了临时文件句柄不释放会留下垃圾文件一旦某行被刷出内存就不能再通过 workbook 随机访问那一行如果需要二次处理请提前把数据另存不需要手动清理临时文件SXSSFWorkbook 的 Dispose 会处理。4.4 实测经验同一批数据两种工作簿的差距我在 4 核 8G 的开发机上测过 30 万行、20 列的数据导出XSSFWorkbook 内存峰值到了 1.2GB 左右SSSSFWorkbook(100) 则稳定在 200MB 上下。耗时方面流式版本会慢一点但完全在可接受范围内。这里特别提醒如果你在导出时还创建了大量样式比如每个单元格都 new 一个 CellStyleSXSSF 也救不了你。样式对象的数量是拖垮性能和膨胀文件体积的隐形杀手。这一条在下一章展开。5. 升级与排错备忘我从 v2.2.1 学到的几件事5.1 NuGet 安装与框架兼容性备注引用方式不复杂命令行装包就行Install-Package NPOI -Version 2.2.1或者用 dotnet CLIdotnet add package NPOI --version 2.2.1v2.2.1 的目标框架是 .NET Standard 2.0所以 .NET Framework 4.6.1、.NET Core 2.0、.NET 5 的项目都能直接用。我目前的主力项目跑在 .NET 6 WebAPI 上引用 v2.2.1 完全没问题。如果你的项目框架更老记得先确认是否达到 4.6.1否则会出现 NuGet 还原警告。5.2 “文件已损坏”的排查路径遇到 Excel 文件打不开先别急着怀疑 NPOI。我整理过一套排查路径按这个顺序基本都能定位确认生成的扩展名与 Workbook 类型匹配xlsx 对应 XSSFWorkbookxls 对应 HSSFWorkbook确认写入过程没有异常半途中断catch 到异常后残留的半成品文件必然打不开用压缩工具直接打开生成的 .xlsx检查 [Content_Types].xml 等关键 XML 部件是否存在检查输出流是否被提前 Dispose。大部分“损坏”问题其实是封装导出功能时把底层流的生命周期管理错了和 NPOI 本身没有关系。把流的管理统一收口到一个方法里能避免绝大多数问题。5.3 SheetName、样式复用与合并边框的老坑CreateSheet 传入的工作表名称必须做校验不能为空不能超过 31 个字符不能包含\ / ? * [ ] :否则会抛 ArgumentException。中文名称没问题但中文全角字符不受限半角空字符在部分版本里也会有兼容问题。我会在工具类里写一个 SanitizeSheetName 方法把非法字符统一替换成下划线。样式复用也是一个经典问题。我之前在一个打印模板功能里循环 5000 次反复创建 ICellStyle结果文件生成时间从 3 秒直接变成 30 秒。样式一定要提出循环一个 workbook 只需要创建一次循环里做的只有“用已有样式”。这个经验对 XSSFWorkbook 和 SXSSFWorkbook 同样适用。合并单元格的边框是 NPOI 的老话题AddMergedRegion 只是把多个单元格标记成合并区域边框不会自动延伸。处理办法有两种一是把合并区域内所有单元格都赋同一个带边框的 style二是用 RegionUtil.SetBorderBottom、SetBorderLeft 等工具类单独设置区域边框。我推荐第二种代码更直观。5.4 一点真实感受项目里的 Excel 工具类我最后收敛成三个入口常规报表导出走 XSSFWorkbook大数据量导出走 SXSSFWorkbook导入解析统一走 WorkbookFactory.Create 加类型规整。v2.2.1 版本号不算新但应付日常业务系统已经绰绰有余。如果你正在被 Excel 导入导出折磨建议先按这篇文章把内存选型、单元格类型判断、流释放三条线捋一遍大部分问题都能在这三个方向里找到答案。最后再多嘴一句所有导出功能一定要留一套统一的日志记录导出人、导出条件、导出行数和耗时。别问为什么等你发现某张报表少了两列数据、却完全想不起来是哪次代码改动导致的时候就明白这句话的价值了。本文还有配套的精品资源点击获取