Criando uma única tabela a partir de vários arquivos

1

Eu quero mesclar vários arquivos delimitados por tabulações de duas colunas no formato:

  a
A 5
C 4  
D 2

  b
A 2
B 5
C 3

  c
B 4
C 4
D 2

Em uma única tabela neste formato:

  a b c
A 5 2 0
B 0 5 4
C 4 3 4
D 2 0 2
    
por P.tin 13.12.2016 / 15:58

3 respostas

2

Você quer essencialmente criar uma matriz bidimensional de valores. A primeira coluna em cada linha corresponde a uma chave , retirada do primeiro campo delimitado por tabulações em cada linha em cada arquivo de entrada. Cada coluna a seguir corresponde a um arquivo de entrada separado.

awk 'BEGIN {
         RS = "(\r\n|\n\r|\r|\n)"
         FS = " *\t *"
         SUBSEP = ":"
     }
     FNR==1 {
         ++file
     }
     NF>=2 {
         if ($1 in keynum)
             key = keynum[$1]
         else {
             key = ++keys
             keynum[$1] = key
             keystr[key] = $1
         }
         value[key,file] = $2
     }
     END {
         files = file
         for (key = 1; key <= keys; key++) {
             printf "%s", keystr[key]
             for (file = 1; file <= files; file++)
                 printf "\t%s", value[key,file]
             printf "\n"
         }
     }' INPUT1 INPUT2 ... INPUTN

A regra BEGIN define o separador de registro para qualquer tipo de nova linha, para que cada linha seja um registro separado. Ele também define o separador de campo para uma guia, incluindo todos os espaços ao redor dele.

No awk, todos os arrays são associativos e basicamente unidimensionais. Arrays multidimensionais são suportados pela concatenação dos índices, com um SUBSEP entre eles. Aqui, usamos : como um separador, porque os índices usados são inteiros positivos. (Você poderia usar muitos outros caracteres se quisesse; por exemplo, uma aba \t .)

A regra FNR==1 é acionada na primeira linha de cada arquivo de entrada. Incrementamos a variável file , de modo que seja 1 para o primeiro arquivo de entrada, 2 para o segundo e assim por diante.

A regra NF>=2 é acionada para todos os registros com pelo menos dois campos. Nesse caso, significa para cada linha que possui um caractere de tabulação. O primeiro campo é a chave e o segundo campo o valor .

A variável key é um inteiro positivo, referindo-se a uma string de chave exclusiva. (1 se refere à primeira única chave vista, 2 à segunda e assim por diante, em todos os arquivos de entrada.)

A matriz associativa keynum mapeia cadeias-chave para números-chave ( key , inteiros positivos). O keystr é o mapeamento inverso, mapeando números de chaves para cadeias de caracteres de chave.

Na regra NF>=2 , se o primeiro campo já é uma chave conhecida, seu número é procurado. Caso contrário, o primeiro campo é adicionado como uma nova string de chave exclusiva. Em seguida, o segundo campo é salvo na matriz value .

A regra END é acionada após todos os arquivos de entrada terem sido processados. A matriz value , uma matriz logicamente bidimensional, contém os campos que queremos.

O loop externo faz um loop key sobre todas as chaves exclusivas vistas, na ordem em que foram vistas pela primeira vez. Cada iteração do loop externo produz uma linha de saída.

O loop interno faz um loop de file sobre cada arquivo de entrada, na ordem em que foram listados. Cada iteração produz uma coluna adicional para a saída da linha atual. Cada linha de saída contém exatamente mais uma coluna do que o número de arquivos de entrada especificados. (Observe que, se nenhum arquivo de entrada for especificado, o awk será lido a partir da entrada padrão e será contado como se fosse um arquivo de entrada.)

Este definitivamente não é o método mais simples para conseguir isso, mas eu gosto disso, porque é robusto (aceita arquivos de entrada criados em Unix, Linux, Macs antigos, novos Macs, Windows - basicamente em qualquer lugar que use um compatível com ASCII conjunto de caracteres; também não se confundirá se alguns arquivos de entrada tiverem apenas um subconjunto de todas as chaves conhecidas), relativamente fáceis de entender, manter e se adaptar a casos semelhantes.

