Сжатие файла с записями «ключ-значение» в CSV

Сжатие файла с записями «ключ-значение» в CSV

Я хочу написать скрипт парсера данных. Пример данных:

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(или любой другой строкой, которую вы можете использовать). После печати поля очищаются, чтобы массив был готов к следующему набору данных.

В конце также печатается последняя запись.

Примечание

  1. Если часть «значение» файла может также включать :комбинации пробелов, вы можете укрепить программу, заменив
    current[i]=$2
    
    к
    sub(/^[^:]*: */,"")
    current[i]=$0
    
    что установит значение «все после первой :комбинации пробелов» в строке, удалив ( sub) все до первой :комбинации пробелов в строке.
  2. Если какое-либо из полей может содержать символ-разделитель вывода (в вашем примере ,), вам придется принять соответствующие меры, чтобы либо экранировать этот символ, либо заключить вывод в кавычки, в зависимости от стандарта, которого вы хотите придерживаться.
  3. Как вы правильно заметили, циклы оболочки очень не рекомендуются в качестве инструментов для обработки текста. Если вам интересно узнать больше, вы можете посмотретьэтот вопрос и ответ.

решение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)
}

Связанный контент