it-roy-ru.com

Использование 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
42
Amelio Vazquez-Reina

Поскольку сравнение 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» в зависимости от того, равны ли объекты, используя определение равенства, которое вы запрашиваете.

Правка: конструкция (.. | arrays) |= sort на самом деле не работает, как ожидалось в некоторых случаях Edge. Эта проблема 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'
17
user3899165

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

46
Erik

Вот решение с использованием универсальной функции 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: [email protected]" >&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.

6
peak

Используйте 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"}

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

6
Joe Burnett

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

1
Shivraj

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