简介:一款面向开发者、数据分析师的Excel转JSON小工具,用于将Excel表格数据快速转换为结构清晰的JSON格式,适用于Web接口联调、数据迁移、自动化脚本处理等场景。压缩包共31个文件,约5.44MB,包含可直接运行的exe程序、配套的libxl.dll等运行库、说明文档,以及完整的C++工程源码——12个h头文件、3个cpp源文件配合Visual Studio工程文件(.sln/.vcxproj),既满足开箱即用,也便于阅读源码了解解析原理或进行定制修改。目前已有1018人学习下载。工具遵循常见映射规则:Excel行对应JSON对象、列标题对应属性名、工作表对应对象数组,并支持数值、日期、布尔等类型的处理,能帮助用户批量完成结构化数据转换,避免手工编写JSON的繁琐与出错,提升数据对接效率。
1. excel转json(exe工具):双击就能跑,不用装Python
某个下午,运营同事发来一个50MB的Excel,说要转成JSON给下游系统用。他电脑上没有Python,也不可能为了一个一次性需求去装环境。我意识到,真正能落地的excel转json工具,不是一段脚本,而是一个双击就能跑的exe。这类工具的核心价值,是把Excel表头映射成JSON字段,自动处理数据类型、日期和编码,让不懂技术的同事直接拖文件进来,拿到结果就走。这篇文章不讲空概念,直接讲怎么设计映射规则、怎么用PyInstaller打包成exe、参数怎么设,以及最容易翻车的几个坑。
2. 先定excel转json的映射规则:表头怎么变成字段,嵌套和数组怎么拆
2.1 为什么映射规则决定工具的可用性
很多第一次做excel转json的人,拿到需求就直接遍历行,把每行压成一个对象。这种工具真正交付出去,往往用不了三天就没人用。原因很简单:Excel不是数据库表。表头可能有两行,可能有合并单元格,同一列里前100行是数字,第101行变成文本;还有那种左侧是主项、右侧是子项的宽表。如果不事先定好映射规则,生成的JSON字段名会带换行符,层级会被平铺成怪名,下游对接的人看到JSON就懵。
我在动手写代码前,会先拿一份真实Excel问三个问题。第一,字段名是否只从第一行表头取,还是需要拼接第二行?第二,列与列之间有没有从属关系,需不需要嵌套对象?第三,一个单元格里的多个值,是当整体字符串还是拆成数组?这三个问题直接决定后面所有代码怎么写,也决定这个exe工具是给一个人用还是能反复交付。
2.2 三种映射模型怎么选
平铺模型最简单。表头是一行,列名直接做字段,每行一个对象。它适合大多数明细表、导出表,输出结构简单,下游解析成本最低。嵌套模型用点号或下划线表达层级,例如“客户.地址.城市”,适合多级主数据、配置表。嵌套结构更接近业务对象,但如果Excel表头不规范,很容易造出深层空对象。数组模型把某一列按分隔符拆成数组,或者把多列归并到同一个数组字段,适合标签、枚举、多值属性。
| 模型 | 输入特征 | 输出示例 | 适用场景 |
|---|---|---|---|
| 平铺 | 单行表头、一列一字段 | {"姓名":"张三","年龄":30} | 明细数据、API入参 |
| 嵌套 | 多级表头、从属列 | {"客户":{"姓名":"张三"}} | 主从结构、配置树 |
| 数组 | 列内有分隔符、多列同义 | {"标签":["后端","运维"]} | 标签、枚举、兴趣点 |
选型时我坚持一个原则:工具规则宁可保守,也不要过度发挥。JSON结构一旦定型,下游代码会依赖它很长时间,转换工具的便利性永远要让位于稳定性。所以工具默认走平铺,嵌套和数组作为可开关的选项,而不是一上来就搞智能识别。
2.3 用openpyxl读取Excel并输出JSON的最小骨架
为什么用openpyxl而不是pandas?pandas的read_excel底层也依赖openpyxl,但它会自动做很多类型转换,比如把日期列变成Timestamp,把字符串“001”变成数字1。对于交付型工具,这种“好心”会悄悄破坏原始数据。所以我把openpyxl当读取层,类型转换全部自己控制。
from openpyxl import load_workbook import json def excel_to_json(filepath, sheetname=None): # data_only=True 读取单元格计算后的值,而不是公式 wb = load_workbook(filepath, data_only=True) ws = wb[sheetname] if sheetname else wb.active rows = list(ws.iter_rows(values_only=True)) if not rows: return [] headers = [] for idx, cell in enumerate(rows[0]): if cell is None or str(cell).strip() == "": headers.append(f"column_{idx}") else: headers.append(str(cell).strip()) data = [] for row in rows[1:]: if all(v is None for v in row): continue # 跳过完全空行 obj = {} for i, value in enumerate(row): obj[headers[i]] = value data.append(obj) return data if __name__ == "__main__": result = excel_to_json("demo.xlsx") print(json.dumps(result, ensure_ascii=False, indent=2))这段代码有三个关键点。data_only=True是必须的,否则公式单元格会返回“=SUM(A1:A5)”这种字符串,下游完全没法用。空表头会被替换成column_0这种键名,避免JSON里出现空字符串键。完全空行直接跳过,防止Excel里常见的大面积空行污染数据。光有骨架还不够,openpyxl默认返回的是Python原生类型,字符串是str,数字是int或float,日期可能是datetime,这些类型直接进json.dumps大部分没问题,但日期、精度和编码要单独处理。
2.4 把嵌套和数组规则做进工具里
为了让工具能处理复杂结构,我会在骨架上加两个辅助函数。一个是嵌套路径写入函数,一个是数组拆分函数。嵌套规则的约定是:表头里带点号就按层级拆,例如“客户.姓名”最终写入时变成{"客户":{"姓名":...}}。数组规则的约定是:指定某些列按逗号分隔拆成列表,并且只对纯字符串列生效,避免数字被误拆。
def set_by_path(obj, path, value): # path 是 ["客户", "姓名"] 这样的层级列表 cur = obj for key in path[:-1]: cur = cur.setdefault(key, {}) cur[path[-1]] = value def maybe_split(value, delimiter=","): # 只有字符串包含分隔符且拆开后元素都非空时才转数组 if isinstance(value, str) and delimiter in value: items = [v.strip() for v in value.split(delimiter) if v.strip()] return items if len(items) > 1 else value return valueset_by_path避免了重复写多层字典的麻烦,maybe_split则把数组拆分的触发条件收紧了。我把这两个函数接到主循环里时,会先判断表头是否带点号,再决定用普通赋值还是嵌套写入。命令行上我用两个开关控制:--nested打开点号嵌套,--split接受列名列表。默认都关着,这样对平铺数据的用户完全透明。
3. 把脚本打包成exe:PyInstaller参数与交付前的三个检查
3.1 为什么选PyInstaller而不是其他打包方式
Python脚本交付给没有解释器的同事,常规做法是PyInstaller、cx_Freeze、Nuitka三选一。我选PyInstaller的原因很直接:它打包exe的路径最短,对openpyxl这类带资源文件的库处理也成熟。Nuitka是把Python编译成C再生成exe,耗时久,第一次用还有一堆编译器参数要调;cx_Freeze配置脚本稍重,对纯exe发布的支持不如PyInstaller顺手。对excel转json这种工具,PyInstaller的稳定性和社区方案都够用。
一个关键经验是:在虚拟环境里打包。直接在base环境打包,会把无关的包也扫进去,exe体积变大,还可能因为依赖版本冲突在对方机器上闪退。我一般会给工具单独建一个venv,只装openpyxl和pyinstaller,然后在这个干净环境里构建。这个习惯能省掉很多排查时间。
3.2 打包最小命令与三个关键参数
先看最小命令,Windows下执行:
python -m venv .venv .venv\Scripts\activate pip install openpyxl pyinstaller pyinstaller --onefile --clean --name excel2json excel_to_json.py--onefile把所有依赖收进单个exe,交付只给一个文件;--clean会清理上次构建缓存,避免旧产物干扰;--name指定exe文件名,默认是脚本名。打包完成后,dist目录下出现excel2json.exe,dist之外的build目录和spec文件都可以删掉。这里有一个容易踩的细节:如果你的代码里用了argparse,记得在入口脚本里保留命令行解析,否则exe双击后没有控制台反馈,用户会以为没反应。
另外两个参数要看场景决定。--console表示运行时保留命令行窗口,方便用户看到报错,也方便我用--help调试;如果以后做了图形界面再改用--windowed,否则双击会出现一个黑框。--icon可以加图标,但图标文件必须是ico格式,直接拿png改后缀不行。还有一个容易被忽略的点:openpyxl依赖里包含一些数据文件,PyInstaller通常能自动找到,但如果你自己import了lxml之类,可能要用--hidden-import手动点名。
3.3 交付前检查:在没装Python的机器上跑一遍
打包好不等于能用。我常用的检查顺序是:先在本机虚拟环境外双击exe,确认不闪退;再复制到一台没有Python的干净机器上,执行excel2json.exe --help,确认依赖真的都被带进去了。如果出现ModuleNotFoundError,多半是PyInstaller漏了动态导入的模块,需要把模块名补进--hidden-import重新打包。
体积和启动速度也要提前有预期。PyInstaller --onefile启动时会先把内部文件解压到临时目录,所以首次双击会有1到2秒延迟,不是死机。exe体积在15到30MB都正常,不要试图用UPX强行压缩,某些压缩后的exe会被误报得更厉害,还可能在目标机器上解压失败。这一点在避坑清单里会细说。
3.4 用spec文件固化打包参数
命令行参数多了以后,我会在项目目录里留一份excel2json.spec,把入口、名称、图标、hidden-import都写进去。这样重新打包只执行pyinstaller excel2json.spec,避免每次敲一长串参数。spec文件不是给最终用户看的,但它是工程交付的一部分。
# excel2json.spec 片段 a = Analysis( ['excel_to_json.py'], pathex=[], hiddenimports=['openpyxl.cell._writer'], ) exe = EXE( a, name='excel2json', console=True, icon='app.ico', )hiddenimports里点名openpyxl的隐藏子模块,是我遇到“打包后本机正常、对方机器突然报模块不存在”后加的。openpyxl部分功能是延迟导入的,PyInstaller静态分析扫不到。这种问题用日志只能看到一行ModuleNotFoundError,不拆开exe根本定位不到,提前在spec里点名最省事。
4. 给exe工具加配置:批量转换、编码与缩进的三个必调参数
4.1 用argparse接收文件路径、输出路径和缩进参数
一个只写死“读取demo.xlsx、输出到标准输出”的工具,没法交付。至少得让用户能指定输入文件、输出文件、是否格式化、从哪个工作表转。标准库argparse足够,不需要click或typer,因为要控制exe体积。
import argparse import json from pathlib import Path from openpyxl import load_workbook def main(): parser = argparse.ArgumentParser(description="excel转json工具") parser.add_argument("input", help="Excel文件(*.xlsx)或文件夹路径") parser.add_argument("-o", "--output", help="输出JSON文件路径") parser.add_argument("--sheet", help="指定工作表名,默认取活动工作表") parser.add_argument("--indent", type=int, default=2, help="JSON缩进空格数,0表示压缩") parser.add_argument("--nested", action="store_true", help="将表头中的点号解释为嵌套层级") parser.add_argument("--split", help="需要拆成数组的列名,逗号分隔") args = parser.parse_args()indent设为2是给人看的,但给接口消费的场景更常用indent=0压缩体积。argparse自带--help,打包成exe后对方也能自查参数。我建议把input放在位置参数,这样用户拖拽文件到exe图标上时,系统会把文件路径当作第一个参数传入,工具能直接接收。这是很多桌面工具都支持的交互方式,但要靠命令行参数设计配合。
4.2 强制UTF-8输出,把日期序列号转成可读字符串
Windows记事本和PowerShell的默认编码经常捣乱。打开JSON文件看到\u4f60\u597d,不是乱码,而是json.dump默认ensure_ascii=True把所有中文转义了。我在json.dump里固定使用ensure_ascii=False,并打开文件时用encoding="utf-8"写。注意读取Excel时openpyxl本身就是Unicode,真正会出问题的是print到Windows控制台这一步,所以命令行工具里我一般直接把内容写入文件而不是print。
日期列是另一个经典坑。Excel把日期存成浮点序列号,data_only=True时openpyxl能拿到datetime对象,但如果你做了类型判断,或者源单元格本身是文本日期,结果就不统一。工具里做统一转换:datetime和date对象全部格式化成YYYY-MM-DD HH:MM:SS;字符串里如果恰好是全数字,不强行猜测。
import datetime def normalize_value(value): if isinstance(value, datetime.datetime): return value.strftime("%Y-%m-%d %H:%M:%S") if isinstance(value, datetime.date): return value.strftime("%Y-%m-%d") return valuenormalize_value在写入JSON前统一收口,能避免不同工作表里的日期格式不一致。如果你发现输出里出现44000这种数字,说明读取阶段没有把excel里的日期序列号转成datetime,大概率是因为该单元格格式被Excel存成了常规格式。这种情况只能在读取时检测数字大小和表头名,表头含“日期”或“时间”就尝试转换。
4.3 支持文件夹拖拽和按工作表批量转换
实际需求里,运营同事经常拖一个文件夹过来,说“全部转一下”。exe要支持文件夹,判断输入路径是文件还是目录,目录就遍历所有xlsx。
def collect_files(input_path): p = Path(input_path) if p.is_file(): return [p] # 过滤Excel临时锁文件 ~$,避免打开报错 return sorted([f for f in p.rglob("*.xlsx") if not f.name.startswith("~$")])批量转换的输出路径,我保留原文件名加.json后缀,放到输入目录下的json_output子目录。文件名方面还要小心:工作表名里的空格和斜杠不能直接当文件名,写入前做一次安全名替换,把空白换下划线,斜杠换成中划线。
def safe_sheet_name(name): for ch in " /\\:*?\"<>|": name = name.replace(ch, "_") return name这一步很小,但能避免在多个工作表导出场景下直接抛文件名非法错误。
5. excel转json工具的避坑清单:乱码、精度、杀软误报
这一章的价值不在多,在于每一条都是真实交付时碰到过的。按现象、原因、解决三段写,方便你直接对照排查。
5.1 编码与数据类型:乱码、科学计数法、日期序列号
坑1:JSON里全是乱码或\uXXXX
现象:用自带记事本打开输出的JSON,中文变成“锟斤拷”,或者全部是\u4f60\u597d这种转义。
原因:写入文件时没有指定UTF-8。Windows上open默认用gbk,JSON字符串里的中文按错误编码写出;或者json.dump默认ensure_ascii=True做了转义。
解决:写文件时统一用encoding="utf-8",json.dump里ensure_ascii=False。如果对方在Windows记事本里看,担心UTF-8头不识别,可以改成UTF-8-SIG,记事本就不会乱码。我工具里默认用UTF-8-SIG,兼容性最好。
坑2:数字变成科学计数法或精度丢失
现象:Excel里的18位订单号转出来变成1.234567890123456e+17,或者金额末尾多出0.00000001。
原因:openpyxl返回数值类型是int或float,订单号在Excel里本身被存成数字,转成float后超出精度。
解决:对超过15位的整数直接转字符串,避免精度丢失。工具里写一个判断:
def to_jsonable(value): if isinstance(value, int) and abs(value) >= 10**15: return str(value) return value这是血泪经验,订单号、身份证号这类必须用字符串。如果你需要通用转换,可以在参数里加一个“文本列清单”,指定列名后读取阶段就按文本处理。
坑3:日期变成44000这种序列号
现象:单元格显示“2020-07-20”,JSON输出是44000.0。
原因:Excel内部存的是日期序列号,openpyxl在未指定data_only=True且单元格格式异常时返回数值而不是datetime。
解决:读取时用data_only=True,并且在normalize_value里识别datetime和date;如果拿到的是数字但表头名包含“日期”或“时间”,再尝试做序列号转换。为了防止这种玄学问题,我更推荐源文件里把日期列显示格式调成明确日期格式,工具只负责透传。
5.2 打包与性能:杀软误报、超大Excel、公式缓存
坑4:打包后的exe被杀毒软件直接删
现象:excel2json.exe发给对方后,对方电脑提示检测到木马,文件被隔离。
原因:PyInstaller --onefile打包的exe包含自解压逻辑和随机命名临时文件,特征和某些加壳程序相似。杀毒软件误报概率比普通脚本高,不是真的病毒。
解决:交付前自己先做一次多引擎扫描,确认无风险。分发时走公司内部可信渠道,打包成zip并说明来源,比裸exe通过率高得多。不要用UPX压缩,压缩会让误报率更高。我见过一个例子,同一份源码正常打包没事,用UPX压过后还没传到对方电脑就被本地杀软拦了,后来直接放弃UPX。
坑5:超大Excel转JSON内存暴涨甚至闪退
现象:300MB的xlsx转出时内存占用到2GB,中途进程被杀。
原因:list(ws.iter_rows())把所有行一次性加载进内存,大文件直接把内存打满。
解决:openpyxl本身是内存敏感库,超大文件可以改用read_only模式迭代读取,写JSON时用流式写入,不要一次性构造大列表。但read_only模式拿不到公式计算结果,需要在速度和完整度之间取舍。另一个简单办法是工具里做行数预算,超过5万行就提示用户拆表,避免静默闪退。
from openpyxl import load_workbook def count_rough_rows(filepath): wb = load_workbook(filepath, read_only=True, data_only=True) ws = wb.active return ws.max_row注意:read_only模式下max_row可能因为Excel格式不规范而不准,所以它只用来做阈值告警,不作为最终转换依据。
6. 高级一点的用法:多工作表导出JSON目录,并在转换后做校验
6.1 一张Excel导出成多个JSON文件
很多Excel天然是一个工作簿多张表,正好对应一个JSON目录。我习惯把输出路径设计成目录,用工作表名作为文件名。这样一次拖一个Excel,能得到一个结构清楚的JSON目录,而不是一个大而全的文件。
def export_all_sheets(filepath, out_dir): wb = load_workbook(filepath, data_only=True) out = Path(out_dir) out.mkdir(parents=True, exist_ok=True) for ws in wb.worksheets: rows = list(ws.iter_rows(values_only=True)) if not rows: continue headers = [str(c).strip() if c is not None else "" for c in rows[0]] data = [] for row in rows[1:]: item = {} for i, v in enumerate(row): item[headers[i]] = v data.append(item) filename = safe_sheet_name(ws.title) + ".json" (out / filename).write_text( json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8-sig", )工作表名做文件名前,必须经过safe_sheet_name。这是我在一次交付里踩出的教训:某张工作表名叫“汇总/统计”,Windows不认斜杠,工具直接把进程抛了。
6.2 内置JSON格式校验
转换完顺手做一次自检,避免交付一个半截文件。用json.loads回读输出,和原始行数对比。这一步能在工具里用一行打印完成,给对方一种“工具很可靠”的感觉,也能少收到很多无效反馈。
def validate_json_file(path, expected_count): with open(path, encoding="utf-8-sig") as f: obj = json.load(f) assert isinstance(obj, list) and len(obj) == expected_count校验失败时,工具会打印错误原因而不是只给一个空文件。我习惯把校验结果写到日志文件里,方便对方转发给我时直接能看到上下文。
6.3 和原始Excel抽检对比
最后我会抽两行原始Excel,人工比对新JSON里的字段和值。重点看三处:空单元格是不是变成null、带小数点的金额值、以及列顺序。JSON的键顺序虽然不重要,但对人眼排查有影响,所以工具保持Excel从左到右的顺序写入。
我自己的一个习惯是,每次改完映射规则,都把上一个版本转出的JSON保存下来,用文本对比工具核查差异,而不是只靠肉眼扫屏幕。这个习惯帮我在一次小改动中提前发现了日期格式化分支写反的问题。那一次如果把带bug的版本发出去,下游系统解析日期会整体偏移一天。工具可以没有华丽界面,但校验和回归这两个步骤不能省。希望帮到你。
本文还有配套的精品资源,点击获取