JSON转换器实战:从脏数据解析到批量格式转换
简介JSONConverter是一款基于Java开发的JSON数据处理工具专注于解析、生成、验证与格式转换支持对象与JSON的互相映射、嵌套结构处理以及大型JSON的流式解析适合需要频繁操作JSON数据的Java开发者应用于接口调试、数据迁移、前后端数据交互等场景。资源包共43个文件压缩后大小约75KB包含10个Java源码、8个class编译文件、8个properties配置、6个json示例数据以及pom.xml、mvnw等Maven构建文件和各类型说明文档源码与配置分层清晰便于导入项目直接使用或二次开发。目前已有351人学习。通过学习本资源可以获取一套完整的JSON转换器实现思路包括基于org.json和Gson等流行库的封装示例、对象与JSON互转的代码实现、复杂嵌套数据的处理技巧以及流式解析大型JSON的有效方法同时附带的README和ReadMe文档对代码结构和用法进行了说明能够帮助读者快速上手并在此基础上扩展自己的JSON处理工具。 做数据处理的年头久了你会发现一个特别朴素的道理格式转换这事儿看着简单做起来全是坑。就拿 JSON 来说你以为是标准格式实际上不同系统吐出来的 JSON 风格千奇百怪有的 key 带下划线有的用驼峰有的数字是字符串有的字符串里嵌着转义 JSON更别说那种嵌套七八层的结构光肉眼定位字段就得半天。我这次的项目 JSONConverter就是冲着这些痛点去的——做一个本地优先、不依赖在线服务、能处理“脏数据”的 JSON 转换工具。这篇文章会把我从需求拆解到核心实现再到踩坑记录的全过程写出来适合那些天天跟接口数据打交道、被格式不一致折磨的后端、爬虫工程师和数据清洗同学参考。1. 项目定位与整体设计思路1.1 为什么要做一个统一的转换器而不是继续用在线工具先说个场景。上个月我在对接一个老旧系统对方返回的“JSON”实际上是 JSONP 的回调包裹还混着 XML 风格的注释。丢给在线转换工具人家直接报错就算解析过了复制出来的结果还得手动改。更麻烦的是有些接口数据涉及内部字段名不适合贴到第三方网站上做转换。所以 JSONConverter 的第一个设计原则就是本地运行输入输出全在机器上闭环。第二个原则是容错优先——现实世界的数据不会乖乖符合 RFC 8259工具必须能“救”回来一部分坏数据而不是动不动抛异常。第三个原则是批量处理一行命令处理几百个 JSON 文件这在线工具根本做不到。这三个原则直接决定了整个项目的形态一个 Python 编写的命令行工具核心库只依赖标准库 json、re、yaml可选的 PyYAML提供子命令式的操作界面。为什么选 Python 而不是 Node.js 或者 Go因为 Python 的标准库 json 足够高效而且做数据清洗类工具时Python 的字符串处理和异常捕获机制写起来最顺手迭代最快。这个工具具体能做什么我列一下核心功能清单JSON 格式化与压缩解决缩进混乱、换行缺失的问题支持自定义缩进空格数JSON 转 YAML / YAML 转 JSON处理配置文件场景的格式互转JSON 扁平化与反扁平化把嵌套结构拍平或者还原嵌套结构方便存入关系型数据库或导入 Excel类型强制转换把字符串形式的数字/布尔值转成真正对应的类型多文件批量转换同一目录下所有 JSON 文件一键统一格式并输出到指定目录1.2 技术选型解析层、转换层、输出层三层架构JSONConverter 在架构上分了三层。最底层是解析层负责把输入的字符串变成 Python 的 dict / list 结构同时尽可能修复可容忍的格式错误中间层是转换层这一层不关心输入是文件还是标准输入只拿到已经解析好的数据结构按用户指定的规则做变换最上面是输出层负责把结果序列化成目标格式、设置编码、写文件或者打印到终端。这个分层设计不是拍脑袋。最初我写过一版“一把梭”的脚本——读文件、正则替换、拼接字符串一次搞定。结果需求一多就崩了想加 YAML 输出得把整个处理流程重写一遍想支持批量又得复制一份逻辑。后来才意识到解析和输出之间必须有一层稳定的中间表示这就是 Python 的 dict / list。只要中间表示不变输入端无论接 JSON、JSONP 还是带 BOM 的文件输出端无论生成 JSON、YAML 还是 CSV彼此之间完全解耦。提示中间表示选 dict / list 还有一个好处——调试方便。你可以在任意转换环节把结构 dump 成 JSON 打印出来直观看到数据被处理成了什么样。如果自己定义一套中间对象调试成本会高出不少。1.3 项目目录结构与模块规划工程结构上我没有一上来就搞大而全的框架而是按功能拆成了几个模块文件json_converter/ ├── cli.py # 命令行入口负责参数解析和分发 ├── parser.py # 解析层包含容错解析逻辑 ├── transforms.py # 转换层扁平化、类型转换、重命名等 ├── renderers.py # 输出层JSON/YAML/CSV 渲染 ├── file_utils.py # 批量文件处理、编码探测 ├── exceptions.py # 自定义异常 └── tests/ ├── fixtures/ # 测试用的各类JSON样本 └── test_*.pycli.py 只做一件事读参数、调对应函数、处理异常。parser.py 不关心用户最终想要什么格式transforms.py 不关心数据从哪来renderers.py 不关心处理过程。这种“职责单一”的拆分让我后续加新功能时特别省心——比如加一个 JSON 转 CSV 的功能只需要新增一个 render_csv 函数然后注册到 cli 的参数映射里就行其他模块一行都不用动。2. 核心模块拆解与实现细节2.1 解析层怎么处理那些“不合法”但常见的 JSONJSONConverter 的解析层是整工具的灵魂。标准库 json.loads 能处理 99% 的合法 JSON但现实中的“脏 JSON”才是把我逼到写容错解析的元凶。最常见的三种脏数据。第一种是JSONP 包裹形如callback({...})这是老接口的遗留物第二种是单引号代替双引号JavaScript 语法允许但 JSON 标准不允许第三种是尾逗号数组或对象最后一个元素后面多了一个逗号浏览器开发者工具里手写时太容易带上。我实现的解析流程是这样的import json import re def tolerant_parse(text: str): # 去掉 BOM if text.startswith(\ufeff): text text[1:] # 处理 JSONP去掉最外层的函数包裹 m re.match(r^\s*[a-zA-Z_$][\w$]*\s*\((.*)\)\s*;?\s*$, text, re.S) if m and not text.strip().startswith({): text m.group(1) # 去掉 // 和 /* */ 注释 text re.sub(r/\*.*?\*/, , text, flagsre.S) text re.sub(r(?!:)//[^\n]*, , text) # 单引号转双引号简化版 text re.sub(r(?!\\)(.*?)(?!\\), r\1, text, flagsre.S) # 去除尾逗号 text re.sub(r,\s*([}\]]), r\1, text) return json.loads(text)这段代码是“尽力而为”的思路不是银弹。还有更复杂的脏数据比如 key 没加引号、字符串里有未转义换行我也有对应的修复分支但在主流程里我会保持克制——修复的规则越复杂误伤的几率越大。宁可让 5% 的极端坏数据报错也不能把 95% 的正常数据处理错。2.2 转换层扁平化和类型转换的算法细节转换层里最常用也最容易被低估的是扁平化。所谓扁平化就是把{a: {b: 1, c: [2, 3]}}变成{a.b: 1, a.c[0]: 2, a.c[1]: 3}。实现这个功能用递归但递归的陷阱在分隔符冲突。我用“.”做 key 的分隔符但用户的数据里如果本身就有一个 key 叫 a.b扁平化之后就会和嵌套的 a-b 冲突。解决方式是加一个可配置项允许用户选择分隔符同时在遇到冲突时保留两个都作为输出项并报警告。def flatten(obj, parent_key, sep.): items [] if isinstance(obj, dict): for k, v in obj.items(): new_key f{parent_key}{sep}{k} if parent_key else str(k) items.extend(flatten(v, new_key, sep).items()) elif isinstance(obj, list): for i, v in enumerate(obj): new_key f{parent_key}[{i}] items.extend(flatten(v, new_key, sep).items()) else: items.append((parent_key, obj)) return dict(items)类型转换则是另一个实用功能。接口返回的字段经常是{age: 25, vip: false}而你需要的是{age: 25, vip: False}。转换规则支持白名单和全量两种模式指定字段名就只转指定字段或者全字段尝试转换。全量转换有风险比如一个字符串00123转成数字会丢失前导零所以我的实现里默认禁止把字符串转 int除非显式加--force-number。这个细节就是我在实际对接数据接口时被坑出来的经验。2.3 输出层不同目标格式的渲染差异输出层最核心的一点是JSON 和 YAML 的布尔值、null 在序列化时行为不同。JSON 的null序列化到 YAML 是null这没问题但 YAML 里的日期字符串如果没有正确加引号解析时会被自动转成 date 对象再转回 JSON 就变成了带 T 的 ISO 字符串和原值不一样了。所以我在 render_yaml 时做了特殊处理def render_yaml(data): import yaml class Dumper(yaml.SafeDumper): pass def _represent_str(dumper, data): # 字符串如果能被 YAML 隐式解析成其他类型就强制加引号 if re.match(r^(true|false|null|yes|no|\d{4}-\d{2}-\d{2})$, data, re.I): return dumper.represent_scalar(tag:yaml.org,2002:str, data, style) return dumper.represent_scalar(tag:yaml.org,2002:str, data) Dumper.add_representer(str, _represent_str) return yaml.dump(data, DumperDumper, allow_unicodeTrue, sort_keysFalse)输出编码上我统一用 UTF-8并且ensure_asciiFalse避免中文被转成\uXXXX一串天书。命令行工具默认不添加 BOM但提供--add-bom参数给那些必须用 Windows 记事本打开文件的场景。这种“默认安全需要时放开”的思路贯穿整个输出层设计。3. 实操演示从命令行到 API 的完整流程3.1 环境准备与基础用法项目用 Python 3.9唯一可选的第三方依赖是 PyYAML。如果你只需要 JSON 格式化、压缩、扁平化连 PyYAML 都不用装。克隆下来之后pip install pyyaml python -m json_converter --help我平时用得最多的几个命令是这些# 格式化一个文件缩进4格输出到新文件 json_converter format input.json -o output.json --indent 4 # 把整个目录的 .json 批量压缩到 out/ 目录 json_converter minify data/*.json -o out/ # JSON 转 YAML json_converter convert input.json -o output.yaml --to yaml # 扁平化并导出 CSV配合管道使用 json_converter flatten input.json --sep . | json_converter to-csv -o flat.csv # 智能解析带 JSONP 包裹的脏文件 json_converter parse legacy.json --tolerant -o clean.json每一行命令背后对应的是 cli.py 里一个短小精悍的分发函数。cli 不直接处理业务逻辑只做参数归一化和异常捕获。这保证了不管从哪个入口进来行为都一致。3.2 完整案例把一个嵌套 JSON 转成可导入数据库的 CSV我拿一个模拟的电商订单数据来演示完整流程。假设原始 JSON 是这样{ order_id: A1024, user: {name: 张三, vip: true}, items: [ {sku: SKU-01, price: 19.9}, {sku: SKU-02, price: 29.0} ], created_at: 2024-06-01 10:00:00 }要导入关系型数据库第一步扁平化json_converter flatten order.json输出order_id: A1024 user.name: 张三 user.vip: true items[0].sku: SKU-01 items[0].price: 19.9 items[1].sku: SKU-02 items[1].price: 29.0 created_at: 2024-06-01 10:00:00第二步转换类型把字符串布尔值和数字转成真实类型json_converter flatten order.json | json_converter cast --field user.vip --field items[*].price --force-number这里的items[*].price是通配符语法我在实现时解析出 items 数组里所有对象的 price 字段然后做类型转换。第三步输出 CSV... | json_converter to-csv --delimiter ;最后得到的 CSV 长这样order_id;user.name;user.vip;items[0].sku;items[0].price;items[1].sku;items[1].price;created_at A1024;张三;true;SKU-01;19.9;SKU-02;29.0;2024-06-01 10:00:00整个流程走下来全程不需要打开任何图形界面也可以写进 shell 脚本实现每天晚上自动跑。这是在用命令行做法最舒服的地方。3.3 性能优化与大批量文件处理处理单文件时性能完全不是问题json.loads 在毫秒级就能搞定大部分文件。真正的瓶颈在批量处理大量小文件的场景。我遇到过一个需求一个目录下有 3000 多个 JSON 文件每个几十 KB需要统一格式化并入库。最初的实现是循环里一个个调用 parse_and_render速度很慢只有每秒几十个文件。分析发现瓶颈不在解析而是频繁的文件打开/关闭和字符串拼接。后来我做了三个优化第一批量文件池用os.scandir一次性拿目录信息避免重复调用os.path.join。第二输出延迟刷新攒够一定数量再统一写磁盘减少 IO 次数。第三多进程并行用concurrent.futures.ProcessPoolExecutor按 CPU 核数均分文件实测 4 核机器上速度从几十个每秒提升到三百多个每秒。def process_many(input_dir, output_dir, workersNone): from concurrent.futures import ProcessPoolExecutor files [f for f in os.scandir(input_dir) if f.name.endswith(.json)] with ProcessPoolExecutor(max_workersworkers) as pool: futures [pool.submit(process_one, f.path, output_dir) for f in files] for fut in tqdm(as_completed(futures), totallen(futures)): fut.result()这段代码在单机上跑得很稳。如果文件数再多一个量级可以考虑换成生产者-消费者模式让 IO 和解析并行。但对于日常使用多进程方案已经足够实在。4. 常见问题与排查技巧实录4.1 解析失败从报错信息反推问题根源使用过程中最常遇到的报错是json.decoder.JSONDecodeError。绝大多数情况是数据源不做校验就吐数据最常见的是直接在 JSON 里写了 JavaScript 的undefined。标准库 json 只认null遇到undefined直接炸。我在 tolerant 解析里加了这个替换规则text re.sub(r\bundefined\b, null, text)这算是一个“明知不严谨但很有用”的修复。另外一个容易踩的坑是字符串里有不可见的特殊空白字符比如\u00a0不间断空格。肉眼看不见json.loads 能正常解析但后续写数据库或者做字符串匹配时容易莫名其妙出问题。我在 parser 层默认把 key 和字符串值首尾的\u00a0去掉同时提供--keep-unicode-space参数关闭这个行为。4.2 中文编码乱码的三种典型表现第一批用这个工具的朋友反馈过三个编码问题。第一种输出文件用记事本打开全乱码。这是因为 Windows 记事本默认解码 GBK而文件是 UTF-8 无 BOM。我的解决方式是--add-bom参数在文件头部写入三个字节的 BOM记事本就能正确识别了。第二种终端打印乱码。这是 Windows 控制台代码页的问题我在 cli 里加了sys.stdout.reconfigure(encodingutf-8)保证管道和重定向时的编码一致性。第三种写入文件时 UnicodeEncodeError。这是因为 Python 在 Windows 上用默认编码打开文件我全部显式指定open(..., encodingutf-8)。4.3 循环引用与超大嵌套深度Python 的 json.dumps 默认不允许序列化循环引用的对象如果你的数据源是一个 Python 对象直接转 JSON比如爬虫里抓到带环的 graph会报ValueError: Circular reference detected。JSONConverter 里的应对方案是在序列化前先检查对象里是否包含自引用如果包含就按照引用路径折叠成字符串。比如一个节点node[children][0]指向自身我会替换成CircularRef: node。这是处理图数据时不得不用的临时方案。如果碰到深层嵌套比如 5000 层json 模块的递归解析会直接触发 RecursionError。我给 parse 函数加了sys.setrecursionlimit(20000)但实测超深层级依然是脆弱的正确做法是改用迭代式解析器。这个场景比较少见遇到后我一般建议用户先检查嵌套异常的数据逻辑。4.4 实战排查流程速查表症状可能原因排查命令解决方案解析报错 JSONDecodeError带 BOM / JSONP / 注释file data.json查看类型加--tolerant参数中文输出乱码编码不一致查看输出文件 hex 头用--add-bom或改终端代码页字段自动变了类型YAML 隐式类型转换对比 JSON 和 YAML 中值开启字符串强制加引号大量文件处理很慢IO 频繁观察 CPU 使用率加--workers 4多进程处理数组元素转类型失效通配符路径写错开启--debug打印匹配结果用items[*].price语法4.5 两个亲测有效的调试技巧最后分享两个我在开发过程中觉得特别值得保留的调试技巧。第一个是打印解析中间态。我在 cli 里藏了一个--debug参数开启后会把每个阶段原始文本、修复后文本、解析后的 dict、转换后的 dict都输出到 stderr。这样用户报 Bug 时只要把--debug的输出发过来我一眼就能定位是解析层还是转换层出了问题。这个习惯后来用在了所有我写的命令行工具上效果非常好。第二个是保留测试样本。我在 tests/fixtures 目录里存了一堆从真实环境收集的“畸形 JSON 样本”每个样本都有说明。这些样本不是构造出来的而是线上真实跑出来的数据脱敏后的版本。每次改代码我都跑一遍全部样本防止修了一个问题又搞坏另一个场景。自动化测试在这种“小工具”上的价值被严重低估了——我见过太多工具项目只有功能代码没有测试等到改 bug 时根本不敢动老代码。从我的实际使用体验来看JSONConverter 这个项目最大的收获不是“我又写了一个工具”而是让我把“处理脏数据”这整件事的方法论沉淀了下来解析要包容、转换要可控、输出要确定、问题要可排查。如果你也经常被 JSON 格式问题困扰建议不要只停留在用在线工具临时解渴而是花一个下午把你最常遇到的转换场景理清楚写成自己的小工具。哪怕只是几十行脚本后面省下的时间绝对远超投入。本文还有配套的精品资源点击获取