Як перетворити довільний простий JSON в CSV за допомогою jq?


105

Як за допомогою 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"

Три з json2csv
пік

Відповіді:


159

По-перше, отримайте масив, що містить усі різні назви властивостей об'єкта у вашому вхідному масиві об’єкта. Це будуть стовпці вашого 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
Йорданія, що працює

9
Дякую, Джордан! Я знаю, що $rowsне потрібно призначати змінну; Я просто думав, що привласнення його до змінної зробило пояснення приємнішим.

3
розглянути можливість перетворення значення рядка | рядок, якщо є вкладені масиви чи карти.
TJR

Гарна пропозиція, @TJR. Можливо, якщо є вкладені структури, jq повинен повторити їх і внести їх значення в стовпці
LS

Чим це відрізнятиметься, якщо JSON знаходився у файлі, і ви хотіли відфільтрувати деякі конкретні дані до CSV?
Нео

91

Худий

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

Зауважте, що вихід не є масивом (який би був ["a", "b"]). Компактний вихід ( -cопція) показує, що кожен елемент масиву (або аргумент до ,фільтра) стає окремим об'єктом у висновку (кожен знаходиться в окремому рядку).

Потік схожий на JSON-послідовність , але він використовує нові рядки, а не 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
Чи можете ви використовувати keys_unsortedзамість того, keysщоб зберегти ключовий порядок від першого об’єкта?
Йорданія, що працює

2
@outis - Преамбула про потоки дещо неточна. Простий факт полягає в тому, що фільтри jq орієнтовані на потоки. Тобто будь-який фільтр може приймати потік сутностей JSON, а деякі фільтри можуть виробляти потік значень. Між елементами в потоці немає "нового рядка" чи будь-якого іншого роздільника - вводиться роздільник лише тоді, коли вони надруковані. Щоб переконатися в цьому, спробуйте: jq -n -c 'зменшити ("a", "b") як $ s ("";. + $ S)'
пік

2
@peak - прийміть це як відповідь, вона на сьогоднішній день є найбільш повною та всеосяжною
btk

@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(.)

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.