¿Cómo convertir JSON simple arbitrario a CSV usando jq?
(5)
El delgado
jq -r ''(.[0] | keys_unsorted) as $keys | $keys, map([.[ $keys[] ]])[] | @csv''
o:
jq -r ''(.[0] | keys_unsorted) as $keys | ([$keys] + map([.[ $keys[] ]])) [] | @csv''
Los detalles
Aparte
Describir los detalles es complicado porque jq está orientado a la transmisión, lo que significa que opera en una secuencia de datos JSON, en lugar de un solo valor.
La secuencia JSON de entrada se convierte en algún tipo interno que se pasa a través de los filtros y luego se codifica en una secuencia de salida al final del programa.
El tipo interno no está modelado por JSON, y no existe como un tipo con nombre.
Se demuestra más fácilmente al examinar la salida de un índice simple (
.[]
) O el operador de coma (examinarlo directamente podría hacerse con un depurador, pero eso sería en términos de los tipos de datos internos de jq, en lugar de los tipos de datos conceptuales detrás de JSON).
$ jq -c ''.[]'' <<<''["a", "b"]'' "a" "b" $ jq -cn ''"a", "b"'' "a" "b"
Tenga en cuenta que la salida no es una matriz (que sería
["a", "b"]
).
La salida compacta (la opción
-c
) muestra que cada elemento de la matriz (o argumento del filtro) se convierte en un objeto separado en la salida (cada uno está en una línea separada).
Una secuencia es como un JSON-seq , pero usa líneas nuevas en lugar de RS como separador de salida cuando se codifica. En consecuencia, este tipo interno se denomina por el término genérico "secuencia" en esta respuesta, con "flujo" reservado para la entrada y salida codificada.
Construyendo el filtro
Las claves del primer objeto se pueden extraer con:
.[0] | keys_unsorted
Las claves generalmente se mantendrán en su orden original, pero no se garantiza preservar el orden exacto. En consecuencia, deberán usarse para indexar los objetos para obtener los valores en el mismo orden. Esto también evitará que los valores se encuentren en las columnas incorrectas si algunos objetos tienen un orden de teclas diferente.
Para generar las claves como la primera fila y ponerlas a disposición para la indexación, se almacenan en una variable. La siguiente etapa de la tubería hace referencia a esta variable y utiliza el operador de coma para anteponer el encabezado a la secuencia de salida.
(.[0] | keys_unsorted) as $keys | $keys, ...
La expresión después de la coma está un poco involucrada.
El operador de índice en un objeto puede tomar una secuencia de cadenas (por ejemplo,
"name", "value"
), devolviendo una secuencia de valores de propiedad para esas cadenas.
$keys
es una matriz, no una secuencia, por lo que
[]
se aplica para convertirla en una secuencia,
$keys[]
que luego se puede pasar a
.[]
.[ $keys[] ]
Esto también produce una secuencia, por lo que el constructor de matriz se utiliza para convertirla en una matriz.
[.[ $keys[] ]]
Esta expresión se debe aplicar a un solo objeto.
map()
se usa para aplicarlo a todos los objetos en la matriz externa:
map([.[ $keys[] ]])
Por último para esta etapa, esto se convierte en una secuencia para que cada elemento se convierta en una fila separada en la salida.
map([.[ $keys[] ]])[]
¿Por qué agrupar la secuencia en una matriz dentro del
map
solo para separarla fuera?
map
produce una matriz;
.[ $keys[] ]
produce una secuencia.
Aplicar el
map
a la secuencia desde
.[ $keys[] ]
produciría una matriz de secuencias de valores, pero dado que las secuencias no son del tipo JSON, entonces obtienes una matriz aplanada que contiene todos los valores.
["NSW","AU","state","New South Wales","AB","CA","province","Alberta","ABD","GB","council area","Aberdeenshire","AK","US","state","Alaska"]
Los valores de cada objeto deben mantenerse separados, de modo que se conviertan en filas separadas en la salida final.
Finalmente, la secuencia se pasa a través del formateador
@csv
.
Alterno
Los artículos se pueden separar tarde, en lugar de temprano.
En lugar de usar el operador de coma para obtener una secuencia (pasando una secuencia como el operando correcto), la secuencia de encabezado (
$keys
) se puede envolver en una matriz y
+
usar para agregar la matriz de valores.
Esto aún necesita convertirse a una secuencia antes de pasar a
@csv
.
Usando jq , ¿cómo puede convertirse JSON arbitraria que codifica una matriz de objetos poco profundos a CSV?
Hay muchas preguntas y respuestas en este sitio que cubren modelos de datos específicos que codifican los campos, pero las respuestas a esta pregunta deberían funcionar con cualquier JSON, con la única restricción de que sea una matriz de objetos con propiedades escalares (no profundo / complejo / subobjetos, ya que aplastarlos es otra pregunta). El resultado debe contener una fila de encabezado con los nombres de los campos. Se dará preferencia a las respuestas que preserven el orden de campo del primer objeto, pero no es un requisito. Los resultados pueden encerrar todas las celdas con comillas dobles, o solo aquellas que requieren comillas (por ejemplo, ''a, b'').
Ejemplos
-
Entrada:
[ {"code": "NSW", "name": "New South Wales", "level":"state", "country": "AU"}, {"code": "AB", "name": "Alberta", "level":"province", "country": "CA"}, {"code": "ABD", "name": "Aberdeenshire", "level":"council area", "country": "GB"}, {"code": "AK", "name": "Alaska", "level":"state", "country": "US"} ]
Salida posible:
code,name,level,country NSW,New South Wales,state,AU AB,Alberta,province,CA ABD,Aberdeenshire,council area,GB AK,Alaska,state,US
Salida posible:
"code","name","level","country" "NSW","New South Wales","state","AU" "AB","Alberta","province","CA" "ABD","Aberdeenshire","council area","GB" "AK","Alaska","state","US"
-
Entrada:
[ {"name": "bang", "value": "!", "level": 0}, {"name": "letters", "value": "a,b,c", "level": 0}, {"name": "letters", "value": "x,y,z", "level": 1}, {"name": "bang", "value": "/"!/"", "level": 1} ]
Salida posible:
name,value,level bang,!,0 letters,"a,b,c",0 letters,"x,y,z",1 bang,"""!""",0
Salida posible:
"name","value","level" "bang","!","0" "letters","a,b,c","0" "letters","x,y,z","1" "bang","""!""","1"
Creé una función que genera una matriz de objetos o matrices en csv con encabezados. Las columnas estarían en el orden de los encabezados.
def to_csv($headers):
def _object_to_csv:
($headers | @csv),
(.[] | [.[$headers[]]] | @csv);
def _array_to_csv:
($headers | @csv),
(.[][:$headers|length] | @csv);
if .[0]|type == "object"
then _object_to_csv
else _array_to_csv
end;
Entonces puedes usarlo así:
to_csv([ "code", "name", "level", "country" ])
El siguiente filtro es ligeramente diferente, ya que garantizará que cada valor se convierta en una cadena. (Nota: use jq 1.5+)
# For an array of many objects
jq -f filter.jq (file)
# For many objects (not within array)
jq -s -f filter.jq (file)
Filtro:
filter.jq
def tocsv($x):
$x
|(map(keys)
|add
|unique
|sort
) as $cols
|map(. as $row
|$cols
|map($row[.]|tostring)
) as $rows
|$cols,$rows[]
| @csv;
tocsv(.)
Esta variante del programa de Santiago también es segura, pero garantiza que los nombres de clave en el primer objeto se usen como encabezados de la primera columna, en el mismo orden en que aparecen en ese objeto:
def tocsv:
if length == 0 then empty
else
(.[0] | keys_unsorted) as $keys
| (map(keys) | add | unique) as $allkeys
| ($keys + ($allkeys - $keys)) as $cols
| ($cols, (.[] as $row | $cols | map($row[.])))
| @csv
end ;
tocsv
Primero, obtenga una matriz que contenga todos los diferentes nombres de propiedades de objeto en su entrada de matriz de objetos. Esas serán las columnas de su CSV:
(map(keys) | add | unique) as $cols
Luego, para cada objeto en la entrada de la matriz de objetos, asigne los nombres de columna que obtuvo a las propiedades correspondientes en el objeto. Esas serán las filas de su CSV.
map(. as $row | $cols | map($row[.])) as $rows
Finalmente, coloque los nombres de las columnas antes de las filas, como encabezado para el CSV, y pase la secuencia de filas resultante al filtro
@csv
.
$cols, $rows[] | @csv
Todos juntos ahora.
Recuerde usar el indicador
-r
para obtener el resultado como una cadena sin formato:
jq -r ''(map(keys) | add | unique) as $cols | map(. as $row | $cols | map($row[.])) as $rows | $cols, $rows[] | @csv''