Aula 2 – Separação de campos

Aula 1 – O que é o AWK | Índice | Aula3 – Prática: lendo um feed RSS


Seu apoio é muito importante para a criação e a manutenção dos cursos gratuitos do canal debxp:


Nesta aula…

  • A separação padrão e a variável FS
  • Separando campos com expressões regulares
  • Tornando cada caractere um campo
  • Campos de linha inteira
  • Lendo dados de largura fixa
  • Definindo campos pelo conteúdo
  • Registros com múltiplas linhas

Especificando a separação de campos

O separador de campos, representado pela variável pré-definida FS (Field Separator, separador de campos), controla como o AWK vai dividir o registro na entrada em campos.

Não confundir FS com IFS, que é a variável do separador de campos do shell!

Ao ler um registro, o AWK buscará por sequências de caracteres que casem com o separador, e será considerado um campo todo texto que estiver entre essas correspondências, ou entre uma correspondência e um dos limites do registro.

O valor de FS pode ser alterado com o operador de atribuição =.

Normalmente, a melhor hora de fazer essa alteração é antes do processamento dos arquivos, na regra iniciada pelo padrão especial BEGIN. Assim, o primeiro registro já será lido com o separador apropriado.

A separação padrão e a variável FS

Geralmente, os campos são separados por espaços em branco (espaços, tabulações e quebras de linha), não por espaços únicos — dois espaços juntos não delimitam um campo vazio!

O valor padrão de FS é uma string contendo um único espaço:

FS=" "

O AWK trata o espaço como um caso especial, ou seja, ele o interpreta como o separador que indica a forma padrão de tratar a delimitação de campos.

Se o valor de FS for qualquer outro caractere único, aí sim, duas ocorrências seguidas deste caractere, ou uma ocorrência junto a um dos limites do registro, irão delimitar um campo vazio.

É preciso tomar cuidado para que a escolha do caractere separador esteja de acordo com o layout dos dados e não cause problemas com ocorrências do mesmo caractere em pontos do texto que não representem separações de campo! Às vezes, a melhor opção é tratar dados complexos em vários programas em AWK antes de passá-los para um processamento final.

Exemplos

Considere o registro abaixo…

João da Silva,Av. das Flores, 134,Jd. das Flores,Osasco,SP

Separe este registro de forma que ele apresente os campos:

  1. Nome
  2. Endereço
  3. Bairro
  4. Cidade
  5. Estado

Exemplo 1 — Separação padrão:

awk '{ print $1"\n"$2"\n"$3"\n"$4"\n"$5 }'

Saída:

João
da
Silva,Av.
das
Flores,

Exemplo 2 — Separando pelas vírgulas:

awk 'BEGIN { FS="," }; { print $1"\n"$2"\n"$3"\n"$4"\n"$5 }'

Saída:

João da Silva
Av. das Flores
 134
Jd. das Flores
Osasco

Exemplo 3 — Uma possível solução:

awk 'BEGIN { FS="," }; { print $1"\n"$2","$3"\n"$4"\n"$5"\n"$6 }'

Saída:

João da Silva
Av. das Flores, 134
Jd. das Flores
Osasco
SP

Importante! De acordo com as normas POSIX, o AWK deve se comportar como se cada registro fosse separado em campos no momento em que é lido. Ou seja, se nós alterarmos o valor de FS depois da leitura do registro, a separação em campos deve refletir o antigo valor de FS e não o novo!

Nós devemos ficar atentos a isso quando estivermos lidando com implementações que podem não seguir este padrão e, consequentemente, irão produzir resultados bem diferentes dos esperados. De modo geral, esta seria uma saída incorreta:

:~$ awk '{ FS=":"; print $1 }' <<< "root:x:0:0:root:/root:/bin/bash"
root

Enquanto que o GAWK e outras implementações que seguem o padrão POSIX retornarão…

:~$ awk '{ FS=":"; print $1 }' <<< "root:x:0:0:root:/root:/bin/bash"
root:x:0:0:root:/root:/bin/bash

Separando campos com expressões regulares

No exemplo anterior, nós vimos a utilização de um único caractere como separador de campos. A rigor, porém, a string atribuída à variável FS (ou à opção -F da linha de comando) é tratada pelo AWK como uma expressão regular.

