CSV для JSON с использованием jq

Если у вас есть набор данных CSV, как это:

name, age, gender
john, 20, male
jane, 30, female
bob, 25, male

Можете ли вы добраться до этого:

[ {"name": "john", "age": 20, "gender: "male"},
  {"name": "jane", "age": 30, "gender: "female"},
  {"name": "bob", "age": 25, "gender: "male"} ]

используя только JQ?

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

Можно ли динамически извлекать заголовки, а затем объединять их со значениями с помощью однострочника jq?

Ответ 1

Короче говоря - да, за исключением, возможно, однострочного бита.

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

Поскольку jq 1.5rc1 включает в себя поддержку регулярных выражений и доступен с 1 января 2015 года, следующая программа предполагает версию jq 1.5; если вы хотите, чтобы он работал с jq 1.4, посмотрите два комментария "Для jq 1.4".

Также обратите внимание, что эта программа не поддерживает CSV во всей своей общности и сложности. (Для подобного подхода, который действительно обрабатывает CSV, см. https://github.com/stedolan/jq/wiki/Cookbook#convert-a-csv-file-with-headers-to-json)

# objectify/1 takes an array of string values as inputs, converts
# numeric values to numbers, and packages the results into an object
# with keys specified by the "headers" array
def objectify(headers):
  # For jq 1.4, replace the following line by: def tonumberq: .;
  def tonumberq: tonumber? // .;
  . as $in
  | reduce range(0; headers|length) as $i ({}; .[headers[$i]] = ($in[$i] | tonumberq) );

def csv2table:
  # For jq 1.4, replace the following line by:  def trim: .;
  def trim: sub("^ +";"") |  sub(" +$";"");
  split("\n") | map( split(",") | map(trim) );

def csv2json:
  csv2table
  | .[0] as $headers
  | reduce (.[1:][] | select(length > 0) ) as $row
      ( []; . + [ $row|objectify($headers) ]);

csv2json

Пример (при условии, что csv.csv является заданным текстовым файлом CSV):

$ jq -R -s -f csv2json.jq csv.csv
[
  {
    "name": "john",
    "age": 20,
    "gender": "male"
  },
  {
    "name": "jane",
    "age": 30,
    "gender": "female"
  },
  {
    "name": "bob",
    "age": 25,
    "gender": "male"
  }
]

Ответ 2

У меня была небольшая игра, и я придумал это. Но это может быть не самый лучший способ, и мне было бы интересно посмотреть, каковы были ваши попытки, потому что в конце концов, если мы оба придем к решению, я уверен, что это будет вдвое лучше!

Но я бы начал с чего-то вроде:

true as $doHeaders
| . / "\n"
| map(. / ", ")
| (if $doHeaders then .[0] else [range(0; (.[0] | length)) | tostring] end) as $headers
| .[if $doHeaders then 1 else 0 end:][]
| . as $values
| keys
| map({($headers[.]): $values[.]})

Рабочий пример

Переменная $doHeaders определяет, следует ли читать верхнюю строку в виде строки заголовка. В вашем случае вы хотите, чтобы это было правдой, но я добавил его для будущих пользователей SO, и потому, что сегодня у меня был отличный завтрак, и погода прекрасна, так почему бы и нет?

Маленькое объяснение:

1) . / "\n" Разделить по строке...

2) map(. / ", ")... и запятая ( Большая информация:. В вашей версии вы захотите использовать разделение на основе регулярного выражения, так как вы разделите запятую внутри кавычек тоже. Я просто использовал это, потому что он краткий, и это заставляет мое решение выглядеть круто правильно?)

3) if $doHeaders then... Здесь мы создаем массив строк или номеров строк в зависимости от количества элементов в первой строке и является ли первая строка строкой заголовка

4) .[if $doHeaders then 1 else 0 end:] Итак, обрезаем верхнюю строку, если заголовок

5) map({($headers[.]): $values[.]}) Выше мы переходим по каждой строке в прежнем csv и помещаем $values в переменную и ключи в трубу. Затем мы создадим желаемый объект.

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

Ответ 3

Начиная с 2018 года, современным решением без кода было бы использование инструмента Python csvkit с csvjson data.csv > data.json.

Смотрите их документацию https://csvkit.readthedocs.io/en/1.0.2/

Этот инструментарий также очень удобен и дополняет jq, если ваш скрипт должен отлаживать оба формата csv и json.

Возможно, вы также захотите проверить мощный инструмент под названием visidata. Вот пример скриншота, похожий на оригинальный постер. Вы также можете сгенерировать скрипт из visidata

Ответ 4

с Миллером (http://johnkerl.org/miller/doc/) очень просто. Используя этот файл input.csv

name,age,gender
john,20,male
jane,30,female
bob,25,male

и работает

mlr --c2j --jlistwrap cat input.csv

У вас будет

[
{ "name": "john", "age": 20, "gender": "male" }
,{ "name": "jane", "age": 30, "gender": "female" }
,{ "name": "bob", "age": 25, "gender": "male" }
]

Ответ 5

Вот решение, предполагающее, что вы запускаете jq с параметрами -s и -R.

[
  [                                               
    split("\n")[]                  # transform csv input into array
  | split(", ")                    # where first element has key names
  | select(length==3)              # and other elements have values
  ]                                
  | {h:.[0], v:.[1:][]}            # {h:[keys], v:[values]}
  | [.h, (.v|map(tonumber?//.))]   # [ [keys], [values] ]
  | [ transpose[]                  # [ [key,value], [key,value], ... ]
      | {key:.[0], value:.[1]}     # [ {"key":key, "value":value}, ... ]
    ]
  | from_entries                   # { key:value, key:value, ... }
]

Пример прогона:

jq -s -R -f filter.jq data.csv

Пример вывода

[
  {
    "name": "john",
    "age": 20,
    "gender": "male"
  },
  {
    "name": "jane",
    "age": 30,
    "gender": "female"
  },
  {
    "name": "bob",
    "age": 25,
    "gender": "male"
  }
]