JSON в CSV: переменное количество столбцов в строке - PullRequest
1 голос
/ 12 июня 2019

Мне нужно преобразовать JSON в CSV, где JSON имеет массивы переменной длины, например:

JSON-объекты:

{"labels": ["label1"]}
{"labels": ["label2", "label3"]}
{"labels": ["label1", "label4", "label5"]}

Результирующий CSV:

labels,labels,labels
"label1",,
"label2","label3",
"label1","label4","label5"

В исходном JSON есть много других свойств, это просто пример для простоты.

Кроме того, я должен сказать, что процесс должен работать с JSON как потоком, потому что исходный JSON может бытьочень большой (> 1 ГБ).

Я хотел бы использовать jq с двумя проходами, первый проход будет собирать максимальную длину массива меток, второй проход создаст CSV как число результирующих столбцовк этому времени известно.Но у jq нет понятия глобальных переменных, поэтому я не знаю, где я могу хранить промежуточные суммы.

Я бы хотел сделать это в Windows через CLI.Заранее спасибо.

Ответы [ 2 ]

2 голосов
/ 13 июня 2019

Вопрос показывает поток объектов JSON, поэтому следующие решения предполагают, что входной файл уже является последовательностью, как показано. Эти решения также могут быть легко адаптированы для случая, когда входной файл содержит огромный массив объектов, например, как обсуждено в эпилоге.

Решение с двумя вызовами

Вот двухпроходное решение, использующее два вызова jq. Презентация предполагает среду, похожую на bash, на случай, если у вас :

n=$(jq -n 'reduce (inputs|.labels|length) as $i (-1;
  if $i > . then $i else . end)' stream.json)
jq -nr --argjson n $n '
  def fill($n): . + [range(length;$n)|null];
  [range(0;$n)|"labels"],
  (inputs | .labels | fill($n))
  | @csv' stream.json

Предполагая, что ввод такой, как описано, это гарантирует получение действительного CSV. Надеемся, что вы можете адаптировать вышесказанное к вашей оболочке по мере необходимости - возможно, эта ссылка поможет Назначить вывод программы переменной с помощью командного файла MS

Использование input_filename и один вызов jq

К сожалению, у jq нет функции "перемотки", но есть альтернатива: прочитать файл дважды за один вызов jq. Это более громоздко, чем приведенное выше решение с двумя вызовами, но позволяет избежать любых трудностей, связанных с последним.

cat sample.json | jq -nr '

  def fill($n): . + [range(length;$n)|null];
  def max($x): if . < $x then $x else . end;

  foreach (inputs|.labels) as $in ( {n:0};
    if input_filename == "<stdin>" 
    then .n |= max($in|length)
    else .printed+=1
    end;
    if .printed == null then empty
    else .n as $n
    | (if .printed == 1 then [range(0;$n)|"labels"] else empty end),
      ($in | fill($n))
    end)
  | @csv'  -  sample.json

Еще одно решение с одним вызовом

Следующее решение использует специальное значение (здесь null) для разграничения двух потоков:

(cat stream.json; echo null; cat stream.json) | jq -nr '
  def fill($n): . + [range(length; $n) | null];
  def max($x): if . < $x then $x else . end;

  (label $loop | foreach inputs as $in (0; 
     if $in == null then . else max($in|.labels|length) end;
     if $in == null then ., break $loop else empty end)) as $n
  | [range(0;$n)|"labels"],
    (inputs | .labels | fill($n))
  | @csv '

Эпилог

Файл с массивом JSON верхнего уровня, который слишком велик для размещения в памяти, можно преобразовать в поток элементов массива, вызвав jq с параметром --stream, например, следующим образом:

jq -cn --stream 'fromstream(1|truncate_stream(inputs))'
1 голос
/ 13 июня 2019

Для такого большого файла вы, вероятно, захотите сделать это в двух отдельных вызовах, один для получения счетчика, а другой для фактического вывода CSV.Если вы хотите прочитать весь файл в память, вы можете сделать это за один раз, но мы определенно не хотим этого делать, мы хотим передавать его туда, где это возможно.

Все становится немногонекрасиво, когда дело доходит до сохранения результата команд в переменной, запись в файл может быть проще.Но я бы предпочел не использовать временные файлы, если нам это не нужно.

REM assuming in a batch file
for /f "usebackq delims=" %%i in (`jq -n --stream "reduce (inputs | .[0][1] + 1) as $l (0; if $l > . then $l else . end)" input.json`) do set cols=%%i
jq -rn --stream --argjson cols "%cols%" "[range($cols)|\"labels\"],(fromstream(1|truncate_stream(inputs))|[.[],(range($cols-length)|null)])|@csv" input.json

> jq -n --stream "reduce (inputs | .[0][1] + 1) as $l (0; if $l > . then $l else . end)" input.json

Для первого вызова, чтобы получить количество столбцов, мы просто используем преимуществотот факт, что пути к значениям массива могут быть использованы для указания длины массивов.Мы просто хотим взять максимум для всех элементов.


> jq -rn --stream --argjson cols "%cols%" ^
"[range($cols)|\"labels\"],(fromstream(1|truncate_stream(inputs))|[.[],(range($cols-length)|null)])|@csv" input.json

Затем, чтобы вывести остальное, мы просто берем массив labels (предполагая, что это единственное свойство объектов) и дополняем их null до $cols.Затем выведите в виде csv.


Если метки находятся в другом, глубоко вложенном пути, нежели тот, что в вашем примере здесь, вам нужно будет выбрать на основе соответствующих путей.

set labelspath=foo.bar.labels
jq -rn --stream --argjson cols "%cols%" --arg labelspath "%labelspath%" ^
"($labelspath|split(\".\")|[.,length]) as [$path,$depth] | [range($cols)|\"labels\"],(fromstream($depth|truncate_stream(inputs|select(.[0][:$depth] == $path)))|[.[],(range($cols-length)|null)])|@csv" input.json
...