Combiner une grande quantité de fichiers

15

J'ai ± 10 000 fichiers ( res.1- res.10000) tous constitués d'une colonne et d'un nombre égal de lignes. Ce que je veux est, par essence, simple; fusionner tous les fichiers colonne par colonne dans un nouveau fichier final.res. J'ai essayé d'utiliser:

paste res.*

Cependant (bien que cela semble fonctionner pour un petit sous - ensemble de fichiers de résultats, cela donne l'erreur suivante lorsqu'elle est effectuée sur l'ensemble: Too many open files.

Il doit y avoir un moyen «facile» de faire cela, mais malheureusement je suis assez nouveau pour unix. Merci d'avance!

PS: Pour vous donner une idée de ce à quoi (un de mes) fichiers de données ressemble:

0.5
0.5
0.03825
0.5
10211.0457
10227.8469
-5102.5228
0.0742
3.0944
...
tapis
la source
Avez-vous essayé d'utiliser l' --serialoption avec la pastecommande?
shivams
@shivams paste --serialne fusionne pas les fichiers colonne par colonne ...
Stephen Kitt
@StephenKitt Attendez. Je suis un peu confus. Veut-il dire que dans le fichier de sortie, il a besoin d'une colonne différente pour les données de chaque fichier? Ou toutes les données dans une seule colonne?
shivams
@Stephen Kitt shivams Using paste -sfonctionne en effet, mais colle les fichiers de résultats séparés en ligne plutôt qu'en colonne. Cependant, c'est quelque chose que je peux résoudre. Merci!
tapis
@shivams Je veux une colonne différente pour les données de chaque fichier dans le fichier de sortie
mats

Réponses:

17

Si vous avez des autorisations root sur cette machine, vous pouvez augmenter temporairement la limite du "nombre maximum de descripteurs de fichiers ouverts":

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

Puis

paste res.* >final.res

Après cela, vous pouvez rétablir les valeurs d'origine.


Une deuxième solution , si vous ne pouvez pas modifier la limite:

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

Il appelle pastechaque fichier une fois, et à la fin il y a un énorme fichier avec toutes les colonnes (cela prend sa minute).

Edit : Utilisation inutile du chat ... Non !

Comme mentionné dans les commentaires, l'utilisation de cathere ( cat final.res | paste - $f >temp) n'est pas inutile. La première fois que la boucle s'exécute, le fichier final.resn'existe pas déjà. pasteéchouerait alors et le fichier n'est jamais rempli, ni créé. Avec ma solution catéchoue seulement la première fois avec No such file or directoryet pastelit depuis stdin juste un fichier vide, mais il continue. L'erreur peut être ignorée.

le chaos
la source
Merci! Une idée comment je peux vérifier quelles sont les valeurs d'origine?
tapis
Juste ulimit -Snpour la limite douce et ulimit -Hnpour la limite dure
chaos
Merci, cela fonctionne partiellement. Cependant, pour une autre série de fichiers que je reçois l'erreur suivante: -bash: /usr/bin/paste: Argument list too long. Des idées pour résoudre ce problème? Désolé de vous déranger les gars.
tapis
@mats semble que votre noyau n'autorise pas plus d'arguments, vous pouvez le vérifier avec getconf ARG_MAX, vous ne pouvez augmenter cette valeur que lors de la recompilation du noyau. Vous pouvez essayer ma deuxième solution?
chaos
2
Au lieu d'utiliser à catchaque fois la boucle, vous pouvez commencer par créer un final.resfichier vide . C'est probablement une bonne idée de toute façon, au cas où il y aurait déjà un final.resfichier.
Barmar
10

Si la réponse du chaos n'est pas applicable (car vous ne disposez pas des autorisations requises), vous pouvez regrouper les pasteappels comme suit:

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

Cela répertorie les fichiers 1000 à la fois dans des fichiers nommés lists00, lists01etc., puis colle les res.fichiers correspondants dans des fichiers nommés merge00, merge01etc., et fusionne finalement tous les fichiers partiellement fusionnés résultants.

Comme mentionné par le chaos, vous pouvez augmenter le nombre de fichiers utilisés simultanément; la limite est la valeur indiquée ulimit -nmoins le nombre de fichiers que vous avez déjà ouverts, vous diriez donc

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

d'utiliser la limite moins dix.

Si votre version de splitne prend pas en charge -d, vous pouvez la supprimer: il suffit de dire splitd'utiliser des suffixes numériques. Par défaut, les suffixes seront aa, abetc. au lieu de 01, 02etc.

S'il y a tellement de fichiers qui ls -1 res.*échouent ("liste d'arguments trop longue"), vous pouvez le remplacer par findce qui évitera cette erreur:

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

(Comme indiqué par don_crissti , cela -1ne devrait pas être nécessaire lors de lsla sortie de la tuyauterie ; mais je le laisse pour gérer les cas où lsest aliasé avec -C.)

Stephen Kitt
la source
4

Essayez de l'exécuter de cette façon:

ls res.*|xargs paste >final.res

Vous pouvez également diviser le lot en plusieurs parties et essayer quelque chose comme:

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

et à la fin, combiner les fichiers finaux

paste final.* >final.res
Roméo Ninov
la source
@ Romeo Ninov Cela donne la même erreur que celle que j'ai mentionnée dans ma question initiale:Too many open files
tapis
@mats, dans ce cas, avez-vous envisagé de diviser le lot en plusieurs parties.
Editera
À droite, @StephenKitt, je modifie ma réponse
Romeo Ninov
Pour éviter les fichiers temporaires, envisagez de créer les final.x00canaux be - soit en tant que FIFO nommés, soit implicitement, en utilisant la substitution de processus (si votre shell le prend en charge - par exemple bash). Ce n'est pas amusant d'écrire à la main, mais pourrait bien convenir à un makefile.
Toby Speight
4
i=0
{ paste res.? res.?? res.???
while paste ./res."$((i+=1))"[0-9][0-9][0-9]
do :; done; } >outfile

Je ne pense pas que ce soit aussi compliqué que cela - vous avez déjà fait le gros travail en commandant les noms de fichiers. Ne les ouvrez pas tous en même temps, c'est tout.

Autrement:

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

... mais je pense que cela les fait reculer ... Cela pourrait mieux fonctionner:

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

Et voici encore une autre façon:

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

Cela permet tarde rassembler pour vous tous les fichiers dans un flux délimité par des valeurs nulles, d'analyser toutes ses métadonnées d'en-tête sauf le nom de fichier et de transformer toutes les lignes de tous les fichiers en onglets. Il s'appuie cependant sur l'entrée comme étant des fichiers texte réels - ce qui signifie que chaque extrémité se termine par une nouvelle ligne et qu'il n'y a pas d'octets nuls dans les fichiers. Oh - et cela repose également sur le fait que les noms de fichiers eux-mêmes sont sans nouvelle ligne (bien que cela puisse être géré de manière robuste avec tarl' --xformoption GNU ) . Étant donné que ces conditions sont remplies, il devrait faire très peu de travail sur n'importe quel nombre de fichiers - et tarfera presque tout.

