C#开发Excel转JSON工具:从配置表读取到序列化全解析 简介这是一份C#实现Excel转JSON的工具与源码资源面向需要处理表格数据转换的.NET开发者和学习者尤其适合将Excel结构化数据导入Web应用或NoSQL数据库的场景。压缩包共36个文件、约193KB包含6个cs源码文件、3个exe可执行程序、2个dll库文件及pdb调试符号另有config、resources、resx等工程配置文件整体为完整可打开的Visual Studio解决方案。代码基于OleDb读取Excel并利用JavaScriptSerializer序列化为JSON覆盖连接字符串、工作表获取、DataTable填充与文件输出等关键环节同时对.xls与.xlsx的差异、异常处理、性能优化及数据清洗均有说明便于直接复用或二次开发。已有936人学习下载适合想快速掌握C#数据转换并拿到可运行示例的开发者。1. 一次配置表交接翻车后我把 Excel 转 JSON 做成了独立工具某天运营发来一张用户配置 Excel前端说接口要 JSON。我手工复制了几行编号 10023 变成 1002.3日期变成 45276数组还漏了一项。翻车之后我决定把这个重复劳动固化成工具C# 读取 .xls/.xlsx把第一行当字段名数据行逐行转成 JSON 对象最后输出一个干净的 JSON 文件。工具带完整源码和可执行程序核心代码三百行左右适合经常处理配置表、需要把 Excel 批量变成接口数据的开发也适合想完整看一遍 Excel 解析到 JSON 序列化链路的人。下面从选型开始按我拆代码的顺序讲。2. 技术选型与目录结构为什么我选了无 Office 依赖的读取方案2.1 三种读 Excel 的路只有一条适合做工具动手之前我先排过一轮方案。直接在 Windows 上调用 COM 组件、用官方 OpenXML 包、用开源的 Excel 读取组件三条路我都试过差别很明显。表里列一下对比方案是否依赖 Excel格式支持适合场景COM 组件是必须装 Officexls / xlsx临时小脚本官方 OpenXML 包否仅 xlsx读写格式复杂的文档开源读取组件否xls / xlsx提取单元格数据做转换COM 方案的坑最直观服务器上没有 Office 就直接崩有 Office 也慢Excel 进程时不时挂在那里释放不掉脚本跑完还得手工清理进程。官方 OpenXML 包不需要装 Excel但只认 xlsx老用户手里的 .xls 得先用别的工具转一遍批处理里多一步就是多一个出错点。开源读取组件是纯托管代码把文件流解析成内存对象不碰系统里的 Excel 程序API 也贴近 Excel 的对象模型社区用的人多遇到问题搜索一大把答案。最终我选开源读取组件核心 API 只有三件事打开工作簿、定位 Sheet、遍历行对“读数据转 JSON”这个目标完全够用。选 C# 而不是 Python 不是为了炫技团队技术栈是 .NET后续想把这个转换能力接到后台服务里直接把这个项目编成 DLL 就能复用再加上 .NET 内置 System.Text.Json序列化都不用额外引库。工具本身是控制台程序方便放进批处理和定时任务双击也能跑。读数据这个场景对 Excel 的格式其实不敏感颜色、边框、公式的中间计算过程一概不关心只有单元格的值类型有意义。所以我在选型时把“文本、数值、日期、布尔、空”这五类处理清楚就够了。真正写代码的时候你会发现90% 的时间花在判断一个单元格到底属于哪一类而不是在读取文件本身。2.2 工程目录每个文件只干一件事项目结构很扁平没有过度抽象。拆完之后目录长这样ExcelToJson/ |-- ExcelToJson.sln |-- ExcelToJson/ | |-- Program.cs | |-- ExcelReader.cs | |-- JsonOutput.cs | |-- MappingOptions.cs | |-- ExcelToJson.csproj | |-- Examples/ | |-- sample.xlsx | |-- sample.json各文件的职责和改动入口如下文件职责二次开发动哪里Program.cs解析命令行参数、编排调用流程加新参数、换流程ExcelReader.cs打开 Excel、遍历行、单元格取值改读取规则和类型判断JsonOutput.cs把字典列表序列化成 JSON改输出结构、缩进、嵌套MappingOptions.cs表头映射、嵌套分隔符配置加映射规则设计上刻意让 ExcelReader 只输出 ListDictionarystring, objectJsonOutput 只做序列化中间数据格式是标准字典两边互不感知。这样如果运营说表头要改只改 MappingOptions如果前端说 JSON 结构要包一层 data只改 JsonOutputExcel 读取那部分一行都不用动。我见过不少工具把读取和输出写在一个类里最后改哪个需求都心惊胆战。这个项目从一开始就把边界画清了。csproj 里也没放多余的东西TargetFramework 用 net6.0 或 net8.0 都行NuGet 只加一个 Excel 读取组件的引用System.Text.Json 是 BCL 自带的。我习惯把 LangVersion 开到 latest写代码时能直接用 switch 表达式和文件级命名空间少敲不少花括号。2.3 命令行接口一进一出加三个可选参数工具是控制台程序用法是ExcelToJson.exe -i ./input.xlsx -o ./output.json -s Sheet1 -h 0参数含义参数必填说明-i是Excel 文件路径支持 .xls / .xlsx-o否输出 JSON 路径不填直接打印到控制台-s否Sheet 名缺省取第一个非空 Sheet-h否表头所在行索引默认 0参数解析是自己写的没引第三方库。也就四个参数手写 20 行遍历 string[]遇到 -i 就取下一个值匹配不了就抛“未知参数异常”比引一个重型库划算得多。还有一个设计细节输出到控制台时用 Console.Out 直接写方便在命令行里用管道接着处理输出到文件时统一 UTF-8 编码避免中文乱码。平时在命令行里反复敲这一长串也烦我会在项目根放一个 run.ps1把常用路径封装成默认值真正每天用的是脚本而不是裸命令。3. 核心模块拆解一个单元格从 Excel 到 JSON 的完整链路3.1 打开工作簿用扩展名决定读取器读取的第一步是把文件流变成工作簿对象。这里的关键是按文件扩展名选择不同的实现using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using NPOI.HSSF.UserModel; IWorkbook workbook; using (var stream File.OpenRead(inputPath)) { workbook Path.GetExtension(inputPath).ToLowerInvariant() switch { .xls new HSSFWorkbook(stream), // 2003 及以前 .xlsx new XSSFWorkbook(stream), // 2007 及以后 _ throw new NotSupportedException($不支持的格式: {inputPath}) }; }HSSFWorkbook 和 XSSFWorkbook 都实现 IWorkbook 接口后面所有逻辑只要面向 IWorkbook 写就不需要再关心文件是老格式还是新格式。File.OpenRead 包在 using 里工作簿用完跟着释放避免文件句柄堆积。这里有个初学容易踩的点把 .xls 文件传给 XSSFWorkbook 会直接抛异常我一开始就吃过这个亏所以选构造器之前一定要先看扩展名。如果你要读取的文件来自内存流比如从数据库取出的 byte[]同样用这两个构造器传入 MemoryStream 就行。后面我会讲拿到 IWorkbook 后取 Sheet、遍历行这些操作完全一致。3.2 表头清洗去空格、默认名、重名加后缀读取数据之前得先把表头变成一份干净的列名清单。Excel 表头经常有换行、前后空格甚至两列同名这些脏数据不能直接当 JSON 字段名private Liststring ParseHeader(ISheet sheet, int headerRowIndex) { var names new Liststring(); var seen new HashSetstring(StringComparer.OrdinalIgnoreCase); var row sheet.GetRow(headerRowIndex) ?? throw new Exception(表头行为空); for (int c 0; c row.LastCellNum; c) { var name row.GetCell(c)?.ToString()?.Trim() ?? ; if (name.Length 0) name $column{c}; // 空表头补默认名 if (!seen.Add(name)) name ${name}_{c}; // 重名追加列号 names.Add(name); } return names; }这里用 HashSet 做大小写不敏感的重名检测第一列叫 Name第二列也叫 Name第二列自动改成 Name_1。空表头给 column0、column1 这样的占位名保证整个文件每列都有字段名不丢列。返回顺序按遍历顺序来List 本身是有序的所以后续行数据用列索引取值时不会错位。表头清洗这一步做成独立方法是值得的。很多转换工具把表头处理和行数据读写在同一个循环里一旦遇到脏表头改起来非常痛苦。单独拆出来之后运营的表头规则再怎么变你只用改这一个方法。3.3 单元格取值文本、数字、日期、布尔、公式五个分支这是整个工具最核心的函数也是当初最让我头疼的部分。NPOI 的单元格取值不是直接 ToString 就完事必须按 CellType 分类型取private 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.ToString(yyyy-MM-dd HH:mm:ss); var raw cell.NumericCellValue; return raw % 1 0 ? (long)raw : raw; case CellType.Boolean: return cell.BooleanCellValue; case CellType.Formula: return cell.CachedFormulaResultType CellType.Numeric ? cell.NumericCellValue : cell.StringCellValue; default: return null; } }Numeric 分支是重点。Excel 里的单元格只要格式是数字取出来的就是 double一个 id 列明明是 10023取出来变成 10023.0。用 raw % 1 0 判断它是不是整数是就转 long这样输出 JSON 时才不会有小数点。日期在 Excel 内部也是个 double比如 45276 代表从 1900 年起的第 45276 天所以必须先调 DateUtil.IsCellDateFormatted 判断是不是日期格式再决定按日期字符串输出还是按数值输出。Formula 分支很容易漏。单元格写的是 A1B1 这种公式NPOI 默认拿不到计算后的值只能从 CachedFormulaResultType 取缓存结果。如果是布尔公式还要再加一个分支目前这个写法覆盖了最常见的数字公式和文本公式够用。3.4 组装行数据跳过空行支持嵌套字段有了列名清单和单元格取值函数就可以遍历数据行了。每行一个 Dictionary最终拼成 ListDictionarystring, objectvar rows new ListDictionarystring, object(); for (int r headerRowIndex 1; r sheet.LastRowNum; r) { var row sheet.GetRow(r); if (IsEmptyRow(row)) continue; var dict new Dictionarystring, object(StringComparer.OrdinalIgnoreCase); for (int c 0; c columns.Count; c) { var value GetCellValue(row?.GetCell(c)); if (value null) continue; dict[columns[c]] value; } rows.Add(dict); }IsEmptyRow 判断整行所有单元格是否都为 null 或空字符串为空就直接跳过。这个判断很重要因为 Excel 的格式会拖出一大片空白行LastRowNum 虚高不跳过的话 JSON 里全是一堆空对象。如果想把表头“用户姓名”映射成嵌套的 user.name可以给 MappingOptions 增加一个字段名分隔符再用递归写入private void SetNested(Dictionarystring, object target, string path, object value) { var parts path.Split(.); var current target; for (int i 0; i parts.Length - 1; i) { if (!current.TryGetValue(parts[i], out var next) || next is not Dictionarystring, object) { next new Dictionarystring, object(StringComparer.OrdinalIgnoreCase); current[parts[i]] next; } current (Dictionarystring, object)next; } current[parts[^1]] value; }这段代码把 user.name 拆成两层先建 user 再塞 name。实际项目中这个功能被我用得很频繁尤其把运营给的扁平配置表转成前端要的树形结构时不用改 Excel只要在映射配置里把目标字段名写成带点的路径。3.5 序列化缩进、中文和空值策略组装好的 ListDictionarystring, object 直接用 System.Text.Json 序列化配置项反而比读取简单var options new JsonSerializerOptions { WriteIndented true, Encoder JavaScriptEncoder.UnsafeRelaxedJsonEscaping, DefaultIgnoreCondition JsonIgnoreCondition.WhenWritingNull }; string json JsonSerializer.Serialize(rows, options); File.WriteAllText(outputPath, json, new UTF8Encoding(false));WriteIndented 让 JSON 带缩进方便人眼检查和 git diffUnsafeRelaxedJsonEscaping 让中文字符正常显示而不是变成 \uXXXX 的转义序列这个对配置表场景尤其重要否则打开 JSON 全是一堆乱码一样的编码WhenWritingNull 控制空字段不输出JSON 干净不少。UTF8Encoding(false) 表示写文件时不要 BOM。如果输出的 JSON 要交给 Java 或 Go 后端读取带 BOM 的文件可能解析失败这也是一个常见坑。4. 跑通与避坑编译运行和五个经典翻车场景4.1 编译运行restore、build、run 三步走拿到源码后先用命令还原依赖再编译dotnet restore ExcelToJson.sln dotnet build ExcelToJson.sln -c Release --no-restore dotnet run --project ExcelToJson/ExcelToJson.csproj -c Release -- -i ./Examples/sample.xlsx -o ./result.json -s Sheet1日常调试不用这么麻烦用 IDE 打开 ExcelToJson.sln在项目属性里把命令行参数填成-i ./Examples/sample.xlsx -o ./result.json直接 F5 就能跑断点。首次 build 时间主要花在 NuGet 还原上之后增量编译很快。如果跑出来的 JSON 和预期不符优先检查是不是用了新版本的 Excel 格式而程序里选了错误的 Workbook 构造器。4.2 坑一整数列输出成 10023.0现象Excel 里一列都是整数编号转换后 JSON 里变成 id: 10023.0前端拿到手校验失败。原因NPOI 对数字单元格统一返回 double10023 在 double 里就是 10023.0直接塞进字典再序列化就带小数点了。解决取数之后判断raw % 1 0能整除就转成 long 再返回。我见过有人图省事用 ToString(0.####)但那样会把数值变成字符串前端类型对不上不如保持数值类型。这里要注意如果数字超过 long 的范围转 long 会溢出实际配置表场景里不太可能真遇到就改用 decimal。4.3 坑二合并单元格只保留左上角的值现象Excel 里“所属部门”跨三行合并转换后只有第一行有值后面两行是 null。原因合并单元格只在左上角存值其余单元格在底层模型里是空的NPOI 读取时不会自动回填。解决在遍历数据行之前把 Sheet 的 MergedRegions 扫一遍构建一个坐标映射把合并区域里的所有单元格都写上和左上角一样的值。代码大概是foreach (var region in sheet.MergedRegions) { var firstRow region.FirstRow; var firstCell region.FirstCell; var value GetCellValue(sheet.GetRow(firstRow)?.GetCell(firstCell)); for (int r region.FirstRow; r region.LastRow; r) for (int c region.FirstCell; c region.LastCell; c) if (value ! null) sheet.GetRow(r).GetCell(c).SetCellValue(value.ToString()); }要注意这个回填动作必须在真正读取行之前做否则已经读过的行不会重新取值。另外回填会改动内存里的 Sheet如果你的后续逻辑还需要保留原始合并信息记得用副本或者先记录再处理。4.4 坑三日期变成 45276 这种序号现象Excel 里显示 2024-01-15转换后输出 45276。原因日期在 NPOI 里本质是 double只有判断了单元格格式才能确认它是日期光看值完全看不出来。解决统一在 Numeric 分支调用 DateUtil.IsCellDateFormatted(cell)命中就按yyyy-MM-dd HH:mm:ss输出。这个函数判断的是单元格的显示格式只要 Excel 里设置了日期格式就能识别。如果某些单元格是文本写的日期走的是 String 分支那就保持原文输出不会变成序号。最怕的是同一列里日期格式和文本格式混着用所以我在 GetCellValue 里把两个分支都做了独立处理才能保证一列结果格式统一。4.5 坑四公式单元格取不到值现象Excel 里一列是A1B1这种公式转换后值为 null。原因公式单元格的 CellType 是 Formula不是 Numeric 也不是 String直接按默认分支处理就丢了。解决用 CachedFormulaResultType 判断公式的缓存结果类型。NPOI 读取的缓存值是 Excel 最近一次计算好的结果不需要额外触发公式计算引擎性能也不差。要注意如果文件是被程序生成的可能没有计算过公式缓存结果为空这种情况只能要求上游先打开保存一次或者用支持公式计算的扩展库但那种方案复杂得多配置表场景没必要。4.6 坑五尾部空行和换行符污染 JSON现象转换出的 JSON 里有大量空对象字符串值里还带 \r\n看起来非常脏。原因Excel 的格式行多、LastRowNum 虚高单元格内容被人为敲了换行。解决数据行循环里加 IsEmptyRow 判断整行没有有效值就 continue。字符串取值后做一次 Trim只去掉控制字符和首尾空格不要动数据内部的空格。我见过有人把所有空格全替换掉结果“张 三”变成“张三”这种坑比空行更隐蔽所以在清洗时保留内部空格只处理\r\n和首尾空位。5. 二次开发加映射配置、批量转换以及每次都该做的验证这个工具最常见的诉求是“一次转一个目录”。下一步改造我有两个建议先加字段映射配置再做批量处理。运营的 Excel 表头经常变今天叫“姓名”明天叫“用户姓名”每次改源码不划算。可以在根目录放一个 mapping.json{ user.name: 用户姓名, user.age: 年龄, user.address.city: 城市 }键是目标 JSON 路径值是 Excel 表头。读取每行数据后先去 mapping.json 里找当前列对应的目标字段名再用第 3 章的 SetNested 写进字典。这样表头怎么改都不用动代码改配置就行。批量转换的逻辑也不复杂foreach (var file in Directory.EnumerateFiles(inputDir, *.xlsx, SearchOption.AllDirectories)) { try { var json ConvertFile(file, mapping); File.WriteAllText(Path.ChangeExtension(file, .json), json); } catch (Exception ex) { File.AppendAllText(convert-error.log, ${file}: {ex.Message}\n); } }单文件失败不中断整个目录循环错误统一写进 log方便跑完再排查。这个模式很适合接进定时任务每天凌晨把一份目录里的表格全量刷成 JSON。最后说我的验证习惯。从那以后我每次改转换逻辑都强制先跑一遍 Examples 里的样例表把新旧 JSON 做一次 diff确认数字、日期、合并单元格三个高频坑没被改回来。这个习惯救了我好几次有一次重构完以为没问题结果 diff 出来 id 全部带上 .0一眼就拦下了。希望帮到你。本文还有配套的精品资源点击获取