使用 jq 或其他命令行工具比较 JSON 文件

是否有任何命令行实用程序可用于查找两个 JSON 文件是否具有字典内键和列表内元素排序的不变性?

这可以用 jq或其他类似的工具来完成吗?

例子:

这两个 JSON 文件是相同的

返回文章页面

{
"People": ["John", "Bryan"],
"City": "Boston",
"State": "MA"
}

返回文章页面

{
"People": ["Bryan", "John"],
"State": "MA",
"City": "Boston"
}

但是这两个 JSON 文件是不同的:

返回文章页面

{
"People": ["John", "Bryan", "Carla"],
"City": "Boston",
"State": "MA"
}

返回文章页面

{
"People": ["Bryan", "John"],
"State": "MA",
"City": "Boston"
}

那就是:

$ some_diff_command A.json B.json


$ some_diff_command A.json C.json
The files are not structurally identical
100733 次浏览

由于 jq 的比较已经在不考虑键顺序的情况下比较了对象,所以剩下的就是在比较之前对对象中的所有列表进行排序。假设您的两个文件分别命名为 a.jsonb.json,在最新的 jq 每晚:

jq --argfile a a.json --argfile b b.json -n '($a | (.. | arrays) |= sort) as $a | ($b | (.. | arrays) |= sort) as $b | $a == $b'

这个程序应该返回“真”或“假”,这取决于对象是否相等,使用您要求的相等定义。

编辑: (.. | arrays) |= sort结构在某些边缘情况下实际上并不能像预期的那样工作。这个 GitHub 的问题解释了原因,并提供了一些替代方案,例如:

def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); (post_recurse | arrays) |= sort

应用于上面的 jq 调用:

jq --argfile a a.json --argfile b b.json -n 'def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); ($a | (post_recurse | arrays) |= sort) as $a | ($b | (post_recurse | arrays) |= sort) as $b | $a == $b'

下面是一个使用通用函数 步行/1的解决方案:

# Apply f to composite entities recursively, and to atoms
def walk(f):
. as $in
| if type == "object" then
reduce keys[] as $key
( {}; . + { ($key):  ($in[$key] | walk(f)) } ) | f
elif type == "array" then map( walk(f) ) | f
else f
end;


def normalize: walk(if type == "array" then sort else . end);


# Test whether the input and argument are equivalent
# in the sense that ordering within lists is immaterial:
def equiv(x): normalize == (x | normalize);

例如:

{"a":[1,2,[3,4]]} | equiv( {"a": [[4,3], 2,1]} )

生产:

true

以 bash 脚本的形式结束:

#!/bin/bash


JQ=/usr/local/bin/jq
BN=$(basename $0)


function help {
cat <<EOF


Syntax: $0 file1 file2


The two files are assumed each to contain one JSON entity.  This
script reports whether the two entities are equivalent in the sense
that their normalized values are equal, where normalization of all
component arrays is achieved by recursively sorting them, innermost first.


This script assumes that the jq of interest is $JQ if it exists and
otherwise that it is on the PATH.


EOF
exit
}


if [ ! -x "$JQ" ] ; then JQ=jq ; fi


function die     { echo "$BN: $@" >&2 ; exit 1 ; }


