Как преобразовать произвольный простой JSON в CSV с помощью jq?

106

Как с помощью jq преобразовать произвольную JSON-кодировку массива мелких объектов в CSV?

На этом сайте есть множество вопросов и ответов, которые охватывают определенные модели данных, которые жестко кодируют поля, но ответы на этот вопрос должны работать с любым JSON, с единственным ограничением, что это массив объектов со скалярными свойствами (без глубоких / сложных / подобъекты, поскольку их выравнивание - другой вопрос). Результат должен содержать строку заголовка с именами полей. Предпочтение будет отдаваться ответам, которые сохраняют порядок полей первого объекта, но это не является обязательным требованием. Результаты могут заключать все ячейки в двойные кавычки или только те, которые требуют заключения в кавычки (например, «a, b»).

Примеры

  1. Вход:

    [
        {"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"}
    ]
    

    Возможный выход:

    code,name,level,country
    NSW,New South Wales,state,AU
    AB,Alberta,province,CA
    ABD,Aberdeenshire,council area,GB
    AK,Alaska,state,US
    

    Возможный выход:

    "code","name","level","country"
    "NSW","New South Wales","state","AU"
    "AB","Alberta","province","CA"
    "ABD","Aberdeenshire","council area","GB"
    "AK","Alaska","state","US"
    
  2. Вход:

    [
        {"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}
    ]
    

    Возможный выход:

    name,value,level
    bang,!,0
    letters,"a,b,c",0
    letters,"x,y,z",1
    bang,"""!""",0
    

    Возможный выход:

    "name","value","level"
    "bang","!","0"
    "letters","a,b,c","0"
    "letters","x,y,z","1"
    "bang","""!""","1"
    
Outis
источник
Три с лишним года спустя ... универсальный json2csv
пик

Ответы:

160

Сначала получите массив, содержащий все различные имена свойств объекта во входных данных массива объектов. Это будут столбцы вашего CSV:

(map(keys) | add | unique) as $cols

Затем для каждого объекта во входном массиве объектов сопоставьте полученные имена столбцов с соответствующими свойствами объекта. Это будут строки вашего CSV.

map(. as $row | $cols | map($row[.])) as $rows

Наконец, поместите имена столбцов перед строками в качестве заголовка для CSV и передайте полученный поток строк @csvфильтру.

$cols, $rows[] | @csv

Все вместе сейчас. Не забудьте использовать -rфлаг, чтобы получить результат в виде необработанной строки:

jq -r '(map(keys) | add | unique) as $cols | map(. as $row | $cols | map($row[.])) as $rows | $cols, $rows[] | @csv'

источник
6
Приятно, что ваше решение захватывает все имена свойств из всех строк, а не только из первой. Однако мне интересно, как это сказывается на производительности для очень больших документов. PS Если хотите, вы можете избавиться от $rowsприсвоения переменной, просто вставив ее:(map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
Jordan Running
9
Спасибо, Джордан! Я знаю, что $rowsэто не обязательно должно быть присвоено переменной; Я просто подумал, что присвоение его переменной сделало объяснение лучше.
3
рассмотреть возможность преобразования значения строки | строка, если есть вложенные массивы или карты.
TJR
Хорошее предложение, @TJR. Может быть, если есть вложенные структуры, jq должен рекурсивно переходить в них и также превращать их значения в столбцы
LS
Чем бы это отличалось, если бы JSON был в файле, и вы хотели бы отфильтровать некоторые конкретные данные в CSV?
Neo
92

Тощий

jq -r '(.[0] | keys_unsorted) as $keys | $keys, map([.[ $keys[] ]])[] | @csv'

или:

jq -r '(.[0] | keys_unsorted) as $keys | ([$keys] + map([.[ $keys[] ]])) [] | @csv'

Детали

В стороне

Описать детали сложно, потому что jq ориентирован на поток, то есть он работает с последовательностью данных JSON, а не с одним значением. Входной поток JSON преобразуется в некоторый внутренний тип, который проходит через фильтры, а затем кодируется в потоке вывода в конце программы. Внутренний тип не моделируется JSON и не существует как именованный тип. Это проще всего продемонстрировать, изучив вывод простого индекса ( .[]) или оператора запятой (непосредственное изучение этого может быть выполнено с помощью отладчика, но это будет с точки зрения внутренних типов данных jq, а не концептуальных типов данных, стоящих за JSON) .

$ jq -c '. []' <<< '["a", "b"]'
"а"
"б"
$ jq -cn '"а", "б"'
"а"
"б"

Обратите внимание, что вывод не является массивом (что могло бы быть ["a", "b"]). Компактный вывод ( -cопция) показывает, что каждый элемент массива (или аргумент ,фильтра) становится отдельным объектом в выводе (каждый находится в отдельной строке).

Поток похож на JSON-seq , но использует символы новой строки, а не RS в качестве разделителя вывода при кодировании. Следовательно, этот внутренний тип упоминается в этом ответе общим термином «последовательность», причем «поток» зарезервирован для закодированных входных и выходных данных.

Построение фильтра

Ключи первого объекта можно извлечь с помощью:

.[0] | keys_unsorted

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

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

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

Выражение после запятой немного запутано. Оператор индекса объекта может принимать последовательность строк (например "name", "value"), возвращая последовательность значений свойств для этих строк. $keysпредставляет собой массив, а не последовательность, поэтому []применяется для преобразования его в последовательность,

$keys[]

который затем может быть передан .[]

.[ $keys[] ]

Это тоже создает последовательность, поэтому конструктор массива используется для преобразования ее в массив.

[.[ $keys[] ]]

Это выражение должно применяться к одному объекту. map()используется для применения ко всем объектам внешнего массива:

map([.[ $keys[] ]])

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

map([.[ $keys[] ]])[]

Зачем объединять последовательность в массив внутри mapтолько для того, чтобы разделить ее снаружи? mapпроизводит массив; .[ $keys[] ]производит последовательность. Применение mapк последовательности from .[ $keys[] ]приведет к созданию массива последовательностей значений, но поскольку последовательности не относятся к типу JSON, вместо этого вы получите плоский массив, содержащий все значения.

["NSW","AU","state","New South Wales","AB","CA","province","Alberta","ABD","GB","council area","Aberdeenshire","AK","US","state","Alaska"]

Значения каждого объекта должны храниться отдельно, чтобы они становились отдельными строками в окончательном выводе.

Наконец, последовательность проходит через @csvформаттер.

Альтернативный

Предметы можно разделить поздно, а не раньше. Вместо использования оператора запятой для получения последовательности (передача последовательности в качестве правого операнда) заголовок sequence ( $keys) может быть заключен в массив и +использоваться для добавления массива значений. Это все еще необходимо преобразовать в последовательность перед передачей в @csv.

Outis
источник
3
Можете ли вы использовать keys_unsortedвместо этого, keysчтобы сохранить порядок клавиш от первого объекта?
Jordan Running
2
@outis - Преамбула о потоках несколько неточная. Простой факт в том, что фильтры jq ориентированы на поток. То есть любой фильтр может принимать поток сущностей JSON, а некоторые фильтры могут создавать поток значений. Между элементами в потоке нет «новой строки» или любого другого разделителя - разделитель вводится только тогда, когда они печатаются. Чтобы убедиться в этом, попробуйте: jq -n -c 'reduce ("a", "b") as $ s ("";. + $ S)'
пик
2
@peak - пожалуйста , примите это как ответ, это, безусловно , наиболее полным и всеобъемлющим
БТК
@btk - я не задавал вопрос и поэтому не могу его принять.
пик
1
@Wyatt: внимательно посмотрите на свои данные и пример ввода. Речь идет о массиве объектов, а не об одном объекте. Попробуй [{"a":1,"b":2,"c":3}].
outis
6

Я создал функцию, которая выводит массив объектов или массивов в csv с заголовками. Столбцы будут в порядке заголовков.

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;

Таким образом, вы можете использовать это так:

to_csv([ "code", "name", "level", "country" ])
Джефф Меркадо
источник
6

Следующий фильтр немного отличается тем, что обеспечивает преобразование каждого значения в строку. (Примечание: используйте 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)

Фильтр: 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
источник
1
Это хорошо работает для простого JSON, но как насчет JSON с вложенными свойствами, которые спускаются на многие уровни?
Амир
Это, конечно, сортирует ключи. Кроме того, вывод в uniqueлюбом случае сортируется, поэтому unique|sortего можно упростить до unique.
пик
1
@TJR При использовании этого фильтра обязательно включить необработанный вывод с помощью -rопции. В противном случае все кавычки "станут экранированными, что не является допустимым CSV.
Тош
Амир: вложенные свойства не отображаются в CSV.
chrishmorris
2

Этот вариант программы Сантьяго также безопасен, но гарантирует, что имена ключей в первом объекте используются в качестве заголовков первых столбцов в том же порядке, в каком они появляются в этом объекте:

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
вершина горы
источник