bash dodaje / dołącza nowe kolumny z innych plików

Nov 24 2020

Mam plik name.txt z jedną kolumną np

A
B
C
D
E
F

Następnie mam wiele plików, egxtxt, y.txt i z.txt

x.txt ma

A 1
C 3
D 2

y.txt ma

A 1
B 4
E 3

z.txt ma

B 2
D 2
F 1

Pożądane wyjście to (wypełnienie 0, jeśli nie ma mapowania)

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

Czy można to zrobić za pomocą basha? (może awk?)
Wielkie dzięki !!!


pierwsze edycje - moje wstępne wysiłki
Ponieważ jestem całkiem nowy w bashu, bardzo trudno jest mi znaleźć możliwe rozwiązanie w awk. Bardziej znam R, w którym można to osiągnąć

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

Podsumowując, naprawdę doceniam poniższą pomoc, która pomaga mi dowiedzieć się więcej o awki join!


Edycja za drugim razem - wymyślono super wydajne podejście!

Na szczęście zainspirowany kilkoma naprawdę genialnymi odpowiedziami poniżej, opracowałem bardzo wydajny obliczeniowo sposób, jak poniżej. Może to być pomocne dla innych osób mających podobne pytania, zwłaszcza jeśli mają do czynienia z bardzo dużą liczbą plików o bardzo dużym rozmiarze.

Najpierw dotknij pliku join_awk.bash

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

Na przykład wykonaj ten skrypt bash dla name.txt i x.txt

join_awk.bash name.txt x.txt

wygeneruje

1
0
3
2
0
0

Zwróć uwagę, że tutaj zachowuję tylko drugą kolumnę, aby zaoszczędzić miejsce na dysku, ponieważ w moim zestawie danych pierwsze kolumny to bardzo długie nazwy, które zajmowałyby ogromną ilość miejsca na dysku.

Następnie po prostu zaimplementuj

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

Jest to zainspirowane genialną odpowiedzią poniżej, wykorzystującą równolegle GNU i dołącz. Różnica polega na tym, że odpowiedź poniżej musi zostać określona j1ze parallelwzględu na logikę szeregowego dołączania, co sprawia, że ​​nie jest ona tak naprawdę „równoległa”. Ponadto prędkość będzie coraz wolniejsza w miarę kontynuowania dołączania szeregowego. W przeciwieństwie do tego, tutaj manipulujemy każdym plikiem oddzielnie, równolegle. Może być niezwykle szybki, gdy mamy do czynienia z dużą liczbą plików o dużych rozmiarach z wieloma procesorami.

Na koniec po prostu połącz wszystkie jednokolumnowe pliki wyjściowe razem przez

cd outdir
paste output* > merged.txt

Będzie to również bardzo szybkie, ponieważ pastejest z natury równoległe.

Odpowiedzi

12 anubhava Nov 24 2020 at 13:42

Możesz użyć tego 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

Dodanie jeszcze jednego sposobu zrobienia tego. Czy mógłbyś spróbować śledzić, napisać i przetestować pokazane próbki. IMHO powinno działać w każdym awk, chociaż mam tylko wersję 3.1 GNU awk. Jest to bardzo prosty i zwykły sposób, utwórz tablicę w pierwszym (głównym) odczycie pliku Input_file, a następnie w każdym pliku dodaj 0każdy element tej tablicy, który NIE jest znaleziony w tym konkretnym pliku Input_file, testowany tylko z małymi podanymi próbkami.

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

Wyjaśnienie: dodanie szczegółowego wyjaśnienia powyższego.

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

Tak, możesz to zrobić i tak, awkjest to narzędzie. Korzystanie z tablic i normalne numer wiersza pliku ( FNR numer akt ewidencji ), a całkowity wiersze ( NR rekordy ) można przeczytać wszystkie litery od names.txtdo a[]tablicy, a następnie śledzenie numeru pliku w zmiennej fno, można dodać wszystkie dodatki z x.txtczym przed przetworzeniem pierwszej linii następnego pliku ( y.txt), zapętlaj wszystkie litery widoczne w ostatnim pliku, a dla tych, których nie widać, umieść a 0, a następnie kontynuuj przetwarzanie w normalny sposób. Powtórz dla każdego dodatkowego pliku.

Dalsze wyjaśnienia, wiersz po wierszu, przedstawiono w komentarzach:

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

Przykładowe użycie / wyjście

Po prostu skopiuj powyższe i wklej środkowym przyciskiem myszy do xterma z bieżącym katalogiem zawierającym twoje pliki, a otrzymasz:

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

Tworzenie samodzielnego skryptu

Jeśli chcesz utworzyć skrypt do uruchomienia zamiast wklejania w wierszu poleceń, po prostu dołącz zawartość (bez otaczania w apostrofy), a następnie sprawisz, że plik będzie wykonywalny. Na przykład, umieszczasz tłumacza jako pierwszą linię, a zawartość w następujący sposób:

#!/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 przetworzy nazwy plików podane jako argumenty w podanej kolejności.

Przykładowe użycie / wyjście

Używając pliku skryptu (umieściłem go, names.awka następnie uczyniłem chmod +x names.awkgo wykonywalnym), zrobiłbyś:

$ ./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

Daj mi znać, jeśli masz dalsze pytania.

4 Sundeep Nov 24 2020 at 14:40

Inne podejście z 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]
    }
}

Wywołanie skryptu:

$ 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

Dane wyjściowe pokazują tę samą kolejność, co name.txt, ale nie sądzę, że będzie to prawdą dla wszystkich rodzajów danych wejściowych.

3 potong Nov 24 2020 at 19:47

To może zadziałać dla Ciebie (równolegle do GNU i dołącz):

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

Dane wyjściowe będą w pliku out.

2 DiegoTorresMilano Nov 24 2020 at 15:12

Możesz użyć 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

A co bashpowiesz na:

#!/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"