简介:面向开发者的轻量JSON/XML格式化与比对工具包,适合前端、后端及测试人员在日常接口调试、配置核查、日志分析中快速校验数据格式、定位错误并对比差异。工具提供实时格式校验,能准确显示错误所在行号与列号,一键格式化、复制和清空功能让数据整理更高效;JSON比对模块采用双面板设计,支持行号显示、层级折叠、差异可视化分析,并可交换左右数据、窗口最大化,便于处理复杂嵌套结构。压缩包约37KB,共7个文件,以HTML、CSS、JavaScript为主,其中HTML/CSS负责页面与界面样式,JS实现格式化与差异比对核心逻辑,另附JSON配置、PNG图标及Markdown说明文档,整体结构清晰,既可直接使用也便于参考改造。已有244人学习下载,适合需要频繁处理接口数据、配置文件和日志的开发人员使用。
1. 为什么这三个小工具值得花时间折腾
json 格式化工具、xml 格式化工具、json 数据比对工具,这三个东西单独看都是小工具,凑在一起就是一套本地数据处理三板斧。接口联调时最磨人的场景不是逻辑写错,而是两边各返回一坨挤成一行、没有换行的 JSON 和 XML,肉眼盯十分钟也看不出差异;等你把差异找到了,往往发现只是键的顺序不同,根本不是 bug。这套三件套解决的就是这类问题:先格式化把结构摊开,再用可靠的比对方式找出真实差异,最后把同一个流程固化成一个命令,下次遇到直接跑。
这套东西适合谁?后端排查接口响应、前端联调 mock 数据、数据工程师检查配置文件、以及所有被"肉眼 diff"折磨过的人。不需要装大型 IDE,命令行和一个小脚本就够用。下面我把原理、最小命令、自建比对脚本和踩过的坑按顺序讲清楚。
2. 格式化与比对的底层逻辑:先搞清楚工具在替你做什么
2.1 JSON 格式化到底在格式化什么
JSON 格式化不是简单地在逗号后面加个换行,它背后一定有一个解析器先把文本读成内存里的结构体,再把这个结构体重新序列化输出。这个过程至少有四件事:缩进排版、键序调整(可选)、字符串转义还原、以及最重要的语法校验。一段非法 JSON,让格式化工具跑一遍会直接报错,所以格式化工具天然是校验工具。
理解这一点你就明白,为什么"格式化"和"查询"经常出现在同一个工具里。像 jq 这种命令行的 json 查询函数,本质是在解析后的结构体上做取值、过滤和映射,而不是像正则那样硬啃字符串。很多人遇到"spark 中读取 json 报错"的第一反应是去改 SQL,其实更快的办法是先把 source 文件拿下来格式化一遍,看 JSON 解析器在哪一行吐;因为 spark 读 json 时用的也是解析器,解析失败的原因绝大多数是被引号、末尾逗号或坏 Unicode 搞崩的。
另一个容易忽略的点:JSON 的键顺序在语义上没有意义,{"a":1,"b":2}和{"b":2,"a":1}是同一个对象。所以一个合格的格式化工具应该允许你选择是否排序键,而一个合格的比对工具应该默认忽略键序差异。这个原则贯穿后面所有操作。
2.2 XML 格式化与标签语义:dom4j 那套规则
XML 格式化比 JSON 麻烦一点,因为 XML 除了节点嵌套,还有属性、命名空间、CDATA、实体引用这些东西。格式化工具要做的是调整文本排版,但绝不能动标签的配对关系。你写一个 Java 程序用 dom4j 解析 XML,步骤无非是加载 Document、遍历 Node、再重新输出;命令行里的 XML 格式化工具做的也是同一件事,只是把"重新输出"这一步变成了带缩进的序列化。
所以我建议你把格式化工具当成一个"解析器的排版外皮"来用:它能排版,前提是文件能被解析。经常有人说"xml 格式文件没有标签怎么办",这种文件本质上是纯文本,标签都没了说明它或者根本不是 XML,或者标签在传输中被某个环节吃掉了。格式化工具不能凭空补标签,它只能帮你把已有的结构摊开。先认清这一点,排查方向就不会跑偏。
XML 里还有一个容易踩的语义细节:属性顺序不影响语义,CDATA 里的内容则是原样保留的。所以一个可靠的 XML 格式化工具必须做到两点——不重排属性顺序,不破坏 CDATA 内容。市面上有些在线工具会把属性按字典序重排,看起来整齐了,实际上改变了原始文档的指纹,做签名校验时就会翻车。
2.3 比对工具的核心:文本 diff 与结构 diff 的差距
先说文本 diff,就是diff命令干的事:逐字节或逐行比较,找到第一处不同就报出来。对代码文件它很合适,对 JSON 和 XML 就经常误报。两个 JSON 语义完全一样,只要键顺序不同,diff会报出一大片红色;两个 XML 只要标签间的空白文本节点不一样,也会到处标红。
结构 diff 是另一条路:先把两边分别解析成树,再递归比较对应节点。这样键序变化不会误报,数字 1 和 1.0 也能靠类型规则区分。市面上好的 json 数据比对工具都走结构 diff,但它比文本 diff 贵——要先完整解析,而且对数组顺序怎么处理是个策略问题:是把数组当成有序列表逐项比较,还是当成无序集合做匹配?大多数场景里数组是有序的,我建议比对工具默认按顺序比。
选型上我的建议是分三层:最小范围用jq -S排序后接diff,这能覆盖七成简单场景;复杂嵌套结构用自写递归比对脚本,能输出精确的差异路径;实在要给人看的可视化比对才上图形化工具。不要一上来就装大客户端,命令行三件套在你服务器上也能用。
3. 用 jq 和 xmllint 跑通格式化:最小命令与参数对照
3.1 JSON 格式化最小命令:jq 的缩进与排序参数
如果你的系统里有 jq(没有的话包管理器直接装),格式化就是一条命令的事。先看最小命令:
# 格式化输出到标准输出,不做键排序 jq '.' raw.json # 按键名排序后再格式化,常用于比对前的预处理 jq -S '.' raw.json # 自定义缩进为 4 个空格,读取 json 数组文件也一样适用 jq --indent 4 '.' raw.json逻辑说明:.是 jq 的过滤器,表示"原样取出整个文档"。jq 拿到这个过滤器后会把输入解析成内部结构,再按默认的 2 空格缩进输出,所以jq '.'就是最基础的格式化命令。-S是--sort-keys的简写,会递归地按键名排序,这个参数在比对场景里是核心——前面说过键序不影响语义,排序后再 diff 就能消除一类假差异。--indent 4是给喜欢 4 空格缩进的人准备的,不影响语义。
参数说明:如果只想校验不想看输出,用jq -e '.' file > /dev/null,-e会让 jq 在解析失败时返回非零退出码,方便写进脚本判断。注意 jq 默认输出 UTF-8,不会把中文转成\uXXXX,这点和后面要讲的 Pythonjson.dumps正好相反。
3.2 XML 格式化最小命令:xmllint 的缩进与编码参数
XML 这边推荐xmllint,它来自 libxml2 工具集,基本是 Linux 和 macOS 都会自带的。最小命令:
# 格式化输出到标准输出 xmllint --format raw.xml # 格式化并写回另一个文件,原文件保持不变 xmllint --format raw.xml --output pretty.xml # 指定输出编码,避免中文乱码 xmllint --format raw.xml --encode UTF-8 --output pretty.xml逻辑说明:--format会重新缩进所有节点,把自闭合标签、换行、缩进统一处理掉,同时保留属性顺序和 CDATA 内容。--output指定写回的文件,这是一个好习惯——不要让工具直接覆盖原文件,万一格式化失败原文件还在。--encode UTF-8是处理中文的重要参数,如果你的 XML 文件是 GBK 编码,不加这个参数输出到终端会乱码。
参数说明:如果你只是想校验 XML 合法性,用xmllint --noout file.xml,它只报错不输出。和 JSON 一样,格式化工具同时是校验工具,xmllint遇到多个根节点、未闭合标签会直接报错并返回非零退出码。顺手提一句:如果你在写 Java,用 dom4j 解析 XML 的步骤里,最后 serializer 输出的就是格式化后的文本,本质上和 xmllint 做的是同一件事。
3.3 批量格式化与写回文件:先临时文件再覆盖
单文件格式化学会后,自然要做批量。这里有一个我踩过的坑:千万别直接在原文件上重定向。下面这个模式是安全的:
# 批量格式化当前目录下所有 json 文件 for f in *.json; do jq -S '.' "$f" > "$f.tmp" && mv "$f.tmp" "$f" done # 批量格式化所有 xml 文件,先临时文件再覆盖 for f in *.xml; do xmllint --format "$f" --output "$f.tmp" && mv "$f.tmp" "$f" done逻辑说明:第一条命令把格式化结果写到临时文件,只有 jq 成功退出(&&保证退出码为 0)才用mv覆盖原文件。这样即使某个文件解析失败,原文件也不会被一个空文件或半截输出覆盖。第二个循环同理,xmllint的--output指定临时文件名,成功后才覆盖。
参数说明:批量处理前先确认所有文件编码一致,混着 GBK 和 UTF-8 的目录会让这组命令产生乱码输出。另外这两条命令对子目录不生效,需要递归的话把*.json换成find . -name "*.json"配合while read循环。这种"先临时文件再覆盖"的习惯能帮你省掉很多后悔药。
实际使用中,我从网上下载接口样例或 json 格式文件时,第一步都是先跑一遍格式化看看结构。在 spark 中读取 json 之前也一样:先把下载的杂乱的 json 文件用jq '.'洗一遍,能直接从报错行判断坏数据在哪,比在分布式环境里反复 job 失败快得多。
4. 自建 JSON 数据比对脚本:递归比对与差异路径输出
4.1 为什么不能直接 diff:键序与空白带来的假差异
先做一个实验:两个语义完全相同的 JSON,diff会怎样报?看下面这段:
echo '{"name":"a","age":18}' | jq -S '.' > left.json echo '{"age":18,"name":"a"}' | jq -S '.' > right.json diff left.json right.json正常执行后没有任何输出,因为jq -S把两个文件的键都排成了age在前、name在后,文本完全一致。但如果去掉-S直接 diff,你会看到两行全被标红。这就是键序带来的假差异。另一种假差异来自空白:一个文件是 2 空格缩进,另一个是 4 空格缩进,diff会把每一行都当成不同。
所以我的结论是:文本 diff 只能用于"已知两边已被相同规则格式化过的 JSON"。满足这个前提后,jq -S加diff是最快的冒烟比对方式。但它有个致命盲区——数组顺序不同时,它只会告诉你在哪一行不同,而不会告诉你"哪个元素在左边有右边没有"。这个问题需要结构比对来解决。
4.2 Python 递归比对脚本:处理嵌套、数组与类型
我自用的比对脚本核心是一个递归函数,输出的是可读的差异路径,比如root.items[2].price。下面是这个脚本的完整版:
import json import sys def diff_json(a, b, path="root"): # bool 是 int 的子类,先排除,避免 True 和 1 被判为同类 if isinstance(a, bool) != isinstance(b, bool): yield f"{path}: type {type(a).__name__} != {type(b).__name__}" return # 数字类型统一成 float 比较,1 和 1.0 视为相等 if isinstance(a, (int, float)) and isinstance(b, (int, float)): if float(a) != float(b): yield f"{path}: {a!r} != {b!r}" return if type(a) != type(b): yield f"{path}: type {type(a).__name__} != {type(b).__name__}" return if isinstance(a, dict): for k in sorted(set(a.keys()) | set(b.keys())): if k not in a: yield f"{path}.{k}: missing in left" elif k not in b: yield f"{path}.{k}: missing in right" else: yield from diff_json(a[k], b[k], f"{path}.{k}") elif isinstance(a, list): if len(a) != len(b): yield f"{path}: length {len(a)} != {len(b)}" for i, (x, y) in enumerate(zip(a, b)): yield from diff_json(x, y, f"{path}[{i}]") else: if a != b: yield f"{path}: {a!r} != {b!r}" if __name__ == "__main__": if len(sys.argv) != 3: print("usage: jsondiff.py left.json right.json") sys.exit(2) with open(sys.argv[1], encoding="utf-8") as f1, \ open(sys.argv[2], encoding="utf-8") as f2: left = json.load(f1) right = json.load(f2) diffs = list(diff_json(left, right)) if diffs: print("\n".join(diffs)) sys.exit(1) print("same")逻辑说明:函数从根节点开始递归,遇到 dict 就取两边键的并集,排序后逐个比较,缺失的键单独报;遇到 list 先比长度,再按索引逐项比较;其余类型直接比值。所有差异都用生成器yield抛出来,主函数统一收集后打印。退出码是 1 表示有差异,0 表示相同,方便接进 shell 脚本。
参数说明:脚本第 10 行到第 15 行处理了最常见的一类误报——1和1.0在 Python 里分别被解析成int和float,但 JSON 语义上它们相等,所以统一转成float比较。bool单独排除,是因为True == 1在 Python 里成立,不排除的话true和1会被误判为相等。数组顺序按"重要差异"处理,只要顺序不同就报,这是大多数接口联调场景想要的。
4.3 两个实战变体:接口响应比对与 json merge conflict 合并复查
第一个变体是接口响应比对。把两个环境的接口响应各自存成文件,然后跑脚本:
curl -s "https://api-a.example.com/v1/orders" -H "Authorization: Bearer $TOKEN" > resp-a.json curl -s "https://api-b.example.com/v1/orders" -H "Authorization: Bearer $TOKEN" > resp-b.json python3 jsondiff.py resp-a.json resp-b.json脚本会输出类似root[3].total: 12.5 != 12.0的行,直接定位到数组第 4 个元素的total字段差异。这比截两张图左右对比快十倍。注意接口响应里如果有时间戳这种必然变化的字段,比对前先过滤掉,否则每次都有差异。
第二个变体是多人协作时遇到的 json merge conflict。Git 合并两个都改过package.json或配置文件的版本时,会留下冲突标记。手工解决冲突后,拿这个脚本把"我合出来的结果"和"本来的预期结果"做一次比对,能确认合并过程中没有意外丢掉字段。做法是把冲突解决后的文件存成merged.json,把主干版本存成base.json,然后:
python3 jsondiff.py base.json merged.json输出全是missing in right就说明手动合并时丢了内容。这个场景里脚本的价值不是替代 Git 的 diff 工具,而是给"手工合并结果"一个客观的验收出口。我习惯在提交前跑一遍,比反复肉眼确认踏实得多。
5. 格式化与比对的 5 个翻车现场与避坑清单
5.1 现象:JSON 中文被转成 \uXXXX
格式化之后 JSON 里的中文全变成\u4e2d\u6587这种形式。功能上没错,但没法直接读。
原因:某些格式化工具默认启用"ASCII 安全输出",把非 ASCII 字符全部转义。Python 的json.dumps默认ensure_ascii=True就是这个行为;jq 在加了-a(--ascii-output)时也会这样。另一个来源是复制到某些在线工具后,它默认按转义模式输出。
解决:用json.dumps(data, ensure_ascii=False)写脚本;用 jq 时不加-a。如果你手里已经有一个被转义的文件,用 jq 重新格式化一次会还原成中文,因为 jq 解析时会自动把\uXXXX解码回字符。记住这条规则:格式化工具转义不转义,只是序列化配置,不是数据损坏。
5.2 现象:XML 格式化后内容全部挤在一行
把 XML 丢给 xmllint 后,发现输出确实缩进了,但所有内容还是在同一行上,看起来跟没格式化一样。
原因:最常见的是你在命令后面又加了一层管道处理,比如xmllint --format raw.xml | tr -d '\n',把 xmllint 辛苦加的换行全删了。另一个隐蔽原因是文件里的换行是 实体转义,解析后是文本内容里的真实换行,不是排版换行;这种文件格式化后内容连在一起是正常的,因为它本来就没有标签间空白。
解决:别在管道里二次处理,直接xmllint --format raw.xml --output pretty.xml写文件看。如果文件本身没有标签间空白,你想让它可读只能先插入缩进空白——但这时候修改的是文档树,有可能影响依赖空白节点的下游逻辑。我的习惯是:格式化只用于人眼排查,不与签名校验共用同一份输出。排查完还是用原始文件干活。
5.3 现象:嵌套 JSON 比对误报差异
用自写脚本比对时,报出一堆root.a.b: 1 != 1.0,或者root.c: True != 1这种差异,但业务上明明是等价的。
原因:JSON 解析器对数字的处理不一致。Python 的json.loads把1解析成int,把1.0解析成float;而 JavaScript 的解析器一律变成number,没有类型区别。所以在 Python 里直接==比较就会出现1 != 1.0。True和1的坑更隐蔽,因为bool是int的子类,True == 1成立,反过来会让你漏掉真正的类型差异。
解决:按前面脚本里的做法,先排除 bool,再把 int 和 float 统一成float比较。如果你不想改脚本,就在生成比对文件时把两边都过一遍jq——jq 会把1和1.0在内部统一处理,输出格式一致后再比。但注意 jq 这条路线对 bool 不生效,true和1在 jq 里仍然是不同类型。
5.4 现象:接口返回的 XML 带 BOM 或多根节点
xmllint 报错,提示parser error或者Extra content at the end of the doc。文件看起来是合法 XML,但就是过不了。
原因:从 Windows 环境拿到的 XML 文件开头带了一个 UTF-8 BOM(EF BB BF),xmllint 在某些配置下会把 BOM 当成内容读进去,导致解析失败。另一个常见原因是工具或接口把多个 XML 文档拼在一个文件里返回了,比如一个列表接口把每条记录各吐了一个 XML 根节点,拼在一起就成了多根节点文档。
解决:去 BOM 用一行sed -i '1s/^\xEF\xBB\xBF//' file.xml。多根节点的情况,如果文件是多个 XML 文档拼接,可以用xmllint --recover --format file.xml尝试恢复,但更干净的办法是手工包一个外层根节点,比如:
# 把多根节点包进一个 root 标签 { echo "<root>"; cat broken.xml; echo "</root>"; } | xmllint --format -注意这只能用来排查,包根节点后的文档和原始语义不等价,别拿它当正式数据。至于"xml 格式文件没有标签怎么办"那种情况,文件里根本没有<字符的那就不是 XML,格式化工具无能为力,先回头查生成方。
5.5 现象:大文件格式化卡死
几十 MB 的 JSON 或 XML 丢给 jq、xmllint 后,CPU 飙满,等了几分钟没反应,最后被系统 OOM 杀掉。
原因:这两个工具都要把完整文档读入内存再重建结构,JSON 的解析树在内存里通常是文件体积的几倍到十几倍。你本地格式化一个 200MB 的 json 数组,内存占用轻松到 2GB 以上。这不算工具 bug,是结构化数据的固有开销。
解决:格式化前先看文件大小,超过 50MB 就换个策略。JSON 用小样本先验证格式,或者用流式解析库逐条读;XML 用xmllint --noout只校验不排版,省掉序列化那部分开销。在 spark 中读取 json 的大文件场景,正确做法是让 spark 自己去解析,不在本地先格式化——分布式引擎对大数据量的处理设计就是为这个场景存在的。记住:格式化工具是给人眼看的,不是给机器跑批的。
6. 把三件套接进日常调试:一个 20 行的入口脚本
前面讲了原理和命令,最后把这些落成一个我每天都在用的入口脚本。它做的事情只有三件:校验、格式化、比对,全部基于退出码判断结果,能直接粘到~/.bashrc或~/.zshrc里:
# JSON 校验:成功输出 ok,失败输出错误并返回 1 jsoncheck() { jq -e '.' "$1" > /dev/null && echo "json ok" || echo "json broken"; } # XML 校验:只解析不输出 xmlcheck() { xmllint --noout "$1" && echo "xml ok" || echo "xml broken"; } # JSON 比对:调用前面的 jsondiff.py,有差异打印路径并返回 1 jsondiff() { python3 "$HOME/bin/jsondiff.py" "$1" "$2"; } # 一键格式化:输出到 .pretty 文件,不动原文件 jsonpretty() { jq -S '.' "$1" > "$1.pretty"; } xmlpretty() { xmllint --format "$1" --output "$1.pretty"; }用法就是jsoncheck resp.json、jsondiff left.json right.json,有差异时脚本把路径一行行列出来,退出码可以接进 CI 或者 pre-commit 钩子。我还习惯在比对前先做一次jq -S预处理,让键序一致,这样即使万一用到diff也不会被排序差异干扰。验证方式很简单:自己造两个只差一个字段值的 JSON 文件,跑一遍脚本确认输出路径准确,再把它们排序后跑一遍确认退出码是 0。
这套东西陪我处理过很多次"看起来一样、实际不一样"的接口问题。现在我的习惯是:任何一次接口联调,拿到响应先格式化,结构看清了再比对;比对出差异先看路径,路径指向的字段往往比我想象的更靠内层。格式化工具最大的价值不是排版好看,而是让差异无处可藏。希望帮到你。
本文还有配套的精品资源,点击获取