Se você deseja executar o código acima como um script, salve o seguinte como, por exemplo, paste.awk :

#!/usr/bin/awk -f
BEGIN {
    RS = "(\r\n|\n\r|\r|\n)"
    FS = " *\t *"
    SUBSEP = ":"
}
FNR==1 {
    ++file
}
NF>=2 {
    if ($1 in keynum)
        key = keynum[$1]
    else {
        key = ++keys
        keynum[$1] = key
        keystr[key] = $1
    }
    printf "key = %s, file = %s, value = %s\n", key, file, $2 >/dev/stderr
    value[key,file] = $2
}
END {
    files = file
    for (key = 1; key <= keys; key++) {
        printf "%s", keystr[key]
        for (file = 1; file <= files; file++)
            printf "\t%s", value[key,file]
        printf "\n"
    }
}

Se você tiver input1 contendo

        a
A       5
C       4
D       2

e input2 contendo

        b
A       2
B       5
C       3

e input3 contendo

        c
B       4
C       4
D       2

mas com o segundo caractere em cada linha sendo Tab ; isto é, criado usando, por exemplo,

printf ' \ta\nA\t5\nC\t4\nD\t2\n' > input1
printf ' \tb\nA\t2\nB\t5\nC\t3\n' > input2
printf ' \tc\nB\t4\nC\t4\nD\t2\n' > input3

ou, se você copiar e colar o texto acima em arquivos, execute sed -e 's|^\(.\) *|\t|' -i input1 input2 input3 para corrigi-los; então, correndo

paste.awk input1 input2 input3

saídas

        a       b       c
A       5       2       
C       4       3       4
D       2               2
B               5       4

exceto que os espaços consecutivos acima são realmente tab s. O software neste site converte guias em espaços, você vê.

Editado para adicionar: Se você quiser usar algum valor predefinido para entradas ausentes, modifique a regra END em

END {
    files = file
    for (key = 1; key <= keys; key++) {
        printf "%s", keystr[key]
        for (file = 1; file <= files; file++)
            if ((key SUBSEP file) in value)
                printf "\t%s", value[key,file]
            else
                printf "\t%s", blank
        printf "\n"
    }
}

e defina a variável blank para refletir o valor desejado. (Você pode defini-lo a partir da linha de comando, usando ./paste.awk -v blank=0 input1 input2 input3 ou modificar o código awk e definir o valor em algum lugar na regra BEGIN ou no início da regra END .)

    
por 13.12.2016 / 16:52
3

join é a ferramenta a ser usada, mas suas opções são um pouco desagradáveis:

join -t $'\t' -a1 -a2 -o 0,1.2,2.2     file1 file2 |
join -t $'\t' -a1 -a2 -o 0,1.2,1.3,2.2     - file3 |
sed 's/\t\(\t\|$\)/\t0/g'
    a   b   c
A   5   2   0
B   0   5   4
C   4   3   4
D   2   0   2

Eu usei pela primeira vez a opção -e , mas isso causou problemas com a linha de cabeçalho.

    
por 13.12.2016 / 17:30
1

Aqui está uma versão do GNU awk. Primeiro eu encontro todos os valores chave, então eu posso preencher valores vazios com zero:

keys=$(cut -d $'\t' -f1 file{1,2,3} | sort -u | paste -sd,)
gawk -F'\t' -v keys="$keys" '
    BEGIN {
        n = split(keys,k,/,/)
        for (i=1; i<=n; i++) values[k[i]] = k[i]
    }
    {v[$1] = $2} 
    ENDFILE {
        for (key in values) 
            values[key] = values[key] FS (v[key] ? v[key] : 0)
        delete v
    } 
    END {
        for (key in values) print values[key]
    }
' file1 file2 file3 | sort -t $'\t' -k 1,1
    
por 13.12.2016 / 17:44