Використання jq або альтернативних інструментів командного рядка для порівняння файлів JSON


86

Чи існують будь-які утиліти командного рядка, за допомогою яких можна виявити, чи два файли 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

Відповіді:


39

Оскільки порівняння 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'

1
Я намагався змінити --argfile a a.jsonна --arg a $a(будучи $ aa json рядком), не пощастивши. будь-яка ідея, як підійти до рядків, а не до файлів?
Саймон Ернесто Карденас Зарате

1
@SimonErnestoCardenasZarate, якщо у вас все ще є ця проблема, можливо, вам --argjson
Брайан

103

В принципі, якщо у вас є доступ до 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може бути кращою різниця залежно від смаку.


1
Зверніть увагу, що для цього, як видається, потрібна версія 1.5 або пізніша версіяjq
Адам Бакстер,

1
@voltagex З перегляду онлайн-посібника ( stedolan.github.io/jq/manual/v1.4/#Invokingjq ) Схоже, що його фактично додали в 1.4, хоча я не знаю, чи jqє аргументи стилю posix, щоб ви могли повинні закликатиjq -c -S ...
Ерік,

4
Чистіша візуальна форма ІМОvimdiff <(jq -S . a.json) <(jq -S . b.json)
Ашвін Джаяпракаш

1
Так, вам слід видалити -c(що робить висновок компактним), переваги стилю не відповідають вашій відповіді.
odinho - Velmont

@ odinho-Velmont @Ashwin Jayaprakash Це правда, що cце не є суто необхідним, але для мене немає причин для cmp порівнювати однакові пробіли, і немає жодної причини, щоб jq турбувався про його випромінювання. diff,, vimdiffабо будь-який інструмент, який виконує порівняння файлів, буде працювати, але cmpце все, що потрібно.
Ерік,

23

Використовуйте 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


2
Тож недооцінене повинно бути проступком. Дає фактичний diffвихід, сумісний з форматуванням. Дивовижний.
ijoseph

1
Ви можете скористатися інструментом командного рядка або веб-інструментом: play.jd-tool.io
Джо Бернетт

Це інструмент святого Грааля для створення файлів з конфігураціями jsonyamlпісля перетворення), щоб зрозуміти, чому саме нечія конфігурація не працює порівняно з чужою.
ijoseph

6

Ось рішення із використанням загальної функції 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слід використовувати версію, що використовує.


1
Дякуємо, що walkзгадали, що було додано в jq 1.5. Я прагнув компромісного оператора між filterі, mapсхоже, це все.
Ной Суссман

5

Там в відповідь на це тут , що було б корисно.

По суті, ви можете використовувати diffфункціональність Git (навіть для файлів, що не відстежуються Git), яка також включає колір у вихідні дані:

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


3
Це чутливо до порядку, який ОП хотів проігнорувати
Андреас,


1

Витягнувши найкраще з двох найкращих відповідей, щоб отримати jqjson 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 (що дозволяє нам обробляти масиви як набори) та чисте перенаправлення bash на diffз https://stackoverflow.com/a/37175540/ 538507 Це стосується випадку, коли ви хочете мати різницю у двох json-файлах, а порядок вмісту масиву не має значення.


0

Якщо ви також хочете побачити відмінності, використовуючи відповідь @ 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"
 }]

6
... або знаєте, просто видаліть -cіз jqкомандного рядка. Не знаю, вважаю за краще не вводити зайві непотрібні інструменти;)
odinho - Velmont

0

Ще один інструмент для тих, на кого попередні відповіді не підходять, ви можете спробувати jdd .

Він заснований на HTML, тому ви можете використовувати його в Інтернеті за адресою www.jsondiff.com, або, якщо ви бажаєте запускати його локально, просто завантажте проект і відкрийте index.html.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.