Squash-Datei mit Schlüssel-Wert-Datensätzen nach CSV

Squash-Datei mit Schlüssel-Wert-Datensätzen nach CSV

Ich möchte ein Datenparser-Skript schreiben. Die Beispieldaten sind:

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

Die Schlüssel-Wert-Paare liegen zwar immer in der gleichen Reihenfolge vor ( name, description, email, lastLogon, status), es können jedoch einige Felder fehlen. Auch ist nicht gewährleistet, dass der erste Datensatz vollständig ist.

Die erwartete Ausgabe sind durch Trennzeichen getrennte Werte (z. B. CSV):

John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
...
Foo Bar,XX,[email protected],n/a,inactive

Meine Lösung besteht in der Verwendung einer while- readSchleife. Der Hauptteil meines Skripts:

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

Das funktioniert. Aber natürlich sehr langsam. Die Ausführungszeit mit 703 Datenzeilen:

real    0m37.195s
user    0m2.844s
sys     0m22.984s

Ich denke über diesen awkAnsatz nach, habe aber nicht genug Erfahrung damit.

Antwort1

Das folgende awkProgramm sollte funktionieren. Idealerweise speichern Sie es in einer separaten Datei (z. B. 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)
}

Sie können dies dann als aufrufen

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

Dadurch werden einige Initialisierungsvorgänge im BEGINBlock durchgeführt:

  • Setzen Sie das Eingabefeldtrennzeichen auf „a :gefolgt von null oder mehr Leerzeichen“.
  • Setzen Sie den Ausgabefeldtrenner auf,
  • Initialisieren Sie ein Array von Feldnamen (wir wählen einen statischen Ansatz und codieren die Liste fest).

Wenn das nameFeld gefunden wird, wird geprüft, ob es sich in der ersten Zeile der Datei befindet, undwenn nicht, druckt die zuvor erfassten Daten aus. Anschließend wird mit dem Erfassen des nächsten Datensatzes im Array begonnen current, beginnend mit dem namegerade gefundenen Feld.

Für alle anderen Zeilen (der Einfachheit halber gehe ich davon aus, dass es keine leeren Zeilen oder Kommentarzeilen gibt – aber andererseits sollte dieses Programm diese einfach stillschweigend ignorieren) prüft das Programm, welches der Felder in der Zeile erwähnt wird, und speichert den Wert an der entsprechenden Position im currentArray, das für den aktuellen Datensatz verwendet wird.

Die Funktion printrecverwendet ein solches Array als Parameter und führt die eigentliche Ausgabe aus. Fehlende Werte werden durch n/a(oder einen beliebigen anderen String Ihrer Wahl) ersetzt. Nach dem Drucken werden die Felder gelöscht, sodass das Array für die nächste Datenmenge bereit ist.

Zum Schluss wird auch der letzte Datensatz ausgedruckt.

Notiz

  1. Wenn der "value"-Teil der Datei auch :-space-Kombinationen enthalten kann, können Sie das Programm härten, indem Sie ersetzen
    current[i]=$2
    
    von
    sub(/^[^:]*: */,"")
    current[i]=$0
    
    Dadurch wird der Wert auf „alles nach der ersten :Leerzeichenkombination“ in der Zeile gesetzt, indem suballes bis einschließlich der ersten :Leerzeichenkombination in der Zeile entfernt () wird.
  2. Wenn eines der Felder das Ausgabetrennzeichen enthalten kann (in Ihrem Beispiel ,), müssen Sie je nach dem Standard, den Sie einhalten möchten, geeignete Maßnahmen ergreifen, um dieses Zeichen entweder zu maskieren oder die Ausgabe in Anführungszeichen zu setzen.
  3. Wie Sie richtig bemerkt haben, sind Shell-Loops als Werkzeuge für die Textverarbeitung sehr zu empfehlen. Wenn Sie mehr darüber lesen möchten, können Sie einen Blick aufdiese Frage und Antwort.

Antwort2

$ 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

Wenn Sie auch eine Kopfzeile drucken möchten, fügen Sie einfach Folgendes am Ende des BEGINAbschnitts hinzu:

for ( tagNr=1; tagNr<=numTags; tagNr++ ) {
    tag = tags[tagNr]
    printf "%s%s", tag, (tagNr<numTags ? OFS : ORS)
}

verwandte Informationen