Combinando grande quantidade de arquivos

15

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

paste res.*

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

Deve haver uma maneira 'fácil' de fazer isso, mas infelizmente sou novo no unix. Desde já, obrigado!

PS: Para ter uma idéia de como (um dos meus) arquivos de dados se parece:

0.5
0.5
0.03825
0.5
10211.0457
10227.8469
-5102.5228
0.0742
3.0944
...
tapetes
fonte
Você tentou usar a --serialopção com o pastecomando?
Shivams
@shivams paste --serialnão mescla arquivos coluna sábio ...
Stephen Kitt
@StephenKitt Wait. Estou um pouco confuso. Ele quer dizer que, no arquivo de saída, ele precisa de uma coluna diferente para os dados de cada arquivo? Ou todos os dados em uma única coluna?
Shivams
@Stephen Kitt shivams Usar paste -srealmente funciona, mas cola os arquivos de resultados separados em linhas, em vez de em colunas. No entanto, isso é algo que posso resolver. Obrigado!
esteiras
@shivams Quero uma coluna diferente para os dados de cada arquivo no arquivo de saída
esteiras

Respostas:

17

Se você possui permissões de root nessa máquina, pode aumentar temporariamente o limite "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 configurá-lo de volta aos valores originais.


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

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

Ele chama pastecada arquivo uma vez e, no final, existe um arquivo enorme com todas as colunas (leva um minuto).

Edit : Uso inútil de gato ... Não !

Como mencionado nos comentários, o uso de cathere ( cat final.res | paste - $f >temp) não é inútil. A primeira vez que o loop é executado, o arquivo final.resainda não existe. pastefalhará e o arquivo nunca será preenchido nem criado. Com a minha solução, apenas catfalha na primeira vez No such file or directorye pastelê do stdin apenas um arquivo vazio, mas continua. O erro pode ser ignorado.

caos
fonte
Obrigado! Alguma idéia de como posso verificar quais são os valores originais?
esteiras
Apenas ulimit -Snpara o limite suave e ulimit -Hnpara o limite rígido
caos
Obrigado, isso funciona parcialmente. No entanto, para um outro conjunto de arquivos eu recebo o seguinte erro: -bash: /usr/bin/paste: Argument list too long. Idéias como resolver isso? Desculpe por incomodar vocês.
esteiras
@mats parece que o seu kernel não permite mais argumentos, você pode checá-lo getconf ARG_MAX, você só pode aumentar esse valor ao recompilar o kernel. Você pode tentar minha segunda solução?
caos
2
Em vez de usar catsempre o loop, você pode começar criando um final.resarquivo vazio . Provavelmente, essa é uma boa ideia, caso já final.resexista um arquivo.
Barmar 27/05
10

Se a resposta do caos não for aplicável (porque você não possui as permissões necessárias), você pode agrupar as pastechamadas da seguinte maneira:

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

Esta lista os arquivos 1000 em um momento em arquivos chamados lists00, lists01etc., em seguida, cola os correspondentes res.arquivos em arquivos chamados merge00, merge01etc., e, finalmente, funde-se todos os arquivos resultantes parcialmente fundidas.

Conforme mencionado pelo caos, você pode aumentar o número de arquivos usados ​​de uma só vez; o limite é o valor fornecido ulimit -nmenos os arquivos que você já abriu, 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 do splitnão suportar -d, você poderá removê-lo: splitbasta digitar o sufixo numérico. Por padrão, os sufixos será aa, abetc., em vez de 01, 02etc.

Se houver tantos arquivos que ls -1 res.*falharem ("lista de argumentos muito longa"), você poderá substituí-lo pelo findque evitará esse erro:

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

(Como apontado por don_crissti , -1não deve ser necessário quando lsa saída da tubulação ; mas estou deixando isso para lidar com casos em que lshá alias -C.)

Stephen Kitt
fonte
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 combine os arquivos finais

paste final.* >final.res
Romeo Ninov
fonte
@ Romeo Ninov Isto dá o mesmo erro que eu já relatado na minha pergunta inicial:Too many open files
esteiras
@mats, nesse caso, você considera dividir o lote em partes. Vai editar a minha resposta para lhe dar ideia
Romeo Ninov
Certo, @StephenKitt, eu edito minha resposta #
Romeo Ninov
Para evitar os arquivos temporários, considere criar os final.x00canais "be" - ou como FIFOs nomeados, ou implicitamente, usando a substituição de processo (se o seu shell suportar - por exemplo, bash). Não é divertido escrever à mão, mas pode ser adequado a um makefile.
Toby Speight
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 ordenando os nomes dos arquivos. Só 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 é feito ao contrário ... 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 está outra maneira:

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

Isso permite tarreunir todos os arquivos em um fluxo delimitado por nulo para você, analisa todos os metadados do cabeçalho, exceto o nome do arquivo, e transforma todas as linhas em todos os arquivos em guias. No entanto, ele depende da entrada como 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 conta que os nomes de arquivos são livres de novas linhas (embora isso possa ser tratado com robustez com tara --xformopção do GNU ) . Dadas essas condições, ele deve fazer um trabalho muito curto de qualquer número de arquivos - e tarfará quase tudo.

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

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

E assim por diante.

Eu testei criando primeiro 5 arquivos de teste. Eu realmente não estava com vontade de gerar 10000 arquivos agora, então fiquei um pouco maior para cada um - e também garanti que os comprimentos dos arquivos diferissem bastante. Isso é importante ao testar tarscripts, porque tarbloqueará a entrada em comprimentos fixos - se você não tentar pelo menos alguns comprimentos diferentes, nunca saberá se realmente lidará com apenas um.

Enfim, para os arquivos de teste que fiz:

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

ls depois relatou:

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

... então eu corri ...

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

... apenas para mostrar apenas os primeiros 25 campos delimitados por tabulação por linha (porque cada arquivo é uma única linha - há muito ) ...

A saída foi:

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

Dada a quantidade de arquivos, tamanhos de linha, etc. envolvidos, acho que ultrapassará o tamanho padrão das ferramentas (awk, sed, paste, *, etc)

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

As definições de FILESe ROWSdevem 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 */
   }
}
Laurence R. Ugalde
fonte