)
簡介面向開發(fā)者的輕量JSON/XML格式化與比對工具包適合前端、后端及測試人員在日常接口調試、配置核查、日志分析中快速校驗數據格式、定位錯誤并對比差異。工具提供實時格式校驗能準確顯示錯誤所在行號與列號一鍵格式化、復制和清空功能讓數據整理更高效JSON比對模塊采用雙面板設計支持行號顯示、層級折疊、差異可視化分析并可交換左右數據、窗口最大化便于處理復雜嵌套結構。壓縮包約37KB共7個文件以HTML、CSS、JavaScript為主其中HTML/CSS負責頁面與界面樣式JS實現格式化與差異比對核心邏輯另附JSON配置、PNG圖標及Markdown說明文檔整體結構清晰既可直接使用也便于參考改造。已有244人學習下載適合需要頻繁處理接口數據、配置文件和日志的開發(fā)人員使用。1. 為什么這三個小工具值得花時間折騰json 格式化工具、xml 格式化工具、json 數據比對工具這三個東西單獨看都是小工具湊在一起就是一套本地數據處理三板斧。接口聯調時最磨人的場景不是邏輯寫錯而是兩邊各返回一坨擠成一行、沒有換行的 JSON 和 XML肉眼盯十分鐘也看不出差異等你把差異找到了往往發(fā)現只是鍵的順序不同根本不是 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 那套規(guī)則XML 格式化比 JSON 麻煩一點因為 XML 除了節(jié)點嵌套還有屬性、命名空間、CDATA、實體引用這些東西。格式化工具要做的是調整文本排版但絕不能動標簽的配對關系。你寫一個 Java 程序用 dom4j 解析 XML步驟無非是加載 Document、遍歷 Node、再重新輸出命令行里的 XML 格式化工具做的也是同一件事只是把重新輸出這一步變成了帶縮進的序列化。所以我建議你把格式化工具當成一個解析器的排版外皮來用它能排版前提是文件能被解析。經常有人說xml 格式文件沒有標簽怎么辦這種文件本質上是純文本標簽都沒了說明它或者根本不是 XML或者標簽在傳輸中被某個環(huán)節(jié)吃掉了。格式化工具不能憑空補標簽它只能幫你把已有的結構攤開。先認清這一點排查方向就不會跑偏。XML 里還有一個容易踩的語義細節(jié)屬性順序不影響語義CDATA 里的內容則是原樣保留的。所以一個可靠的 XML 格式化工具必須做到兩點——不重排屬性順序不破壞 CDATA 內容。市面上有些在線工具會把屬性按字典序重排看起來整齊了實際上改變了原始文檔的指紋做簽名校驗時就會翻車。2.3 比對工具的核心文本 diff 與結構 diff 的差距先說文本 diff就是diff命令干的事逐字節(jié)或逐行比較找到第一處不同就報出來。對代碼文件它很合適對 JSON 和 XML 就經常誤報。兩個 JSON 語義完全一樣只要鍵順序不同diff會報出一大片紅色兩個 XML 只要標簽間的空白文本節(jié)點不一樣也會到處標紅。結構 diff 是另一條路先把兩邊分別解析成樹再遞歸比較對應節(jié)點。這樣鍵序變化不會誤報數字 1 和 1.0 也能靠類型規(guī)則區(qū)分。市面上好的 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會重新縮進所有節(jié)點把自閉合標簽、換行、縮進統一處理掉同時保留屬性順序和 CDATA 內容。--output指定寫回的文件這是一個好習慣——不要讓工具直接覆蓋原文件萬一格式化失敗原文件還在。--encode UTF-8是處理中文的重要參數如果你的 XML 文件是 GBK 編碼不加這個參數輸出到終端會亂碼。參數說明如果你只是想校驗 XML 合法性用xmllint --noout file.xml它只報錯不輸出。和 JSON 一樣格式化工具同時是校驗工具xmllint遇到多個根節(jié)點、未閉合標簽會直接報錯并返回非零退出碼。順手提一句如果你在寫 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覆蓋原文件。這樣即使某個文件解析失敗原文件也不會被一個空文件或半截輸出覆蓋。第二個循環(huán)同理xmllint的--output指定臨時文件名成功后才覆蓋。參數說明批量處理前先確認所有文件編碼一致混著 GBK 和 UTF-8 的目錄會讓這組命令產生亂碼輸出。另外這兩條命令對子目錄不生效需要遞歸的話把*.json換成find . -name *.json配合while read循環(huán)。這種先臨時文件再覆蓋的習慣能幫你省掉很多后悔藥。實際使用中我從網上下載接口樣例或 json 格式文件時第一步都是先跑一遍格式化看看結構。在 spark 中讀取 json 之前也一樣先把下載的雜亂的 json 文件用jq .洗一遍能直接從報錯行判斷壞數據在哪比在分布式環(huán)境里反復 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正常執(zhí)行后沒有任何輸出因為jq -S把兩個文件的鍵都排成了age在前、name在后文本完全一致。但如果去掉-S直接 diff你會看到兩行全被標紅。這就是鍵序帶來的假差異。另一種假差異來自空白一個文件是 2 空格縮進另一個是 4 空格縮進diff會把每一行都當成不同。所以我的結論是文本 diff 只能用于已知兩邊已被相同規(guī)則格式化過的 JSON。滿足這個前提后jq -S加diff是最快的冒煙比對方式。但它有個致命盲區(qū)——數組順序不同時它只會告訴你在哪一行不同而不會告訴你哪個元素在左邊有右邊沒有。這個問題需要結構比對來解決。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)邏輯說明函數從根節(jié)點開始遞歸遇到 dict 就取兩邊鍵的并集排序后逐個比較缺失的鍵單獨報遇到 list 先比長度再按索引逐項比較其余類型直接比值。所有差異都用生成器yield拋出來主函數統一收集后打印。退出碼是 1 表示有差異0 表示相同方便接進 shell 腳本。參數說明腳本第 10 行到第 15 行處理了最常見的一類誤報——1和1.0在 Python 里分別被解析成int和float但 JSON 語義上它們相等所以統一轉成float比較。bool單獨排除是因為True 1在 Python 里成立不排除的話true和1會被誤判為相等。數組順序按重要差異處理只要順序不同就報這是大多數接口聯調場景想要的。4.3 兩個實戰(zhàn)變體接口響應比對與 json merge conflict 合并復查第一個變體是接口響應比對。把兩個環(huán)境的接口響應各自存成文件然后跑腳本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解碼回字符。記住這條規(guī)則格式化工具轉義不轉義只是序列化配置不是數據損壞。5.2 現象XML 格式化后內容全部擠在一行把 XML 丟給 xmllint 后發(fā)現輸出確實縮進了但所有內容還是在同一行上看起來跟沒格式化一樣。原因最常見的是你在命令后面又加了一層管道處理比如xmllint --format raw.xml | tr -d \n把 xmllint 辛苦加的換行全刪了。另一個隱蔽原因是文件里的換行是#10;實體轉義解析后是文本內容里的真實換行不是排版換行這種文件格式化后內容連在一起是正常的因為它本來就沒有標簽間空白。解決別在管道里二次處理直接xmllint --format raw.xml --output pretty.xml寫文件看。如果文件本身沒有標簽間空白你想讓它可讀只能先插入縮進空白——但這時候修改的是文檔樹有可能影響依賴空白節(jié)點的下游邏輯。我的習慣是格式化只用于人眼排查不與簽名校驗共用同一份輸出。排查完還是用原始文件干活。5.3 現象嵌套 JSON 比對誤報差異用自寫腳本比對時報出一堆root.a.b: 1 ! 1.0或者root.c: True ! 1這種差異但業(yè)務上明明是等價的。原因JSON 解析器對數字的處理不一致。Python 的json.loads把1解析成int把1.0解析成float而 JavaScript 的解析器一律變成number沒有類型區(qū)別。所以在 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 或多根節(jié)點xmllint 報錯提示parser error或者Extra content at the end of the doc。文件看起來是合法 XML但就是過不了。原因從 Windows 環(huán)境拿到的 XML 文件開頭帶了一個 UTF-8 BOMEF BB BFxmllint 在某些配置下會把 BOM 當成內容讀進去導致解析失敗。另一個常見原因是工具或接口把多個 XML 文檔拼在一個文件里返回了比如一個列表接口把每條記錄各吐了一個 XML 根節(jié)點拼在一起就成了多根節(jié)點文檔。解決去 BOM 用一行sed -i 1s/^\xEF\xBB\xBF// file.xml。多根節(jié)點的情況如果文件是多個 XML 文檔拼接可以用xmllint --recover --format file.xml嘗試恢復但更干凈的辦法是手工包一個外層根節(jié)點比如# 把多根節(jié)點包進一個 root 標簽 { echo root; cat broken.xml; echo /root; } | xmllint --format -注意這只能用來排查包根節(jié)點后的文檔和原始語義不等價別拿它當正式數據。至于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。這套東西陪我處理過很多次看起來一樣、實際不一樣的接口問題?,F在我的習慣是任何一次接口聯調拿到響應先格式化結構看清了再比對比對出差異先看路徑路徑指向的字段往往比我想象的更靠內層。格式化工具最大的價值不是排版好看而是讓差異無處可藏。希望幫到你。本文還有配套的精品資源點擊獲取