if [ $# != 2 -o "$1" = -h  -o "$1" = --help ] ; then help ; exit ; fi


test -f "$1" || die "unable to find $1"
test -f "$2" || die "unable to find $2"


$JQ -r -n --argfile A "$1" --argfile B "$2" -f <(cat<<"EOF"
# Apply f to composite entities recursively, and to atoms
def walk(f):
. as $in
| if type == "object" then
reduce keys[] as $key
( {}; . + { ($key):  ($in[$key] | walk(f)) } ) | f
elif type == "array" then map( walk(f) ) | f
else f
end;


def normalize: walk(if type == "array" then sort else . end);


# Test whether the input and argument are equivalent
# in the sense that ordering within lists is immaterial:
def equiv(x): normalize == (x | normalize);


if $A | equiv($B) then empty else "\($A) is not equivalent to \($B)" end


EOF
)

POSTSCRIPT: walk/1是 jq > 1.5的内置版本,因此如果您的 jq 包含它,那么可以省略它,但是将它冗余地包含在 jq 脚本中也没有什么坏处。

POSTSCRIPT: walk的内置版本最近发生了变化,因此它不再对对象中的键进行排序。具体来说,它使用 keys_unsorted。对于手头的任务,应该使用使用 keys的版本。

如果您的 shell 支持进程替换(Bash 样式如下,请参阅 医生) :

diff <(jq --sort-keys . A.json) <(jq --sort-keys . B.json)

对象的键顺序将被忽略,但数组顺序仍然很重要。如果需要,可以通过其他方式对数组值进行排序,或者使它们类似于集合(例如,["foo", "bar"]{"foo": null, "bar": null}; 这也将删除重复数据)来解决这个问题。

或者,根据您的需要,将 diff替换为其他比较器,例如 cmpcolordiffvimdiff。如果您想要的只是一个是或否的答案,那么考虑使用 cmp并将 --compact-output传递给 jq,以避免为了潜在的小的性能提高而格式化输出。

如果你也想看到其中的差异,那么就用@Erik 的回答作为灵感和 JS 美化:

$ echo '[{"name": "John", "age": 56}, {"name": "Mary", "age": 67}]' > file1.json
$ echo '[{"age": 56, "name": "John"}, {"name": "Mary", "age": 61}]' > file2.json


$ diff -u --color \
<(jq -cS . file1.json | js-beautify -f -) \
<(jq -cS . file2.json | js-beautify -f -)
--- /dev/fd/63  2016-10-18 13:03:59.397451598 +0200
+++ /dev/fd/62  2016-10-18 13:03:59.397451598 +0200
@@ -2,6 +2,6 @@
"age": 56,
"name": "John Smith"
}, {
-    "age": 67,
+    "age": 61,
"name": "Mary Stuart"
}]

使用带有 -set选项的 jd:

没有输出意味着没有差别。

$ jd -set A.json B.json

差异显示为@path 和 + 或-。

$ jd -set A.json C.json


@ ["People",{}]
+ "Carla"

输出差异也可以用作带有 -p选项的补丁文件。

$ jd -set -o patch A.json C.json; jd -set -p patch B.json


{"City":"Boston","People":["John","Carla","Bryan"],"State":"MA"}

Https://github.com/josephburnett/jd#command-line-usage

也许您可以使用这个 sort and diff 工具: http://novicelab.org/jsonsortdiff/,它首先对对象进行语义排序,然后对其进行比较。它是基于 https://www.npmjs.com/package/jsonabc

从前两个答案中选出最好的,得到一个基于 jq的 json diff:

diff \
<(jq -S 'def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); (. | (post_recurse | arrays) |= sort)' "$original_json") \
<(jq -S 'def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); (. | (post_recurse | arrays) |= sort)' "$changed_json")

这需要从 https://stackoverflow.com/a/31933234/538507获得优雅的数组排序解决方案(它允许我们将数组视为集合) ,并从 https://stackoverflow.com/a/37175540/538507获得干净的 bash 重定向到 diff。这解决了这样的情况,即您需要两个 json 文件的差异和数组内容的顺序是不相关的。

有一个关于 给你的答案会很有用。

基本上你可以使用 Git diff功能(甚至对于非 Git 跟踪的文件) ,它在输出中也包含颜色:

git diff --no-index payload_1.json payload_2.json

还有一个工具,对于那些以前的答案不是很适合,你可以尝试 JDD

它是基于 HTML 的,因此您可以在 Www.jsondiff.com上在线使用它,或者,如果您喜欢在本地运行它,只需下载项目并打开 index.HTML。

JSONiq中,您可以简单地使用 deep-equals 函数:

deep-equal(
{
"People": ["John", "Bryan", "Carla"],
"City": "Boston",
"State": "MA"
},
{
"People": ["Bryan", "John"],
"State": "MA",
"City": "Boston"
}
)

回来了

false

您还可以从文件中读取(本地或 HTTP URL 也可以工作) ,如下所示:

deep-equal(
json-doc("path to doc A.json"),
json-doc("path to doc B.json")
)

一个可能的实现是 RumbleDB

但是,您需要注意,前两个文档是否相同并不完全正确: JSON将数组定义为值的有序列表。

["Bryan", "John"]

不同于:

["John", "Bryan"]