Использование jq или альтернативных инструментов командной строки для сравнения файлов JSON
Существуют ли какие-либо утилиты командной строки, которые можно использовать, чтобы определить, идентичны ли два файла JSON с неизменностью порядка слов-словаря и порядка элементов-списка?
Может ли это быть сделано с jq
или какой-то другой эквивалентный инструмент?
Примеры:
Эти два файла JSON идентичны
A:
{
"People": ["John", "Bryan"],
"City": "Boston",
"State": "MA"
}
B:
{
"People": ["Bryan", "John"],
"State": "MA",
"City": "Boston"
}
но эти два файла JSON отличаются:
A:
{
"People": ["John", "Bryan", "Carla"],
"City": "Boston",
"State": "MA"
}
C:
{
"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
10 ответов
Поскольку сравнение jq уже сравнивает объекты без учета порядка клавиш, все, что осталось, это отсортировать все списки внутри объекта перед их сравнением. Предполагая, что ваши два файла названы a.json
а также b.json
, по последнему дню ночные:
jq --argfile a a.json --argfile b b.json -n '($a | (.. | arrays) |= sort) as $a | ($b | (.. | arrays) |= sort) as $b | $a == $b'
Эта программа должна возвращать "true" или "false" в зависимости от того, равны ли объекты, используя определение равенства, которое вы запрашиваете.
РЕДАКТИРОВАТЬ: (.. | 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'
В принципе, если у вас есть доступ к bash или какой-либо другой расширенной оболочке, вы можете сделать что-то вроде
cmp <(jq -cS . A.json) <(jq -cS . B.json)
используя подпроцессы. Это отформатирует JSON с отсортированными ключами и последовательным представлением чисел с плавающей запятой. Это единственные две причины, по которым я могу придумать, почему json с одинаковым содержанием будет печататься по-разному. Поэтому выполнение простого сравнения строк впоследствии приведет к правильному тесту. Вероятно, также стоит отметить, что если вы не можете использовать bash, вы можете получить те же результаты с временными файлами, это просто не так чисто.
Это не совсем отвечает на ваш вопрос, потому что так, как вы сформулировали вопрос, который вы хотели ["John", "Bryan"]
а также ["Bryan", "John"]
сравнивать одинаково. Поскольку у json нет понятия набора, только списка, их следует рассматривать как отдельные. Порядок важен для списков. Вам нужно написать какое-то пользовательское сравнение, если вы хотите, чтобы они сравнивались одинаково, и для этого вам нужно будет определить, что вы подразумеваете под равенством. Имеет ли значение порядок для всех списков или только для некоторых? Как насчет дублирующих элементов? В качестве альтернативы, если вы хотите, чтобы они были представлены в виде набора, а элементы представляли собой строки, вы можете поместить их в объекты типа {"John": null, "Bryan": null}
, Порядок не будет иметь значения при сравнении на равенство.
Обновить
Из обсуждения комментариев: Если вы хотите получить лучшее представление о том, почему JSON не то же самое, то
diff <(jq -S . A.json) <(jq -S . B.json)
будет производить более интерпретируемый вывод. vimdiff
может быть предпочтительнее, чем diff в зависимости от вкуса.
Использование jd
с -set
опция:
Отсутствие вывода означает отсутствие разницы.
$ jd -set A.json B.json
Различия отображаются в виде @ пути и + или -.
$ 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"}
Взяв лучшее из двух верхних ответов, чтобы получить 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")
Для этого используется элегантное решение сортировки массивов из /questions/23853878/ispolzovanie-jq-ili-alternativnyih-instrumentov-komandnoj-stroki-dlya-sravneniya-fajlov-json/23853887#23853887 (которое позволяет нам обрабатывать массивы как наборы) и чистое перенаправление bash вdiff
из /questions/23853878/ispolzovanie-jq-ili-alternativnyih-instrumentov-komandnoj-stroki-dlya-sravneniya-fajlov-json/23853894#23853894 Это касается случая, когда вам нужна разница двух файлов json и порядок содержимого массива не имеет значения.
Вот решение с использованием универсальной функции walk / 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.
POST-POSTSCRIPT: встроенная версия walk
недавно был изменен, так что он больше не сортирует ключи внутри объекта. В частности, он использует keys_unsorted
, Для поставленной задачи версия, использующая keys
должен быть использован.
Там в ответ на это здесь, что было бы полезно.
По сути, вы можете использовать Git diff
функциональность (даже для файлов, не отслеживаемых Git), которая также включает цвет в выводе:
git diff --no-index payload_1.json payload_2.json
Еще один инструмент для тех, кому предыдущие ответы не подходят, вы можете попробовать jdd.
Он основан на HTML, поэтому вы можете использовать его в Интернете на сайте http://www.jsondiff.com/ или, если вы предпочитаете запускать локально, просто загрузите проект и откройте index.html.
Возможно, вы могли бы использовать этот инструмент сортировки и сравнения: http://novicelab.org/jsonsortdiff/ который сначала сортирует объекты семантически, а затем сравнивает их. Он основан на https://www.npmjs.com/package/jsonabc
В JSONiq вы можете просто использовать функцию глубокого равенства:
deep-equal(
{
"People": ["John", "Bryan", "Carla"],
"City": "Boston",
"State": "MA"
},
{
"People": ["Bryan", "John"],
"State": "MA",
"City": "Boston"
}
)
который возвращается
false
Вы также можете читать из файлов (локально или URL-адрес HTTP также работает) следующим образом:
deep-equal(
json-doc("path to doc A.json"),
json-doc("path to doc B.json")
)
Возможная реализация - RumbleDB.
Однако вы должны знать, что не совсем правильно, чтобы первые два документа были одинаковыми: JSON определяет массивы как упорядоченные списки значений.
["Bryan", "John"]
не то же самое, что:
["John", "Bryan"]
Если вы также хотите увидеть различия, используйте ответ @Erik как вдохновение и js-beautify:
$ 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"
}]