Combinazione di grandi quantità di file


15

Ho ± 10.000 file ( res.1- res.10000) tutti costituiti da una colonna e un numero uguale di righe. Quello che voglio è, in sostanza, semplice; unire tutti i file in ordine di colonna in un nuovo file final.res. Ho provato ad usare:

paste res.*

Tuttavia (anche se questo sembra funzionare per un piccolo sottoinsieme di file dei risultati, questo dà il seguente errore quando eseguito su tutto l'insieme: Too many open files.

Deve esserci un modo 'semplice' per farlo, ma sfortunatamente sono abbastanza nuovo per unix. Grazie in anticipo!

PS: Per darti un'idea di come (uno dei miei) file di dati assomigli:

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

Hai provato a usare l' --serialopzione con il pastecomando?
Shivams,

@shivams paste --serialnon unisce i file per quanto riguarda le colonne ...
Stephen Kitt,

@StephenKitt Wait. Sono leggermente confuso. Vuol dire che nel file di output ha bisogno di una colonna diversa per i dati di ciascun file? O tutti i dati in una singola colonna?
Shivams,

@Stephen Kitt shivams L'utilizzo di paste -seffettivamente funziona, ma incolla i file dei risultati separati per riga anziché per colonna. Tuttavia, questo è qualcosa che posso risolvere. Grazie!
stuoie

@shivams Voglio una colonna diversa per i dati di ciascun file nel file di output
tappetini

Risposte:


17

Se disponi delle autorizzazioni di root su quella macchina, puoi temporaneamente aumentare il limite "numero massimo di descrittori di file aperti":

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

E poi

paste res.* >final.res

Successivamente è possibile ripristinarlo ai valori originali.


Una seconda soluzione , se non è possibile modificare il limite:

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

Richiede pasteogni file una volta e alla fine c'è un file enorme con tutte le colonne (ci vuole il suo minuto).

Modifica : uso inutile del gatto ... No !

Come menzionato nei commenti, l'uso di cathere ( cat final.res | paste - $f >temp) non è inutile. Alla prima esecuzione del ciclo, il file final.resnon esiste già. pastefallirebbe e il file non verrà mai compilato, né creato. Con la mia soluzione catfallisce solo la prima volta No such file or directorye pastelegge dallo stdin solo un file vuoto, ma continua. L'errore può essere ignorato.


Grazie! Qualche idea su come posso verificare quali sono i valori originali?
stuoie

Solo ulimit -Snper soft limit e ulimit -Hnper hard limit
caos,

Grazie, questo funziona parzialmente. Tuttavia, per un altro insieme di file ottengo il seguente errore: -bash: /usr/bin/paste: Argument list too long. Idee come risolverlo? Mi dispiace disturbarla ragazzi.
stuoie

@mats sembra che il tuo kernel non consenta più argomenti, puoi verificarlo getconf ARG_MAX, puoi solo aumentare quel valore quando ricompili il kernel. Puoi provare la mia seconda soluzione?
caos,

2
Invece di utilizzare catogni volta attraverso il ciclo, è possibile iniziare creando un final.resfile vuoto . Questa è probabilmente una buona idea in ogni caso, nel caso ci sia già un final.resfile lì.
Barmar,

10

Se la risposta del caos non è applicabile (perché non si dispone delle autorizzazioni necessarie), è possibile raggruppare le pastechiamate come segue:

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

Questo elenca i file 1000 alla volta in file denominati lists00, lists01ecc., Quindi incolla i res.file corrispondenti in file denominati merge00, merge01ecc. E infine unisce tutti i file risultanti parzialmente uniti.

Come accennato dal caos è possibile aumentare il numero di file utilizzati contemporaneamente; il limite è il valore dato ulimit -nmeno comunque molti file che hai già aperto, quindi diresti

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

per usare il limite meno dieci.

Se la tua versione di splitnon supporta -d, puoi rimuoverla: tutto ciò che fa è dire splitdi usare suffissi numerici. Per impostazione predefinita, i suffissi saranno aa, abecc. Anziché 01, 02ecc.

Se ci sono così tanti file che ls -1 res.*falliscono ("elenco argomenti troppo lungo"), puoi sostituirlo con il findquale eviterà quell'errore:

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

(Come sottolineato da don_crissti , -1non dovrebbe essere necessario durante lsl'output del piping ; ma lo sto lasciando per gestire i casi in cui lsè aliasato -C.)


4

Prova a eseguirlo in questo modo:

ls res.*|xargs paste >final.res

Puoi anche dividere il batch in parti e provare qualcosa del tipo:

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

e alla fine combinare i file finali

paste final.* >final.res

@ Romeo Ninov Questo dà lo stesso errore che ho menzionato nella mia domanda iniziale:Too many open files
tappetini

@mats, in tal caso hai considerato di dividere il batch in parti. Modificherò la mia risposta per darti un'idea
Romeo Ninov,

Bene, @StephenKitt, modifico la mia risposta
Romeo Ninov,

Per evitare i file temporanei, prendi in considerazione la creazione di final.x00pipe - come denominati FIFO o implicitamente, usando la sostituzione del processo (se la tua shell lo supporta - ad es. Bash). Non è divertente scrivere a mano, ma potrebbe adattarsi a 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

Non penso che sia così complicato - hai già fatto il duro lavoro ordinando i nomi dei file. Basta non aprirli tutti contemporaneamente, tutto qui.

Un altro modo:

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

... ma penso che li faccia all'indietro ... Questo potrebbe funzionare meglio:

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

Ed ecco ancora un altro modo:

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

Ciò consente tardi raccogliere tutti i file in un flusso delimitato da zero per te, analizza tutti i suoi metadati di intestazione tranne il nome file e trasforma tutte le righe di tutti i file in schede. Si basa sul fatto che l'input sia un vero e proprio file di testo, il che significa che ognuno termina con una nuova riga e non ci sono byte nulli nei file. Oh - e si basa anche sul fatto che i nomi dei file stessi siano privi di newline (sebbene ciò possa essere gestito in modo robusto con tarl' --xformopzione GNU ) . Dato che queste condizioni sono soddisfatte, dovrebbe rendere molto breve qualsiasi numero di file - e tarfarà quasi tutto.

Il risultato è un insieme di linee che sembrano:

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

E così via.

L'ho provato creando prima 5 file di test. In realtà non mi andava di generare 10000 file in questo momento, quindi sono andato un po 'più grande per ciascuno di essi e ho anche assicurato che le lunghezze dei file differivano di molto. Questo è importante durante il test degli tarscript perché tarbloccherà l'input a lunghezze fisse - se non provi almeno alcune lunghezze diverse non saprai mai se in realtà gestirai solo quella.

Ad ogni modo, per i file di test ho fatto:

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

ls successivamente riportato:

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

... poi ho corso ...

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

... solo per mostrare solo i primi 25 campi delimitati da tabulazioni per riga (poiché ogni file è una singola riga - ce ne sono molti ) ...

L'output è stato:

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

Data la quantità di file, dimensioni delle linee, ecc. Coinvolti, penso che supererà le dimensioni predefinite degli strumenti (awk, sed, paste, *, ecc.)

Vorrei creare un piccolo programma per questo, non avrebbe né 10.000 file aperti, né una lunghezza di centinaia di migliaia (10.000 file di 10 (dimensione massima della linea nell'esempio)). Richiede solo una matrice di ~ 10.000 di numeri interi, per memorizzare il numero di byte letti da ciascun file. Lo svantaggio è che ha un solo descrittore di file, viene riutilizzato per ogni file, per ogni riga e questo potrebbe essere lento.

Le definizioni di FILESe ROWSdevono essere modificate con i valori esatti effettivi. L'output viene inviato all'output 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 */
   }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.