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