Por exemplo:

# Considere o registro como:
# "banal banana bandana bandido"

BEGIN {
    FS="ban"
}
{
    print "["$1"] ["$2"] ["$3"] ["$4"] ["$5"]"
}

# Isso imprimiria na saída:
# [] [al ] [ana ] [dana ] [dido]

Neste caso, a string "ban" expressa literalmente um padrão constituído da sequência de caracteres b a n. Mas isso também é possível…

BEGIN {
    FS="(nal* )|(do$)"
}
{
    print "["$1"] ["$2"] ["$3"] ["$4"] ["$5"]"
}

# O que imprimiria na saída:
# [ba] [bana] [banda] [bandi] []

Tornando cada caractere um campo

Existem situações onde queremos tratar cada caractere de uma string como um campo separado do registro. No GAWK, isso pode ser conseguido definindo o separador como uma string vazia ("").

Por exemplo:

# Considere o registro:
# "abcdefghij"

BEGIN {
    FS=""
}
{
    for (n = 1; n <= NF; n++)
        print "O campo",n,"é",$n
}

# O que resultaria na saída...

O campo 1 é a
O campo 2 é b
O campo 3 é c
O campo 4 é d
O campo 5 é e
O campo 6 é f
O campo 7 é g
O campo 8 é h
O campo 9 é i
O campo 10 é j

No modo de compatibilidade (opção -c), o GAWK irá se comportar como a maioria das outras implementações, e a string vazia fará com que todo o registro seja interpretado como um único campo, como no exemplo abaixo:

BEGIN { 
    FS="" 
}
{
    for (n = 1; n <= NF; n++)
        print "O campo",n,"é",$n
}

# Saída...
# O campo 1 é abcdefghij

Campos de linha inteira

Também é possível estarmos diante de uma situação onde o que interessa é considerar o registro inteiro como sendo um único campo, o que pode ser feito facilmente atribuindo uma quebra de linha (\n):

awk 'BEGIN { FS="\n" } { ... }' <arquivos>

Neste caso, $1 terá o mesmo valor de $0, o que nos deve levar a pensar se realmente precisaríamos (ou deveríamos) alterar o separador de campos nesta situação.

"Esse é o problema ou a dificuldade em fazer uma das soluções possíveis?"
— Prof. Paulo Kretcheu

Lendo dados de largura fixa

Não é raro precisarmos processar dados em que os campos são determinados por uma quantidade fixa de caracteres. Isso pode ser especialmente problemático quando ocorrem campos vazios (que corresponderiam a uma quantidade fixa de espaços) ou grandes demais (geralmente deixando apenas um espaço de intervalo para o campo seguinte) — a definição normal de separadores com base na variável FS não funcionaria nada bem nesses casos.

Considere o conteúdo do arquivo abaixo:

<-----7<------------------21<------------------21<----------13
CÓDIGO PRODUTO              FORNECEDOR           PREÇO UN (R$)
====== ==================== ==================== =============
 00012 RALADOR DE QUEIJO    RALA&ROLA LTDA                5.00
 01345 FACA CEGA            BLIND BLADE S/A              13.50
 13432 FATIADOR DE OVOS     EGGSPLIT MEI                 16.25
 00123 GARFO SEM DENTES                                   7.80
 98765 SERRA DE PÃO         PÃO DA SERRA LTDA            15.99

Como podemos ver, cada campo tem 7, 21, 21 e 13 caracteres respectivamente, e nós podemos passar esses valores para a variável FIELDWIDTHS:

# Os tamanhos das colunas (incluindo espaços entre elas) são
# informados como uma lista de valores separados por espaço.

BEGIN {
    FIELDWIDTHS="7 21 21 13"
}

NR > 2 {
    print "[" $1 "] [" $2 "] [" $3 "] [" $4 "]"
}

# A saída seria...

[ 00012 ] [RALADOR DE QUEIJO    ] [RALA&ROLA LTDA       ] [         5.00]
[ 01345 ] [FACA CEGA            ] [BLIND BLADE S/A      ] [        13.50]
[ 13432 ] [FATIADOR DE OVOS     ] [EGGSPLIT MEI         ] [        16.25]
[ 00123 ] [GARFO SEM DENTES     ] [                     ] [         7.80]
[ 98765 ] [SERRA DE PÃO         ] [PÃO DA SERRA LTDA    ] [        15.99]

