
Quiero escribir un script de análisis de datos. Los datos de ejemplo son:
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
Los pares clave-valor siempre están en el mismo orden ( name
, description
, email
, lastLogon
, status
), pero es posible que falten algunos campos. Tampoco se garantiza que el primer registro esté completo.
El resultado esperado son valores separados por delimitadores (por ejemplo, CSV):
John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
...
Foo Bar,XX,[email protected],n/a,inactive
Mi solución es utilizar un read
bucle while. La parte principal de mi guión:
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
Esto funciona. Pero obviamente, muy lento. El tiempo de ejecución con 703 líneas de datos:
real 0m37.195s
user 0m2.844s
sys 0m22.984s
Estoy pensando en el awk
enfoque, pero no tengo suficiente experiencia en su uso.
Respuesta1
El siguiente awk
programa debería funcionar. Lo ideal sería guardarlo en un archivo separado (por ejemplo 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)
}
Luego puedes llamar a esto como
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
Esto realizará alguna inicialización en el BEGIN
bloque:
- establezca el separador del campo de entrada en "a
:
seguido de cero o más espacios" - establezca el separador de campo de salida en
,
- inicializar una serie de nombres de campos (adoptamos un enfoque estático y codificamos la lista)
Si se encuentra el name
campo, comprobará si está en la primera línea del archivo ysi no, imprima los datos recopilados anteriormente. Luego comenzará a recopilar el siguiente registro de la matriz current
, comenzando con el name
campo que acaba de encontrar.
Para todas las demás líneas (para simplificar, asumo que no hay líneas vacías ni de comentarios, pero, de nuevo, este programa debería simplemente ignorarlas silenciosamente), el programa verifica cuál de los campos se menciona en la línea y almacena el valor en la posición apropiada en la current
matriz utilizada para el registro actual.
La función printrec
toma dicha matriz como parámetro y realiza la salida real. Los valores faltantes se sustituyen por n/a
(o cualquier otra cadena que desee utilizar). Después de imprimir, los campos se borran para que la matriz esté lista para el siguiente conjunto de datos.
Al final, también se imprime el último registro.
Nota
- Si la parte "valor" del archivo también puede incluir
:
combinaciones de espacio, puede reforzar el programa reemplazando
porcurrent[i]=$2
que establecerá el valor en "todo después de la primerasub(/^[^:]*: */,"") current[i]=$0
:
combinación de espacios" en la línea, eliminando (sub
) todo hasta incluir la primera:
combinación de espacios en la línea. - Si alguno de los campos puede contener el carácter separador de salida (en su ejemplo
,
), tendrá que tomar las medidas adecuadas para escapar de ese carácter o citar la salida, según el estándar que desee cumplir. - Como señaló correctamente, se desaconsejan los bucles de shell como herramientas para el procesamiento de texto. Si está interesado en leer más, puede consultarestas preguntas y respuestas.
Respuesta2
$ 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
Si también desea imprimir una línea de encabezado, simplemente agregue esto al final de la BEGIN
sección:
for ( tagNr=1; tagNr<=numTags; tagNr++ ) {
tag = tags[tagNr]
printf "%s%s", tag, (tagNr<numTags ? OFS : ORS)
}