☰
C# NPOI操作Excel:工作簿选型、格式探测与单元格读写
2026/10/26 5:00:23 网站建设 项目流程

简介:C# 结合 NPOI 操作 Excel 的示例包面向 .NET 开发者、桌面与后台业务系统维护者,以及需要兼容 .xls 与 .xlsx 的初中级程序员,帮助快速理解工作簿创建、工作表与行列读写、单元格样式、文件保存及常见异常处理。压缩包共 15 个文件,以 12 个 dll 组件、2 个 cs 示例源码和 1 个 xml 配置说明文件为主,整体约 2.19MB,目录按 2012Version 与 201607Version 区分,便于对照不同 NPOI 版本及 Excel 格式的实现差异。已有 6358 人学习或下载。包内示例侧重可复用的 Excel 工具类封装,读者可从中梳理读写流程、版本适配和基础排错思路,并据此搭建自己的导入导出小工具。

1. 从一份能跑的示例包说起:C# NPOI 操作 Excel 的边界与选型

C# NPOI 操作 Excel 这件事,真正让人翻车的往往不是 API 记不住,而是 .xls 和 .xlsx 两套格式在同一个工程里打架。这份示例包把两种格式的读写入口都摆了出来,核心价值是让不装 Office 的服务器也能生成报表、导入台账、回写模板。适合维护老系统还要兼容 .xls 的 .NET 开发者,也适合做后台批量导入导出的业务团队。它不解决复杂 BI 展示,也不替代 Excel 的计算引擎;公式、日期、合并单元格这些边角,仍然要按 NPOI 的规则处理。先把它当成可运行的脚手架,再按业务字段替换,比从零拼 API 稳。

2. 工作簿与引擎:HSSFWorkbook、XSSFWorkbook 和 SXSSFWorkbook 怎么选

示例包拆开之后,第一层不是看单元格怎么写,而是看工作簿类型怎么选。NPOI 是 Apache POI 的 .NET 移植,纯托管实现,不依赖 Office COM 组件,所以它能在没有装 Excel 的服务器上跑。代价是不同格式对应不同的工作簿实现,选错了要么直接抛异常,要么在几万行数据上把内存吃满。血泪经验是:先把格式边界定死,再谈样式和公式。

2.1 三种 Workbook 的底层差异与内存账

HSSFWorkbook 对应 .xls,底层是 OLE2 复合文档;XSSFWorkbook 对应 .xlsx,底层是 OOXML;SXSSFWorkbook 不是另一种文件格式,它是对 XSSF 的流式包装,只在内存里保留一个窗口行,超出的行刷到临时文件。三者的行列上限和内存模型差别很大,选型时先看数据量和目标格式,不要等报错才回头改。

类型对应格式行/列上限内存模型适用场景
HSSFWorkbook.xls65536 行 / 256 列全量在内存老系统兼容、小表、历史模板
XSSFWorkbook.xlsx1048576 行 / 16384 列全量在内存常规读写、样式、公式、合并单元格
SXSSFWorkbook.xlsx同 XSSF窗口行 + 临时文件几万到几十万行导出

窗口行是 SXSSFWorkbook 构造函数的参数,常见写法是new SXSSFWorkbook(500),意思是内存里最多保留 500 行可访问。这个值不是越大越好:调大能减少刷盘次数,但内存占用会上去;调小能压内存,但频繁刷盘会拖慢导出。我一般先用 500 跑一批真实数据,看内存峰值和耗时再微调。另一个容易忽略的点是临时文件目录,SXSSF 刷出去的行会落到系统临时目录,磁盘空间不足时会在写到一半时失败,日志里不一定能直接看出是临时目录的问题。

2.2 新建、读取、保存的最小闭环

示例包里最值得先跑的是最小闭环:新建一个工作簿,写两行,保存成 .xlsx。这段代码能验证 NuGet 包是否装对、命名空间是否引用全、输出目录是否有写权限。代码不复杂,但每一步都有边界。

using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; IWorkbook workbook = new XSSFWorkbook(); ISheet sheet = workbook.CreateSheet("订单"); IRow header = sheet.CreateRow(0); header.CreateCell(0).SetCellValue("订单号"); header.CreateCell(1).SetCellValue("金额"); IRow data = sheet.CreateRow(1); data.CreateCell(0).SetCellValue("A-001"); data.CreateCell(1).SetCellValue(128.50); using var output = new FileStream("demo.xlsx", FileMode.Create, FileAccess.Write); workbook.Write(output);

逻辑说明:IWorkbook是统一抽象,XSSFWorkbook负责 OOXML 格式。CreateSheet的 Sheet 名不能超过 31 个字符,不能包含: \ / ? * [ ]这些字符,超出或含非法字符会抛异常。SetCellValue有 string、double、bool、DateTime 等重载,金额写 double 会有二进制浮点误差,财务场景建议写 decimal 转字符串,或者放大成整数分再写。参数说明:FileMode.Create会覆盖同名文件,FileAccess.Write只写不读;流用using包住,确保写完立即关闭,否则后面替换文件时容易遇到占用。工作簿本身在写完后也建议释放,尤其是后面用 SXSSF 时,释放动作会清理临时文件。

读取的最小闭环和新建要分开看。读取时不要假设扩展名等于真实格式,也不要用new XSSFWorkbook(fs)去打开 .xls。示例包里如果同时有 .xls 和 .xlsx 两个入口,重点看它是不是用了统一的工厂入口。

using NPOI.SS.UserModel; public static IWorkbook Open(string path) { using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); // 由文件头判断 OLE2 还是 OOXML,避免把 .xls 当 .xlsx 打开 return WorkbookFactory.Create(fs); }

