Как использовать JQ для развертывания списка объектов в денормализованные объекты?

У меня есть следующий пример строк JSON:

{"toplevel_key": "top value 1", "list": [{"key1": "value 1", "key2": "value 2"},{"key1": "value 3", "key2": "value 4"}]}
{"toplevel_key": "top value 2", "list": [{"key1": "value 5", "key2": "value 6"}]}

Я хочу преобразовать его с помощью JQ, развернув список до фиксированного количества «столбцов», в результате чего получится список плоских объектов JSON в следующем формате:

{
    "top-level-key": "top value 1",
    "list_0_key1": "value 1",
    "list_0_key2": "value 2",
    "list_1_key1": "value 3",
    "list_1_key2": "value 4",
}
{
    "top-level-key": "top value 2",
    "list_0_key1": "value 4",
    "list_0_key2": "value 5",
    "list_1_key1": "",
    "list_1_key2": "",
}

Примечание: на самом деле я хочу, чтобы они были по одной на строку, отформатированы здесь для удобочитаемости.

Единственный способ получить желаемый результат — записать все столбцы в моем выражении JQ:

$ cat example.jsonl | jq -c '{toplevel_key, list_0_key1: .list[0].key1, list_0_key2: .list[0].key2, list_1_key1: .list[1].key1, list_1_key2: .list[1].key2}'

Это дает мне результат, который я хочу, но мне приходится вручную писать ВСЕ фиксированные «столбцы» (а в производстве их будет намного больше).

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

Есть ли способ сделать это в чистом JQ?

Это то, что я смог получить до сих пор:

$ cat example.jsonl | jq -c '(.list | to_entries | map({("list_" + (.key | tostring)): .value})) | add'
{"list_0":{"key1":"value 1","key2":"value 2"},"list_1":{"key1":"value 3","key2":"value 4"}}
{"list_0":{"key1":"value 5","key2":"value 6"}}

person Elias Dorneles    schedule 22.10.2015    source источник


Ответы (2)


Пока вы знаете названия конкретных ключей, ответ Джеффа великолепен. Вот ответ, который не жестко кодирует конкретные имена ключей, то есть работает с объектами любой структуры и уровней вложенности:

[leaf_paths as $path | {
    "key": $path | map(tostring) | join("_"),
    "value": getpath($path)
}] | from_entries

Объяснение: paths — это встроенная функция, которая рекурсивно выводит массив, представляющий позицию каждого элемента ввода, который вы ей передаете: элементы в указанном массиве — это упорядоченные имена ключей и индексы, которые ведут к запрошенному элементу массива. leaf_paths — это его версия, которая получает пути только к «листовым» элементам, то есть к элементам, не содержащим других элементов.

Чтобы уточнить, учитывая ввод [[1, 2]], paths выведет [0], [0, 0], [0, 1] (то есть пути к [1, 2], 1 и 2 соответственно), а leaf_paths выведет только [0, 0], [0, 1].

Это самая сложная часть. После этого мы получаем каждый из путей как $path (в форме ["list", 1, "key2"]), преобразуем каждый из его элементов в его строковое представление с помощью map(tostring) (что дает нам ["list", "1", "key2"]) и join с подчеркиванием. Мы сохраняем это как ключ «входа» в объект, который мы хотим создать: в качестве значения мы получаем значение исходного объекта в заданном $path.

Наконец, мы используем from_entries, чтобы превратить массив пар ключ-значение в объект JSON. Это даст нам вывод, аналогичный ответу Джеффа: то есть тот, в котором появляются только ключи со значениями.

Однако ваш первоначальный вопрос требовал, чтобы значения, появляющиеся на любом из входных объектов, отображались во всех выходных данных, при этом соответствующие значения были установлены как пустые строки, если они отсутствуют на входе. Вот программа jq, которая делает это: как говорит Джефф в своем ответе, вам нужно проглотить (-s) все входные значения, чтобы это было возможно:

(map(leaf_paths) | unique) as $paths |
map([$paths[] as $path | {
    "key": $path | map(tostring) | join("_"),
    "value": (getpath($path) // "")
}] | from_entries)[]

Вы заметите, что она очень похожа на первую программу: основное отличие состоит в том, что мы получаем все уникальные пути в выделенном объекте как $paths, и для каждого объекта мы проходим по ним, а не по путям этого объекта. Мы также используем альтернативный оператор (//), чтобы установить отсутствующие значения в пустые строки.

Надеюсь это поможет!

person Community    schedule 22.10.2015
comment
Вау, чувак, ты меня просто поразил! :) Я наткнулся на эту функцию путей и подумал, что она может помочь, но не мог понять, как соединить части. Приятно узнать об этом операторе //, полезный материал! Благодарю вас! - person Elias Dorneles; 23.10.2015
comment
Эти пути... так трудно привыкнуть к ним. Хороший. Я должен включить их в свой репертуар. - person Jeff Mercado; 23.10.2015

Вот как вы можете создать это:

{ "top-level-key": .toplevel_key } + ([
    range(.list|length) as $i
        | .list[$i]
        | to_entries[]
        | .key = "list_\($i)_\(.key)"
    ] | from_entries)

Это будет отображаться для каждой соответствующей записи списка.

{
  "top-level-key": "top value 1",
  "list_0_key1": "value 1",
  "list_0_key2": "value 2",
  "list_1_key1": "value 3",
  "list_1_key2": "value 4"
}
{
  "top-level-key": "top value 2",
  "list_0_key1": "value 5",
  "list_0_key2": "value 6"
}

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

person Jeff Mercado    schedule 22.10.2015
comment
Спасибо, это потрясающе! :) - person Elias Dorneles; 22.10.2015