
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- read
Schleife. 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 awk
Ansatz nach, habe aber nicht genug Erfahrung damit.
Antwort1
Das folgende awk
Programm 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 BEGIN
Block 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 name
Feld 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 name
gerade 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 current
Array, das für den aktuellen Datensatz verwendet wird.
Die Funktion printrec
verwendet 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
- Wenn der "value"-Teil der Datei auch
:
-space-Kombinationen enthalten kann, können Sie das Programm härten, indem Sie ersetzen
voncurrent[i]=$2
Dadurch wird der Wert auf „alles nach der erstensub(/^[^:]*: */,"") current[i]=$0
:
Leerzeichenkombination“ in der Zeile gesetzt, indemsub
alles bis einschließlich der ersten:
Leerzeichenkombination in der Zeile entfernt () wird. - 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. - 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 BEGIN
Abschnitts hinzu:
for ( tagNr=1; tagNr<=numTags; tagNr++ ) {
tag = tags[tagNr]
printf "%s%s", tag, (tagNr<numTags ? OFS : ORS)
}