
Quero escrever um script de analisador de dados. Os dados de exemplo são:
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
Os pares de valores-chave estão sempre na mesma ordem ( name
, description
, email
, lastLogon
, status
), mas alguns campos podem estar faltando. Também não é garantido que o primeiro registro esteja completo.
A saída esperada são valores separados por delimitadores (por exemplo, CSV):
John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
...
Foo Bar,XX,[email protected],n/a,inactive
Minha solução é usar um read
loop while. A parte principal do meu script:
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
Isso funciona. Mas obviamente, muito lento. O tempo de execução com 703 linhas de dados:
real 0m37.195s
user 0m2.844s
sys 0m22.984s
Estou pensando na awk
abordagem, mas não tenho experiência suficiente em usá-la.
Responder1
O awk
programa a seguir deve funcionar. Idealmente, você o salvaria em um arquivo separado (por exemplo 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)
}
Você pode então chamar isso de
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
Isso executará alguma inicialização no BEGIN
bloco:
- defina o separador do campo de entrada como "a
:
seguido de zero ou mais espaços" - defina o separador do campo de saída como
,
- inicializamos uma matriz de nomes de campos (adotamos uma abordagem estática e codificamos a lista)
Se o name
campo for encontrado, ele verificará se está na primeira linha do arquivo ese não, imprima os dados coletados anteriormente. Ele então começará a coletar o próximo registro no array current
, começando com o name
campo que acabou de ser encontrado.
Para todas as outras linhas (presumo, para simplificar, que não haja linhas vazias ou de comentários - mas, novamente, este programa deve simplesmente ignorá-las silenciosamente), o programa verifica quais dos campos são mencionados na linha e armazena o valor no posição apropriada na current
matriz usada para o registro atual.
A função printrec
usa esse array como parâmetro e executa a saída real. Os valores ausentes são substituídos por n/a
(ou qualquer outra string que você queira usar). Após a impressão, os campos são limpos para que o array esteja pronto para o próximo conjunto de dados.
Ao final, o último registro também é impresso.
Observação
- Se a parte "valor" do arquivo também puder incluir
:
combinações -space, você poderá proteger o programa substituindo
porcurrent[i]=$2
que definirá o valor como "tudo após a primeirasub(/^[^:]*: */,"") current[i]=$0
:
combinação -space" na linha, removendo (sub
) tudo até incluir a primeira:
combinação -space na linha. - Se algum dos campos puder conter o caractere separador de saída (no seu exemplo
,
), você terá que tomar as medidas apropriadas para escapar desse caractere ou citar a saída, dependendo do padrão que deseja aderir. - Como você observou corretamente, os shell loops são muito desencorajados como ferramentas para processamento de texto. Se você estiver interessado em ler mais, você pode querer dar uma olhada emestas perguntas e respostas.
Responder2
$ 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
Se você quiser que uma linha de cabeçalho também seja impressa, basta adicionar isto ao final da BEGIN
seção:
for ( tagNr=1; tagNr<=numTags; tagNr++ ) {
tag = tags[tagNr]
printf "%s%s", tag, (tagNr<numTags ? OFS : ORS)
}