bash ajouter / ajouter de nouvelles colonnes à partir d'autres fichiers

Nov 24 2020

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 awket 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 j1en parallelraison 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 pasteest intrinsèquement parallèle.

Réponses

12 anubhava Nov 24 2020 at 13:42

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
9 RavinderSingh13 Nov 24 2020 at 14:15

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 awkuniquement. 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 0quel é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.
6 DavidC.Rankin Nov 24 2020 at 13:35

Oui, vous pouvez le faire, et oui, awkc'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.txtdans 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.awkpuis utilisé chmod +x names.awkpour 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.

4 Sundeep Nov 24 2020 at 14:40

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.

3 potong Nov 24 2020 at 19:47

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.

2 DiegoTorresMilano Nov 24 2020 at 15:12

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
1 tshiono Nov 24 2020 at 13:48

Avec bashque 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"