Использование jq или альтернативных инструментов командной строки для diff файлов 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
691   6  

6 ответов:

Поскольку сравнение jq уже сравнивает объекты без учета порядка ключей, все, что остается, - это отсортировать все списки внутри объекта перед их сравнением. Предположим, что ваши два файла имеют имена a.json и b.json, в последнем jq nightly:

jq --argfile a a.json --argfile b b.json -n '($a | (.. | arrays) |= sort) as $a | ($b | (.. | arrays) |= sort) as $b | $a == $b'
Эта программа должна возвращать "true" или "false" в зависимости от того, равны ли объекты, используя определение равенства, которое вы просите.

EDIT: конструкция (.. | 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 в зависимости от вкусов.

Вот решение, использующее универсальную функцию 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.

Использование jd с опцией -set:

Отсутствие выхода означает отсутствие разницы.

$ 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

Если вы также хотите увидеть различия, используя ответ @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"
 }]

Возможно, вы могли бы использовать этот инструмент сортировки и дифференцирования: http://novicelab.org/jsonsortdiff/ , который сначала семантически сортирует объекты, а затем сравнивает их. Он основан на https://www.npmjs.com/package/jsonabc

Comments

    Ничего не найдено.