找回密码
 立即注册
首页 业界区 业界 [Python][Go]比较两个JSON文件之间的差异

[Python][Go]比较两个JSON文件之间的差异

那虻 2025-8-10 20:36:21
前言

前段时间同事说他有个需求是比较两个JSON文件之间的差异点,身为DB大神的同事用SQL实现了这个需求,让只会CRUD的我直呼神乎其技。当时用一个一千万多字符、四十多万行的JSON文件来测试,SQL查出来要9秒。周六有时间,拜读了下同事的SQL,打算用Python和Go实现下试试。
测试的json文件如下,其中dst.json从src.json文件复制而来,随便找了个地方改了下。单文件为409510行,字符数为11473154左右。
  1. $ wc -ml ./src.json dst.json
  2. 409510 11473154 ./src.json
  3. 409510 11473155 dst.json
  4. 819020 22946309 总计
复制代码
第三方库jsondiff

先在网上搜了下有没有现成的第三方库,找到一个叫jsondiff的第三方python库。使用pip安装后,用法如下
  1. import json
  2. import jsondiff
  3. import os
  4. from typing import Any
  5. def read_json(filepath: str) -> Any:
  6.     if not os.path.exists(filepath):
  7.         raise FileNotFoundError(filepath)
  8.     try:
  9.         with open(filepath, "r") as f:
  10.             data = json.load(f)
  11.     except json.JSONDecodeError as e:
  12.         raise Exception(f"{filepath} is not a valid json file") from e
  13.     else:
  14.         return data
  15.    
  16. if __name__ == "__main__":
  17.     src_data = read_json("src.json")
  18.     dst_data = read_json("dst.json")
  19.     diffs = jsondiff.diff(src_data, dst_data)
  20.     print(diffs)
复制代码
运行测试
  1. $ /usr/bin/time -f 'Elapsed Time: %e s Max RSS: %M kbytes' python third.py
  2. {'timepicker': {'time_options': {insert: [(7, '7dd')], delete: [7]}}}
  3. Elapsed Time: 1576.30 s Max RSS: 87732 kbytes
复制代码
运行时间太长了,接近半小时,肯定不能拿给别人用。
Python-仅用标准库

只试了jsondiff这一个第三方库,接下来打算直接参考同事那个SQL的思路,自己只用标准库实现一个。
  1. from typing import Any, List
  2. import json
  3. import os
  4. from dataclasses import dataclass
  5. from collections.abc import MutableSequence, MutableMapping
  6. @dataclass
  7. class DiffResult:
  8.     path: str
  9.     kind: str
  10.     left: Any
  11.     right: Any
  12. def add_path(parent: str, key: str) -> str:
  13.     """将父路径和key name组合成完整的路径字符串"""
  14.     if parent == "":
  15.         return key
  16.     else:
  17.         return parent + "." + key
  18.    
  19. def read_json(filepath: str) -> Any:
  20.     if not os.path.exists(filepath):
  21.         raise FileNotFoundError(filepath)
  22.     try:
  23.         with open(filepath, "r") as f:
  24.             data = json.load(f)
  25.     except json.JSONDecodeError as e:
  26.         raise Exception(f"{filepath} is not a valid json file") from e
  27.     else:
  28.         return data
  29.    
  30. def collect_diff(path: str, left: Any, right: Any) -> List[DiffResult]:
  31.     """比较两个json数据结构之间的差异
  32.    
  33.     Args:
  34.         path (str): 当前路径
  35.         left (Any): 左侧数据
  36.         right (Any): 右侧数据
  37.     Returns:
  38.         List[DiffResult]: 差异列表
  39.     """
  40.     diffs: List[DiffResult] = []
  41.     if isinstance(left, MutableMapping) and isinstance(right, MutableMapping):
  42.         # 处理字典:检查 key 的增删改
  43.         all_keys = set(left.keys()) | set(right.keys())  # 左右两边字典中所有键的并集,用于后续比较这些键在两个字典中的存在情况及对应的值
  44.         for k in all_keys:
  45.             l_exists = k in left
  46.             r_exists = k in right
  47.             key_path = add_path(path, k)
  48.             if l_exists and not r_exists:  # 如果一个键只存在于left,则记录为 removed 差异
  49.                 diffs.append(DiffResult(key_path, "removed", left=left[k]))
  50.             elif not l_exists and r_exists:  # 如果一个键只存在于 right,则记录为 added 差异
  51.                 diffs.append(DiffResult(key_path, "added", right=right[k]))
  52.             else:
  53.                 # 都存在,递归比较这两个键对应的值
  54.                 diffs.extend(collect_diff(key_path, left[k], right[k]))
  55.     elif isinstance(left, MutableSequence) and isinstance(right, MutableSequence):
  56.         # 处理列表:按索引比较
  57.         max_len = max(len(left), len(right))  # 找两个列表中最长的长度
  58.         for i in range(max_len):
  59.             l_exists = i < len(left)
  60.             r_exists = i < len(right)
  61.             idx_path = f"{path}[{i}]"
  62.             lv = left[i] if l_exists else None
  63.             rv = right[i] if r_exists else None
  64.             if l_exists and not r_exists:  # 某个索引的元素只存在于 left,则记录为 removed 差异
  65.                 diffs.append(DiffResult(idx_path, "removed", left=lv))
  66.             elif not l_exists and r_exists:  # 某个索引的元素只存在于 right,则记录为 added 差异
  67.                 diffs.append(DiffResult(idx_path, "added", right=rv))
  68.             else:  # 都存在,递归比较这两个索引对应的值
  69.                 diffs.extend(collect_diff(idx_path, lv, rv))
  70.     else:
  71.         # 基本类型或类型不一致
  72.         if left != right:
  73.             diffs.append(DiffResult(path, "modified", left=left, right=right))
  74.     return diffs
  75. if __name__ == "__main__":
  76.     src_dict = read_json("src.json")
  77.     dst_dict = read_json("dst.json")
  78.     diffs = collect_diff("", src_dict, dst_dict)
  79.     if len(diffs) == 0:
  80.         print("No differences found.")
  81.     else:
  82.         print(f"Found {len(diffs)} differences:")
  83.         for diff in diffs:
  84.             match diff.kind:
  85.                 case "added":
  86.                     print(f"Added: {diff.path}, {diff.right}")
  87.                 case "removed":
  88.                     print(f"Removed: {diff.path}, {diff.left}")
  89.                 case "modified":
  90.                     print(f"Modified: {diff.path}, {diff.left} -> {diff.right}")
  91.     # print(diffs)
