¿Cómo convertir JSON simple arbitrario a CSV usando jq?

105

Usando jq , ¿cómo se puede convertir a CSV la codificación JSON arbitraria de una matriz de objetos poco profundos?

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 dado cualquier JSON, con la única restricción de que es una matriz de objetos con propiedades escalares (no profundo / complejo / subobjetos, ya que aplanarlos es otra cuestión). El resultado debe contener una fila de encabezado con los nombres de los campos. Se dará preferencia a las respuestas que conserven el orden de campo del primer objeto, pero no es un requisito. Los resultados pueden incluir todas las celdas con comillas dobles, o solo incluir aquellas que requieran comillas (por ejemplo, 'a, b').

Ejemplos

  1. 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"
  2. 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"
outis
fuente
Más de tres años después ... un genérico json2csvestá en stackoverflow.com/questions/57242240/…
pico

Respuestas:

159

Primero, obtenga una matriz que contenga todos los diferentes nombres de propiedad de objeto en su entrada de matriz de objeto. 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 un encabezado para el CSV, y pase el flujo de filas resultante al @csvfiltro.

$cols, $rows[] | @csv

Todos juntos ahora. Recuerde usar la -rbandera 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'

fuente
6
Es bueno que su solución capture todos los nombres de propiedad de todas las filas, en lugar de solo la primera. Sin embargo, me pregunto cuáles son las implicaciones de rendimiento de esto para documentos muy grandes. PD Si lo desea, puede deshacerse de la $rowsasignación de variable simplemente insertándola:(map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
Jordan Running
9
¡Gracias, Jordan! Soy consciente de que $rowsno tiene que asignarse a una variable; Pensé que asignarlo a una variable hacía que la explicación fuera más agradable.
3
considere convertir el valor de la fila | cadena en caso de que haya matrices o mapas anidados.
TJR
Buena sugerencia, @TJR. Tal vez si hay estructuras anidadas, jq debería recurrir a ellas y convertir sus valores en columnas también
LS
¿En qué se diferenciaría esto si el JSON estuviera en un archivo y quisiera filtrar algunos datos específicos a CSV?
Neo
91

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 valor único. El flujo JSON de entrada se convierte a algún tipo interno que se pasa a través de los filtros y luego se codifica en un flujo 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 examinando 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"]'
"una"
"si"
$ jq -cn '"a", "b"'
"una"
"si"

Tenga en cuenta que la salida no es una matriz (lo que sería ["a", "b"]). La salida compacta (la -copción) 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 utiliza nuevas líneas en lugar de RS como separador de salida cuando se codifica. En consecuencia, este tipo interno se conoce con el término genérico "secuencia" en esta respuesta, con "flujo" reservado para la entrada y salida codificadas.

Construyendo el filtro

Las claves del primer objeto se pueden extraer con:

.[0] | keys_unsorted

Por lo general, las claves se mantendrán en su orden original, pero no se garantiza la conservación del orden exacto. En consecuencia, deberán usarse para indexar los objetos y obtener los valores en el mismo orden. Esto también evitará que los valores estén en las columnas incorrectas si algunos objetos tienen un orden de clave diferente.

Para generar las claves como la primera fila y hacerlas disponibles para indexación, se almacenan en una variable. La siguiente etapa de la canalización hace referencia a esta variable y usa el operador de coma para anteponer el encabezado al flujo de salida.

(.[0] | keys_unsorted) as $keys | $keys, ...

La expresión después de la coma es un poco complicada. El operador de índice de un objeto puede tomar una secuencia de cadenas (por ejemplo "name", "value"), devolviendo una secuencia de valores de propiedad para esas cadenas. $keyses 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 la matriz se usa para convertirla en una matriz.

[.[ $keys[] ]]

Esta expresión debe aplicarse a un solo objeto. map()se usa para aplicarlo a todos los objetos de 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 de la mapúnica para desagregarla fuera? mapproduce una matriz; .[ $keys[] ]produce una secuencia. Aplicar mapa la secuencia de .[ $keys[] ]produciría una matriz de secuencias de valores, pero dado que las secuencias no son de tipo JSON, en su lugar obtiene una matriz plana 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 @csvformateador.

Alterno

Los elementos se pueden separar más tarde que temprano. En lugar de usar el operador de coma para obtener una secuencia (pasando una secuencia como el operando derecho), la secuencia de encabezado ( $keys) se puede envolver en una matriz y +usarse para agregar la matriz de valores. Esto aún debe convertirse en una secuencia antes de pasarlo @csv.

outis
fuente
3
¿Puede utilizar en keys_unsortedlugar de keyspara preservar el orden de las claves del primer objeto?
Jordan Running
2
@outis: el preámbulo sobre las transmisiones es algo inexacto. El simple hecho es que los filtros jq están orientados a flujos. Es decir, cualquier filtro puede aceptar un flujo de entidades JSON y algunos filtros pueden producir un flujo de valores. No hay una "nueva línea" ni ningún otro separador entre los elementos de una secuencia; solo cuando se imprimen, se introduce un separador. Para verlo por ti mismo, prueba: jq -n -c 'reduce ("a", "b") as $ s ("";. + $ S)'
pico
2
@peak - por favor acepte esto como la respuesta, es de lejos el más completo y completo
btk
@btk: no hice la pregunta y, por lo tanto, no puedo aceptarla.
pico
1
@Wyatt: observe más de cerca sus datos y la entrada de ejemplo. La pregunta es sobre una serie de objetos, no sobre un solo objeto. Prueba [{"a":1,"b":2,"c":3}].
Salida
6

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 podrías usarlo así:

to_csv([ "code", "name", "level", "country" ])
Jeff Mercado
fuente
6

El siguiente filtro es ligeramente diferente porque asegurará 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)

Filtrar: 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(.)
TJR
fuente
1
Esto funciona bien para JSON simple, pero ¿qué pasa con JSON con propiedades anidadas que bajan muchos niveles?
Amir
Esto, por supuesto, ordena las claves. Además, la salida de uniquese ordena de todos modos, por lo que unique|sortse puede simplificar a unique.
pico
1
@TJR Cuando se usa este filtro, es obligatorio activar la salida sin procesar usando la -ropción. De lo contrario, todas las citas se "convierten en extra-escape, lo que no es un archivo CSV válido.
tosh
Amir: las propiedades anidadas no se asignan a CSV.
chrishmorris
2

Esta variante del programa de Santiago también es segura, pero asegura 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
pico
fuente