MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Come convertire un JSON nidificato arbitrario in CSV con jq, in modo da poterlo riconvertire?

Il seguente tocsv e fromcsv le funzioni forniscono una soluzione al problema indicato, fatta eccezione per una complicazione relativa al requisito (6) relativo alle intestazioni. In sostanza, questo requisito può essere soddisfatto utilizzando le funzioni qui fornite aggiungendo un passaggio di trasposizione della matrice.

Indipendentemente dal fatto che venga aggiunto o meno un passaggio di trasposizione, il vantaggio dell'approccio adottato qui è che non ci sono restrizioni sulle chiavi o sui valori JSON. In particolare, possono contenere punti (punti), newline e/o caratteri NUL.

Nell'esempio viene fornito un array di oggetti, ma in realtà qualsiasi flusso di documenti JSON validi può essere utilizzato come input per tocsv; grazie alla magia di jq, lo stream originale verrà ricreato da fromcsv (nel senso di uguaglianza entità per entità).

Naturalmente, poiché non esiste uno standard CSV, il CSV prodotto da tocsv la funzione potrebbe non essere compresa da tutti i processori CSV. In particolare, tieni presente che il tocsv funzione definita qui mapembedded newlines in stringhe JSON o nomi di chiavi alla stringa di due caratteri "\n" (cioè, una barra rovesciata letterale seguita dalla lettera "n"); l'operazione inversa esegue la traduzione inversa per soddisfare il "andata e ritorno" requisito.

(L'uso di tail è solo per semplificare la presentazione; sarebbe banale modificare la soluzione per renderla unica-jq.)

Il CSV viene generato partendo dal presupposto che qualsiasi valore può essere incluso in un campo purché (a) il campo sia tra virgolette e (b) le doppie virgolette all'interno del campo siano raddoppiate.

Qualsiasi soluzione generica che supporti i "viaggi di andata e ritorno" è destinata a essere alquanto complicata. Il motivo principale per cui la soluzione qui presentata è più complessa di quanto ci si potrebbe aspettare è perché viene aggiunta una terza colonna, in parte per facilitare la distinzione tra interi e stringhe con valori interi, ma principalmente perché rende facile distinguere tra dimensione-1 e dimensione -2 array prodotti da --stream di jq opzione. Inutile dire che ci sono altri modi in cui questi problemi potrebbero essere affrontati; anche il numero di chiamate a jq potrebbe essere ridotto.

La soluzione viene presentata come uno script di test che verifica il requisito di andata e ritorno in un caso di test significativo:

#!/bin/bash

function json {
    cat<<EOF
[
  {
    "a": 1,
    "b": [
      1,
      2,
      "1"
    ],
    "c": "d\",ef",
    "embed\"ed": "quote",
    "null": null,
    "string": "null",
    "control characters": "a\u0000c",
    "newline": "a\nb"
  },
  {
    "x": 1
  }
]
EOF
}

function tocsv {
 jq -ncr --stream '
   (["path", "value", "stringp"],
    (inputs | . + [.[1]|type=="string"]))
   | map( tostring|gsub("\"";"\"\"") | gsub("\n"; "\\n"))
   | "\"\(.[0])\",\"\(.[1])\",\(.[2])" 
'
}

function fromcsv { 
    tail -n +2 | # first duplicate backslashes and deduplicate double-quotes
    jq -rR '"[\(gsub("\\\\";"\\\\") | gsub("\"\"";"\\\"") ) ]"' |
    jq -c '.[2] as $s 
           | .[0] |= fromjson 
           | .[1] |= if $s then . else fromjson end 
           | if $s == null then [.[0]] else .[:-1] end
             # handle newlines
           | map(if type == "string" then gsub("\\\\n";"\n") else . end)' |
    jq -n 'fromstream(inputs)'
}    

# Check the roundtrip:
json | tocsv | fromcsv | jq -s '.[0] == .[1]' - <(json)

Ecco il CSV che verrebbe prodotto da json | tocsv , tranne per il fatto che SO sembra non consentire NUL letterali, quindi l'ho sostituito con \0 :

"path","value",stringp
"[0,""a""]","1",false
"[0,""b"",0]","1",false
"[0,""b"",1]","2",false
"[0,""b"",2]","1",true
"[0,""b"",2]","false",null
"[0,""c""]","d"",ef",true
"[0,""embed\""ed""]","quote",true
"[0,""null""]","null",false
"[0,""string""]","null",true
"[0,""control characters""]","a\0c",true
"[0,""newline""]","a\nb",true
"[0,""newline""]","false",null
"[1,""x""]","1",false
"[1,""x""]","false",null
"[1]","false",null