bash adicionar / anexar novas colunas de outros arquivos

Nov 24 2020

Eu tenho um arquivo name.txt de uma coluna, por exemplo

A
B
C
D
E
F

Então eu tenho muitos arquivos, egxtxt, y.txt e z.txt

x.txt tem

A 1
C 3
D 2

y.txt tem

A 1
B 4
E 3

z.txt tem

B 2
D 2
F 1

A saída desejável é (preenchendo 0 se não houver mapeamento)

A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1

É possível fazer isso com o bash? (talvez awk?)
Muito obrigado !!!


primeiras edições - meus esforços experimentais
Como sou muito novo no bash, é realmente difícil para mim descobrir uma solução possível com o awk. Estou mais familiarizado com R, no qual isso pode ser feito por

namematrix[namematrix[,1]==xmatrix[,1],]

Em suma, agradeço muito a ajuda gentil abaixo, ajudando-me a aprender mais sobre awke join!


Novas edições - uma abordagem supereficiente descoberta!

Felizmente inspirado por algumas respostas realmente brilhantes abaixo, eu resolvi uma maneira computacionalmente eficiente como abaixo. Isso pode ser útil para outras pessoas que se deparam com questões semelhantes, em particular se lidam com um grande número de arquivos com um tamanho muito grande.

Primeiramente toque em um join_awk.bash

#!/bin/bash
join -oauto -e0 -a1 $1 $2 | awk '{print $2}'

Por exemplo, execute este script bash para name.txt e x.txt

join_awk.bash name.txt x.txt

geraria

1
0
3
2
0
0

Observe que aqui eu mantenho apenas a segunda coluna para economizar espaço em disco, porque em meu conjunto de dados as primeiras colunas são nomes muito longos que ocupariam muito espaço em disco.

Em seguida, basta implementar

parallel join_awk.bash name.txt {} \> outdir/output.{} ::: {a,b,c}.txt

Isso foi inspirado pela brilhante resposta abaixo usando GNU parallel and join. A diferença é que a resposta abaixo deve especificar j1para paralleldevido à sua lógica de anexação serial, o que o torna não realmente "paralelo". Além disso, a velocidade ficará cada vez mais lenta à medida que o acréscimo serial continua. Em contraste, aqui manipulamos cada arquivo separadamente em paralelo. Pode ser extremamente rápido quando lidamos com um grande número de arquivos de tamanho grande com várias CPUs.

Finalmente, basta mesclar todos os arquivos de saída de coluna única juntos por

cd outdir
paste output* > merged.txt

Isso também será muito rápido, pois pasteé inerentemente paralelo.

Respostas

12 anubhava Nov 24 2020 at 13:42

Você pode usar isto awk:

awk 'NF == 2 {
   map[FILENAME,$1] = $2
   next
}
{
   printf "%s", $1 for (f=1; f<ARGC-1; ++f) printf "%s", OFS map[ARGV[f],$1]+0
   print ""
}' {x,y,z}.txt name.txt
A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1
9 RavinderSingh13 Nov 24 2020 at 14:15

Adicionando mais uma maneira de fazer isso. Você poderia tentar seguir, escrito e testado com os exemplos mostrados. IMHO deve funcionar em qualquer awk, embora eu tenha apenas a versão 3.1 do GNU awk. Esta é uma maneira muito simples e comum, crie um array na primeira (principal) leitura do Input_file e, posteriormente, em cada arquivo, adicione o 0elemento desse array NÃO encontrado naquele Input_file específico, testado apenas com pequenas amostras fornecidas.

awk '
function checkArray(array){
  for(i in array){
    if(!(i in found)){ array[i]=array[i] OFS "0" }
  }
}
FNR==NR{
  arr[$0] next } foundCheck && FNR==1{ checkArray(arr) delete found foundCheck="" } { if($1 in arr){
    arr[$1]=(arr[$1] OFS $2) found[$1]
    foundCheck=1
    next
  }
}
END{
  checkArray(arr)
  for(key in arr){
    print key,arr[key]
  }
}
' name.txt x.txt y.txt  z.txt

Explicação: Adicionando explicação detalhada acima.

awk '                               ##Starting awk program from here.
function checkArray(array){         ##Creating a function named checkArray from here.
  for(i in array){                  ##CTraversing through array here.
    if(!(i in found)){ array[i]=array[i] OFS "0" }   ##Checking condition if key is NOT in found then append a 0 in that specific value.
  }
}
FNR==NR{                            ##Checking condition if FNR==NR which will be TRUE when names.txt is being read.
  arr[$0] ##Creating array with name arr with index of current line. next ##next will skip all further statements from here. } foundCheck && FNR==1{ ##Checking condition if foundCheck is SET and this is first line of Input_file. checkArray(arr) ##Calling function checkArray by passing arr array name in it. delete found ##Deleting found array to get rid of previous values. foundCheck="" ##Nullifying foundCheck here. } { if($1 in arr){                    ##Checking condition if 1st field is present in arr.
    arr[$1]=(arr[$1] OFS $2) ##Appening 2nd field value to arr with index of $1.
    found[$1]                       ##Adding 1st field to found as an index here.
    foundCheck=1                    ##Setting foundCheck here.
    next                            ##next will skip all further statements from here.
  }
}
END{                                ##Starting END block of this program from here.
  checkArray(arr)                   ##Calling function checkArray by passing arr array name in it.
  for(key in arr){                  ##Traversing thorugh arr here.
    print key,arr[key]              ##Printing index and its value here.
  }
}
' name.txt x.txt y.txt z.txt        ##Mentioning Input_file names here.
6 DavidC.Rankin Nov 24 2020 at 13:35

