wake-up-neo.com

Verwenden von jq oder alternativen Befehlszeilentools zum Vergleichen von JSON-Dateien

Gibt es Befehlszeilen-Dienstprogramme, die verwendet werden können, um herauszufinden, ob zwei JSON-Dateien mit der Invarianz zu der Reihenfolge innerhalb des Dictionary-Schlüssels und innerhalb des Listenelements identisch sind?

Könnte dies mit jq oder einem anderen gleichwertigen Werkzeug gemacht werden?

Beispiele:

Diese beiden JSON-Dateien sind identisch

A:

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

B:

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

diese beiden JSON-Dateien unterscheiden sich jedoch:

A:

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

C:

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

Das wäre:

$ some_diff_command A.json B.json

$ some_diff_command A.json C.json
The files are not structurally identical
42

Da der Vergleich von jq bereits Objekte vergleicht, ohne die Schlüsselreihenfolge zu berücksichtigen, müssen Sie lediglich alle Listen innerhalb des Objekts sortieren, bevor Sie sie vergleichen. Angenommen, Ihre beiden Dateien heißen a.json und b.json in der letzten nächtlichen jq:

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

Dieses Programm sollte "true" oder "false" zurückgeben, abhängig davon, ob die Objekte mit der Definition der Gleichheit, nach der Sie fragen, gleich sind oder nicht.

BEARBEITEN: Das (.. | arrays) |= sort-Konstrukt funktioniert in einigen Edge-Fällen nicht wie erwartet. Diese GitHub-Ausgabe erklärt warum und bietet einige Alternativen, wie:

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

Auf den jq-Aufruf oben angewendet:

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

Wenn Sie Zugriff auf Bash oder eine andere erweiterte Shell haben, können Sie im Prinzip Folgendes tun

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

mit Unterprozessen. Dadurch wird der Json mit sortierten Schlüsseln und einer konsistenten Darstellung von Fließpunkten formatiert. Dies sind die einzigen zwei Gründe, aus denen ich mir vorstellen kann, warum json mit dem gleichen Inhalt anders gedruckt wird. Wenn Sie anschließend einen einfachen String-Vergleich durchführen, wird ein ordnungsgemäßer Test durchgeführt. Es ist wahrscheinlich auch erwähnenswert, dass, wenn Sie bash nicht verwenden können, die gleichen Ergebnisse mit temporären Dateien erzielt werden können, es einfach nicht so sauber ist.

Dies beantwortet Ihre Frage nicht ganz, da Sie in der Art und Weise, in der Sie die Frage angegeben haben, dass ["John", "Bryan"] und ["Bryan", "John"] identisch verglichen werden sollen. Da json nicht das Konzept einer Menge hat, sondern nur eine Liste, sollten diese als gesondert betrachtet werden. Reihenfolge ist wichtig für Listen. Sie müssten einen benutzerdefinierten Vergleich schreiben, wenn Sie möchten, dass sie gleich miteinander verglichen werden. Um dies zu tun, müssen Sie definieren, was Sie unter Gleichheit verstehen. Ist die Reihenfolge für alle Listen oder nur für einige wichtig? Was ist mit doppelten Elementen? Wenn Sie möchten, dass sie als Satz dargestellt werden und die Elemente Strings sind, können Sie sie in Objekte wie {"John": null, "Bryan": null} einfügen. Beim Vergleich der Gleichheit spielt die Ordnung keine Rolle.

Aktualisieren

Aus der Kommentardiskussion: Wenn Sie eine bessere Vorstellung davon bekommen wollen, warum der Json nicht derselbe ist, dann

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

erzeugt eine interpretierbarere Ausgabe. vimdiff kann je nach Geschmack vorzuziehen sein.

46
Erik

Hier ist eine Lösung, die die generische Funktion walk/1 verwendet: 

# 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);

Beispiel:

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

produziert:

true

Und als Bash-Skript verpackt:

#!/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 ist eine integrierte Version von jq> 1.5 und kann daher weggelassen werden, wenn Ihr jq es enthält. Es ist jedoch kein Nachteil, wenn Sie es redundant in ein jq-Skript einfügen.

POST-POSTSCRIPT: Die integrierte Version von walk wurde kürzlich geändert, so dass die Schlüssel innerhalb eines Objekts nicht mehr sortiert werden. Insbesondere wird keys_unsorted verwendet. Für die vorliegende Aufgabe sollte die Version verwendet werden, die keys verwendet.

6
peak

Verwenden Sie jd mit der Option -set:

Keine Ausgabe bedeutet keine Differenz.

$ jd -set A.json B.json

Unterschiede werden als @ Pfad und + oder - angezeigt.

$ jd -set A.json C.json

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

Die Ausgabedifferenzen können mit der Option -p auch als Patch-Dateien verwendet werden.

$ 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

Vielleicht könnten Sie dieses Sorten- und Diff-Werkzeug verwenden: http://novicelab.org/jsonsortdiff/ , das die Objekte zunächst semantisch sortiert und dann vergleicht. Es basiert auf https://www.npmjs.com/package/jsonabc

1
Shivraj

Wenn Sie auch die Unterschiede sehen möchten, verwenden Sie @ Eriks Antwort als Inspiration und 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

Das Beste aus den beiden obersten Antworten ziehen, um ein jq -basiertes JSON-Diff zu erhalten:

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")

Dies übernimmt die elegante Array-Sortierlösung von https://stackoverflow.com/a/31933234/538507 (die es uns ermöglicht, Arrays als Mengen zu behandeln) und die saubere Bash-Umleitung in diff from https://stackoverflow.com/a/37175540/538507 Dies betrifft den Fall, dass Sie einen Unterschied von zwei JSON-Dateien wünschen und die Reihenfolge des Array-Inhalts nicht relevant ist.

0
Andrew