Подтвердить что ты не робот

Использование 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
4b9b3361

Ответ 1

Поскольку сравнение 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 самом деле не работает должным образом в некоторых крайних случаях. Эта проблема 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'

Ответ 2

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

Ответ 3

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

Ответ 4

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

#!/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 script.

POST-POSTSCRIPT: встроенная версия walk была недавно изменена, так что она больше не сортирует ключи внутри объекта. В частности, он использует keys_unsorted. Для этой задачи следует использовать версию с использованием keys.

Ответ 5

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

Ответ 6

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

Ответ 7

Извлеките лучшие из двух лучших ответов, чтобы получить основанный на 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")

Для этого требуется элегантное решение для сортировки массивов из fooobar.com/info/251695/... (которое позволяет обрабатывать массивы как наборы) и перенаправление чистого bash в diff из fooobar.com/info/251695/.... Это относится к случаю, когда вам нужен diff из двух файлов json и порядок содержимого массива не имеет значения.