bash adicionar / anexar novas colunas de outros arquivos
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 awk
e 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 j1
para parallel
devido à 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
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
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 0
elemento 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.
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.txt
na 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.txt
e, 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.awk
e depois usei chmod +x names.awk
para 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.
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.
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
.
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
Com bash
que 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"