简介:本资源是一套开箱即用的C# Excel转JSON工具及完整源码工程,面向.NET开发者、数据处理工程师及初学C#文件操作与序列化的学习者,解决结构化数据在Web应用集成、NoSQL导入等场景下的格式转换刚需。压缩包含36个文件,总计193KB,主体为6个核心C#源文件(含Form1.cs、Program.cs及ToJson逻辑类)、3个可执行exe文件(含GUI工具)、2个dll依赖库,以及sln/cspoj工程配置和resx/resources本地化资源,结构完整,支持直接编译运行。已有936人学习下载,资源由作者kenkao发布,代码基于OleDb读取Excel并使用JavaScriptSerializer序列化DataTable,兼顾原理清晰性与工程可用性;同时附带适配.xls格式的连接字符串说明及关键注意事项,便于读者理解底层机制、快速调试排错,并可基于此扩展对.xlsx的支持或集成EPPlus等现代库。
1. C#实现Excel转JSON:为什么你写的转换工具总在生产环境崩得悄无声息?
某开发者在交付一个数据配置平台时,用C#写了段“5分钟搞定”的Excel转JSON工具——本地跑通、测试用例全绿、UI也漂亮。结果上线第三天,运营同事上传一份带合并单元格的Excel,服务直接500;第四天,有人填了空行+中文逗号分隔的日期,生成的JSON里时间字段变成null还带双引号;第五天,日志里刷出System.OutOfMemoryException,只因一张20MB的销售明细表被拖进来……这不是玄学,是C#处理Excel时绕不开的三重暗礁:Excel格式黑匣子(xlsx/xls/biff5混杂)、单元格语义丢失(日期/数字/文本类型坍塌)、内存与异常边界失控。本文不讲NPOI或EPPlus的API文档复读,而是带你从零手写一个可嵌入生产系统、能打日志、可设超时、支持大文件流式读取、错误定位到行列号的C# Excel转JSON工具——附完整可运行源码,所有关键参数都标清作用域和取舍逻辑,连DateTime序列化时区怎么对齐都给你踩过坑。
2. 选型不是挑库,是选“可控性”:为什么放弃DataTable+JsonConvert.SerializeObject,而用流式Cell遍历
2.1 三种主流路径的血泪对比:从“快”到“稳”的代价
很多新手第一反应是:读Excel进DataTable,再用Newtonsoft.Json.JsonConvert.SerializeObject()一把梭。看似3行代码,实则埋下5个雷:
DataTable.ReadXml()或OleDbDataAdapter.Fill()对.xlsx支持极差,遇到<x:xf>样式定义直接抛InvalidDataExceptionDataTable自动类型推断把2024/3/15识别为DateTime,但把2024-03-15识别为string,JSON序列化后字段类型不一致- 空行、隐藏列、合并单元格会被
Fill()跳过,但业务方明确要求“第17行必须有注释” - 内存占用是Excel原始大小的8~12倍(
DataTable每行存Object数组+索引哈希表) - 错误堆栈不报Excel行列号,只报
JsonConvert内部NullReference
提示:如果你的Excel来自用户上传、格式不可控、单文件可能超50MB,请立刻放弃DataTable路径。它适合“已知结构+小文件+离线脚本”,不适合生产级转换服务。
2.2 最终方案:EPPlus + 自定义Cell流式解析器(非Full Load)
我们选用 EPPlus (v6.2.10+,.NET 6+),原因很务实:
- 原生支持
.xlsx(Office Open XML),不依赖Office COM组件 - 提供
ExcelPackage.Workbook.Worksheets[0].Cells[row, col]逐单元格访问,不加载整张表到内存 ExcelRange.Value返回object,但ExcelRange.DataType能告诉你这是Number/DateTime/String/Boolean/Empty- 支持
LoadFromCollection<T>反向写,但我们要的是读的确定性
核心设计原则:
✅不调用worksheet.Cells[1, 1, maxRow, maxCol].Value一次性取二维数组(会触发Full Load)
✅用for (int row = headerRow; row <= worksheet.Dimension.End.Row; row++)逐行迭代
✅每个Cell单独判断DataType,再决定如何转JSON值
✅用yield return返回IEnumerable<Dictionary<string, object>>,让调用方控制消费节奏
2.3 初始化EPPlus并设置安全上下文
// Program.cs 或 Startup.cs 中全局注册(仅需一次) ExcelPackage.LicenseContext = LicenseContext.NonCommercial; // 开源版限制,商用需购License // 若需商用,此处替换为 LicenseContext.Commercial + 有效LicenseKey // 创建解析器实例时传入自定义配置 var config = new ExcelToJsonConfig { HeaderRow = 1, // 表头在第几行(默认1) SkipEmptyRows = true, // 是否跳过全空行(业务常需保留空行作分隔) DateTimeFormat = "yyyy-MM-dd HH:mm:ss", // DateTime转字符串格式 MaxRows = 100000, // 单表最大行数防OOM(超限抛CustomException) Encoding = Encoding.UTF8 // 显式指定编码,避免BOM导致JSON解析失败 }; var converter = new ExcelToJsonConverter(config);ExcelToJsonConfig是我们封装的配置类,所有可调参数都在这里集中管理,后续章节会逐个说明其影响边界。
3. 核心转换逻辑:从Worksheet到JSON Array的7步映射链
3.1 步骤1:定位表头行并提取字段名(支持多级表头合并场景)
Excel中常见“第一行合并单元格写大标题,第二行才是字段名”。EPPlus的MergedCells属性能拿到所有合并区域,但我们不暴力拆分——而是只认“最下方非空行”为有效表头:
private string[] ExtractHeaders(ExcelWorksheet worksheet, int headerRow) { var headers = new List<string>(); var lastRowWithData = headerRow; // 向下扫描,找第一个有内容的行作为实际表头(兼容空行) for (int r = headerRow; r <= Math.Min(headerRow + 5, worksheet.Dimension?.End.Row ?? 1); r++) { bool hasContent = false; for (int c = 1; c <= worksheet.Dimension?.End.Column; c++) { if (worksheet.Cells[r, c].Value != null && !string.IsNullOrWhiteSpace(worksheet.Cells[r, c].Value.ToString())) { hasContent = true; break; } } if (hasContent) { lastRowWithData = r; break; } } // 取lastRowWithData这一行的所有非空单元格作为字段名 for (int col = 1; col <= worksheet.Dimension?.End.Column; col++) { var cell = worksheet.Cells[lastRowWithData, col]; var value = cell.Value?.ToString().Trim() ?? $"Column_{col}"; // 处理重复字段名:加序号后缀 if (headers.Contains(value)) { int suffix = 1; string newValue = $"{value}_{suffix}"; while (headers.Contains(newValue)) { newValue = $"{value}_{++suffix}"; } value = newValue; } headers.Add(value); } return headers.ToArray(); }参数说明:
headerRow默认为1,但若业务约定“表头总在第2行”,直接传2即可。此逻辑不依赖合并单元格API,规避了EPPlus对复杂合并的解析Bug(如跨工作表合并会抛NullReference)。
3.2 步骤2:逐行构建JSON对象:Cell值→C#类型→JSON值的三层转换
关键不在“怎么读”,而在“读出来是什么类型”。EPPlus的ExcelRange.Value返回object,但它的实际类型取决于Excel存储格式:
| Excel单元格类型 | EPPlus .Value 类型 | JSON序列化后 | 风险点 |
|---|---|---|---|
| 普通文本(含数字字符串) | string | "123" | 业务要数值?需int.TryParse |
| 数值(无格式) | double | 123.0 | 导致前端JStypeof 123.0 === 'number',但123也是number,无问题 |
| 日期(Excel序列号) | DateTime | "2024-03-15T00:00:00" | 时区错乱!Excel日期无时区,.NET默认转成本地时区 |
| 布尔值 | bool | true | 安全 |
| 空单元格 | null | null | 前端可能报错,需统一转""或0 |
因此,我们写一个CellValueToJsonObject方法:
private object CellValueToJsonObject(ExcelRange cell, string dateFormat) { if (cell.Value == null) return null; return cell.DataType switch { OfficeOpenXml.Style.ExcelDataType.String => cell.Value.ToString(), OfficeOpenXml.Style.ExcelDataType.Boolean => (bool)cell.Value, OfficeOpenXml.Style.ExcelDataType.Number => ConvertDoubleToNumber(cell.Value), OfficeOpenXml.Style.ExcelDataType.DateTime => FormatDateTime((DateTime)cell.Value, dateFormat), _ => cell.Value.ToString() // fallback }; } private object ConvertDoubleToNumber(object value) { var d = (double)value; // 判断是否为整数(Excel数字常存为double,但业务要int) if (Math.Abs(d - Math.Round(d)) < double.Epsilon) return (long)Math.Round(d); return d; } private string FormatDateTime(DateTime dt, string format) { // 关键:Excel日期是UTC序列号,但无时区信息,强制转为UTC再格式化 // 避免dt.ToLocalTime()把上海时间转成东八区再转回字符串 return dt.ToUniversalTime().ToString(format, CultureInfo.InvariantCulture); }注意:
FormatDateTime中ToUniversalTime()是硬性要求。曾有项目因未处理,导致导出的"2024-03-15"在海外服务器反序列化成2024-03-14。
3.3 步骤3:组装JSON对象:用Dictionary<string, object>而非ExpandoObject
很多人用ExpandoObject动态加属性,但它在Newtonsoft.Json中序列化性能比Dictionary慢40%,且无法控制属性顺序。我们坚持用Dictionary<string, object>:
public IEnumerable<Dictionary<string, object>> ConvertWorksheetToJson(ExcelWorksheet worksheet) { var headers = ExtractHeaders(worksheet, _config.HeaderRow); var startRow = _config.HeaderRow + 1; for (int row = startRow; row <= worksheet.Dimension?.End.Row; row++) { // 跳过空行检测(按配置) if (_config.SkipEmptyRows && IsRowEmpty(worksheet, row)) continue; var rowDict = new Dictionary<string, object>(headers.Length); for (int col = 0; col < headers.Length; col++) { var cell = worksheet.Cells[row, col + 1]; // EPPlus列从1开始 rowDict[headers[col]] = CellValueToJsonObject(cell, _config.DateTimeFormat); } yield return rowDict; } }yield return确保内存只驻留当前行数据,10万行Excel峰值内存≈单行Dictionary大小×2(GC前),而非10万×单行大小。
4. 避坑:生产环境高频翻车现场与后悔药
4.1 现象:JSON输出里出现{"name":"\u0000\u0000\u0000..."}乱码
原因:Excel单元格含不可见控制字符(如\u0000空字符、\u0007响铃符),EPPlus原样返回,Newtonsoft.Json默认不清理
解决:在CellValueToJsonObject中增加清洗逻辑
private string SanitizeString(string input) { if (string.IsNullOrEmpty(input)) return input; // 移除ASCII控制字符(0x00-0x1F),保留空格、换行、制表符 return new string(input.Where(c => c >= 0x20 || c == '\r' || c == '\n' || c == '\t').ToArray()); } // 在String分支调用:SanitizeString(cell.Value.ToString())4.2 现象:大文件(>50MB)转换时进程卡死,CPU 100%,无日志
原因:EPPlus默认启用CalcMode = ExcelCalculationMode.Automatic,对含公式的Excel会尝试重算,公式引擎锁死线程
解决:初始化ExcelPackage时强制关闭计算
using var package = new ExcelPackage(new FileInfo(filePath)); package.Workbook.CalcMode = ExcelCalculationMode.Manual; // 关键!4.3 现象:中文字段名导出后JSON里显示"å§å"(UTF-8 BOM导致)
原因:StreamWriter未指定Encoding.UTF8,或JsonSerializerSettings未设StringEscapeHandling = StringEscapeHandling.EscapeNonAscii
解决:序列化时显式指定编码与转义
var settings = new JsonSerializerSettings { StringEscapeHandling = StringEscapeHandling.EscapeNonAscii, DateFormatHandling = DateFormatHandling.IsoDateFormat, NullValueHandling = NullValueHandling.Ignore }; var json = JsonConvert.SerializeObject(jsonData, Formatting.Indented, settings); File.WriteAllText(outputPath, json, Encoding.UTF8); // 必须传Encoding.UTF84.4 现象:日期字段导出为"2024-03-15T00:00:00",但前端要求"2024-03-15"
原因:DateTime.ToString("yyyy-MM-dd")在FormatDateTime中未覆盖所有场景(如DateTimeKind.Unspecified)
解决:增强格式化逻辑,统一转为DateTimeKind.Utc再截断
private string FormatDateTime(DateTime dt, string format) { var utcDt = dt.Kind == DateTimeKind.Utc ? dt : DateTime.SpecifyKind(dt, DateTimeKind.Utc); return utcDt.ToString(format, CultureInfo.InvariantCulture); }4.5 现象:调用方传入.xlsx路径,程序抛System.IO.FileFormatException: File contains corrupted data.
原因:文件后缀是.xlsx,但实际是.xls(Excel 97-2003二进制格式),EPPlus不支持
解决:在Convert方法入口做格式嗅探
private bool IsXlsxFile(string filePath) { try { using var fs = File.OpenRead(filePath); var header = new byte[4]; fs.Read(header, 0, 4); // PK开头是ZIP(.xlsx本质是ZIP包) return header[0] == 0x50 && header[1] == 0x4B && header[2] == 0x03 && header[3] == 0x04; } catch { return false; } } // 调用前检查:if (!IsXlsxFile(filePath)) throw new NotSupportedException("Only .xlsx files supported");5. 工具封装:命令行+Web API双模式,附完整可运行源码结构
5.1 命令行工具:ExcelToJsonCli.exe(.NET 6+ Self-Contained)
支持以下参数,全部可配置化:
| 参数 | 示例 | 说明 |
|---|---|---|
-i, --input | -i "data.xlsx" | 必填:输入Excel路径 |
-o, --output | -o "out.json" | 必填:输出JSON路径 |
-s, --sheet | -s "Sheet1" | 可选:指定工作表名,默认第一个 |
-h, --header-row | -h 2 | 可选:表头所在行号,默认1 |
-f, --date-format | -f "yyyy-MM-dd" | 可选:日期格式,默认yyyy-MM-dd HH:mm:ss |
-m, --max-rows | -m 50000 | 可选:最大读取行数,默认100000 |
使用示例:
ExcelToJsonCli.exe -i "sales_2024.xlsx" -o "sales.json" -s "Q1 Data" -h 3 -f "yyyy/MM/dd"5.2 Web API服务:ASP.NET Core Minimal API(.NET 7+)
暴露POST /api/convert端点,接收multipart/form-data,返回JSON或错误详情:
app.MapPost("/api/convert", async (HttpRequest req, IWebHostEnvironment env) => { var form = await req.ReadFormAsync(); var file = form.Files["file"]; if (file.Length == 0) return Results.BadRequest("No file uploaded"); var tempPath = Path.Combine(env.ContentRootPath, "temp", $"{Guid.NewGuid()}.xlsx"); Directory.CreateDirectory(Path.GetDirectoryName(tempPath)); await using var stream = new FileStream(tempPath, FileMode.Create); await file.CopyToAsync(stream); try { var converter = new ExcelToJsonConverter(new ExcelToJsonConfig { HeaderRow = int.Parse(form["headerRow"] ?? "1"), DateTimeFormat = form["dateFormat"] ?? "yyyy-MM-dd HH:mm:ss", MaxRows = int.Parse(form["maxRows"] ?? "100000") }); var result = converter.Convert(tempPath, form["sheetName"]); return Results.Ok(new { success = true, data = result }); } catch (Exception ex) { return Results.Problem(ex.Message, statusCode: 400); } finally { if (File.Exists(tempPath)) File.Delete(tempPath); } });提示:生产环境务必加
[RequestSizeLimit(100_000_000)]限制上传大小,避免DoS。
5.3 源码结构与依赖(可直接克隆编译)
ExcelToJsonTool/ ├── ExcelToJson.Core/ # 核心转换逻辑(.NET Standard 2.1) │ ├── ExcelToJsonConverter.cs │ ├── ExcelToJsonConfig.cs │ └── ... ├── ExcelToJson.Cli/ # 命令行工具(.NET 6+) │ ├── Program.cs │ └── ... ├── ExcelToJson.WebApi/ # Web API(.NET 7+) │ ├── Program.cs │ └── ... ├── tests/ # 单元测试(xUnit) │ ├── ConversionTests.cs # 覆盖日期/空行/合并表头等场景 │ └── ... └── README.mdNuGet依赖(精简到最小):
EPPlus(6.2.10) —— 仅用于.xlsx读取Newtonsoft.Json(13.0.3) —— JSON序列化(比System.Text.Json更易控格式)Microsoft.Extensions.Logging.Abstractions—— 日志抽象(方便接入Serilog等)
注意:
EPPlusv6+需手动添加<PackageReference Include="EPPlus" Version="6.2.10" />,不要装v5(已停止维护,且不支持.NET 6+)。
6. 进阶技巧:让JSON更“懂业务”——动态Schema校验与字段映射规则
6.1 场景驱动:为什么不能只转“看起来像JSON”的字符串?
某模拟项目X要求:
- Excel中“价格”列必须是数字,且≥0
- “状态”列只能是
"active"/"inactive"/"pending" - “创建时间”必须是ISO格式日期,且不能晚于今天
如果只做无脑转换,这些业务规则全靠前端或下游服务校验,错误反馈链路过长。我们的方案是在转换层注入轻量Schema规则:
public class FieldRule { public string FieldName { get; set; } // 对应Excel列名 public FieldType Type { get; set; } // Number/String/Date/Enum public bool? Required { get; set; } // 是否必填 public string[] AllowedValues { get; set; } // Enum白名单 public double? MinValue { get; set; } // Number最小值 public DateTime? MaxDate { get; set; } // Date最大值 } // 使用示例:加载规则JSON文件 var rules = JsonConvert.DeserializeObject<List<FieldRule>>(File.ReadAllText("rules.json")); var converter = new ExcelToJsonConverter(config).WithRules(rules);WithRules()会在CellValueToJsonObject后追加校验,并将错误收集到ConversionResult中:
public class ConversionResult { public List<Dictionary<string, object>> Data { get; set; } public List<ValidationError> Errors { get; set; } // 如:[{ "row": 15, "col": 3, "message": "价格不能为负数" }] }这样,调用方能拿到结构化错误,前端可精准标红第15行第3列。
6.2 字段映射:Excel列名→JSON字段名→数据库字段名的三级别映射
业务常有“Excel列叫‘客户姓名’,JSON要叫‘customerName’,DB字段是‘cust_name’”。我们支持映射表:
// mapping.json [ { "excel": "客户姓名", "json": "customerName", "db": "cust_name" }, { "excel": "订单金额", "json": "orderAmount", "db": "amt" } ]转换器自动按excel字段匹配,生成JSON时用json键名。无需改代码,改配置即生效。
6.3 我的血泪习惯:每次上线前必做的3件事
- 用真实脏数据压测:找运营要一份“最乱”的历史Excel(含合并单元格、空行、特殊符号、超长文本),跑一遍,看日志是否清晰报出
Row 234, Col 5: Invalid date format "2024/13/01" - 检查JSON Schema兼容性:用
JsonSchemaValidator验证输出JSON是否符合下游服务要求的Schema(哪怕只是简单required: ["id","name"]) - 加一行
Console.WriteLine($"Converted {rowCount} rows, {errorCount} errors");:生产日志里这行字,比100行debug日志更有价值
工具的价值不在“能转”,而在“转错了能马上知道哪错了、为什么错、怎么修”。这篇笔记里所有代码、参数、避坑点,都来自某跨平台系统上线前两周的真实排障记录——没有虚构案例,只有反复验证过的路径。
希望帮到你。
本文还有配套的精品资源,点击获取