Große Anzahl von Dateien kombinieren


15

Ich habe ± 10.000 Dateien ( res.1- res.10000), die alle aus einer Spalte und einer gleichen Anzahl von Zeilen bestehen. Was ich will, ist im Wesentlichen einfach; Füge alle Dateien spaltenweise in einer neuen Datei zusammen final.res. Ich habe versucht mit:

paste res.*

Jedoch (obwohl dies für einen kleinen Teil der Ergebnisdateien zu funktionieren scheint, dies gibt die folgenden Fehler , wenn sie auf dem ganzen Satz durchgeführt: Too many open files.

Es muss einen "einfachen" Weg geben, dies zu tun, aber ich bin leider ziemlich neu in Unix. Danke im Voraus!

PS: Um Ihnen eine Vorstellung davon zu geben, wie (eine meiner) Datendateien aussieht:

0.5
0.5
0.03825
0.5
10211.0457
10227.8469
-5102.5228
0.0742
3.0944
...

Haben Sie versucht, die --serialOption mit dem pasteBefehl zu verwenden?
Shivams

@shivams paste --serialführt Dateien nicht spaltenweise zusammen ...
Stephen Kitt

@ StephenKitt Warten. Ich bin etwas verwirrt. Bedeutet er, dass er in der Ausgabedatei eine andere Spalte für die Daten jeder Datei benötigt? Oder alle Daten in einer einzigen Spalte?
Shivams

@Stephen Kitt shivams Using paste -sfunktioniert zwar, fügt aber die separaten Ergebnisdateien zeilenweise statt spaltenweise ein. Dies kann ich jedoch lösen. Vielen Dank!
Matten

@shivams Ich möchte eine andere Spalte für die Daten jeder Datei in der Ausgabedatei
mats

Antworten:


17

Wenn Sie über Root-Berechtigungen auf diesem Computer verfügen, können Sie das Limit für die "maximale Anzahl offener Dateideskriptoren" vorübergehend erhöhen:

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

Und dann

paste res.* >final.res

Danach können Sie die ursprünglichen Werte wiederherstellen.


Eine zweite Lösung , wenn Sie das Limit nicht ändern können:

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

Es ruft pastejede Datei einmal auf und am Ende gibt es eine riesige Datei mit allen Spalten (es dauert seine Minute).

Edit : Nutzloser Einsatz von Katze ... Nicht !

Wie in den Kommentaren erwähnt, ist die Verwendung von cathere ( cat final.res | paste - $f >temp) nicht nutzlos. Beim ersten Ausführen der Schleife ist die Datei final.resnoch nicht vorhanden. pastewürde dann fehlschlagen und die Datei wird nie gefüllt, noch erstellt. Mit meiner lösung catscheitert nur das erste mal mit stdin No such file or directoryund pasteliest nur eine leere datei aus, aber es geht weiter. Der Fehler kann ignoriert werden.


Vielen Dank! Hast du eine Idee, wie ich die ursprünglichen Werte überprüfen kann?
Matten

Nur ulimit -Snfür die weiche Grenze und ulimit -Hnfür die harte Grenze
Chaos

Danke, das funktioniert teilweise. Doch für einen anderen Satz von Dateien bekomme ich folgende Fehlermeldung: -bash: /usr/bin/paste: Argument list too long. Ideen, wie man das löst? Tut mir leid, dass ich euch gestört habe.
Matten

@mats scheint, dass Ihr Kernel keine weiteren Argumente zulässt. Sie können dies mit überprüfen getconf ARG_MAX. Sie können diesen Wert nur erhöhen, wenn Sie den Kernel neu kompilieren. Sie können meine zweite Lösung versuchen?
Chaos

2
Anstatt catjedes Mal durch die Schleife zu gehen, können Sie zunächst eine leere final.resDatei erstellen. Dies ist wahrscheinlich in jedem Fall eine gute Idee, falls dort bereits eine final.resDatei vorhanden ist.
Barmar

10

Wenn die Antwort von chaos nicht zutrifft (weil Sie nicht über die erforderlichen Berechtigungen verfügen), können Sie die pasteAnrufe wie folgt stapeln:

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

Diese listet die Dateien 1000 zu einer Zeit , in Dateien mit dem Namen lists00, lists01usw., dann fügt die entsprechenden res.Dateien in Dateien mit dem Namen merge00, merge01usw., und geht schließlich alle die daraus resultierenden teilweise Dateien zusammengefügt.

Wie von Chaos erwähnt , können Sie die Anzahl der gleichzeitig verwendeten Dateien erhöhen. Das Limit ist der angegebene Wert ulimit -nabzüglich der Anzahl der Dateien, die Sie bereits geöffnet haben

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

das Limit minus zehn zu verwenden.

Wenn Ihre Version von splitnicht unterstützt -d, können Sie sie entfernen: Sie müssen lediglich splitnumerische Suffixe verwenden. Standardmäßig werden die Suffixe aa, abusw. statt 01, 02usw.

Wenn es so viele fehlgeschlagene Dateien gibt ls -1 res.*("Argumentliste zu lang"), können Sie sie ersetzen, findum diesen Fehler zu vermeiden:

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

(Wie von don_crissti hervorgehoben , -1sollte dies bei lsder Ausgabe von Pipelines nicht erforderlich sein. Ich überlasse es jedoch Fällen, in denen lsein Alias ​​vorliegt -C.)


4

Versuchen Sie es auf diese Weise auszuführen:

ls res.*|xargs paste >final.res

Sie können den Stapel auch in Teile aufteilen und Folgendes ausprobieren:

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

und am Ende kombinieren Sie die endgültigen Dateien

paste final.* >final.res

@ Romeo Ninov Dies ergibt den gleichen Fehler, den ich in meiner Ausgangsfrage getroffen habe:Too many open files
mats

@mats, in diesem Fall sollten Sie die Charge in Teile aufteilen. Wird meine Antwort bearbeiten, um Ihnen eine Idee zu geben
Romeo Ninov

Richtig, @StephenKitt, ich bearbeite meine Antwort
Romeo Ninov

Um die temporären Dateien zu vermeiden, sollten Sie erwägen, die final.x00be-Pipes entweder als benannte FIFOs oder implizit durch Prozessersetzung zu erstellen (sofern Ihre Shell dies unterstützt - z. B. bash). Es macht keinen Spaß, mit der Hand zu schreiben, kann aber durchaus zu einem Makefile passen.
Toby Speight

4
i=0
{ paste res.? res.?? res.???
while paste ./res."$((i+=1))"[0-9][0-9][0-9]
do :; done; } >outfile

Ich denke nicht, dass dies so kompliziert ist wie das alles - Sie haben bereits die harte Arbeit geleistet, indem Sie die Dateinamen bestellt haben. Nur nicht alle gleichzeitig öffnen, ist alles.

Ein anderer Weg:

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

... aber ich denke das macht sie rückwärts ... Das könnte besser funktionieren:

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

Und hier ist noch ein anderer Weg:

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

Auf diese Weise können Sie taralle Dateien in einem durch Nullen getrennten Stream zusammenfassen, alle Header-Metadaten mit Ausnahme des Dateinamens analysieren und alle Zeilen in allen Dateien in Registerkarten umwandeln. Es ist jedoch darauf angewiesen, dass es sich bei den Eingaben um tatsächliche Textdateien handelt. Dies bedeutet, dass jede Zeile mit einem Zeilenumbruch endet und die Dateien keine Nullbytes enthalten. Oh - und es stützt sich auch auf den Dateinamen selbst Newline frei zu sein (obwohl das vielleicht robust mit GNU behandelt werden tar‚s - --xformOption) . Wenn diese Bedingungen erfüllt sind, sollte eine beliebige Anzahl von Dateien in kürzester Zeit bearbeitet werden - und tardies ist fast alles.

Das Ergebnis ist eine Reihe von Zeilen, die wie folgt aussehen:

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

Und so weiter.

Ich habe es getestet, indem ich zuerst 5 Testdateien erstellt habe. Ich habe wirklich nicht das Gefühl , wie genning 10000 Dateien gerade jetzt, so dass ich nur ein wenig größer für jede ging - und auch dafür gesorgt , dass die Dateilängen von viel unterschieden. Dies ist beim Testen von tarSkripten wichtig, da tarEingaben mit festen Längen blockiert werden. Wenn Sie nicht mindestens ein paar verschiedene Längen ausprobieren, wissen Sie nie, ob Sie tatsächlich nur die eine verarbeiten.

Wie auch immer, für die Testdateien, die ich gemacht habe:

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

ls danach berichtet:

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

... dann rannte ich ...

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

... um nur die ersten 25 durch Tabulatoren getrennten Felder pro Zeile anzuzeigen (da jede Datei eine einzelne Zeile ist - es gibt viele ) ...

Die Ausgabe war:

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

4

In Anbetracht der Anzahl der betroffenen Dateien, Zeilengrößen usw. denke ich, dass die Standardgrößen der Werkzeuge (awk, sed, paste, * usw.) übertroffen werden.

Ich würde ein kleines Programm dafür erstellen, es hätte weder 10.000 Dateien geöffnet, noch eine Zeile von Hunderttausenden Länge (10.000 Dateien von 10 (maximale Zeilengröße im Beispiel)). Es werden nur ~ 10.000 ganze Zahlen benötigt, um die Anzahl der Bytes zu speichern, die aus jeder Datei gelesen wurden. Der Nachteil ist, dass es nur einen Dateideskriptor gibt, der für jede Datei und für jede Zeile wiederverwendet wird. Dies kann langsam sein.

Die Definitionen von FILESund ROWSsollten auf die tatsächlichen exakten Werte geändert werden. Die Ausgabe wird an die Standardausgabe gesendet.

#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 */
   }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.