Używanie jq lub alternatywnych narzędzi wiersza poleceń do różnicowania plików JSON

Czy są jakieś narzędzia wiersza poleceń, które można użyć, aby znaleźć, czy dwa pliki JSON są identyczne z niezmienniczością wewnątrz-dictionary-key i wewnątrz-list-element ordering?

Czy można to zrobić z jq lub inne równoważne narzędzie?

Przykłady:

Te dwa pliki JSON są identyczne

A:
{
  "People": ["John", "Bryan"],
  "City": "Boston",
  "State": "MA"
}

B:
{
  "People": ["Bryan", "John"],
  "State": "MA",
  "City": "Boston"
}

Ale te dwa pliki JSON są różne:

A:
{
  "People": ["John", "Bryan", "Carla"],
  "City": "Boston",
  "State": "MA"
}

C:
{
  "People": ["Bryan", "John"],
  "State": "MA",
  "City": "Boston"
}

Czyli:

$ some_diff_command A.json B.json

$ some_diff_command A.json C.json
The files are not structurally identical
 32
Author: peak, 2015-08-11

6 answers

Ponieważ porównanie jq już porównuje obiekty bez uwzględniania kolejności kluczy, pozostaje tylko posortować wszystkie listy wewnątrz obiektu przed ich porównaniem. Zakładając, że Twoje dwa pliki są nazwane a.json i b.json, w najnowszym jq:

jq --argfile a a.json --argfile b b.json -n '($a | (.. | arrays) |= sort) as $a | ($b | (.. | arrays) |= sort) as $b | $a == $b'

Ten program powinien zwracać "true" lub "false" w zależności od tego, czy obiekty są równe, używając definicji równości, o którą prosisz.

Edit: konstrukcja (.. | arrays) |= sort nie działa tak jak oczekiwano na jakiejś krawędzi sprawy. ten problem z Githubem wyjaśnia dlaczego i dostarcza kilka alternatyw, takich jak:

def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); (post_recurse | arrays) |= sort

Zastosowane do wywołania jq powyżej:

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'
 12
Author: UrsinusTheStrong,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-19 16:39:29

W zasadzie, Jeśli masz dostęp do Basha lub innej zaawansowanej powłoki, możesz zrobić coś w stylu

cmp <(jq -cS . A.json) <(jq -cS . B.json)

Używanie podprocesów. Spowoduje to sformatowanie json z posortowanymi kluczami i spójną reprezentacją zmiennoprzecinkowych punktów. To są tylko dwa powody, dla których mogę myśleć o tym, dlaczego json z tej samej treści byłyby drukowane inaczej. Dlatego wykonanie prostego porównania łańcuchów spowoduje odpowiedni test. Prawdopodobnie warto również zauważyć, że jeśli nie możesz użyć Basha, może uzyskać takie same wyniki z plikami tymczasowymi, po prostu nie jest tak czysty.

To nie do końca odpowiada na twoje pytanie, ponieważ w sposób, w jaki powiedziałeś pytanie, które chciałeś ["John", "Bryan"] i ["Bryan", "John"] porównać identycznie. Ponieważ json nie ma pojęcia zbioru, tylko listy, należy je uznać za odrębne. Kolejność jest ważna dla list. Trzeba by napisać jakieś niestandardowe porównanie, jeśli chcesz je porównać równo, i aby to zrobić, trzeba by zdefiniować, co masz na myśli przez równość. Czy kolejność ma znaczenie dla wszystkich list, czy tylko dla niektórych? A co z duplikatami elementów? Alternatywnie, jeśli chcesz, aby były reprezentowane jako zbiór, a elementy są ciągami znaków, możesz umieścić je w obiektach takich jak {"John": null, "Bryan": null}. Kolejność nie będzie miała znaczenia przy porównywaniu tych dla równości.

Update

Z dyskusji komentarza: jeśli chcesz lepiej zrozumieć, dlaczego json nie jest taki sam, to

diff <(jq -S . A.json) <(jq -S . B.json)

Wytworzy więcej interpretowalnych danych wyjściowych. vimdiff może być lepiej niż diff w zależności od upodobań.

 37
Author: Erik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-17 20:26:47

Oto rozwiązanie przy użyciu funkcji ogólnej 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);

Przykład:

{"a":[1,2,[3,4]]} | equiv( {"a": [[4,3], 2,1]} )

Produkuje:

true

I zawinięty jako skrypt Basha:

#!/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 jest wbudowanym w wersje jq > 1.5 i dlatego może być pominięty, jeśli twój jq zawiera go, ale nie ma nic złego w włączaniu go redundantnie do skryptu jq.

POST-POSTSCRIPT: wbudowana wersja walk została ostatnio zmieniona tak, że nie sortuje już kluczy wewnątrz obiektu. W szczególności używa keys_unsorted. Dla danego zadania należy użyć wersji keys.

 5
Author: peak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-17 11:41:49

Użycie jd z opcją -set:

Brak wyjścia oznacza brak różnicy.

$ jd -set A.json B.json

Różnice są pokazywane jako @ path oraz + lub -.

$ jd -set A.json C.json

@ ["People",{}]
+ "Carla"

Wyjściowe dyfuzory mogą być również używane jako pliki łatek z opcją -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
Author: Joe Burnett,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-06 03:53:36

Jeśli chcesz również zobaczyć różnice, użyj @Erik ' s answer jako inspiracji i 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
Author: tokland,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-18 11:04:59

Być może przydałoby się takie narzędzie sortowania i różnicowania: http://novicelab.org/jsonsortdiff / który najpierw sortuje obiekty semantycznie, a następnie porównuje je. Opiera się na https://www.npmjs.com/package/jsonabc

 0
Author: Shivraj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-06 06:34:58