资讯动态

C# 实现 Excel 转 JSON 工具:NPOI 与 Newtonsoft.Json 实战

发布时间:2026/10/20 3:01:38 来源:尧图企业网站定制
简介这是一份面向C#开发者与数据处理初学者的Excel转JSON工具源码包针对需要将结构化表格数据导入Web应用或NoSQL数据库的场景提供可直接运行的转换方案。压缩包共36个文件约193KB包含6个cs源码文件、3个exe可执行程序、2个dll类库、2个pdb调试文件及csproj、sln、config等工程配置另有json输出示例与resx资源文件覆盖从源码到可执行程序的完整工程结构。资源围绕OleDbConnection读取Excel、DataTable承载数据、JavaScriptSerializer序列化为JSON的完整链路展开并提示了.xls与.xlsx连接字符串差异、异常处理、分批读取与数据清洗等实践要点。目前已有936人学习下载适合希望快速掌握C#文件I/O与数据序列化、并需要现成工具脚本与排错思路的开发者参考复用。1. 从 Excel 到 JSON 的 C# 转换为什么我最终选择自己写一个工具做数据对接的同行大概都遇到过这种场景业务方甩过来一个几百行的 Excel 表格字段列头写得随心所欲合并单元格、空行、日期格式混乱一应俱全而下游系统只认干净的 JSON。市面上在线转换工具不少但把带商业数据的表格传到别人服务器上心里总归不踏实手写 Python 脚本吧交付给纯 Windows 环境的客户又得额外装运行时。折腾几轮之后我决定用 C# 写一个本地跑的 Excel 转 JSON 工具源码留在手里随时改字段映射规则。这份资源就是围绕这个需求做的一套 C# 实现的 Excel 转 JSON 工具加完整源码。它能解决的核心问题是——把 .xlsx / .xls 里的表格数据按你指定的列名和层级结构稳定地序列化成 JSON 文件或字符串全程离线不依赖 Office 安装。适合谁做后端接口对接、数据迁移、报表导出、配置表生成的 C# 开发者尤其是需要把转换逻辑嵌进自己项目里、而不是每次手动点按钮的人。下面我从技术选型和代码落地讲起把踩过的坑一并交代清楚。2. 技术选型读 Excel 和写 JSON 分别用什么库2.1 读 Excel 的三条路线对比C# 读 Excel 主流有三条路选错了后面全是坑。第一条是Microsoft.Office.Interop.Excel也就是调用本机装的 Office 组件。优点是兼容性最好什么古怪格式都能读缺点是必须装 Office服务器上跑容易卡进程、残留EXCEL.EXE而且速度慢得让人怀疑人生。我早期一个批量导入功能就栽在这上面几百个文件跑完任务管理器里躺着一排僵尸进程。第二条是OleDb用Microsoft.Jet.OLEDB或ACE OLEDB驱动把 Excel 当数据库查。写起来像写 SQLSELECT * FROM [Sheet1$]就能拿数据。但它对驱动版本敏感32 位 / 64 位不匹配直接报「未在本地计算机上注册提供程序」而且读混合类型列时经常把数字当文本、把文本当数字玄学得很。第三条是纯托管库代表是EPPlus和NPOI。它们不依赖 Office直接解析 xlsx 的 XML 结构跨平台、速度快、可控性强。EPPlus从 5.0 起改成了商业授权非商业免费NPOI则是 Apache 2.0商用无顾虑。这份工具我选的是NPOI理由很简单免费、稳定、社区资料多读 xls 和 xlsx 用同一套 API。方案依赖 Office跨平台授权推荐场景Interop是否随 Office格式极复杂、本机有 OfficeOleDb需驱动部分随驱动简单查询式读取NPOI否是Apache 2.0通用首选EPPlus否是商业5.0非商业项目2.2 JSON 序列化的选择写 JSON 这块Newtonsoft.Json和System.Text.Json都能用。前者生态成熟、容错强后者是 .NET 内置、性能好。工具里我用Newtonsoft.Json因为要动态构造层级结构比如把「省-市-区」三列拼成嵌套对象它的JObject/JArray操作起来更顺手。如果你项目里已经统一用System.Text.Json替换成本也不高核心逻辑不变。2.3 项目结构与依赖安装新建一个 .NET 6 控制台项目通过 NuGet 装两个包dotnet new console -n ExcelToJsonTool cd ExcelToJsonTool dotnet add package NPOI dotnet add package Newtonsoft.JsonNPOI负责解析工作簿Newtonsoft.Json负责序列化。选 .NET 6 是因为它是 LTS客户机器上装个运行时就能跑也可以发布成自包含单文件连运行时都不用装。项目结构保持简单一个ExcelReader类管读取一个JsonBuilder类管组装Program.cs做入口和参数解析。这样拆的好处是读取逻辑和转换逻辑解耦以后要支持 CSV 或数据库来源只换 Reader 就行。3. 核心实现把工作表读成数据再拼成 JSON3.1 用 NPOI 打开工作簿并定位表头先看读取部分。核心是拿到ISheet确定表头行再把数据行逐行读成字典。using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using NPOI.HSSF.UserModel; public class ExcelReader { // 根据扩展名选择 xlsx 或 xls 的工作簿实现 public ISheet GetSheet(string filePath, int sheetIndex 0) { IWorkbook workbook; using (var fs new FileStream(filePath, FileMode.Open, FileAccess.Read)) { // xlsx 走 XSSFxls 走 HSSF不能混用 workbook filePath.EndsWith(.xlsx) ? new XSSFWorkbook(fs) : new HSSFWorkbook(fs); } return workbook.GetSheetAt(sheetIndex); } // 读取表头行返回列名列表 public Liststring ReadHeaders(ISheet sheet, int headerRowIndex 0) { var headerRow sheet.GetRow(headerRowIndex); var headers new Liststring(); for (int i 0; i headerRow.LastCellNum; i) { var cell headerRow.GetCell(i); // 表头为空时给个占位名避免后续字典键冲突 headers.Add(cell?.ToString()?.Trim() ?? $Column{i}); } return headers; } }这里有几个参数要留意。sheetIndex默认取第一个工作表多 sheet 场景可以传索引或改成按名字取。headerRowIndex默认 0但实际表格经常前几行是标题、说明表头在第 3 行甚至第 5 行这个参数必须可配。LastCellNum返回的是最后一列的索引加一注意它和LastRowNum一样是「最后一个有内容的」而不是「总行数」空行会被跳过这点后面避坑章节会细说。3.2 逐行读取并处理单元格类型NPOI 的单元格有类型之分直接ToString()有时会得到科学计数法或者日期序列号必须按类型分支处理。public ListDictionarystring, object ReadRows(ISheet sheet, Liststring headers, int startRow) { var rows new ListDictionarystring, object(); for (int r startRow; r sheet.LastRowNum; r) { var row sheet.GetRow(r); if (row null) continue; // 整行为空直接跳过 var dict new Dictionarystring, object(); for (int c 0; c headers.Count; c) { var cell row.GetCell(c); dict[headers[c]] GetCellValue(cell); } rows.Add(dict); } return rows; } // 按单元格类型取值避免日期变数字、数字变文本 private object GetCellValue(ICell cell) { if (cell null) return null; switch (cell.CellType) { case CellType.String: return cell.StringCellValue.Trim(); case CellType.Numeric: // 日期格式的数值要转成 DateTime否则会得到 45000 这种序列号 if (DateUtil.IsCellDateFormatted(cell)) return cell.DateCellValue?.ToString(yyyy-MM-dd HH:mm:ss); return cell.NumericCellValue; case CellType.Boolean: return cell.BooleanCellValue; case CellType.Formula: // 公式单元格取缓存的计算结果 return cell.CachedFormulaResultType CellType.Numeric ? cell.NumericCellValue : cell.StringCellValue; default: return cell.ToString(); } }GetCellValue是整个读取环节最关键的方法。日期判断用DateUtil.IsCellDateFormatted这是 NPOI 提供的工具方法比自己判断格式字符串靠谱。公式单元格要取CachedFormulaResultType因为 NPOI 不会重新计算公式只能读 Excel 上次保存时缓存的结果——如果你的表格是程序生成的、没经过 Excel 打开保存公式缓存可能是空的这是常见翻车点。3.3 用 JObject 组装 JSON 并输出数据读成ListDictionary之后转 JSON 就简单了。基础版直接序列化using Newtonsoft.Json; var json JsonConvert.SerializeObject(rows, Formatting.Indented); File.WriteAllText(output.json, json, Encoding.UTF8);但实际需求往往要嵌套结构。比如表格里是「省份、城市、区县」三列希望输出成{ 省份: { 城市: [ 区县1, 区县2 ] } }。这时候用JObject手动组装using Newtonsoft.Json.Linq; var root new JObject(); foreach (var row in rows) { var province row[省份]?.ToString(); var city row[城市]?.ToString(); var district row[区县]?.ToString(); // 逐层创建节点已存在则复用 if (root[province] null) root[province] new JObject(); var cityObj (JObject)root[province]; if (cityObj[city] null) cityObj[city] new JArray(); ((JArray)cityObj[city]).Add(district); } File.WriteAllText(nested.json, root.ToString(Formatting.Indented), Encoding.UTF8);JObject的索引器在键不存在时返回 null所以用if (root[province] null)判断是否要新建节点这是构造层级结构的通用套路。Formatting.Indented让输出带缩进方便人看如果给程序消费可以去掉省体积。写文件时显式指定Encoding.UTF8否则中文在部分环境下会乱码——这个坑我在跨系统对接时踩过不止一次。4. 避坑与排查那些让我加班到深夜的细节4.1 空行和 LastRowNum 的陷阱现象转换出来的 JSON 里多出一堆空对象或者明明有数据却读不全。原因LastRowNum返回的是最后一个「物理存在」的行号如果表格中间有格式残留比如曾经填过又删掉这些行GetRow不为 null 但所有单元格为空就会生成空字典。反过来如果表格末尾有空行但被判定为最后一行也可能影响边界。解决读取时对每行做空值检查所有单元格都为空就跳过。判断逻辑是遍历该行所有单元格cell null || string.IsNullOrWhiteSpace(cell.ToString())全部成立才跳过。别只判断第一列有些表格第一列是空的但后面有数据。4.2 日期和数字被读成奇怪的值现象Excel 里明明是2024-01-15JSON 里变成45306或者13800138000这种长数字变成1.38001E10。原因Excel 内部日期就是数字从 1900-01-00 起的天数NPOI 读 Numeric 类型时默认给 double。长数字被科学计数法显示是因为单元格格式设成了常规或科学NPOI 按数值读出来再ToString就成这样了。解决日期用DateUtil.IsCellDateFormatted判断后转DateCellValue。长数字如手机号、身份证建议在 Excel 里就设成文本格式或者在代码里判断数值范围超过一定位数用cell.ToString()而非NumericCellValue。更稳妥的做法是让业务方把这类列统一设成文本从源头避免。4.3 合并单元格只有左上角有值现象表格里「部门」列做了合并只有第一行有「技术部」下面几行读出来是 null。原因合并单元格在 NPOI 里只有左上角那个 cell 有值其余是空的这是 Excel 的存储机制决定的。解决读取前先获取所有合并区域sheet.MergedRegions对每个区域记录起始行、结束行、起始列读取时如果当前 cell 为空且在某个合并区域内就取该区域左上角的值。这段逻辑稍微绕但封装成一个GetMergedCellValue方法后一劳永逸。4.4 大文件内存暴涨现象读一个几万行的 xlsx内存直接飙到几个 G甚至 OOM。原因XSSFWorkbook会把整个工作簿的 XML 解析进内存行数一多就撑不住。解决NPOI 提供了SXSSFWorkbook用于流式写但读大文件要用事件模型XSSFReader配合 SAX 解析代码复杂度高不少。如果只是几万行可以加大XSSFWorkbook的构造参数或分批处理真到几十万行级别建议换ExcelDataReader这类流式读取库或者干脆让业务方导出 CSV。选型时要对数据量有预判别等上线了才发现。4.5 中文乱码与 BOM 问题现象生成的 JSON 用记事本打开正常用某些工具或接口读却乱码。原因File.WriteAllText默认用 UTF-8 无 BOM但部分老系统或特定解析器期望带 BOM 的 UTF-8。解决如果下游明确要求 BOM用new UTF8Encoding(true)作为编码参数。不确定的话先按无 BOM 输出对接方报乱码再调整。这个没有绝对对错取决于消费端沟通清楚比猜更省事。5. 进阶技巧把转换逻辑做成可配置的映射规则基础版能跑通之后真正提升效率的是把「哪列映射到哪个 JSON 字段、要不要嵌套、要不要类型转换」做成配置而不是每次改代码。我现在的习惯是维护一个映射配置用 JSON 描述规则工具读配置执行。// 映射规则示例source 是 Excel 列名target 是 JSON 字段路径 // type 支持 string/int/datetimenested 表示是否构造层级 public class FieldMapping { public string Source { get; set; } public string Target { get; set; } public string Type { get; set; } string; public bool Nested { get; set; } false; } // 按配置转换单行数据 public JObject MapRow(Dictionarystring, object row, ListFieldMapping mappings) { var obj new JObject(); foreach (var m in mappings) { if (!row.ContainsKey(m.Source)) continue; var value row[m.Source]; // 按声明类型做转换转换失败给默认值而不是抛异常 object converted m.Type switch { int int.TryParse(value?.ToString(), out var i) ? i : 0, datetime DateTime.TryParse(value?.ToString(), out var d) ? d.ToString(yyyy-MM-dd) : null, _ value?.ToString() }; obj[m.Target] converted null ? JValue.CreateNull() : JToken.FromObject(converted); } return obj; }配置化之后换一个表格只需要改映射文件不用重新编译。Type字段做类型转换时用TryParse而不是Parse转换失败给默认值避免一行脏数据让整个任务崩掉——这是批量处理场景的基本素养。Nested字段可以扩展成支持点号路径如dept.name自动构造嵌套对象思路和前面JObject组装一样只是把硬编码换成配置驱动。验证转换结果时我一般会写个简单的对拍脚本把原始 Excel 的行数和 JSON 数组长度比一下再抽查几行关键字段。行数对不上八成是空行或合并单元格的问题字段值不对回去查单元格类型处理。这套流程走下来基本能覆盖九成以上的转换异常。从那以后我每次做数据转换都强制先拿一份「脏数据样本」跑一遍——带合并单元格、带空行、带混合类型的那种而不是拿干净的测试数据自欺欺人。真实业务表格的混乱程度永远超出你的想象。希望这份工具和源码能帮你少加几个班。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