Combinando grande quantidade de arquivos

14

Eu tenho ± 10.000 arquivos ( res.1 - res.10000 ) todos consistindo de uma coluna e um número igual de linhas. O que eu quero é, em essência, simples; mesclar todos os arquivos em coluna em um novo arquivo final.res . Eu tentei usar:

paste res.*

No entanto (embora isso pareça funcionar para um pequeno subconjunto de arquivos de resultados, isso dá o seguinte erro quando executado em todo o conjunto: Too many open files .

Deve haver uma maneira "fácil" de fazer isso, mas infelizmente sou novo no unix. Obrigado antecipadamente!

PS: Para ter uma ideia de como (um dos meus) datafile (s) se parece:

0.5
0.5
0.03825
0.5
10211.0457
10227.8469
-5102.5228
0.0742
3.0944
...
    
por mats 26.05.2015 / 11:42

5 respostas

15

Se você tiver permissões de root nessa máquina, poderá aumentar temporariamente o limite de "número máximo de descritores de arquivos abertos":

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

e depois

paste res.* >final.res

Depois disso, você pode defini-lo de volta para os valores originais.

Uma segunda solução , se você não puder alterar o limite:

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

Ele chama paste para cada arquivo uma vez e, no final, há um arquivo enorme com todas as colunas (leva o minuto).

Editar : Uso inútil de cat ... Não !

Como mencionado nos comentários, o uso de cat here ( cat final.res | paste - $f >temp ) não é inútil. Na primeira vez em que o loop é executado, o arquivo final.res ainda não existe. paste falharia e o arquivo nunca seria preenchido nem criado. Com a minha solução, apenas cat falha na primeira vez com No such file or directory e paste lê de stdin apenas um arquivo vazio, mas continua. O erro pode ser ignorado.

    
por 26.05.2015 / 12:06
10

Se a resposta caos 'não for aplicável (porque você não tem as permissões necessárias), você pode agrupar as chamadas paste da seguinte forma:

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

Isso lista os arquivos 1000 por vez em arquivos denominados lists00 , lists01 etc., depois cola os arquivos res. correspondentes em arquivos denominados merge00 , merge01 etc. e, finalmente, mescla todos os arquivos arquivos parcialmente mesclados resultantes.

Como mencionado pelo caos , você pode aumentar o número de arquivos usados de uma só vez; o limite é o valor dado ulimit -n menos quantos arquivos você já tiver aberto, então você diria

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

para usar o limite menos dez.

Se a sua versão de split não der suporte a -d , você poderá removê-la: tudo o que ela faz é informar split para usar sufixos numéricos. Por padrão, os sufixos serão aa , ab etc. em vez de 01 , 02 etc.

Se houver tantos arquivos que ls -1 res.* falha ("lista de argumentos longa demais"), você pode substituí-lo por find , o que evitará esse erro:

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

(Como foi apontado por don_crissti , -1 não deve ser necessário quando o piping ls de saída, mas estou deixando isso para lidar com casos em que ls está com alias em -C .)

    
por 26.05.2015 / 12:12
4

Tente executá-lo desta maneira:

ls res.*|xargs paste >final.res

Você também pode dividir o lote em partes e tentar algo como:

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

e no final combinar arquivos finais

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

Eu não acho que isso seja tão complicado quanto tudo isso - você já fez o trabalho duro ao pedir os nomes dos arquivos. Apenas não abra todos eles ao mesmo tempo, é tudo.

Outra maneira:

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

... mas acho que isso os faz para trás ... Isso pode funcionar melhor:

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

E aqui ainda está outra forma:

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

Isso permite que tar reúna todos os arquivos em um fluxo delimitado por nulo, analisa todos os metadados de cabeçalho, mas o nome do arquivo, e transforma todas as linhas em todos os arquivos em guias. No entanto, ele depende da entrada de arquivos de texto reais - o que significa que cada um termina com uma nova linha e não há bytes nulos nos arquivos. Ah - e também depende dos próprios nomes dos arquivos serem sem nova linha (embora isso possa ser manuseado de forma robusta com a opção tar do% co do GNU%) . Se essas condições forem atendidas, o trabalho de qualquer número de arquivos será muito curto - e --xform fará quase tudo.

O resultado é um conjunto de linhas que se parecem com:

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

E assim por diante.

Eu testei primeiro criando 5 arquivos de teste. Eu realmente não me sinto com vontade de fazer 10000 arquivos agora, então eu fui um pouco maior para cada um - e também assegurei que o arquivo comprimentos diferisse bastante. Isso é importante ao testar tar scripts porque tar bloqueará a entrada para comprimentos fixos - se você não tentar pelo menos alguns comprimentos diferentes, nunca saberá se realmente lidará com apenas um.

De qualquer forma, para os arquivos de teste que fiz:

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

tar informado posteriormente:

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

... então eu corri ...

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

... apenas para mostrar apenas os primeiros 25 campos delimitados por tabulações por linha (porque cada arquivo é uma única linha - há um lote ) ...

A saída foi:

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

Dada a quantidade de arquivos, tamanhos de linhas, etc. envolvidos, eu acho que ele superará os tamanhos padrão de ferramentas (awk, sed, paste, *, etc)

Eu criaria um pequeno programa para isso, não teria nem 10.000 arquivos abertos, nem uma linha de centenas de milhares de comprimentos (10.000 arquivos de 10 (tamanho máximo da linha no exemplo)). Requer apenas um conjunto de 10.000 números inteiros, para armazenar o número de bytes que foram lidos de cada arquivo. A desvantagem é que ele tem apenas um descritor de arquivo, ele é reutilizado para cada arquivo, para cada linha, e isso pode ser lento.

As definições de FILES e ROWS devem ser alteradas para os valores exatos reais. A saída é enviada para a saída padrão.

#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 */
   }
}
    
por 26.05.2015 / 21:13