接口返回、后台导出报表、配置文件和日志,很多都以 JSON 形式存在。JSON 适合传输和存储,但字段一多、层级一深,逐条核对、筛选、汇总就很费劲。转成 Excel 后每条记录一行、每个字段一列,可以直接用筛选、排序、求和和数据透视表处理。本文围绕 Python 标准库 json 加 Spire.XLS for Python 的组合,把「读取 JSON → 写入工作簿 → 保存 xlsx」这条链路拆开讲清楚,覆盖基础转换、列顺序固定、嵌套结构展开、表头格式化、列宽自适应和文件夹批量处理几个场景。
一、为什么要做这层转换
原文给出的理由可以归为三点:
1. 便于查看:JSON 嵌套结构下字段多,肉眼核对效率低;转成表格后记录与列一一对应,直观很多。
2. 便于分析:xlsx 可以直接套用筛选、排序、求和、透视表,不必再写第二套统计脚本。
3. 便于交付:Excel 是常见的数据交换载体,自动生成 .xlsx 能省掉手工复制整理。
本质上是把 JSON 里的数据重排成适合二维表展示的结构,行是记录,列是字段。
二、环境准备
JSON 解析用 Python 标准库,不需要额外装包;工作簿的创建、单元格写入和保存由 Spire.XLS 负责:
导入模块:
- import json
- from spire.xls import *
- from spire.xls.common import *
复制代码
三、基础流程:字典列表写入工作表
最常见的数据形态是「根节点为一个非空列表,列表元素是字段结构基本一致的字典」。json.load() 读文件后直接得到 Python 的 list,取第一条记录的键作为表头,再从第二行开始逐条写入。
- import json
- from spire.xls import *
- from spire.xls.common import *
- inputFile = "data.json"
- outputFile = "data.xlsx"
- with open(inputFile, "r", encoding="utf-8") as f:
- records = json.load(f)
- workbook = Workbook()
- sheet = workbook.Worksheets[0]
- headers = list(records[0].keys())
- for col, key in enumerate(headers, start=1):
- sheet.Range[1, col].Text = key
- for row, record in enumerate(records, start=2):
- for col, key in enumerate(headers, start=1):
- sheet.Range[row, col].Text = str(record.get(key, ""))
- workbook.SaveToFile(outputFile, ExcelVersion.Version2013)
- workbook.Dispose()
复制代码
几个关键点:
- sheet.Range[行, 列] 定位单元格,行列都从 1 开始计数,Range[1, 1] 就是 A1。
- 表头写在第 1 行,数据从第 2 行起,enumerate 的 start 参数直接控制起始编号。
- Text 属性按文本赋值,所以用 str() 把 JSON 中的值先转成字符串。
- 这段代码的前提是根节点为非空列表且元素为字典;结构不同就必须调整读取和写入方式。
四、固定列顺序:不要依赖第一条记录的键
直接用 records[0].keys() 有个隐患:不同文件的字段虽然相同,出现顺序却可能不一致,生成的 Excel 列顺序会跟着数据漂移,不利于多份文件合并对比。更稳的做法是提前声明字段列表。
- import json
- from spire.xls import *
- from spire.xls.common import *
- inputFile = "users.json"
- outputFile = "users.xlsx"
- with open(inputFile, "r", encoding="utf-8") as f:
- records = json.load(f)
- workbook = Workbook()
- sheet = workbook.Worksheets[0]
- columns = ["id", "name", "email", "department"]
- for col, name in enumerate(columns, start=1):
- sheet.Range[1, col].Text = name
- for row, record in enumerate(records, start=2):
- for col, name in enumerate(columns, start=1):
- value = record.get(name, "")
- sheet.Range[row, col].Text = "" if value is None else str(value)
- workbook.SaveToFile(outputFile, ExcelVersion.Version2013)
- workbook.Dispose()
复制代码
record.get(name, "") 保证字段缺失时以空串填充而不是抛异常;额外的 None 判断则避免把 None 写成字符串 "None"。这种写法适合批量转换结构相似的文件,能让输出的 Excel 保持一致的列布局。
五、嵌套 JSON 展开:递归 + 点号路径
真实数据往往带嵌套:用户记录里有 address 对象,订单里有商品列表。如果直接 str() 整个对象塞进单元格,会得到带括号和引号的字符串,既不能单独筛选也不好分析。
处理思路是先展平:字典递归下钻,用点号把父键和子键连起来(address 中的 city 变成 address.city);列表则用逗号拼成一个字符串,去掉方括号和引号。
- import json
- from spire.xls import *
- from spire.xls.common import *
- inputFile = "/sample.json"
- outputFile = "/orders.xlsx"
- with open(inputFile, "r", encoding="utf-8") as f:
- records = json.load(f)
- def flatten(record, prefix=""):
- items = {}
- for key, value in record.items():
- composed = f"{prefix}{key}"
- if isinstance(value, dict):
- items.update(flatten(value, composed + "."))
- elif isinstance(value, list):
- items[composed] = ", ".join(map(str, value))
- else:
- items[composed] = value
- return items
- flat_records = [flatten(record) for record in records]
- all_keys = []
- for record in flat_records:
- for key in record.keys():
- if key not in all_keys:
- all_keys.append(key)
- workbook = Workbook()
- sheet = workbook.Worksheets[0]
- for col, name in enumerate(all_keys, start=1):
- sheet.Range[1, col].Text = name
- sheet.Range[1, col].Style.Font.IsBold = True
- for row, record in enumerate(flat_records, start=2):
- for col, name in enumerate(all_keys, start=1):
- value = record.get(name, "")
- sheet.Range[row, col].Text = "" if value is None else str(value)
- sheet.AllocatedRange.AutoFitColumns()
- workbook.SaveToFile(outputFile, ExcelVersion.Version2013)
- workbook.Dispose()
复制代码
这段代码的三个设计选择值得留意:
1. 表头取所有记录字段的并集。只读第一条记录的键,会漏掉后续记录才出现的嵌套字段;遍历展平后的全部记录并去重,能保证出现过的字段都进表头。
2. 可以手动限定字段。如果不需要全量导出,直接把目标列写成列表即可,例如 ["id", "address.city", "address.zip"],只写指定列。
3. 列表值合并为字符串。flatten() 遇到 list 时用 ", ".join(map(str, value)) 拼接,像 ["Python", "SQL", "Excel"] 会变成 Python, SQL, Excel 落在同一个单元格里。
六、表头样式与列宽自适应
数据落盘后,通常要让表头和数据区区分开。Spire.XLS 通过 Style.Font 控制字体样式,IsBold 控制加粗,Color 指定字体颜色:
- columns = ["id", "name", "email", "department"]
- for col, name in enumerate(columns, start=1):
- sheet.Range[1, col].Text = name
- sheet.Range[1, col].Style.Font.IsBold = True
- sheet.Range[1, col].Style.Font.Color = Color.get_CadetBlue()
- for row, record in enumerate(records, start=2):
- for col, name in enumerate(columns, start=1):
- sheet.Range[row, col].Text = str(record.get(name, ""))
复制代码
除字体外,字号、背景填充、对齐方式等属性同样可以按需设置,这里不再展开。
列宽问题单独说一下。JSON 字段值长短不一,默认列宽容易截断内容。写完数据后调用 AutoFitColumns() 按内容自适应:
- sheet.AllocatedRange.AutoFitColumns()
- workbook.SaveToFile(outputFile, ExcelVersion.Version2013)
- workbook.Dispose()
复制代码
AllocatedRange 表示工作表中已分配的单元格区域,用它比手写固定范围更省事,数据量变化时不用改代码。
七、批量转换文件夹中的 JSON
文件多了就交给 os 遍历:读一个 JSON、建一个 Workbook、存一个 xlsx,然后释放。
- import json
- import os
- from spire.xls import *
- from spire.xls.common import *
- inputFolder = "./json_files"
- outputFolder = "./excel_files"
- if not os.path.exists(outputFolder):
- os.makedirs(outputFolder)
- for name in os.listdir(inputFolder):
- if not name.lower().endswith(".json"):
- continue
- inputPath = os.path.join(inputFolder, name)
- outputPath = os.path.join(
- outputFolder,
- os.path.splitext(name)[0] + ".xlsx"
- )
- with open(inputPath, "r", encoding="utf-8") as f:
- records = json.load(f)
- workbook = Workbook()
- sheet = workbook.Worksheets[0]
- columns = list(records[0].keys())
- for col, key in enumerate(columns, start=1):
- sheet.Range[1, col].Text = key
- sheet.Range[1, col].Style.Font.IsBold = True
- for row, record in enumerate(records, start=2):
- for col, key in enumerate(columns, start=1):
- value = record.get(key, "")
- sheet.Range[row, col].Text = "" if value is None else str(value)
- sheet.AllocatedRange.AutoFitColumns()
- workbook.SaveToFile(outputPath, ExcelVersion.Version2013)
- workbook.Dispose()
复制代码
要点:os.listdir() 拿到目录条目后用 lower().endswith(".json") 过滤,文件名大小写混用也不会漏;os.path.splitext(name)[0] 去掉原扩展名再拼 .xlsx,同名文件不会互相覆盖。这个示例仍是简单字典列表结构,如果目录里的 JSON 结构不一致或含嵌套对象,要按前面介绍的展平逻辑改造字段提取部分。
八、容易踩坑的几处细节
- 数据结构先确认。基础转换和批量处理都基于「非空字典列表」这一前提。根节点是单个字典,或者存在多层嵌套时,读取和写入逻辑都得改。
- 列顺序按需固定。只需部分字段时,提前定义列名列表,别直接摊开 JSON 的全部键,多份文件转换后的列序才稳定。
- 单元格类型要清楚。示例统一用 Text 写入,数字和日期也会变成文本;如果后续要在 Excel 里做数值计算,得换成对应的单元格赋值方式。
- 读文件指定编码。open() 加 encoding="utf-8" 才能正确读入含中文等非 ASCII 字符的 JSON,减少乱码。
- 批量时及时释放。每个工作簿保存完调用 Dispose(),文件数量大时才不会持续占用内存等资源。
整体流程可以概括为:json.load() 读数据 → 确定列(取自键、固定列表或展平后的并集)→ 用 sheet.Range[行, 列].Text 逐格写入 → 按需设置表头样式和列宽 → SaveToFile() 落盘 → Dispose() 收尾。遇到嵌套结构,在读取和写入之间插入一层递归展平即可,其余代码几乎不用动。 |