Подтвердить что ты не робот

Как конвертировать произвольный простой JSON в CSV, используя jq?

Используя jq, как можно преобразовать произвольный JSON-массив из мелких объектов в CSV?

Есть много Q & Как на этом сайте, который охватывает конкретные модели данных, которые жестко кодируют поля, но ответы на этот вопрос должны работать с любым JSON, единственным ограничением которого является массив объектов со скалярными свойствами ( нет глубоких/сложных/под-объектов, как уплощение, это еще один вопрос). Результат должен содержать строку заголовка, дающую имена полей. Предпочтение будет отдаваться ответам, которые сохраняют порядок полей первого объекта, но это не требование. Результаты могут заключать все ячейки с двойными кавычками или заключать только те, которые требуют цитирования (например, "a, b" ).

Примеры

  • Input:

    [
        {"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"
    
  • Input:

    [
        {"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"
    
4b9b3361

Ответ 1

Сначала получите массив, содержащий все имена свойств объекта в вашем массиве объектов. Это будут столбцы вашего 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'

Ответ 2

Тощий

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"]'
"a"
"b"
$ jq -cn '"a", "b"'
"a"
"b"

Обратите внимание, что вывод не является массивом (который будет ["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 к последовательности из .[ $keys[] ] приведет к созданию массива последовательностей значений, но поскольку последовательности не являются типами JSON, вы вместо этого получаете сплющенный массив, содержащий все значения.

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

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

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

чередовать

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

Ответ 3

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

Ответ 4

Я создал функцию, которая выводит массив объектов или массивов в 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" ])

Ответ 5

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

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