复制代码
运行测试
  1. $ /usr/bin/time -f 'Elapsed Time: %e s Max RSS: %M kbytes' python main.py
  2. Found 1 differences:
  3. Modified: timepicker.time_options[7], 7d -> 7dd
  4. Elapsed Time: 0.46 s Max RSS: 87976 kbytes
复制代码
只要 0.46 秒就能比较出来差异点,单论比较性能来说,比jsondiff要好很多。
Go实现

再换go来实现个命令行工具,同样只需要用标准库即可。
  1. package main
  2. import (
  3.         "encoding/json"
  4.         "flag"
  5.         "fmt"
  6.         "io"
  7.         "os"
  8. )
  9. var (
  10.         src_file string
  11.         dst_file string
  12. )
  13. type DiffResult struct {
  14.         Path  string
  15.         Kind  string
  16.         Left  any
  17.         Right any
  18. }
  19. func addPath(parent, key string) string {
  20.         if parent == "" {
  21.                 return key
  22.         }
  23.         return parent + "." + key
  24. }
  25. func collectDiff(path string, left, right any) []DiffResult {
  26.         var diffs []DiffResult
  27.         switch l := left.(type) {
  28.         case map[string]any:
  29.                 if r, ok := right.(map[string]any); ok {
  30.                         for k, lv := range l {
  31.                                 rk, exists := r[k]
  32.                                 if !exists {
  33.                                         diffs = append(diffs, DiffResult{
  34.                                                 Path:  addPath(path, k),
  35.                                                 Kind:  "removed",
  36.                                                 Left:  lv,
  37.                                                 Right: nil,
  38.                                         })
  39.                                 } else {
  40.                                         diffs = append(diffs, collectDiff(addPath(path, k), lv, rk)...)
  41.                                 }
  42.                         }
  43.                         for k, rv := range r {
  44.                                 if _, exists := l[k]; !exists {
  45.                                         diffs = append(diffs, DiffResult{
  46.                                                 Path:  addPath(path, k),
  47.                                                 Kind:  "added",
  48.                                                 Left:  nil,
  49.                                                 Right: rv,
  50.                                         })
  51.                                 }
  52.                         }
  53.                 } else {
  54.                         diffs = append(diffs, DiffResult{
  55.                                 Path:  path,
  56.                                 Kind:  "modified",
  57.                                 Left:  left,
  58.                                 Right: right,
  59.                         })
  60.                 }
  61.         case []any:
  62.                 if r, ok := right.([]any); ok {
  63.                         // 比较 slice(这里简化:按索引比较)
  64.                         maxLen := len(l)
  65.                         if len(r) > maxLen {
  66.                                 maxLen = len(r)
  67.                         }
  68.                         for i := 0; i < maxLen; i++ {
  69.                                 var lv, rv any
  70.                                 var lExists, rExists bool
  71.                                 if i < len(l) {
  72.                                         lv = l[i]
  73.                                         lExists = true
  74.                                 }
  75.                                 if i < len(r) {
  76.                                         rv = r[i]
  77.                                         rExists = true
  78.                                 }
  79.                                 switch {
  80.                                 case lExists && !rExists:
  81.                                         diffs = append(diffs, DiffResult{
  82.                                                 Path:  fmt.Sprintf("%s[%d]", path, i),
  83.                                                 Kind:  "removed",
  84.                                                 Left:  lv,
  85.                                                 Right: nil,
  86.                                         })
  87.                                 case !lExists && rExists:
  88.                                         diffs = append(diffs, DiffResult{
  89.                                                 Path:  fmt.Sprintf("%s[%d]", path, i),
  90.                                                 Kind:  "added",
  91.                                                 Left:  nil,
  92.                                                 Right: rv,
  93.                                         })
  94.                                 case lExists && rExists:
  95.                                         diffs = append(diffs, collectDiff(fmt.Sprintf("%s[%d]", path, i), lv, rv)...)
  96.                                 }
  97.                         }
  98.                 } else {
  99.                         diffs = append(diffs, DiffResult{
  100.                                 Path:  path,
  101.                                 Kind:  "modified",
  102.                                 Left:  left,
  103.                                 Right: right,
  104.                         })
  105.                 }
  106.         default:
  107.                 if fmt.Sprintf("%v", left) != fmt.Sprintf("%v", right) {
  108.                         diffs = append(diffs, DiffResult{
  109.                                 Path:  path,
  110.                                 Kind:  "modified",
  111.                                 Left:  left,
  112.                                 Right: right,
  113.                         })
  114.                 }
  115.         }
  116.         return diffs
  117. }
  118. func readJSON(r io.Reader) (map[string]any, error) {
  119.         var data map[string]any
  120.         decoder := json.NewDecoder(r)
  121.         if err := decoder.Decode(&data); err != nil {
  122.                 return nil, err
  123.         }
  124.         return data, nil
  125. }
  126. func main() {
  127.         flag.StringVar(&src_file, "src", "src.json", "source file")
  128.         flag.StringVar(&dst_file, "dst", "dst.json", "destination file")
  129.         flag.Parse()
  130.         srcFile, err := os.Open(src_file)
  131.         if err != nil {
  132.                 fmt.Fprintf(os.Stderr, "Error opening src.json: %v\n", err)
  133.                 return
  134.         }
  135.         defer srcFile.Close()
  136.         dstFile, err := os.Open(dst_file)
  137.         if err != nil {
  138.                 fmt.Fprintf(os.Stderr, "Error opening dst.json: %v\n", err)
  139.                 return
  140.         }
  141.         defer dstFile.Close()
  142.         srcJson, err := readJSON(srcFile)
  143.         if err != nil {
  144.                 fmt.Fprintf(os.Stderr, "Error reading src.json: %v\n", err)
  145.                 return
  146.         }
  147.         dstJson, err := readJSON(dstFile)
  148.         if err != nil {
  149.                 fmt.Fprintf(os.Stderr, "Error reading dst.json: %v\n", err)
  150.                 return
  151.         }
  152.         diffs := collectDiff("", srcJson, dstJson)
  153.         if len(diffs) == 0 {
  154.                 fmt.Println("No differences found.")
  155.         } else {
  156.                 fmt.Printf("%d differences found:\n", len(diffs))
  157.                 for _, diff := range diffs {
  158.                         switch diff.Kind {
  159.                         case "added":
  160.                                 fmt.Printf("Added: %s: %v\n", diff.Path, diff.Right)
  161.                         case "removed":
  162.                                 fmt.Printf("Removed: %s: %v\n", diff.Path, diff.Left)
  163.                         case "modified":
  164.                                 fmt.Printf("Modified: %s: %v -> %v\n", diff.Path, diff.Left, diff.Right)
  165.                         }
  166.                 }
  167.         }
  168. }
复制代码
运行测试,速度同样很快。
  1. $ /usr/bin/time -f 'Elapsed Time: %e s Max RSS: %Mkbytes' ./diffjson -src ./src.json -dst ./dst.json
  2. 1 differences found:
  3. Modified: timepicker.time_options[7]: 7d -> 7dd
  4. Elapsed Time: 0.29 s Max RSS: 117468 kbytes
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册