Usar jq o herramientas de línea de comando alternativas para comparar archivos JSON
diff (7)
Aquí hay una solución que usa la función genérica 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);
Ejemplo:
{"a":[1,2,[3,4]]} | equiv( {"a": [[4,3], 2,1]} )
produce:
true
Y envuelto como un script 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 es una versión incorporada de jq> 1.5 y, por lo tanto, puede omitirse si su jq lo incluye, pero no hay ningún daño en incluirlo de forma redundante en un script jq.
POST-POSTSCRIPT: la versión incorporada de
walk
se ha cambiado recientemente para que ya no clasifique las claves dentro de un objeto.
Específicamente, utiliza
keys_unsorted
.
Para la tarea en cuestión, se debe usar la versión que utiliza
keys
.
¿Hay alguna utilidad de línea de comando que pueda usarse para encontrar si dos archivos JSON son idénticos con invariancia al orden dentro de la clave del diccionario y dentro de la lista de elementos?
¿Podría hacerse esto con
jq
o alguna otra herramienta equivalente?
Ejemplos:
Estos dos archivos JSON son idénticos.
A
:
{
"People": ["John", "Bryan"],
"City": "Boston",
"State": "MA"
}
B
:
{
"People": ["Bryan", "John"],
"State": "MA",
"City": "Boston"
}
pero estos dos archivos JSON son diferentes:
A
:
{
"People": ["John", "Bryan", "Carla"],
"City": "Boston",
"State": "MA"
}
C
:
{
"People": ["Bryan", "John"],
"State": "MA",
"City": "Boston"
}
Eso sería:
$ some_diff_command A.json B.json
$ some_diff_command A.json C.json
The files are not structurally identical
Dado que la comparación de jq ya compara objetos sin tener en cuenta el orden de las claves, todo lo que queda es ordenar todas las listas dentro del objeto antes de compararlas.
Suponiendo que sus dos archivos se
a.json
y
b.json
, en la última noche de jq:
jq --argfile a a.json --argfile b b.json -n ''($a | (.. | arrays) |= sort) as $a | ($b | (.. | arrays) |= sort) as $b | $a == $b''
Este programa debe devolver "verdadero" o "falso" dependiendo de si los objetos son iguales o no utilizando la definición de igualdad que solicita.
EDITAR: La construcción
(.. | arrays) |= sort
no funciona realmente como se esperaba en algunos casos extremos.
Este problema de GitHub
explica por qué y ofrece algunas alternativas, como:
def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); (post_recurse | arrays) |= sort
Aplicado a la invocación jq anterior:
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''
En principio, si tiene acceso a bash o algún otro shell avanzado, podría hacer algo como
cmp <(jq -cS . A.json) <(jq -cS . B.json)
utilizando subprocesos. Esto formateará el json con claves ordenadas y una representación consistente de puntos flotantes. Esas son las dos únicas razones por las que puedo pensar por qué json con el mismo contenido se imprimiría de manera diferente. Por lo tanto, hacer una comparación de cadena simple después dará como resultado una prueba adecuada. Probablemente también valga la pena señalar que si no puede usar bash puede obtener los mismos resultados con archivos temporales, simplemente no es tan limpio.
Esto no responde a su pregunta, porque en la forma en que dijo la pregunta que deseaba que
["John", "Bryan"]
y
["Bryan", "John"]
compararan idénticamente.
Como json no tiene el concepto de un conjunto, solo una lista, deben considerarse distintos.
El orden es importante para las listas.
Tendría que escribir una comparación personalizada si quisiera que se compararan por igual, y para hacer eso necesitaría definir qué quiere decir con igualdad.
¿Importa el orden para todas las listas o solo para algunas?
¿Qué pasa con los elementos duplicados?
Alternativamente, si desea que se representen como un conjunto, y los elementos son cadenas, puede colocarlos en objetos como
{"John": null, "Bryan": null}
.
El orden no importará al comparar los de igualdad.
Actualizar
De la discusión de comentarios: si desea tener una mejor idea de por qué el json no es el mismo, entonces
diff <(jq -S . A.json) <(jq -S . B.json)
producirá una salida más interpretable.
vimdiff
puede ser preferible a diff dependiendo de los gustos.
Quizás podría usar esta herramienta de clasificación y diferencia: http://novicelab.org/jsonsortdiff/ que primero clasifica los objetos semánticamente y luego los compara. Se basa en https://www.npmjs.com/package/jsonabc
Sacando lo mejor de las dos respuestas principales para obtener un json diff basado en
jq
:
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")
Esto toma la elegante solución de clasificación de matrices de
https://.com/a/31933234/538507
(que nos permite tratar las matrices como conjuntos) y la redirección limpia de bash en
diff
desde
https://.com/a/37175540 / 538507
Esto soluciona el caso en el que desea una diferencia de dos archivos json y el orden del contenido de la matriz no es relevante.
Si también quieres ver las diferencias, usa la respuesta de @ Erik como inspiración y 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"
}]
Use
jd
con la opción
-set
:
Sin salida significa que no hay diferencia.
$ jd -set A.json B.json
Las diferencias se muestran como @ ruta y + o -.
$ jd -set A.json C.json
@ ["People",{}]
+ "Carla"
Los diferenciales de salida también se pueden usar como archivos de parche con la opción
-p
.
$ jd -set -o patch A.json C.json; jd -set -p patch B.json
{"City":"Boston","People":["John","Carla","Bryan"],"State":"MA"}