Преобразование CSV в JSON в bash
Попытка конвертировать CSV файл в JSON
Вот две строки:
-21.3214077;55.4851413;Ruizia cordata
-21.3213078;55.4849803;Cossinia pinnata
Я хотел бы получить что-то вроде:
"occurrences": [
{
"position": [-21.3214077, 55.4851413],
"taxo": {
"espece": "Ruizia cordata"
},
...
}]
Вот мой script:
echo '"occurences": [ '
cat se.csv | while read -r line
do
IFS=';' read -r -a array <<< $line;
echo -n -e '{ "position": [' ${array[0]}
echo -n -e ',' ${array[1]} ']'
echo -e ', "taxo": {"espece":"' ${array[2]} '"'
done
echo "]";
Я получаю действительно странные результаты:
"occurences": [
""position": [ -21.3214077, 55.4851413 ], "taxo": {"espece":" Ruizia cordata
""position": [ -21.3213078, 55.4849803 ], "taxo": {"espece":" Cossinia pinnata
Что не так с моим кодом?
Ответы
Ответ 1
Правильный инструмент для этой работы - jq
.
jq -Rsn '
{"occurrences":
[inputs
| . / "\n"
| (.[] | select(length > 0) | . / ";") as $input
| {"position": [$input[0], $input[1]], "taxo": {"espece": $input[2]}}]}
' <se.csv
излучает, учитывая ваш вклад:
{
"occurences": [
{
"position": [
"-21.3214077",
"55.4851413"
],
"taxo": {
"espece": "Ruizia cordata"
}
},
{
"position": [
"-21.3213078",
"55.4849803"
],
"taxo": {
"espece": "Cossinia pinnata"
}
}
]
}
Кстати, менее оригинальная версия вашего скрипта может выглядеть так:
#!/usr/bin/env bash
items=( )
while IFS=';' read -r lat long pos _; do
printf -v item '{ "position": [%s, %s], "taxo": {"espece": "%s"}}' "$lat" "$long" "$pos"
items+=( "$item" )
done <se.csv
IFS=','
printf '{"occurrences": [%s]}\n' "${items[*]}"
Примечание:
- Абсолютно бессмысленно использовать
cat
для передачи в цикл (и веские причины не делать); таким образом, мы используем перенаправление (<
), чтобы открыть файл непосредственно как цикл stdin.
read
может быть передан список переменных назначения; таким образом, нет необходимости читать в массив (или сначала читать в строку, а затем генерировать гистерезис и читать из него в массив). _
в конце гарантирует, что лишние столбцы отбрасываются (помещая их в фиктивную переменную с именем _
), а не добавляются в pos
.
"${array[*]}"
генерирует строку путем объединения элементов array
с символом в IFS
; таким образом, мы можем использовать это, чтобы гарантировать, что запятые присутствуют в выводе только тогда, когда они необходимы.
printf
используется вместо echo
, как указано в разделе "ИСПОЛЬЗОВАНИЕ ПРИЛОЖЕНИЯ" спецификации самого echo
.
- Это по-прежнему ошибочно, поскольку генерирует JSON через конкатенацию строк. Не используйте это.
Ответ 2
Вот статья на эту тему: https://infiniteundo.com/post/99336704013/convert-csv-to-json-with-jq
Он также использует JQ, но немного другой подход с использованием split()
и map()
.
jq --slurp --raw-input \
'split("\n") | .[1:] | map(split(";")) |
map({
"position": [.[0], .[1]],
"taxo": {
"espece": .[2]
}
})' \
input.csv > output.json
Однако он не обрабатывает выход разделителя.
Ответ 3
Принятый ответ использует jq
для анализа ввода. Это работает, но jq
не обрабатывает выходы, т.е. ввод из CSV, созданного из Excel или аналогичных инструментов, цитируется следующим образом:
foo,"bar,baz",gaz
приведет к неправильному выводу, поскольку jq будет видеть 4 поля, а не 3.
Одним из вариантов является использование значений, разделенных табуляцией, вместо запятой (если ваши входные данные не содержат вкладок!), А также принятый ответ.
Другой вариант - объединить ваши инструменты и использовать лучший инструмент для каждой части: анализатор CSV для чтения ввода и преобразования его в JSON, и jq
для преобразования JSON в целевой формат.
Основанный на Python csvkit будет интеллектуально анализировать CSV и поставляется с инструментом csvjson
, который будет намного лучше превращать CSV в JSON. Затем он может быть передан через jq для преобразования плоского вывода JSON с помощью csvkit в целевую форму.
С данными, предоставленными OP, для желаемого результата это так же просто, как:
csvjson --no-header-row |
jq '.[] | {occurrences: [{ position: [.a, .b], taxo: {espece: .c}}]}'
Обратите внимание, что csvjson автоматически определяет ;
в качестве разделителя и без строки заголовка на входе назначает клавиши json как a
, b
и c
.
То же самое относится и к записи в файлы CSV - csvkit
может читать массив JSON или JSON с разделителями новой строки и интеллектуально выводить CSV через in2csv
.
Ответ 4
Если вы хотите сойти с ума, вы можете написать парсер, используя jq. Вот моя реализация, которую можно рассматривать как инверсию фильтра @csv
. Добавьте это в свой файл .jq.
def do_if(pred; update):
if pred then update else . end;
def _parse_delimited($_delim; $_quot; $_nl; $_skip):
[($_delim, $_quot, $_nl, $_skip)|explode[]] as [$delim, $quot, $nl, $skip] |
[0,1,2,3,4,5] as [$s_start,$s_next_value,$s_read_value,$s_read_quoted,$s_escape,$s_final] |
def _append($arr; $value):
$arr + [$value];
def _do_start($c):
if $c == $nl then
[$s_start, null, null, _append(.[3]; [""])]
elif $c == $delim then
[$s_next_value, null, [""], .[3]]
elif $c == $quot then
[$s_read_quoted, [], [], .[3]]
else
[$s_read_value, [$c], [], .[3]]
end;
def _do_next_value($c):
if $c == $nl then
[$s_start, null, null, _append(.[3]; _append(.[2]; ""))]
elif $c == $delim then
[$s_next_value, null, _append(.[2]; ""), .[3]]
elif $c == $quot then
[$s_read_quoted, [], .[2], .[3]]
else
[$s_read_value, [$c], .[2], .[3]]
end;
def _do_read_value($c):
if $c == $nl then
[$s_start, null, null, _append(.[3]; _append(.[2]; .[1]|implode))]
elif $c == $delim then
[$s_next_value, null, _append(.[2]; .[1]|implode), .[3]]
else
[$s_read_value, _append(.[1]; $c), .[2], .[3]]
end;
def _do_read_quoted($c):
if $c == $quot then
[$s_escape, .[1], .[2], .[3]]
else
[$s_read_quoted, _append(.[1]; $c), .[2], .[3]]
end;
def _do_escape($c):
if $c == $nl then
[$s_start, null, null, _append(.[3]; _append(.[2]; .[1]|implode))]
elif $c == $delim then
[$s_next_value, null, _append(.[2]; .[1]|implode), .[3]]
else
[$s_read_quoted, _append(.[1]; $c), .[2], .[3]]
end;
def _do_final($c):
.;
def _do_finalize:
if .[0] == $s_start then
[$s_final, null, null, .[3]]
elif .[0] == $s_next_value then
[$s_final, null, null, _append(.[3]; [""])]
elif .[0] == $s_read_value then
[$s_final, null, null, _append(.[3]; _append(.[2]; .[1]|implode))]
elif .[0] == $s_read_quoted then
[$s_final, null, null, _append(.[3]; _append(.[2]; .[1]|implode))]
elif .[0] == $s_escape then
[$s_final, null, null, _append(.[3]; _append(.[2]; .[1]|implode))]
else # .[0] == $s_final
.
end;
reduce explode[] as $c (
[$s_start,null,null,[]];
do_if($c != $skip;
if .[0] == $s_start then
_do_start($c)
elif .[0] == $s_next_value then
_do_next_value($c)
elif .[0] == $s_read_value then
_do_read_value($c)
elif .[0] == $s_read_quoted then
_do_read_quoted($c)
elif .[0] == $s_escape then
_do_escape($c)
else # .[0] == $s_final
_do_final($c)
end
)
)
| _do_finalize[3][];
def parse_delimited($delim; $quot; $nl; $skip):
_parse_delimited($delim; $quot; $nl; $skip);
def parse_delimited($delim; $quot; $nl):
parse_delimited($delim; $quot; $nl; "\r");
def parse_delimited($delim; $quot):
parse_delimited($delim; $quot; "\n");
def parse_delimited($delim):
parse_delimited($delim; "\"");
def parse_csv:
parse_delimited(",");
Для ваших данных вы можете изменить разделитель на точки с запятой.
$ cat se.csv
-21.3214077;55.4851413;Ruizia cordata
-21.3213078;55.4849803;Cossinia pinnata
$ jq -R 'parse_delimited(";")' se.csv
[
"-21.3214077",
"55.4851413",
"Ruizia cordata"
]
[
"-21.3213078",
"55.4849803",
"Cossinia pinnata"
]
Это будет нормально работать для большинства входных данных для анализа строки за раз, но если ваши данные имеют буквальные переводы строк, вам нужно будет прочитать весь файл как строку.
$ cat input.csv
Year,Make,Model,Description,Price
1997,Ford,E350,"ac, abs, moon",3000.00
1999,Chevy,"Venture ""Extended Edition""","",4900.00
1999,Chevy,"Venture ""Extended Edition, Very Large""",,5000.00
1996,Jeep,Grand Cherokee,"MUST SELL!
air, moon roof, loaded",4799.00
$ jq -Rs 'parse_csv' input.csv
[
"Year",
"Make",
"Model",
"Description",
"Price"
]
[
"1997",
"Ford",
"E350",
"ac, abs, moon",
"3000.00"
]
[
"1999",
"Chevy",
"Venture \"Extended Edition\"",
"",
"4900.00"
]
[
"1999",
"Chevy",
"Venture \"Extended Edition, Very Large\"",
"",
"5000.00"
]
[
"1996",
"Jeep",
"Grand Cherokee",
"MUST SELL!\nair, moon roof, loaded",
"4799.00"
]
Ответ 5
Поскольку решение jq
не обрабатывает экранирование CSV, имена столбцов в первой строке, закомментированные строки и другие распространенные CSV-функции, я расширил инструмент CSV Cruncher, чтобы разрешать чтение CSV и запись это как JSON. Это не совсем "Bash", но и не jq
:)
В первую очередь это приложение для обработки CSV-as-SQL, так что это не совсем тривиально, но вот хитрость:
./crunch -in myfile.csv -out output.csv --json -sql 'SELECT * FROM myfile'
Он также позволяет выводить в виде объекта JSON на строку или правильный массив JSON. Смотрите документацию.
Это в бета-качестве, поэтому все отзывы и запросы на получение приветствуются.
Ответ 6
В общем, если ваш jq имеет встроенный фильтр inputs
(доступен с jq 1.5), то лучше использовать его, а не параметр командной строки -s.
Здесь в любом случае есть решение с использованием inputs
. Это решение также без переменных.
{"occurrences":
[inputs
| select(length > 0)
| . / ";"
| {"position": [.[0], .[1]],
"taxo": {"espece": .[2]}} ]}
SSV, CSV и все такое
Выше, конечно, предполагается, что файл имеет разделенные точкой с запятой -s поля в каждой строке, и что нет никаких осложнений, связанных с файлами CSV.
Если у ввода есть поля, которые строго ограничены одним символом, то у jq не должно возникнуть проблем с обработкой. В противном случае, может быть лучше использовать инструмент, который может надежно конвертировать в формат TSV (табулированное значение -s), который jq может обрабатывать напрямую.
Ответ 7
Для полноты картины Xidel вместе с некоторой магией XQuery может сделать это тоже:
xidel -s input.csv --xquery '
{
"occurrences":for $x in tokenize($raw,"\n") let $a:=tokenize($x,";") return {
"position":[
$a[1],
$a[2]
],
"taxo":{
"espece":$a[3]
}
}
}
'
{
"occurrences": [
{
"position": ["-21.3214077", "55.4851413"],
"taxo": {
"espece": "Ruizia cordata"
}
},
{
"position": ["-21.3213078", "55.4849803"],
"taxo": {
"espece": "Cossinia pinnata"
}
}
]
}