Sim, você pode fazer isso, e sim, awké a ferramenta. Usando matrizes e seu número de linha do arquivo normal ( FNR número de registros de arquivo ) e total de linhas ( NR registros ) você pode ler todas as cartas de names.txtna a[]matriz, em seguida, manter o controle do número de arquivo na variável fno, você pode adicionar todas as adições de x.txte, em seguida, antes de processar a primeira linha do próximo arquivo ( y.txt), faça um loop em todas as letras vistas no último arquivo e, para aquelas não vistas, coloque a 0, então continue o processamento normalmente. Repita para cada arquivo adicional.

Mais explicações linha por linha são mostradas nos comentários:

awk '
    FNR==NR {                           # first file
        a[$1] = "" # fill array with letters as index fno = 1 # set file number counter next # get next record (line) } FNR == 1 { fno++ } # first line in file, increment file count fno > 2 && FNR == 1 { # file no. 3+ (not run on x.txt) for (i in a) # loop over letters if (!(i in seen)) # if not in seen array a[i] = a[i]" "0 # append 0 delete seen # delete seen array } $1 in a {                           # if line begins with letter in array
        a[$1] = a[$1]" "$2 # append second field seen[$1]++                      # add letter to seen array
    }
END {
    for (i in a)                        # place zeros for last column
        if (!(i in seen))
            a[i] = a[i]" "0
    for (i in a)                        # print results
        print i a[i]
}' name.txt x.txt y.txt z.txt

Exemplo de uso / saída

Basta copiar o texto acima e colar com o botão do meio do mouse em um xterm com o diretório atual contendo seus arquivos e você receberá:

A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1

Criação de um script autocontido

Se quiser criar um script para ser executado em vez de colar na linha de comando, basta incluir o conteúdo (sem colocar aspas simples) e, em seguida, tornar o arquivo executável. Por exemplo, você inclui o intérprete como a primeira linha e o conteúdo como segue:

#!/usr/bin/awk -f

FNR==NR {                           # first file
    a[$1] = "" # fill array with letters as index fno = 1 # set file number counter next # get next record (line) } FNR == 1 { fno++ } # first line in file, increment file count fno > 2 && FNR == 1 { # file no. 3+ (not run on x.txt) for (i in a) # loop over letters if (!(i in seen)) # if not in seen array a[i] = a[i]" "0 # append 0 delete seen # delete seen array } $1 in a {                           # if line begins with letter in array
    a[$1] = a[$1]" "$2 # append second field seen[$1]++                      # add letter to seen array
}
END {
    for (i in a)                    # place zeros for last column
        if (!(i in seen))
            a[i] = a[i]" "0
    for (i in a)                    # print results
        print i a[i]
}

awk irá processar os nomes de arquivos fornecidos como argumentos na ordem fornecida.

Exemplo de uso / saída

Usando o arquivo de script (eu o coloquei names.awke depois usei chmod +x names.awkpara torná-lo executável), você faria:

$ ./names.awk name.txt x.txt y.txt z.txt
A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1

Diga-me se tiver mais perguntas.

4 Sundeep Nov 24 2020 at 14:40

Outra abordagem com GNU awk

$ cat script.awk NF == 1 { name[$1] = $1 for (i = 1; i < ARGC - 1; i++) { name[$1] = name[$1] " 0" } next } { name[$1] = gensub(/ ./, " " $2, ARGIND - 1, name[$1])
}

END {
    for (k in name) {
        print name[k]
    }
}

Chamando o script:

$ awk -f script.awk name.txt {x,y,z}.txt
A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1

A saída mostra a mesma ordem name.txt, mas não acho que isso seja verdade para todos os tipos de entrada.

3 potong Nov 24 2020 at 19:47

Isso pode funcionar para você (GNU paralelo e junção):

cp name.txt out && t=$(mktemp) && parallel -j1 join -oauto -e0 -a1 out {} \> $t \&\& mv $t out ::: {x,y,z}.txt

A saída estará em arquivo out.

2 DiegoTorresMilano Nov 24 2020 at 15:12

Você pode usar join

join -a1 -e0 -o '0,2.2' name.txt x.txt | join -a1 -e0 -o '0,1.2,2.2' - y.txt | join -a1 -e0 -o '0,1.2,1.3,2.2' - z.txt
1 tshiono Nov 24 2020 at 13:48

Com bashque tal:

#!/bin/bash

declare -A hash                                 # use an associative array
for f in "x.txt" "y.txt" "z.txt"; do            # loop over these files
    while read -r key val; do                   # read key and val pairs
        hash[$f,$key]=$val # assign the hash to val done < "$f"
done

while read -r key; do
    echo -n "$key" # print the 1st column for f in "x.txt" "y.txt" "z.txt"; do # loop over the filenames echo -n " ${hash[$f,$key]:-0}"          # print the associated value or "0" if undefined
    done
    echo                                        # put a newline
done < "name.txt"