C#实现Excel转JSON:带源码的落地工具,解决数据对接最后一公里

发布时间:2026/10/16 7:30:18
C#实现Excel转JSON:带源码的落地工具,解决数据对接最后一公里
简介C#实现Excel转JSON的工具及完整源码包面向需要在日常开发中处理表格数据、对接网页应用或非关系型数据库的程序员尤其适合学习序列化和文件操作的中级开发者。压缩包共包含36个文件整体约193KB既有能够直接运行的exe程序也有多个核心的C#源文件、工程配置、依赖dll以及窗体相关资源文件精简但覆盖了从界面到核心逻辑的完整结构方便在Visual Studio中打开调试。目前已有936人学习浏览。工程以完整实例演示了Excel向JSON转换的主要流程通过OleDbConnection连接旧版Excel工作簿借助OleDbDataAdapter填充DataTable再调用JavaScriptSerializer将结果序列化为JSON并写入文件清晰地展示了经典实现方式。工程内的窗体文件和入口程序分别承载了用户交互与启动逻辑帮助理解工具的组织方式。其中还涉及新版Excel文件所需的连接驱动差异、异常处理、大数据量分批转换以及EPPlus、NPOI等第三方库的使用建议能够帮助读者避开实际开发中的常见问题并将这一能力迁移到自己的项目中。1. C# 实现 Excel 转 JSON一个带完整源码的落地工具解决数据对接最后一公里如果你做过数据清洗或前后端联调一定遇到过这种场景运营同学甩来一个 Excel里面是几十列的商品配置或用户标签后端接口要的是 JSON你只能手动复制粘贴或者在 Python 里临时写个 pandas 脚本处理完还得另存一份。这个 C# 项目就是为解决这个问题而生的——它把 Excel 文件解析成结构化的 JSON 输出支持多 Sheet、表头映射、数据类型推断而且自带源码你可以直接编译运行也可以把它拆成类库集成到自己的数据平台或接口服务里。适合经常需要做 Excel 与 JSON 数据互转的 .NET 开发者也适合把「Excel 导入生成 JSON 配置」作为功能模块嵌入业务系统的场景。整套工具的体积不大核心思路却很完整读取 Excel 工作簿、按规则解析单元格、生成 JSON 字符串再落盘。项目里把这三步拆成了清晰的分层每一层都能单独替换这也是我愿意把它拆开讲的原因——它不是一个黑匣子工具而是一个可以按需改造成「报表导入接口」或「配置生成器」的基础工程。2. 架构与工程结构从 Excel 文件到 JSON 字符串的三层管线拿到源码的第一件事不是急着编译而是先把整个工程的目录结构摸清楚。这个项目的解决方案里分了三个核心项目一个控制台入口程序、一个 Excel 解析类库、一个 JSON 构建器。控制台程序负责接收参数和文件路径解析类库负责把 XLSX/XLS 文件读成行列表格数据JSON 构建器再把行列数据转成目标 JSON 格式。三层之间通过内存数据模型传递互不依赖具体实现。2.1 解决方案目录与项目职责划分先看整个解决方案的物理结构。根目录下是解决方案文件然后有三个主要文件夹ConsoleApp入口程序、ExcelParser解析类库、JsonBuilderJSON 生成类库。此外还有一个 Tests 文件夹放了几个针对解析器和生成器的单元测试用 MSTest 写的跑一下就能验证核心逻辑是否正确。ExcelToJson.sln ├─ ConsoleApp/ │ ├─ Program.cs │ ├─ ArgumentParser.cs │ └─ appsettings.json ├─ ExcelParser/ │ ├─ ExcelReader.cs │ ├─ WorksheetData.cs │ ├─ CellValue.cs │ └─ DataTypeInferer.cs ├─ JsonBuilder/ │ ├─ JsonObjectBuilder.cs │ ├─ JsonArrayBuilder.cs │ └─ JsonSchemaConfig.cs ├─ Tests/ │ ├─ ExcelParserTests.cs │ └─ JsonBuilderTests.cs └─ docs/ └─ usage.mdConsoleApp 是整个工具的入口Program.cs 里处理命令行参数比如-f指定文件路径、-s指定要导出的 Sheet 名称、-o指定输出 JSON 文件路径。ArgumentParser 负责把参数解析成一个配置对象后续解析器只用读取这个对象不重复解析参数。这种设计让我觉得顺眼的地方在于就算你以后要把它改成 Web API也只需要把 ArgumentParser 换成 ASP.NET Core 的模型绑定核心逻辑不用动。ExcelParser 是整个项目的核心ExcelReader 用 NPOI 库读取工作簿把每个 Sheet 转成一个 WorksheetData 对象里面存储了行号、列名、单元格类型和值。DataTypeInferer 是一个独立的静态类负责判断某一列的数据是整数、浮点、日期还是字符串——这一步很关键因为 Excel 里单元格的显示样式往往会骗人看起来是数字的单元格底层的存储类型可能是字符串。JsonBuilder 则是一个纯内存操作模块JsonArrayBuilder 接收 List 遍历行和列逐层构建 JSON。这个模块不依赖任何 JSON 序列化框架而是用 StringBuilder 手动拼接 JSON 字符串。可能有读者会问直接用 Newtonsoft.Json 序列化不是更省事吗这里手写拼接的原因是为了精确控制键的顺序、空值策略、表头重命名等细节避免反射序列化带来的意外行为。2.2 Excel 解析层的核心设计单元格数据的归一化处理Excel 解析是整个工具里最容易翻车的地方因为 XLSX 的底层 XML 结构和 XLS 的二进制结构差异很大。项目里用 NPOI 库统一处理这两种格式NPOI 可以从 .xls 的 HSSFWorkbook 和 .xlsx 的 XSSFWorkbook 中抽取出统一的 ICell 接口这样上层代码就不用关心文件格式差异了算是一个经典适配器思想的落地。public static WorksheetData ReadSheet(ISheet sheet) { var data new WorksheetData(sheet.SheetName); // 获取第一行作为表头 IRow headerRow sheet.GetRow(0); if (headerRow null) return data; for (int col 0; col headerRow.LastCellNum; col) { var headerCell headerRow.GetCell(col); string headerName headerCell?.ToString()?.Trim(); if (string.IsNullOrEmpty(headerName)) continue; data.Columns.Add(headerName); } // 从第二行开始读取数据 for (int rowIdx 1; rowIdx sheet.LastRowNum; rowIdx) { IRow row sheet.GetRow(rowIdx); if (row null) continue; var rowData new ListCellValue(); for (int col 0; col data.Columns.Count; col) { ICell cell row.GetCell(col); rowData.Add(ExtractCellValue(cell)); } data.Rows.Add(rowData); } return data; }这段代码的关键逻辑有两个第一它把第一行当作表头列名如果某些列没有表头就跳过避免生成无意义的键第二它从第二行开始逐行读取每个单元格都交给 ExtractCellValue 做归一化处理。行数据被存成 List CellValue 是一个自定义结构体它同时保存了原始值和推断类型这是为了让后续的 JSON 构建器能根据类型生成不带引号的数字或布尔值。这里我必须说一下 ExtractCellValue 的实现思路。NPOI 的 ICell 有 CellType 属性枚举值包括 String、Numeric、Boolean、Formula、Blank 等。但实际场景里经常出现单元格被格式化成「文本」但实际上写的是数字或者明明是日期单元格类型却是 Numeric需要靠 DateUtil.IsCellDateFormatted 来判断。因此 ExtractCellValue 里做了好几层兜底判断最终返回一个 CellValue 对象其中的 NormalizedValue 已经确定了数据类型。2.3 JSON 构建层的设计为什么要手写拼接而不是序列化如果你打算改造成自己的工具最需要理解的就是 JsonBuilder 这一层。它不依赖 Newtonsoft.Json而是用 StringBuilder 手动拼字符串核心原因有三个一是要严格控制 JSON 键的输出顺序Excel 的列顺序就是 JSON 的键顺序这在配置类场景里很重要二是要处理空值策略默认跳过空单元格这在字典序列化时不容易控制三是性能问题几万行数据时反射序列化会明显卡顿而手动拼接几乎是非线性开销。public static string BuildArrayJson(Liststring columns, ListListCellValue rows, bool skipEmpty true, string indentation ) { var sb new StringBuilder(); sb.AppendLine([); for (int i 0; i rows.Count; i) { var row rows[i]; sb.Append(indentation {); Liststring kvPairs new Liststring(); for (int c 0; c columns.Count; c) { var val row[c]; if (skipEmpty val.IsEmpty) continue; string key JsonEscape(columns[c]); string value FormatValue(val); kvPairs.Add($\{key}\: {value}); } sb.Append(string.Join(, , kvPairs)); sb.Append(i rows.Count - 1 ? }, : }); sb.AppendLine(); } sb.AppendLine(]); return sb.ToString(); }FormatValue 方法根据 CellValue 的 ValueType 分派整数类型写成123浮点写成12.5布尔写成true/false字符串则调用 JsonEscape 处理特殊字符并加上双引号。这个分派逻辑一定要在 Excel 解析阶段就确定不能在 JSON 构建时再去猜否则遇到全数字字符串比如电话号码就会被错误地去掉引号导致后端把 00123 解析成 123。有一点要特别提醒这个项目的默认配置是输出 JSON 数组每个 Excel 行对应数组中的一个对象。但如果你的 Excel 里只有一行配置你想要的可能是单个 JSON 对象而不是数组那就要用 JsonObjectBuilder 或者对输出再做一层包装。源码里其实已经预留了 JsonSchemaConfig可以通过配置切换「数组模式」和「对象模式」具体的用法我放在后面的改造章节里展开。3. 核心实现解析从单元格类型推断到 JSON 字符串的完整链路这一章我会挑几段最核心的代码逐步讲从类型推断到日期处理再到字符串转义都是实际运行中容易踩坑的位置。每一段代码背后都有具体的数据场景不是那种「示例代码只用来演示语法」的写法而是能直接处理真实 Excel 的健壮实现。3.1 单元格类型推断如何避免把电话号码和日期搞错DataTypeInferer 这个类的输入是一个 CellValue包含原始值和 Excel 原生类型输出是一个 JsonValueType 枚举包括 Integer、Double、Boolean、String、DateTime、Null。规则并不复杂但每一层都对应一种我在实际项目中遇到过的翻车现场。public static JsonValueType InferType(object rawValue, CellType excelType) { if (rawValue null) return JsonValueType.Null; // Excel 的布尔单元格 if (excelType CellType.Boolean) return JsonValueType.Boolean; // Excel 的公式单元格需要先求值 if (excelType CellType.Formula) { if (rawValue is double d) return IsIntegerValue(d) ? JsonValueType.Integer : JsonValueType.Double; return JsonValueType.String; } // 数字单元格判断是否是整数 if (excelType CellType.Numeric) { if (rawValue is DateTime dt IsDateCell) return JsonValueType.DateTime; if (rawValue is double num) return IsIntegerValue(num) ? JsonValueType.Integer : JsonValueType.Double; } // 字符串单元格尝试识别是否为「看起来像数字但其实是文本」的情况 if (excelType CellType.String) { string str rawValue.ToString(); if (string.IsNullOrEmpty(str)) return JsonValueType.Null; // 电话号码、身份证等长数字不转类型 if (str.Length 10 str.All(char.IsDigit)) return JsonValueType.String; if (long.TryParse(str, out _)) return JsonValueType.Integer; if (decimal.TryParse(str, out _)) return JsonValueType.Double; return JsonValueType.String; } return JsonValueType.String; }这段代码里最值得琢磨的是字符串单元格的处理逻辑。很多 Excel 工具把数字列设置成文本格式此时 NPOI 拿到的原始值就是字符串而不是数字。如果盲目把它转成 Integer就会丢掉前导零如果不转后端拿到的又是一堆带引号的字符串。这个项目采用了一个务实的策略长度 10 位以上的纯数字一律保留为字符串把用户当成电话号码或身份证号来处理短数字则尝试转成真正的数字类型。这个启发式策略有例外情况我在避坑章节里会具体讲。还有一种特殊情况是公式单元格。Excel 里经常有A1B1这种公式列NPOI 读取时如果用的是DataFormatter或者直接ToString()拿到的可能是公式表达式本身而不是计算结果。项目里这里做了两层处理如果拿到的是 double就按数值类型推断否则回到字符串。这个方法不完美但作为工具已经够用。3.2 日期单元格最隐蔽的坑全在这一段代码里日期是 Excel 转换里的大坑。你在 Excel 里看到一个单元格显示2024-06-15NPOI 里它的原生类型可能是 Numeric底层值其实是 45458 这样的天数序号。如果直接转成 doubleJSON 里就会出现一个莫名其妙的数字。项目里的 ExtractCellValue 方法专门处理了这个问题用 NPOI 的 DateUtil 判断单元格是否被格式化为日期。private static CellValue ExtractCellValue(ICell cell) { if (cell null) return CellValue.Empty(); switch (cell.CellType) { case CellType.Numeric: // 关键判断这个数字单元格是否被格式化为日期 if (DateUtil.IsCellDateFormatted(cell)) { DateTime dateValue cell.DateCellValue; return new CellValue(dateValue, JsonValueType.DateTime); } return new CellValue(cell.NumericCellValue, IsIntegerValue(cell.NumericCellValue) ? JsonValueType.Integer : JsonValueType.Double); case CellType.String: var strVal cell.StringCellValue; return new CellValue(strVal, DataTypeInferer.InferType(strVal, CellType.String)); case CellType.Boolean: return new CellValue(cell.BooleanCellValue, JsonValueType.Boolean); default: return CellValue.Empty(); } }我当初第一次运行这个工具时遇到过一个火葬场式的场景有一列日期长这样2024/6/1DateUtil.IsCellDateFormatted 返回了 trueDateTime 输出是2024-06-01T00:00:00但客户要求的是2024-06-01这种纯日期格式。这个工具默认对 DateTime 类型做了一次格式化裁剪去掉时间部分但如果你需要保留时分秒就得自己改动 JsonBuilder 里的 FormatDateTime 方法。另外说一个容易踩的细节cell.DateCellValue和cell.NumericCellValue不能混用如果你先访问了 NumericCellValue再访问 DateCellValue有些版本的 NPOI 会直接抛异常。所以这段代码在分支里互斥访问这也是我在实际改代码时加的一个重要保护。3.3 字符串转义与 JSON 合法性不能只在中文符上翻车JSON 字符串拼接看着简单但有一个很容易被忽略的细节——Excel 单元格里可能出现换行、制表符、双引号和反斜杠。如果不对这些字符做转义生成出来的 JSON 文件根本无法通过反序列化。项目里 JsonEscape 方法专门处理这个。public static string JsonEscape(string input) { if (string.IsNullOrEmpty(input)) return string.Empty; StringBuilder sb new StringBuilder(input.Length 16); foreach (char c in input) { switch (c) { case : sb.Append(\\\); break; case \\: sb.Append(\\\\); break; case \n: sb.Append(\\n); break; case \r: sb.Append(\\r); break; case \t: sb.Append(\\t); break; case \b: sb.Append(\\b); break; case \f: sb.Append(\\f); break; default: // 控制字符统一转成 \uXXXX避免非法 JSON if (c 0x20) sb.Append(\\u).Append(((int)c).ToString(x4)); else sb.Append(c); break; } } return sb.ToString(); }这段代码的逻辑很直白但它的价值在于兜底了所有 JSON 规范中的控制字符。我看过不少开源的 Excel 转 JSON 脚本只转义了双引号和反斜杠遇到换行就挂了。这里把小于 0x20 的控制字符统一转成\uXXXX虽然实际场景中少见但加上这个分支能让你生成的 JSON 在任何解析器下都是合法的。还有一个值得说的点非 ASCII 字符比如中文不需要转义JSON 规范允许直接输出 UTF-8 中文。但要注意的是生成文件时必须用 UTF-8 编码写入并且最好带 BOMByte Order Mark否则某些 Windows 工具打开乱码。项目里 Program.cs 写文件时用的是File.WriteAllText(path, json, new UTF8Encoding(true))这个细节不加的话Excel 中文内容在 Windows 记事本里会显示乱码。3.4 命令行入口的交互设计为了让工具能被脚本调用最后看一下 ConsoleApp 的 Program.cs 入口。它不是一个交互式控制台程序不会弹窗让你选文件而是纯粹的命令行参数驱动设计这样做的最大好处是可以被批处理脚本或定时任务调用。比如你每天凌晨跑一个 Excel 转 JSON 的任务只需要在任务计划程序里写一行命令就够了。static int Main(string[] args) { try { var config ArgumentParser.Parse(args); if (config.ShowHelp) { PrintUsage(); return 0; } var workbook ExcelReader.LoadWorkbook(config.FilePath); var sheet ExcelReader.GetSheet(workbook, config.SheetName, config.SheetIndex); var worksheetData ExcelReader.ReadSheet(sheet); string json; if (config.OutputMode OutputMode.Array) { json JsonArrayBuilder.BuildArrayJson(worksheetData.Columns, worksheetData.Rows, config.SkipEmpty); } else { json JsonObjectBuilder.BuildObjectJson(worksheetData.Columns, worksheetData.Rows, config.KeyColumn); } if (!string.IsNullOrEmpty(config.OutputPath)) { File.WriteAllText(config.OutputPath, json, new UTF8Encoding(true)); return 0; } Console.WriteLine(json); return 0; } catch (Exception ex) { Console.Error.WriteLine($转换失败: {ex.Message}); return 1; } }入口代码本身没有太复杂的技术含量但 Main 方法的返回值设计很讲究——返回 0 表示成功返回 1 表示失败这和 shell 脚本的习惯一致方便在 CI/CD 流程里判断任务是否执行成功。异常信息统一写到 stderr这样你可以在批处理里把错误日志重定向到文件而不会和正常的 JSON 输出混在一起。ArgumentParser 支持几个核心参数-f指定文件路径必填-s指定 Sheet 名称如果同时给了-s和-i-s优先-m指定输出模式array 或 object-k在 object 模式下指定哪一列作为 JSON 键--skip-empty控制是否跳过空单元格。参数解析失败时会打印帮助信息告诉用户正确用法这个体验做得还算友好。4. 运行、编译与避坑指南让工具在你的机器上跑起来源码拿到手最关心的问题通常是「怎么编译、怎么跑、有什么坑」。这一章我按实际操作的顺序来写从环境准备开始到编译选项再到运行示例和常见问题排查。如果你之前没怎么碰过 C# 项目跟着这一章走一遍就能跑出第一个 JSON 文件。4.1 环境准备与编译.NET SDK 和 NuGet 依赖这个项目面向 .NET 6 或以上版本用了 nullable reference types 和文件级命名空间。编译前先确认本机安装的 SDK 版本命令是dotnet --version。如果版本不够可以先装一个 .NET 6 SDK不会影响系统里已有的其他 .NET 版本两者可以共存。依赖方面只需要两个 NuGet 包NPOI 用于读写 Excel 文件版本不低于 2.6.0MSTest 用于跑单元测试只在 Tests 项目里引用。由于代码里已经写好了 csproj 文件直接执行以下命令就能还原并编译整个解决方案# 还原 NuGet 包并编译 Release 版本 dotnet restore ExcelToJson.sln dotnet build ExcelToJson.sln -c Release # 编译完成后找到控制台程序的输出目录 cd ConsoleApp/bin/Release/net6.0/编译过程如果卡在 NuGet 还原阶段最常见的原因是网络问题导致 NPOI 包下载超时。解决办法是配置国内的 NuGet 镜像源在项目根目录新建一个NuGet.Config文件把仓库地址指向镜像源。这里提醒一下.csproj文件里的 TargetFramework 是net6.0如果你的机器只有 .NET 8 SDK需要改成net8.0再编译或者直接用dotnet build -f net8.0指定目标框架如果项目没做多框架配置最稳妥的办法是编辑 csproj 文件。4.2 实际运行示例一个真实 Excel 文件的转换过程假设你有一个库存数据表inventory.xlsx列结构是商品编号、商品名称、单价、库存数量、是否上架、入库日期。用这个工具转换的命令如下# 基本用法数组模式输出到指定文件 dotnet run --project ConsoleApp -- -f inventory.xlsx -s 库存表 -o inventory.json # 如果 Excel 里只有一个 Sheet可以不指定 -s dotnet run --project ConsoleApp -- -f inventory.xlsx -o inventory.json # 把 JSON 直接打印到控制台不写文件方便管道处理 dotnet run --project ConsoleApp -- -f inventory.xlsx生成的 JSON 数组会长这样每行 Excel 数据变成数组里的一个对象[ { 商品编号: A1001, 商品名称: 机械键盘, 单价: 299, 库存数量: 320, 是否上架: true, 入库日期: 2024-05-12 }, { 商品编号: A1002, 商品名称: 静音鼠标, 单价: 89.9, 库存数量: 450, 是否上架: false, 入库日期: 2024-05-30 } ]注意看这里的几个细节商品编号是字符串即使 Excel 里它只是数字但 NPOI 读出来是文本单元格所以带上引号单价 299 是整数不带引号是否上架是布尔值入库日期输出成年月日格式。这些都是前面类型推断逻辑的实际表现。如果调试的时候发现dotnet run每次都要重新编译太慢可以先用dotnet build -c Release编译一次然后直接运行ConsoleApp/bin/Release/net6.0/ExcelToJson可执行文件Windows 上是 .exe。这个可执行文件不依赖额外安装的 .NET 运行时吗如果发布成框架依赖目标机器需要安装 .NET Runtime如果你要发给没有 .NET 环境的人用就用dotnet publish发布成自包含模式命令我放在下一节。4.3 发布成单文件工具发给同事也能直接跑项目的主要使用场景是内部工具所以发布方式很关键。如果直接复制编译出来的 DLL 给同事对方机器上没有 .NET 运行时跑不起来。用 .NET 的发布功能可以生成一个单文件的可执行程序把运行时和依赖全部打进一个 exe 里。# 发布自包含单文件适用于 Windows x64 dotnet publish ConsoleApp/ConsoleApp.csproj -c Release -r win-x64 --self-contained true /p:PublishSingleFiletrue -o publish/ # 生成的 ExcelToJson.exe 可以直接双击或命令行调用 publish/ExcelToJson.exe -f inventory.xlsx -o inventory.json发布参数里-r win-x64指定运行时标识符--self-contained true表示自包含PublishSingleFile为 true 时会把所有 DLL 打包进单个 exe配合-p:EnableCompressionInSingleFiletrue还能进一步压缩体积。注意自包含发布体积较大约 60-80MB因为内置了整个 .NET Runtime这是为了兼容性付出的代价。如果你要发给 Mac 或 Linux 用户把win-x64换成osx-x64或linux-x64再发布一次就有对应平台的可执行文件了。这一招在实际项目里很管用——我见过太多在开发机上跑得飞起、拿给同事就报「没有 .NET Runtime」的工具自包含发布算是后悔药省得挨个装环境。4.4 避坑指南NPOI 读取 Excel 时的 4 个高频踩坑记录以下几条都是我在跑这个项目时真实踩过的坑每条都按「现象 → 原因 → 解决」写希望你能跳过这些弯路。坑 1读 .xls 文件时抛异常提示POIFSFileSystem无法识别现象工具能正常读 .xlsx但一读旧的 .xls 文件就异常。原因是没有引入 NPOI 对 .xls 格式的解析支持或者引入的 NPOI 版本较旧对老格式兼容不够。解决确认 NuGet 包是 NPOI 2.6同时代码里ExcelReader.LoadWorkbook需要对文件扩展名做分支用HSSFWorkbook处理 .xls用XSSFWorkbook处理 .xlsx。项目里这两者都写了建议你检查一下是否走对分支。坑 2日期列输出全是2024-01-01T00:00:00这种带时间的长格式现象Excel 里明明只显示了日期没有时间输出 JSON 却带着T00:00:00。原因DateUtil.IsCellDateFormatted 判断了日期格式但 DateTime 值本身带时间部分格式化时没裁剪。解决在 JsonBuilder 里对 DateTime 类型做一次判断如果时分秒都为 0 则只输出日期部分否则输出完整时间。坑 3单元格里明明是数字 00123输出后引号没了变成 123现象商品编号列以文本格式存储数字是 00123生成 JSON 后变成 123。原因数据处理分支认为这种长度小于 10 的纯数字是整数把它转成了数字类型。解决如果要保留前导零可以把这列的 Excel 单元格格式改成「文本」或者修改 DataTypeInferer 的启发式规则把str.All(char.IsDigit)判断提前只要带前导零的字符串一律按字符串处理。我在改造章节里会给出一个更灵活的配置方案。坑 4生成的文件用某些 JSON 编辑器打开报错提示第 1 行第 1 列有非法字符现象用 Visual Studio Code 或 Notepad 打开 JSON 文件时看得到内容但在线 JSON 校验工具报错。原因文件带 UTF-8 BOM部分严格校验器会把 BOM 当作非法字符。解决如果文件是给程序消费的保留 BOM 没问题如果要给人看或给 Node.js 工具处理改用new UTF8Encoding(false)生成无 BOM 文件。这个开关在 Program.cs 里改一行即可。4.5 常见问题运行时报错、输出不一致的排查路径如果你已按前面的步骤编译运行但结果不符合预期按这个顺序排查效率最高。第一确认 Excel 文件第一行确实是表头这个工具默认把第一行作为列名如果你的文件第一行是标题或说明文字生成的 JSON 键会很难看建议先用 Excel 打开检查一遍。第二确认要导出的 Sheet 名称正确-s参数是精确匹配的不支持通配符。可以用-s不指定 Sheet 名称工具会默认导出第一个 Sheet或者看一下帮助信息里的-l参数它列出所有 Sheet 名称。第三如果输出的 JSON 某个值是null而不是你预期的空字符串说明 Excel 单元格是空白的。项目默认跳过空单元格这个字段在 JSON 对象里就直接不存在了。如果你希望输出或null修改 JsonBuilder 中的 skipEmpty 参数即可命令行对应的是--no-skip-empty。第四如果程序报错「至少一个 Sheet 无法读取」多半是 Excel 文件正在被 WPS 或 Office 打开文件被锁定了。关闭 Excel 程序再运行一次。5. 二次开发改造思路把它从「单机工具」变成「数据管道模块」源码在手最有价值的不是直接编译使用而是把它改造成你自己的业务模块。我提供一个我实际操作过的改造路径把 ConsoleApp 的解析逻辑挪到一个类库里暴露一个干净的 API然后在一个 ASP.NET Core Web API 里调用它变成「上传 Excel 文件 → 服务端解析 → 返回 JSON」这样前端就能直接预览解析结果。改造的关键是抽出ExcelToJsonConverter这个门面类把原来的 Program.cs 里的三步流程读工作簿、解析 Sheet、构建 JSON封装成一个可复用的方法public class ExcelToJsonConverter { private readonly ExcelParserOptions _options; public ExcelToJsonConverter(ExcelParserOptions options) { _options options; } public string Convert(Stream fileStream, string fileName) { // 根据扩展名选择 HSSFWorkbook 或 XSSFWorkbook IWorkbook workbook; if (fileName.EndsWith(.xls, StringComparison.OrdinalIgnoreCase)) workbook new HSSFWorkbook(fileStream); else workbook new XSSFWorkbook(fileStream); var sheet workbook.GetSheet(_options.SheetName) ?? workbook.GetSheetAt(0); var data ExcelReader.ReadSheet(sheet); return JsonArrayBuilder.BuildArrayJson( data.Columns, data.Rows, _options.SkipEmpty); } }注意这里的方法签名接收 Stream 而不是文件路径这是为了让它能直接在 Web API 里用IFormFile.OpenReadStream()调用。用了这个门面类之后控制器代码就非常薄了一个上传接口加一个解析方法总共不到二十行。原来的 Program.cs 里的命令行逻辑可以保留也可以删除取决于你是否还需要命令行工具。再做深一步你会发现真正的瓶颈在类型推断的灵活性上。目前的启发式规则是写死的比如「10 位以上纯数字视为字符串」。二次开发时合理的做法是引入一个配置选项ColumnTypeOverrides让调用方指定某列必须当作什么类型处理。这个配置是一个字典键是列名值是String、Number、Boolean、Date四个枚举之一在读取 Sheet 后覆盖默认推断结果。实现方式不复杂ReadSheet之后遍历一次列名如果配置里存在该列则强制修改这一列所有 CellValue 的类型标记。最后提一个容易忽略的性能优化点如果 Excel 文件有几十万行逐行拼字符串的内存开销会变得可观。原来的实现把所有数据先读进内存再拼字符串最高峰时内存占用约是文件体积的 5 到 8 倍。改造时要么分页读取、分批写入文件要么直接用Utf8JsonWriter流式写入后者更优雅而且能保证 JSON 合法性。我一直保留着这个项目里的一个习惯任何 Excel 导出的功能我都会在交付前用一份「带公式、带合并单元格、带空行、带前导零」的四害俱全的测试文件跑一遍完整流程。从那以后每次做完类似的转换工具我都强制自己先跑这份测试文件再交付少被业务方反馈「你这里怎么还是不对」。这个工程里最值得你改的第一行代码就是从把你自己的脏数据文件跑通开始希望这个工具能帮你省下一点在 Excel 和 JSON 之间来回切换的时间。本文还有配套的精品资源点击获取