Łączenie dużej liczby plików

15

Mam ± 10 000 plików ( res.1- res.10000), wszystkie składające się z jednej kolumny i równej liczby wierszy. To, czego chcę, jest w gruncie rzeczy proste; scal wszystkie pliki kolumnowo w nowy plik final.res. Próbowałem użyć:

paste res.*

Jednak (choć to wydaje się działać dla małego podzbioru plików wynikowych, to daje następujący błąd, gdy wykonywane na całym zbiorze: Too many open files.

Musi istnieć „łatwy” sposób, aby to zrobić, ale niestety jestem całkiem nowy w Uniksie. Z góry dziękuję!

PS: Aby dać Ci wyobrażenie o tym, jak wygląda (jeden z moich) plików danych:

0.5
0.5
0.03825
0.5
10211.0457
10227.8469
-5102.5228
0.0742
3.0944
...
maty
źródło
Czy próbowałeś użyć --serialopcji z pastepoleceniem?
shivams
@shivams paste --serialnie łączy plików w kolumnach ...
Stephen Kitt
@StephenKitt Czekaj. Jestem lekko zmieszany. Czy to znaczy, że w pliku wyjściowym potrzebuje innej kolumny dla danych każdego pliku? Czy wszystkie dane w jednej kolumnie?
shivams
@Stephen Kitt shivams Używanie paste -srzeczywiście działa, ale wkleja osobne pliki wyników wierszami zamiast kolumnami. Jest to jednak coś, co mogę rozwiązać. Dzięki!
maty
@shivams Chcę inną kolumnę dla danych każdego pliku w pliku wyjściowym
mat

Odpowiedzi:

17

Jeśli masz uprawnienia roota na tym komputerze, możesz tymczasowo zwiększyć limit „maksymalnej liczby otwartych deskryptorów plików”:

ulimit -Hn 10240 # The hard limit
ulimit -Sn 10240 # The soft limit

I wtedy

paste res.* >final.res

Następnie możesz przywrócić oryginalne wartości.


Drugie rozwiązanie , jeśli nie można zmienić limit:

for f in res.*; do cat final.res | paste - $f >temp; cp temp final.res; done; rm temp

Wzywa pastedo każdego pliku jeden raz, a na końcu jest ogromny plik ze wszystkimi kolumnami (zajmuje to chwilę).

Edycja : Bezużyteczne użycie kota ... Nie !

Jak wspomniano w komentarzach, użycie funkcji cathere ( cat final.res | paste - $f >temp) nie jest bezużyteczne. Przy pierwszym uruchomieniu pętli plik jeszcze final.resnie istnieje. pastewtedy się nie powiedzie i plik nie zostanie nigdy wypełniony ani utworzony. Moje rozwiązanie catkończy się niepowodzeniem tylko za pierwszym razem No such file or directoryi pasteodczytuje ze standardowego pustego pliku, ale kontynuuje. Błąd można zignorować.

chaos
źródło
Dzięki! Masz pomysł, jak mogę sprawdzić, jakie są oryginalne wartości?
maty
Tylko ulimit -Sndla miękkiego limitu i ulimit -Hndla twardego limitu
chaos
Dzięki, to częściowo działa. Jednakże, dla innego zestawu plików pojawia się następujący błąd: -bash: /usr/bin/paste: Argument list too long. Pomysły, jak to rozwiązać? Przepraszam, że przeszkadzam.
maty
@mats wydaje się, że twoje jądro nie pozwala na więcej argumentów, możesz to sprawdzić getconf ARG_MAX, możesz zwiększyć tę wartość tylko podczas ponownej kompilacji jądra. Możesz wypróbować moje drugie rozwiązanie?
chaos
2
Zamiast używać za catkażdym razem pętli, możesz zacząć od utworzenia pustego final.respliku. Jest to prawdopodobnie dobry pomysł, na wypadek, gdyby final.resplik już tam był.
Barmar
10

Jeśli odpowiedź chaosu nie ma zastosowania (ponieważ nie masz wymaganych uprawnień), możesz grupować pastepołączenia w następujący sposób:

ls -1 res.* | split -l 1000 -d - lists
for list in lists*; do paste $(cat $list) > merge${list##lists}; done
paste merge* > final.res

Lista plików 1000 naraz w plikach o nazwach lists00, lists01itp, a następnie wklejenie odpowiednich res.plików do plików o nazwach merge00, merge01itp, a na końcu łączy wszystkie wynikające częściowo połączone pliki.

Jak wspomniano w chaosie , możesz zwiększyć liczbę plików używanych jednocześnie; limit to podana wartość ulimit -nminus jak wiele plików już masz, tak byś powiedział

ls -1 res.* | split -l $(($(ulimit -n)-10)) -d - lists

użyć limitu minus dziesięć.

Jeśli twoja wersja splitnie obsługuje -d, możesz ją usunąć: wystarczy splitużyć sufiksów numerycznych. Domyślnie przyrostków będzie aa, abitd., A nie 01, 02itd.

Jeśli jest tak wiele plików, które ls -1 res.*zawodzą („zbyt długa lista argumentów”), możesz ją zastąpić, findaby uniknąć tego błędu:

find . -maxdepth 1 -type f -name res.\* | split -l 1000 -d - lists

(Jak zauważył don_crissti , -1nie powinno to być konieczne przy lswyjściu potoku ; pozostawiam to jednak do obsługi przypadków, w których lsjest alias -C).

Stephen Kitt
źródło
4

Spróbuj wykonać to w ten sposób:

ls res.*|xargs paste >final.res

Możesz także podzielić partię na części i wypróbować coś takiego:

paste `echo res.{1..100}` >final.100
paste `echo res.{101..200}` >final.200
...

i na koniec połącz pliki końcowe

paste final.* >final.res
Romeo Ninov
źródło
@ Romeo Ninov To daje taki sam błąd, jak wspomniałem w moim początkowym pytaniu:Too many open files
mat
@ mat, w takim przypadku rozważasz podzielenie partii na części. Zmodyfikuję moją odpowiedź, aby dać ci pomysł
Romeo Ninov
Tak, @StephenKitt, edytuję swoją odpowiedź
Romeo Ninov
Aby uniknąć plików tymczasowych, zastanów się nad utworzeniem final.x00potoków be - jako nazwanych FIFO lub pośrednio, stosując podstawianie procesów (jeśli twoja powłoka je obsługuje - np. Bash). Pisanie ręczne nie jest fajne, ale może pasować do makefile.
Toby Speight
4
i=0
{ paste res.? res.?? res.???
while paste ./res."$((i+=1))"[0-9][0-9][0-9]
do :; done; } >outfile

Nie wydaje mi się, żeby było to tak skomplikowane - wykonałeś już ciężką pracę, zamawiając nazwy plików. Tylko nie otwieraj ich wszystkich jednocześnie, to wszystko.

Inny sposób:

pst()      if   shift "$1"
           then paste "$@"
           fi
set ./res.*
while  [ -n "${1024}" ] ||
     ! paste "$@"
do     pst "$(($#-1023))" "$@"
       shift 1024
done >outfile

... ale myślę, że robi to wstecz ... To może działać lepiej:

i=0;  echo 'while paste \'
until [ "$((i+=1))" -gt 1023 ] &&
      printf '%s\n' '"${1024}"' \
      do\ shift\ 1024 done
do    echo '"${'"$i"'-/dev/null}" \'
done | sh -s -- ./res.* >outfile

A oto jeszcze jeden sposób:

tar --no-recursion -c ./ |
{ printf \\0; tr -s \\0; }    |
cut -d '' -f-2,13              |
tr '\0\n' '\n\t' >outfile

Pozwala tarto zebrać wszystkie pliki w strumieniu rozdzielanym zerami, analizuje wszystkie metadane nagłówka oprócz nazwy pliku i przekształca wszystkie wiersze we wszystkich plikach w tabulatory. Opiera się jednak na tym, że dane wejściowe są rzeczywistymi plikami tekstowymi - co oznacza, że ​​każdy kończy się na nowej linii i nie ma w bajtach pustych. Aha - i to również opiera się na nazwach będących nowalinia same wolne (choć które mogą być obsługiwane solidnie z GNU tar„s --xformopcja) . Biorąc pod uwagę, że te warunki są spełnione, powinien bardzo szybko pracować z dowolną liczbą plików - i tarwykona prawie wszystkie.

Wynikiem jest zestaw linii, które wyglądają następująco:

./fname1
C1\tC2\tC3...
./fname2
C1\tC2\t...

I tak dalej.

Przetestowałem to, tworząc najpierw 5 plików testowych. Tak naprawdę nie miałem ochoty na generowanie 10000 plików, więc po prostu zwiększyłem trochę dla każdego z nich - i upewniłem się, że długości plików różnią się znacznie. Jest to ważne podczas testowania tarskryptów, ponieważ tarblokuje wejście do ustalonych długości - jeśli nie spróbujesz przynajmniej kilku różnych długości, nigdy nie dowiesz się, czy faktycznie poradzisz sobie tylko z jedną.

W każdym razie dla plików testowych zrobiłem:

for f in 1 2 3 4 5; do : >./"$f"
seq "${f}000" | tee -a [12345] >>"$f"
done

ls później zgłoszono:

ls -sh [12345]
68K 1 68K 2 56K 3 44K 4 24K 5

... wtedy pobiegłem ...

tar --no-recursion -c ./ |
{ printf \\0; tr -s \\0; }|
cut -d '' -f-2,13          |
tr '\0\n' '\n\t' | cut -f-25

... aby wyświetlić tylko pierwsze 25 pól rozdzielanych tabulatorami w linii (ponieważ każdy plik jest pojedynczą linią - jest ich dużo ) ...

Wynik był:

./1
1    2    3    4    5    6    7    8    9    10    11    12    13    14    15    16    17    18    19    20    21    22    23    24    25
./2
1    2    3    4    5    6    7    8    9    10    11    12    13    14    15    16    17    18    19    20    21    22    23    24    25
./3
1    2    3    4    5    6    7    8    9    10    11    12    13    14    15    16    17    18    19    20    21    22    23    24    25
./4
1    2    3    4    5    6    7    8    9    10    11    12    13    14    15    16    17    18    19    20    21    22    23    24    25
./5
1    2    3    4    5    6    7    8    9    10    11    12    13    14    15    16    17    18    19    20    21    22    23    24    25
mikeserv
źródło
4

Biorąc pod uwagę liczbę plików, rozmiary linii itp., Myślę, że przewyższy domyślne rozmiary narzędzi (awk, sed, wklej, * itp.)

Stworzyłbym do tego mały program, nie miałby ani 10 000 otwartych plików, ani linii o długości setek tysięcy (10 000 plików po 10 (maksymalny rozmiar linii w przykładzie)). Wymaga tylko około 10 000 tablic liczb całkowitych, aby zapisać liczbę bajtów odczytanych z każdego pliku. Wadą jest to, że ma tylko jeden deskryptor pliku, jest ponownie wykorzystywany dla każdego pliku, dla każdej linii, a to może być powolne.

Definicje FILESi ROWSpowinny zostać zmienione na rzeczywiste dokładne wartości. Dane wyjściowe są wysyłane do standardowego wyjścia.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FILES 10000 /* number of files */
#define ROWS 500    /* number of rows  */

int main() {
   int positions[FILES + 1];
   FILE *file;
   int r, f;
   char filename[100];
   size_t linesize = 100;
   char *line = (char *) malloc(linesize * sizeof(char));

   for (f = 1; f <= FILES; positions[f++] = 0); /* sets the initial positions to zero */

   for (r = 1; r <= ROWS; ++r) {
      for (f = 1; f <= FILES; ++f) {
         sprintf(filename, "res.%d", f);                  /* creates the name of the current file */
         file = fopen(filename, "r");                     /* opens the current file */
         fseek(file, positions[f], SEEK_SET);             /* set position from the saved one */
         positions[f] += getline(&line, &linesize, file); /* reads line and saves the new position */
         line[strlen(line) - 1] = 0;                      /* removes the newline */
         printf("%s ", line);                             /* prints in the standard ouput, and a single space */
         fclose(file);                                    /* closes the current file */
      }
      printf("\n");  /* after getting the line from each file, prints a new line to standard output */
   }
}
Laurence R. Ugalde
źródło