简介:这是一份面向C#开发者与数据处理人员的Excel转JSON工具源码包,适合需要在Web应用或NoSQL数据库导入结构化数据时快速完成格式转换的初中级程序员。压缩包共36个文件,约193KB,包含6个cs源码文件、1个csproj工程文件与1个sln解决方案,另有exe、dll、pdb等编译产物及config、resx、json等配置资源,构成一套可直接运行的完整Visual Studio项目。源码演示了通过OleDbConnection读取Excel、用OleDbDataAdapter填充DataTable,再借助JavaScriptSerializer序列化为JSON并写入文件的完整链路,同时提示了.xls与.xlsx连接字符串差异、异常处理、分批读取与数据清洗等实践要点。目前已有936人学习下载,读者可据此掌握文件I/O与数据序列化思路,并封装为可复用工具类或替换为EPPlus、NPOI等第三方库。
1. 一个把 Excel 表格变成 JSON 的 C# 小工具,为什么值得你拆一遍
如果你做过配置表驱动的项目,大概率经历过这种场面:策划在 Excel 里改完数值,程序这边要手动复制粘贴成 JSON,改一次错一次,最后谁也不知道线上跑的是哪版数据。这个用 C# 实现的 Excel 转 JSON 工具,解决的就是这个链路问题——它把 .xlsx 文件按表头映射成结构化 JSON,支持多 Sheet、类型推断和批量导出,适合做游戏配置管线、后台数据初始化、接口 Mock 数据生成这类场景。技术栈是 .NET + C#,核心依赖 Excel 数据读取库和 JSON 序列化库,整个项目结构不复杂,但麻雀虽小,配置表转换里那些坑一个不少。接下来我按「能干什么 → 怎么跑 → 怎么改」的顺序,把这份源码拆开讲。
2. 项目结构与技术选型:从 Excel 到 JSON 的数据链路
2.1 整体架构:三层职责划分
这个工具虽然体量不大,但结构上做了清晰的职责分离,我把它拆成三层来看:
- 读取层:负责打开 Excel 文件、定位 Sheet、读取单元格数据。常见做法是用 NPOI 或 EPPlus 这类库,前者对 .xls 和 .xlsx 都兼容,后者 API 更现代但主要支持 .xlsx。源码里如果用的是 NPOI,说明作者考虑到了老格式兼容;如果用 EPPlus,则更偏向新项目。
- 转换层:把二维表格数据映射成字典或对象。这里的关键是表头解析——第一行通常是字段名,第二行可能是类型声明,第三行开始才是数据。转换层要处理空单元格、合并单元格、类型转换(字符串转数字/布尔)这些脏活。
- 输出层:把转换后的对象序列化成 JSON 字符串,写入文件或返回给调用方。序列化一般用 System.Text.Json 或 Newtonsoft.Json,前者是 .NET 内置,后者生态更成熟。
三层之间通过接口或委托解耦,读取层不关心输出格式,转换层不关心数据来源。这种设计的好处是,以后要加 CSV 导出或数据库导入,只需要替换读取层或输出层,转换逻辑不用动。
2.2 目录结构:文件都在干什么
一个典型的项目目录大概长这样:
ExcelToJson/ ├── Program.cs # 入口,解析命令行参数 ├── Services/ │ ├── ExcelReader.cs # Excel 读取封装 │ ├── JsonConverter.cs # 表格到 JSON 的转换逻辑 │ └── FileWriter.cs # 输出文件写入 ├── Models/ │ ├── SheetConfig.cs # Sheet 配置模型 │ └── CellData.cs # 单元格数据模型 ├── Utils/ │ └── TypeHelper.cs # 类型推断与转换工具 └── appsettings.json # 配置文件(可选)Program.cs是入口,负责解析命令行参数,比如输入文件路径、输出目录、是否合并 Sheet 等。Services目录下是核心业务逻辑,Models放数据模型,Utils放通用工具。如果你拿到源码发现目录名不一样,不用纠结,按这个职责去对应就行。
2.3 技术选型:为什么是 NPOI + System.Text.Json
Excel 读取库的选择上,NPOI 的优势是免费开源、支持 .xls 和 .xlsx、不依赖 Office 安装。EPPlus 从 5.0 开始商业授权变严,如果是公司项目要注意合规。源码里如果用了 NPOI,那基本可以确定作者考虑到了跨平台和授权问题。
JSON 序列化方面,System.Text.Json 是 .NET Core 之后的内置库,性能好、内存占用低,但默认不支持循环引用和某些复杂类型。Newtonsoft.Json 功能更全,但多一个依赖。如果项目目标是 .NET 6+,用 System.Text.Json 更轻量;如果要兼容老项目,Newtonsoft.Json 更稳妥。
提示:如果你在 .NET Framework 4.x 下跑,NPOI 和 Newtonsoft.Json 的组合兼容性最好;如果是 .NET 6/8,System.Text.Json 加 NPOI 是更现代的选择。
3. 核心模块拆解:读取、转换、序列化的完整链路
3.1 Excel 读取:打开文件与定位 Sheet
读取层的第一步是打开工作簿。用 NPOI 的写法大概是这样:
using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; public IWorkbook OpenWorkbook(string filePath) { using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); // 根据扩展名判断用 XSSF 还是 HSSF if (filePath.EndsWith(".xlsx")) return new XSSFWorkbook(fs); else return new HSSFWorkbook(fs); }这段代码做了两件事:用FileStream以只读方式打开文件,然后根据扩展名选择XSSFWorkbook(.xlsx)或HSSFWorkbook(.xls)。注意using声明,确保文件句柄用完释放,否则在 Windows 上会锁文件,后续想覆盖写入就会报错。
定位 Sheet 的逻辑通常是按名称或索引:
public ISheet GetSheet(IWorkbook workbook, string sheetName) { // 优先按名称查找,找不到再按索引 var sheet = workbook.GetSheet(sheetName); if (sheet == null) sheet = workbook.GetSheetAt(0); // 默认第一个 return sheet; }参数说明:sheetName是配置里指定的 Sheet 名,如果为空则回退到第一个 Sheet。这个回退逻辑很实用,因为很多配置表只有一个 Sheet,不需要额外指定。
3.2 表头解析与类型推断
表头解析是转换层的核心。常见的表格结构是:第一行字段名,第二行类型(string/int/bool),第三行开始数据。解析逻辑如下:
public List<Dictionary<string, object>> ParseSheet(ISheet sheet) { var result = new List<Dictionary<string, object>>(); var headerRow = sheet.GetRow(0); var typeRow = sheet.GetRow(1); // 类型行,可能为空 int colCount = headerRow.LastCellNum; // 收集字段名和类型 var fields = new List<(string Name, string Type)>(); for (int i = 0; i < colCount; i++) { var nameCell = headerRow.GetCell(i); var typeCell = typeRow?.GetCell(i); string name = nameCell?.ToString() ?? $"col_{i}"; string type = typeCell?.ToString() ?? "string"; fields.Add((name, type)); } // 从第三行开始读数据 for (int r = 2; r <= sheet.LastRowNum; r++) { var row = sheet.GetRow(r); if (row == null) continue; var dict = new Dictionary<string, object>(); for (int c = 0; c < fields.Count; c++) { var cell = row.GetCell(c); dict[fields[c].Name] = ConvertCell(cell, fields[c].Type); } result.Add(dict); } return result; }这段代码的关键点:headerRow.LastCellNum拿到列数,typeRow可能为 null 所以用了空条件运算符。数据行从索引 2 开始(第三行),每行构造一个字典。ConvertCell负责把单元格值按类型转换,后面单独讲。
3.3 单元格类型转换:空值、日期、布尔的处理
ConvertCell是踩坑最多的地方,因为 Excel 的单元格类型很丰富:
private object ConvertCell(ICell cell, string type) { 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"); return cell.NumericCellValue; case CellType.Boolean: return cell.BooleanCellValue; case CellType.Formula: // 公式单元格按缓存值处理 return cell.CachedFormulaResultType switch { CellType.Numeric => cell.NumericCellValue, CellType.String => cell.StringCellValue, _ => cell.ToString() }; default: return cell.ToString(); } }逻辑说明:先判空,再按CellType分支。数值类型要额外判断是不是日期格式,因为 Excel 里日期本质是数字,不判断的话会输出一串浮点数。公式单元格用CachedFormulaResultType拿缓存值,避免重新计算公式。type参数在这里其实没直接用上,因为 NPOI 已经能判断单元格类型,但如果你的表格里类型行和实际值不一致,可以在这里加一层强制转换。
注意:日期格式的判断依赖单元格的格式设置,如果 Excel 里日期列被设成「常规」,
IsCellDateFormatted会返回 false,输出就是数字。这种情况要么在 Excel 里改格式,要么在代码里按字段名硬编码日期字段。
3.4 JSON 序列化与输出
转换完的List<Dictionary<string, object>>直接序列化:
using System.Text.Json; public string ToJson(List<Dictionary<string, object>> data) { var options = new JsonSerializerOptions { WriteIndented = true, // 格式化输出 Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping // 中文不转义 }; return JsonSerializer.Serialize(data, options); }参数说明:WriteIndented让 JSON 带缩进,方便人看;UnsafeRelaxedJsonEscaping让中文直接输出而不是转成\uXXXX。如果你用 Newtonsoft.Json,对应的是Formatting.Indented和StringEscapeHandling.EscapeNonAscii的反向设置。
输出到文件:
public void WriteToFile(string json, string outputPath) { var dir = Path.GetDirectoryName(outputPath); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); File.WriteAllText(outputPath, json, Encoding.UTF8); }先确保目录存在,再写文件。用 UTF-8 编码,避免中文乱码。
4. 跑起来:环境准备、命令行参数与批量导出
4.1 环境准备与依赖安装
跑这个项目需要 .NET SDK,版本看源码的.csproj文件。如果是net6.0或net8.0,去装对应 SDK;如果是net472,需要 Visual Studio 或 MSBuild。依赖库通过 NuGet 安装:
dotnet add package NPOI dotnet add package System.Text.Json如果源码里用的是 Newtonsoft.Json,把第二行换成dotnet add package Newtonsoft.Json。装完dotnet restore拉齐依赖,然后dotnet build编译。
4.2 命令行参数与单文件转换
入口Program.cs通常解析几个参数:
dotnet run -- --input ./data/config.xlsx --output ./out/config.json --sheet Sheet1参数含义:--input是 Excel 文件路径,--output是 JSON 输出路径,--sheet指定 Sheet 名(不传则用第一个)。解析逻辑可以用System.CommandLine库,也可以手写args遍历。手写的话大概是这样:
string input = null, output = null, sheet = null; for (int i = 0; i < args.Length; i++) { switch (args[i]) { case "--input": input = args[++i]; break; case "--output": output = args[++i]; break; case "--sheet": sheet = args[++i]; break; } }这种写法简单直接,适合小工具。如果参数多了,建议换成System.CommandLine,自带帮助信息和参数校验。
4.3 批量导出:遍历目录下所有 Excel
实际项目里往往是一个目录下几十个 Excel 都要转,批量模式很有必要:
public void BatchConvert(string inputDir, string outputDir) { var files = Directory.GetFiles(inputDir, "*.xlsx", SearchOption.AllDirectories); foreach (var file in files) { var relative = Path.GetRelativePath(inputDir, file); var outPath = Path.Combine(outputDir, Path.ChangeExtension(relative, ".json")); var workbook = OpenWorkbook(file); var sheet = GetSheet(workbook, null); var data = ParseSheet(sheet); var json = ToJson(data); WriteToFile(json, outPath); } }逻辑说明:递归查找所有.xlsx,保持相对目录结构输出到目标目录。Path.ChangeExtension把扩展名换成.json。这样data/hero.xlsx会输出到out/data/hero.json,目录层级不丢。
提示:批量模式下建议加个 try-catch,单个文件出错不影响其他文件,错误信息记到日志里。
5. 避坑与常见问题:那些让我加班到凌晨的细节
5.1 空行导致 JSON 里多出空对象
现象:输出的 JSON 数组里出现{}空对象。原因:Excel 里有些行看起来是空的,但sheet.LastRowNum把它们算进去了,GetRow返回非 null 但所有单元格都是空。解决:在数据行循环里加判断,如果整行所有单元格都为空就跳过:
bool isEmpty = true; for (int c = 0; c < fields.Count; c++) { if (row.GetCell(c) != null && !string.IsNullOrWhiteSpace(row.GetCell(c).ToString())) { isEmpty = false; break; } } if (isEmpty) continue;5.2 数字被读成科学计数法
现象:长数字(如订单号、身份证号)在 JSON 里变成1.23456789012345E+18。原因:Excel 把长数字当数值存储,NPOI 读出来是 double,序列化时精度丢失。解决:在 Excel 里把该列设成文本格式,或者在ConvertCell里对数值类型做判断,超过一定长度就转成字符串:
if (cell.CellType == CellType.Numeric) { double val = cell.NumericCellValue; // 超过 15 位有效数字转字符串 if (Math.Abs(val) >= 1e15) return val.ToString("F0"); return val; }5.3 合并单元格只有第一个格子有值
现象:合并单元格对应的多行数据里,只有第一行有值,后面都是 null。原因:Excel 合并单元格只在左上角存值,其他格子是空的。解决:读取时判断单元格是否属于合并区域,如果是就取区域左上角的值:
private ICell GetMergedCell(ISheet sheet, int row, int col) { for (int i = 0; i < sheet.NumMergedRegions; i++) { var region = sheet.GetMergedRegion(i); if (region.IsInRegion(row, col)) return sheet.GetRow(region.FirstRow).GetCell(region.FirstColumn); } return sheet.GetRow(row)?.GetCell(col); }5.4 中文乱码
现象:输出的 JSON 文件用记事本打开中文是乱码。原因:写入时用了默认编码(可能是 GBK),或者 JSON 序列化时转义了中文。解决:写入用Encoding.UTF8,序列化时设置UnsafeRelaxedJsonEscaping。如果已经生成了乱码文件,用 VS Code 以 UTF-8 重新打开再保存。
5.5 文件被占用无法覆盖
现象:第二次运行时提示「文件正被另一进程使用」。原因:上一次读取的FileStream没释放,或者 Excel 还开着这个文件。解决:确保OpenWorkbook里的FileStream用了using,或者手动Dispose。另外提醒策划改完表要关掉 Excel,不然工具读不了。
6. 二次开发:加类型校验、换序列化库、接配置管线
拿到源码后,最值得改的三个方向:类型校验、序列化库替换、接入自动化管线。
加类型校验:现在的转换逻辑对类型行是「信任」的,如果 Excel 里写int但实际填了字符串,转换会静默失败或输出错误类型。可以在ConvertCell里加一层校验,类型不匹配时抛异常并记录行号列号:
private object ConvertWithValidation(ICell cell, string expectedType, int row, int col) { var value = ConvertCell(cell, expectedType); if (expectedType == "int" && value is not int) throw new InvalidDataException($"第 {row} 行第 {col} 列期望 int,实际为 {value?.GetType().Name}"); return value; }这样配置表出错时能第一时间定位,而不是等到运行时才发现数据不对。
换序列化库:如果项目从 Newtonsoft.Json 迁移到 System.Text.Json,注意几个差异:日期格式默认不同、循环引用处理不同、Dictionary<string, object>的序列化行为基本一致。迁移时先跑一遍对比测试,确认输出 JSON 结构一致再切换。
接入自动化管线:把这个工具挂到 CI 里,每次策划提交 Excel 后自动转 JSON 并提交到仓库。可以用 GitHub Actions 或 Jenkins,命令行调用dotnet run -- --input ./configs --output ./output,然后git add输出目录。这样程序拉到的永远是最新配置,不用手动同步。
验证方法:改完之后,拿一份包含各种边界情况的测试表跑一遍——空行、合并单元格、长数字、日期、公式、中文——对比输出 JSON 是否符合预期。我一般会准备一个test_cases.xlsx,每次改完转换逻辑都跑一遍,确认没回归。
从那以后我每次改转换逻辑,都强制先跑一遍边界测试表,确认空行、合并单元格、长数字这三类都正常才提交。希望这个拆解能帮你少走点弯路。
本文还有配套的精品资源,点击获取