逻辑说明:WorkbookFactory.Create会读文件头,不依赖扩展名,所以扩展名写错但内容正确时也能打开。FileShare.ReadWrite允许别的进程同时读,用户在 Excel 里开着文件时,后台读取不至于直接失败。参数说明:FileMode.Open要求文件存在,FileAccess.Read只读;返回的 workbook 已经把数据读进内存,方法结束关闭流一般不影响后续读取。注意不要对同一个流反复调用Create,流位置会变,读出来可能是空工作簿。

2.3 用 WorkbookFactory 做格式探测与统一入口

示例包里的 .xls 和 .xlsx 示例如果各自 new 各自的 Workbook,业务代码很容易分叉。更稳的做法是把打开动作收口到一个入口,业务层只拿IWorkbook,不关心底层是 HSSF 还是 XSSF。这样以后加 .xlsm 或者换文件来源,只需要改一处。

using NPOI.HSSF.UserModel; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; public static IWorkbook CreateNew(string path) { string ext = Path.GetExtension(path).ToLowerInvariant(); return ext switch { ".xls" => new HSSFWorkbook(), ".xlsx" => new XSSFWorkbook(), _ => throw new NotSupportedException("只支持 .xls 和 .xlsx") }; }

逻辑说明:新建文件时没有文件头可探测,只能按目标扩展名选工作簿类型。读取已有文件时优先用WorkbookFactory.Create,新建时再用这个工厂方法。参数说明:.xls走 HSSF,行列上限是 65536 行、256 列;.xlsx走 XSSF,上限是 1048576 行、16384 列。超过 65536 行的数据不要往 .xls 里塞,NPOI 会在写入时抛异常,这种翻车在批量导出里很常见。扩展名和真实格式不一致时,读取阶段以文件头为准,写入阶段以你选的 Workbook 类型为准,别混着用。

提示:如果手头的 NPOI 版本没有WorkbookFactory,可以读文件前 8 个字节判断:OLE2 以D0 CF 11 E0开头,OOXML 以PK开头。判断完再选 HSSF 或 XSSF,别只看扩展名。

3. 单元格读写:类型判断、日期格式与空值处理的完整链路

工作簿选对之后,第二层是单元格。示例包里最容易抄错的就是读取:看到数字就当金额,看到日期就当 DateTime,结果空单元格、公式单元格、文本格式的数字混在一起,导入结果对不上。单元格读取要先判CellType,再决定取哪个值属性,最后处理 null 和空行。写入则要先想清楚样式是工作簿级共享对象,不能每行 new 一个。

3.1 读取时先判类型:Numeric、String、Formula、Blank

NPOI 的单元格类型枚举包含 String、Numeric、Boolean、Formula、Blank、Error 等。数值单元格不一定都是数字,可能是日期;公式单元格的值要看缓存结果类型;空单元格可能是 null,也可能是 Blank。下面这个GetCellText适合做导入时的统一文本出口,先把类型摊平,再交给业务做转换。

using NPOI.SS.UserModel; public static string GetCellText(ICell cell) { if (cell == null) return string.Empty; switch (cell.CellType) { case CellType.String: return cell.StringCellValue ?? string.Empty; case CellType.Numeric: if (DateUtil.IsCellDateFormatted(cell)) { DateTime dt = cell.DateCellValue; return dt.ToString("yyyy-MM-dd HH:mm:ss"); } return cell.NumericCellValue.ToString("G"); case CellType.Boolean: return cell.BooleanCellValue ? "TRUE" : "FALSE"; case CellType.Formula: return GetFormulaText(cell); case CellType.Blank: return string.Empty; default: return cell.ToString() ?? string.Empty; } } private static string GetFormulaText(ICell cell) { switch (cell.CachedFormulaResultType) { case CellType.Numeric: if (DateUtil.IsCellDateFormatted(cell)) { DateTime dt = cell.DateCellValue; return dt.ToString("yyyy-MM-dd"); } return cell.NumericCellValue.ToString("G"); case CellType.String: return cell.StringCellValue ?? string.Empty; case CellType.Boolean: return cell.BooleanCellValue ? "TRUE" : "FALSE"; default: return string.Empty; } }

逻辑说明:CellType.Numeric先看DateUtil.IsCellDateFormatted,这个判断依赖单元格的格式,不是看值大小;日期格式返回 true 才走DateCellValue,否则按数值输出。公式单元格不能直接取StringCellValue,要先看CachedFormulaResultType,再按缓存类型取值。参数说明:NumericCellValue.ToString("G")会保留通用精度,金额场景不要直接用这个字符串做 decimal 解析后再四舍五入,最好在业务层明确精度。DateCellValue在部分 NPOI 版本里是可空类型,按编译器提示补 null 判断即可。空单元格用GetCell可能返回 null,遍历时先判 null,再判CellType.Blank。

读取循环本身也有坑。sheet.LastRowNum是最后一行的索引,中间有空行也会算进去;row.LastCellNum是最后一个单元格的索引加一,空行可能返回 -1。遍历时不要直接for (int j = 0; j < row.LastCellNum; j++)而不判row为 null。更稳的写法是用MissingCellPolicy.RETURN_BLANK_AS_NULL,把不存在的单元格当 null 处理,避免 NPOI 在读取时创建空单元格对象。

for (int r = 0; r <= sheet.LastRowNum; r++) { IRow row = sheet.GetRow(r); if (row == null) continue; for (int c = 0; c < row.LastCellNum; c++) { ICell cell = row.GetCell(c, MissingCellPolicy.RETURN_BLANK_AS_NULL <p> <a href="https://download.csdn.net/download/qq576826394/9677537" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询