A partir da versão 4.2 do GAWK, é possível informar quantos caracteres devem ser ignorados antes do processamento do tamanho do campo. Isso é feito incluindo essa informação antes da especificação do tamanho do campo, assim…

FIELDWIDTHS="6 1:20 1:20 13"
             ^ ^ ^  ^ ^  ^
             | | |  | |  |
             | | |  | |  +--- Tamanho do campo 4
             | | |  | +------ Tamanho do campo 3
             | | |  +-------- Pular um caractere
             | | +----------- Tamanho do campo 2
             | +------------- Pular um caractere
             +--------------- Tamanho do campo 1

Como resultado, cada campo do nosso arquivo será exibido com o seu tamanho exato, e não mais incluindo o espaço da separação das colunas:

[ 00012] [RALADOR DE QUEIJO   ] [RALA&ROLA LTDA      ] [          5.0]
[ 01345] [FACA CEGA           ] [BLIND BLADE S/A     ] [         13.5]
[ 13432] [FATIADOR DE OVOS    ] [EGGSPLIT MEI        ] [         16.2]
[ 00123] [GARFO SEM DENTES    ] [                    ] [          7.8]
[ 98765] [SERRA DE PÃO        ] [PÃO DA SERRA LTDA   ] [         15.9]

Definindo campos pelo conteúdo

Uma solução considerada "avançada" para situações muito comuns, como a do primeiro exemplo desta aula, é o uso de expressões regulares para a especificação de campos conforme o padrão em que os dados no registro são apresentados.

Relembrando o registro do primeiro exemplo…

João da Silva,Av. das Flores, 134,Jd. das Flores,Osasco,SP

Como estávamos tentando separar os campos definindo um caractere de separação, nós acabamos enfrentando algum problema com o campo correspondente ao endereço. Mas essa dificuldade pode ter sido apenas uma consequência daquilo que nós julgamos que fosse a solução, ou do fato de que aquela era a única abordagem que nós conhecíamos até então.

Conheça a variável ‘FPAT’!

A variável FPAT aceita uma expressão regular como valor que, em vez de casar com possíveis separadores, deve encontrar correspondências no conteúdo dos campos, ou seja, cada correspondência será um campo!

Voltando ao exemplo…

# Registro:
# João da Silva,Av. das Flores, 134,Jd. das Flores,Osasco,SP

BEGIN {
    FPAT="([^,]+)|([^,]+, +[^,]+)"
}
{
    for (n = 1; n <= NF; n++) print $n
}

# A REGEX casará com qualquer grupo de caracteres diferente de vírgula
# ou qualquer grupo de caracteres que tenha ', ' no meio.
#
# O resultado seria...

João da Silva
Av. das Flores, 134
Jd. das Flores
Osasco
SP

Agora é só aprender REGEX e sair pro abraço! É muito fácil! (mua-ha-ha-ha-ha…)

Registros com múltiplas linhas

Finalmente, o que faríamos se cada registro fosse composto por várias linhas? Veja este exemplo:

PEDRO PEDREIRO
27
MECÂNICO
1890.00

MARÍLIA MACEDO
34
SECRETÁRIA
2345.00

ESPEDITO ESTEVÃO
19
ESTAGIÁRIO
789.00

Uma das soluções possíveis é recorrer à variável RS (Record Separator, separador de registro) e utilizá-la em conjunto com a variável FS:

BEGIN {
    RS=""
    FS="\n"
}
{
    print "NOME :",$1
    print "IDADE:",$2
    print "CARGO:",$3
    print "RENDA:",$4
    print ""
}

# O que produziria na saída...

NOME : PEDRO PEDREIRO
IDADE: 27
CARGO: MECÂNICO
RENDA: 1890.00

NOME : MARÍLIA MACEDO
IDADE: 34
CARGO: SECRETÁRIA
RENDA: 2345.00

NOME : ESPEDITO ESTEVÃO
IDADE: 19
CARGO: ESTAGIÁRIO
RENDA: 789.00

Aula 1 – O que é o AWK | Índice | Aula3 – Prática: lendo um feed RSS

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post comment