Le résultat est un ensemble de lignes qui ressemblent à:

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

Etc.

Je l'ai testé en créant d'abord 5 fichiers de test. Je n'avais pas vraiment envie de créer 10000 fichiers à l'instant, alors je suis juste allé un peu plus gros pour chacun - et j'ai également veillé à ce que la longueur des fichiers diffère considérablement. Ceci est important lors du test de tarscripts car taril bloquera l'entrée à des longueurs fixes - si vous n'essayez pas au moins quelques longueurs différentes, vous ne saurez jamais si vous ne gérerez réellement que celle-là.

Quoi qu'il en soit, pour les fichiers de test, j'ai fait:

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

ls a ensuite rapporté:

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

... puis j'ai couru ...

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

... juste pour n'afficher que les 25 premiers champs délimités par tabulation par ligne (car chaque fichier est une seule ligne - il y en a beaucoup ) ...

Le résultat était:

./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
la source
4

Compte tenu de la quantité de fichiers, de tailles de lignes, etc. impliqués, je pense que cela dépassera les tailles par défaut des outils (awk, sed, paste, *, etc.)

Je créerais un petit programme pour cela, il n'aurait ni 10 000 fichiers ouverts, ni une ligne de centaines de milliers de long (10 000 fichiers de 10 (taille max de ligne dans l'exemple)). Il ne nécessite qu'un ~ 10 000 tableau d'entiers, pour stocker le nombre d'octets lus dans chaque fichier. L'inconvénient est qu'il n'a qu'un seul descripteur de fichier, il est réutilisé pour chaque fichier, pour chaque ligne, et cela peut être lent.

Les définitions de FILESet ROWSdoivent être remplacées par les valeurs exactes réelles. La sortie est envoyée à la sortie standard.

#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
la source