
Я хочу написать скрипт парсера данных. Пример данных:
name: John Doe
description: AM
email: [email protected]
lastLogon: 999999999999999
status: active
name: Jane Doe
description: HR
email: [email protected]
lastLogon: 8888888888
status: active
...
name: Foo Bar
description: XX
email: [email protected]
status: inactive
Пары ключ-значение всегда находятся в одном и том же порядке ( name
, description
, email
, lastLogon
, status
), но некоторые поля могут отсутствовать. Также не гарантируется, что первая запись будет полной.
Ожидаемый вывод — значения, разделенные разделителями (например, CSV):
John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
...
Foo Bar,XX,[email protected],n/a,inactive
Мое решение — использовать read
цикл while. Основная часть моего скрипта:
while read line; do
grep -q '^name:' <<< "$line" && status=''
case "${line,,}" in
name*) # capture value ;;
desc*) # capture value ;;
email*) # capture value ;;
last*) # capture value ;;
status*) # capture value ;;
esac
if test -n "$status"; then
printf '%s,%s,%s,%s,%s\n' "${name:-n\a}" ... etc ...
unset name ... etc ...
fi
done < input.txt
Это работает. Но, очевидно, очень медленно. Время выполнения с 703 строками данных:
real 0m37.195s
user 0m2.844s
sys 0m22.984s
Я думаю об этом awk
подходе, но у меня недостаточно опыта его использования.
решение1
Следующая awk
программа должна работать. В идеале вы бы сохранили ее в отдельный файл (например squash_to_csv.awk
, ):
#!/bin/awk -f
BEGIN {
FS=": *"
OFS=","
recfields=split("name,description,email,lastLogon,status",fields,",")
}
function printrec(record) {
for (i=1; i<=recfields; i++) {
if (record[i]=="") record[i]="n/a"
printf "%s%s",record[i],i==recfields?ORS:OFS;
record[i]="";
}
}
$1=="name" && (FNR>1) { printrec(current) }
{
for (i=1; i<=recfields;i++) {
if (fields[i]==$1) {
current[i]=$2
break
}
}
}
END {
printrec(current)
}
Затем вы можете вызвать это как
awk -f squash_to_csv.awk input.dat
John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
Foo Bar,XX,[email protected],n/a,inactive
Это выполнит некоторую инициализацию в BEGIN
блоке:
- установите разделитель полей ввода на «a,
:
за которым следует ноль или более пробелов» - установите разделитель выходных полей на
,
- инициализируем массив имен полей (мы используем статический подход и жестко кодируем список)
Если name
поле обнаружено, он проверит, находится ли оно в первой строке файла, иесли не, распечатает ранее собранные данные. Затем начнется сбор следующей записи в массиве current
, начиная с name
только что обнаруженного поля.
Для всех остальных строк (я предполагаю для простоты, что пустых или комментируемых строк нет, но, с другой стороны, эта программа должна просто молча игнорировать их) программа проверяет, какое из полей упомянуто в строке, и сохраняет значение в соответствующей позиции в массиве, current
используемом для текущей записи.
Функция printrec
принимает такой массив в качестве параметра и выполняет фактический вывод. Отсутствующие значения заменяются n/a
(или любой другой строкой, которую вы можете использовать). После печати поля очищаются, чтобы массив был готов к следующему набору данных.
В конце также печатается последняя запись.
Примечание
- Если часть «значение» файла может также включать
:
комбинации пробелов, вы можете укрепить программу, заменив
кcurrent[i]=$2
что установит значение «все после первойsub(/^[^:]*: */,"") current[i]=$0
:
комбинации пробелов» в строке, удалив (sub
) все до первой:
комбинации пробелов в строке. - Если какое-либо из полей может содержать символ-разделитель вывода (в вашем примере
,
), вам придется принять соответствующие меры, чтобы либо экранировать этот символ, либо заключить вывод в кавычки, в зависимости от стандарта, которого вы хотите придерживаться. - Как вы правильно заметили, циклы оболочки очень не рекомендуются в качестве инструментов для обработки текста. Если вам интересно узнать больше, вы можете посмотретьэтот вопрос и ответ.
решение2
$ cat tst.awk
BEGIN {
OFS = ","
numTags = split("name description email lastLogon status",tags)
}
{
tag = val = $0
sub(/ *:.*/,"",tag)
sub(/[^:]+: */,"",val)
}
(tag == "name") && (NR>1) { prt() }
{ tag2val[tag] = val }
END { prt() }
function prt( tagNr,tag,val) {
for ( tagNr=1; tagNr<=numTags; tagNr++ ) {
tag = tags[tagNr]
val = ( tag in tag2val ? tag2val[tag] : "n/a" )
printf "%s%s", val, (tagNr<numTags ? OFS : ORS)
}
delete tag2val
}
$ awk -f tst.awk file
John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
Foo Bar,XX,[email protected],n/a,inactive
Если вы хотите, чтобы была напечатана также строка заголовка, просто добавьте это в конец раздела BEGIN
:
for ( tagNr=1; tagNr<=numTags; tagNr++ ) {
tag = tags[tagNr]
printf "%s%s", tag, (tagNr<numTags ? OFS : ORS)
}