资讯动态

JSON与XML格式化比对三板斧:jq、xmllint与自建脚本实战

发布时间:2026/10/9 16:14:42 来源:尧图企业网站定制
简介面向开发者的轻量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键序与空白带来的假差异先做一个实验两个语义完全相同的 JSONdiff会怎样报看下面这段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, pathroot): # 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], encodingutf-8) as f1, \ open(sys.argv[2], encodingutf-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_asciiTrue就是这个行为jq 在加了-a--ascii-output时也会这样。另一个来源是复制到某些在线工具后它默认按转义模式输出。解决用json.dumps(data, ensure_asciiFalse)写脚本用 jq 时不加-a。如果你手里已经有一个被转义的文件用 jq 重新格式化一次会还原成中文因为 jq 解析时会自动把\uXXXX解码回字符。记住这条规则格式化工具转义不转义只是序列化配置不是数据损坏。5.2 现象XML 格式化后内容全部挤在一行把 XML 丢给 xmllint 后发现输出确实缩进了但所有内容还是在同一行上看起来跟没格式化一样。原因最常见的是你在命令后面又加了一层管道处理比如xmllint --format raw.xml | tr -d \n把 xmllint 辛苦加的换行全删了。另一个隐蔽原因是文件里的换行是#10;实体转义解析后是文本内容里的真实换行不是排版换行这种文件格式化后内容连在一起是正常的因为它本来就没有标签间空白。解决别在管道里二次处理直接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 BOMEF BB BFxmllint 在某些配置下会把 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。这套东西陪我处理过很多次看起来一样、实际不一样的接口问题。现在我的习惯是任何一次接口联调拿到响应先格式化结构看清了再比对比对出差异先看路径路径指向的字段往往比我想象的更靠内层。格式化工具最大的价值不是排版好看而是让差异无处可藏。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