bash ajouter / ajouter de nouvelles colonnes à partir d'autres fichiers
J'ai un fichier name.txt d'une colonne, par exemple
A
B
C
D
E
F
Ensuite, j'ai de nombreux fichiers, egxtxt, y.txt et z.txt
x.txt a
A 1
C 3
D 2
y.txt a
A 1
B 4
E 3
z.txt a
B 2
D 2
F 1
La sortie souhaitable est (en remplissant 0 s'il n'y a pas de mappage)
A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1
Est-il possible de le faire avec bash? (peut-être génial?)
Merci beaucoup !!!
premières modifications - mes efforts provisoires
Puisque je suis assez nouveau dans bash, il m'est vraiment difficile de trouver une solution possible avec awk. Je connais mieux R, dans lequel cela peut être accompli en
namematrix[namematrix[,1]==xmatrix[,1],]
Dans l'ensemble, j'apprécie vraiment la gentille aide ci-dessous qui m'aide à en savoir plus sur awk
et join
!
Modifications secondaires - une approche super efficace conçue!
Heureusement, inspiré par quelques réponses vraiment brillantes ci-dessous, j'ai trié un moyen très efficace en termes de calcul comme ci-dessous. Cela peut être utile pour d'autres personnes qui rencontrent des questions similaires, en particulier si elles traitent un très grand nombre de fichiers de très grande taille.
Touchez d'abord un join_awk.bash
#!/bin/bash
join -oauto -e0 -a1 $1 $2 | awk '{print $2}'
Par exemple, exécutez ce script bash pour name.txt et x.txt
join_awk.bash name.txt x.txt
générerait
1
0
3
2
0
0
Notez qu'ici je ne garde que la deuxième colonne pour économiser de l'espace disque, car dans mon ensemble de données, les premières colonnes sont des noms très longs qui prendraient un espace disque énorme.
Ensuite, implémentez simplement
parallel join_awk.bash name.txt {} \> outdir/output.{} ::: {a,b,c}.txt
Ceci est inspiré par la réponse brillante ci-dessous en utilisant GNU parallel and join. La différence est que la réponse ci-dessous doit être spécifiée j1
en parallel
raison de sa logique d'ajout en série, ce qui la rend pas vraiment "parallèle". De plus, la vitesse deviendra de plus en plus lente au fur et à mesure que l'ajout en série se poursuivra. En revanche, nous manipulons ici chaque fichier séparément en parallèle. Cela peut être extrêmement rapide lorsque nous traitons un grand nombre de fichiers de grande taille avec plusieurs processeurs.
Enfin, fusionnez simplement tous les fichiers de sortie à une seule colonne en
cd outdir
paste output* > merged.txt
Ce sera également très rapide car il paste
est intrinsèquement parallèle.
Réponses
Vous pouvez utiliser ceci 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
Ajout d'une autre façon de le faire. Pourriez-vous s'il vous plaît essayer de suivre, écrit et testé avec des exemples illustrés. IMHO devrait fonctionner dans n'importe quel awk
, même si je n'ai que la version 3.1 de GNU awk
uniquement. C'est une manière très simple et habituelle, créez un tableau dans la première lecture de Input_file (majeur) puis plus tard dans chaque fichier, ajoutez n'importe 0
quel élément de ce tableau n'est PAS trouvé dans ce Input_file spécifique, testé avec de petits échantillons donnés uniquement.
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
Explication: Ajout d'une explication détaillée ci-dessus.
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.
Oui, vous pouvez le faire, et oui, awk
c'est l'outil. En utilisant des tableaux et votre numéro de ligne de fichier normal ( FNR
numéro de fichier d'enregistrements ) et le nombre total de lignes ( NR
enregistrements ), vous pouvez lire toutes les lettres names.txt
dans le a[]
tableau, puis en gardant une trace du numéro de fichier dans la variable fno
, vous pouvez ajouter tous les ajouts à partir de x.txt
, puis avant de traiter la première ligne du fichier suivant ( y.txt
), bouclez sur toutes les lettres vues dans le dernier fichier, et pour celles qui ne sont pas vues, placez a 0
, puis continuez le traitement normalement. Répétez pour chaque fichier supplémentaire.
D'autres explications ligne par ligne sont présentées dans les commentaires:
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
Exemple d'utilisation / sortie
Copiez simplement ce qui précède et collez au milieu de la souris dans un xterm avec le répertoire actuel contenant vos fichiers et vous recevrez:
A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1
Création d'un script autonome
Si vous souhaitez créer un script à exécuter au lieu de le coller sur la ligne de commande, il vous suffit d'inclure le contenu (sans l'entourer de guillemets simples), puis de rendre le fichier exécutable. Par exemple, vous incluez l'interpréteur comme première ligne et le contenu comme suit:
#!/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
traitera les noms de fichiers donnés comme arguments dans l'ordre donné.
Exemple d'utilisation / sortie
En utilisant le fichier de script (je l'ai mis names.awk
puis utilisé chmod +x names.awk
pour le rendre exécutable), vous feriez alors:
$ ./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
Faites-moi savoir si vous avez d'autres questions.
Une autre approche avec 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]
}
}
Appel du 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
La sortie montre le même ordre que name.txt
, mais je ne pense pas que ce sera vrai pour tous les types d'entrée.
Cela pourrait fonctionner pour vous (parallèle GNU et jointure):
cp name.txt out && t=$(mktemp) && parallel -j1 join -oauto -e0 -a1 out {} \> $t \&\& mv $t out ::: {x,y,z}.txt
La sortie sera dans le fichier out
.
Vous pouvez utiliser 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
Avec bash
que diriez-vous:
#!/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"