Combinando gran cantidad de archivos

15

Tengo ± 10,000 archivos ( res.1- res.10000) todos consistentes en una columna y un número igual de filas. Lo que quiero es, en esencia, simple; fusionar todos los archivos en columnas en un nuevo archivo final.res. He intentado usar:

paste res.*

Sin embargo (aunque esto parece funcionar para un pequeño subconjunto de archivos de resultados, esto da el siguiente error cuando se realiza en todo el conjunto: Too many open files.

Debe haber una forma 'fácil' de hacer esto, pero desafortunadamente soy bastante nuevo en Unix. ¡Gracias por adelantado!

PD: para darle una idea de cómo se ve (uno de mis) archivos de datos:

0.5
0.5
0.03825
0.5
10211.0457
10227.8469
-5102.5228
0.0742
3.0944
...
esteras
fuente
¿Intentaste usar la --serialopción con el pastecomando?
Shivams
@shivams paste --serialno combina archivos en columnas ...
Stephen Kitt
@StephenKitt Espera. Estoy un poco confundido ¿Quiere decir que en el archivo de salida, necesita una columna diferente para los datos de cada archivo? ¿O todos los datos en una sola columna?
Shivams
@Stephen Kitt shivams El uso de paste -shecho funciona, pero pega los archivos de resultados separados en fila en lugar de columna. Sin embargo, esto es algo que puedo resolver. ¡Gracias!
esteras
@shivams Quiero una columna diferente para los datos de cada archivo en el archivo de salida
esteras

Respuestas:

17

Si tiene permisos de root en esa máquina, puede aumentar temporalmente el límite de "número máximo de descriptores de archivo abiertos":

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

Y entonces

paste res.* >final.res

Después de eso, puede restablecerlo a los valores originales.


Una segunda solución , si no puede cambiar el límite:

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

Llama pastea cada archivo una vez, y al final hay un archivo enorme con todas las columnas (toma su minuto).

Editar : Uso inútil del gato ... ¡ No !

Como se menciona en los comentarios, el uso de cathere ( cat final.res | paste - $f >temp) no es inútil. La primera vez que se ejecuta el bucle, el archivo final.resya no existe. pasteentonces fallaría y el archivo nunca se llenará, ni se creará. Con mi solución solo catfalla la primera vez No such file or directoryy pastelee de stdin solo un archivo vacío, pero continúa. El error puede ser ignorado.

caos
fuente
¡Gracias! ¿Alguna idea de cómo puedo verificar cuáles son los valores originales?
alfombras de
Solo ulimit -Snpor límite suave y ulimit -Hnpor límite duro
caos
Gracias, esto funciona parcialmente. Sin embargo, por otro conjunto de archivos me sale el siguiente error: -bash: /usr/bin/paste: Argument list too long. Ideas de cómo resolver esto? Perdón por molestarlos.
alfombras de
@mats parece que su núcleo no permite más argumentos, puede verificarlo getconf ARG_MAX, solo puede aumentar ese valor al volver a compilar el núcleo. ¿Puedes probar mi segunda solución?
caos
2
En lugar de usar catcada vez a través del ciclo, puede comenzar creando un final.resarchivo vacío . Probablemente sea una buena idea, en caso de que ya haya un final.resarchivo allí.
Barmar
10

Si la respuesta del caos no es aplicable (porque no tiene los permisos requeridos), puede agrupar las pastellamadas de la siguiente manera:

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

Esto enumera los archivos 1000 a la vez en archivos llamados lists00,lists01 etc., luego pega los res.archivos correspondientes en archivos con nombre merge00, merge01etc., y finalmente combina todos los archivos parcialmente fusionados resultantes.

Como lo menciona el caos , puede aumentar la cantidad de archivos utilizados a la vez; el límite es el valor dado ulimit -nmenos la cantidad de archivos que ya tiene abiertos, por lo que diría

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

usar el límite menos diez.

Si su versión de splitno es compatible -d, puede eliminarla: todo lo que hace es indicarle splitque use sufijos numéricos. Por defecto, los sufijos serán aa, abetc. en lugar de 01, 02etc.

Si hay tantos archivos que ls -1 res.*fallan ("lista de argumentos demasiado larga"), puede reemplazarla confind que evitará ese error:

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

(Como lo señaló don_crissti , -1no debería ser necesario al canalizar lsla salida; pero lo dejo para manejar los casos en los que lstiene un alias -C).

Stephen Kitt
fuente
4

Intenta ejecutarlo de esta manera:

ls res.*|xargs paste >final.res

También puede dividir el lote en partes y probar algo como:

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

y al final combinar archivos finales

paste final.* >final.res
Romeo Ninov
fuente
@ Romeo Ninov Esto da el mismo error que mencioné en mi pregunta inicial:Too many open files
esteras
@mats, en tal caso, ha considerado dividir el lote en partes. Editaré mi respuesta para darle una idea
Romeo Ninov
Correcto, @StephenKitt, edito mi respuesta
Romeo Ninov
Para evitar los archivos temporales, considere hacer las final.x00canalizaciones, ya sea como denominadas FIFO, o implícitamente, utilizando la sustitución del proceso (si su shell lo admite, por ejemplo, bash). No es divertido escribirlo a mano, pero puede adaptarse a un archivo MAKE.
Toby Speight
4
i=0
{ paste res.? res.?? res.???
while paste ./res."$((i+=1))"[0-9][0-9][0-9]
do :; done; } >outfile

No creo que esto sea tan complicado como todo eso: ya has hecho el trabajo duro ordenando los nombres de archivo. Simplemente no los abras a todos al mismo tiempo, eso es todo.

De otra manera:

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

... pero creo que eso los hace al revés ... Esto podría funcionar mejor:

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

Y aquí hay otra forma:

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

Eso permite tarreunir todos los archivos en una secuencia delimitada por nulos, analiza todos sus metadatos de encabezado pero el nombre de archivo y transforma todas las líneas en todos los archivos en pestañas. Sin embargo, se basa en que la entrada es archivos de texto reales, lo que significa que cada uno termina con una nueva línea y no hay bytes nulos en los archivos. Ah, y también depende de que los nombres de los archivos estén libres de nueva línea (aunque eso podría manejarse de manera sólida con tarla --xformopción de GNU ) . Dado que se cumplen estas condiciones, debería hacer un trabajo muy corto de cualquier número de archivos, ytar lo hará casi todo.

El resultado es un conjunto de líneas que se ven así:

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

Y así.

Lo probé creando primero 5 archivos de prueba. Realmente no tenía ganas de generar archivos 10000 en este momento, así que solo fui un poco más grande para cada uno, y también me aseguré de que las longitudes de los archivos diferían en gran medida. Esto es importante cuando se prueban tarscripts porquetar bloqueará la entrada a longitudes fijas; si no prueba al menos algunas longitudes diferentes, nunca sabrá si realmente manejará solo una.

De todos modos, para los archivos de prueba que hice:

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

ls luego informó:

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

... entonces corrí ...

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

... solo para mostrar solo los primeros 25 campos delimitados por tabulaciones por línea (porque cada archivo es una sola línea, hay muchos ) ...

El resultado fue:

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

Dada la cantidad de archivos, tamaños de línea, etc. involucrados, creo que superará los tamaños predeterminados de las herramientas (awk, sed, paste, *, etc.)

Crearía un pequeño programa para esto, no tendría 10,000 archivos abiertos, ni una línea de cientos de miles de longitud (10,000 archivos de 10 (tamaño máximo de línea en el ejemplo)). Solo requiere una matriz de enteros de ~ 10,000, para almacenar el número de bytes que se han leído de cada archivo. La desventaja es que solo tiene un descriptor de archivo, se reutiliza para cada archivo, para cada línea, y esto podría ser lento.

Las definiciones de FILESy ROWSdeben cambiarse a los valores exactos reales. La salida se envía a la salida estándar.

#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
fuente