简介:这是一份面向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 当数据库查。写起来像写 SQL,SELECT * FROM [Sheet1$]就能拿数据。但它对驱动版本敏感,32 位 / 64 位不匹配直接报「未在本地计算机上注册提供程序」,而且读混合类型列时经常把数字当文本、把文本当数字,玄学得很。
第三条是纯托管库,代表是EPPlus和NPOI。它们不依赖 Office,直接解析 xlsx 的 XML 结构,跨平台、速度快、可控性强。EPPlus从 5.0 起改成了商业授权(非商业免费),NPOI则是 Apache 2.0,商用无顾虑。这份工具我选的是NPOI,理由很简单:免费、稳定、社区资料多,读 xls 和 xlsx 用同一套 API。
| 方案 | 依赖 Office | 跨平台 | 授权 | 推荐场景 |
|---|---|---|---|---|
| Interop | 是 | 否 | 随 Office | 格式极复杂、本机有 Office |
| OleDb | 需驱动 | 部分 | 随驱动 | 简单查询式读取 |
| 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. 核心实现:把工作表读成数据再拼成 JSON
3.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 走 XSSF,xls 走 HSSF,不能混用 workbook = filePath.EndsWith(".xlsx") ? new XSSFWorkbook(fs) : new HSSFWorkbook(fs); } return workbook.GetSheetAt(sheetIndex); } // 读取表头行,返回列名列表 public List<string> ReadHeaders(ISheet sheet, int headerRowIndex = 0) { var headerRow = sheet.GetRow(headerRowIndex); var headers = new List<string>(); 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 List<Dictionary<string, object>> ReadRows(ISheet sheet, List<string> headers, int startRow) { var rows = new List<Dictionary<string, object>>(); for (int r = startRow; r <= sheet.LastRowNum; r++) { var row = sheet.GetRow(r); if (row == null) continue; // 整行为空直接跳过 var dict = new Dictionary<string, 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 并输出
数据读成List<Dictionary>之后,转 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-15,JSON 里变成45306;或者13800138000这种长数字变成1.38001E+10。
原因: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/datetime,nested 表示是否构造层级 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(Dictionary<string, object> row, List<FieldMapping> 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 数组长度比一下,再抽查几行关键字段。行数对不上,八成是空行或合并单元格的问题;字段值不对,回去查单元格类型处理。这套流程走下来,基本能覆盖九成以上的转换异常。
从那以后我每次做数据转换,都强制先拿一份「脏数据样本」跑一遍——带合并单元格、带空行、带混合类型的那种,而不是拿干净的测试数据自欺欺人。真实业务表格的混乱程度,永远超出你的想象。希望这份工具和源码能帮你少加几个班。
本文还有配套的精品资源,点击获取