Объединение большого количества файлов

15

У меня есть ± 10000 файлов ( res.1- res.10000), все состоящие из одного столбца и равного количества строк. То, что я хочу, по сути, просто; объединить все файлы по столбцам в новый файл final.res. Я пытался с помощью:

paste res.*

Однако (хотя это , кажется, работает для небольшого подмножества результирующих файлов, это дает следующее сообщение об ошибке , когда выполняется на всем множестве: Too many open files.

Должен быть «простой» способ сделать это, но, к сожалению, я совсем новичок в Unix. Заранее спасибо!

PS: Чтобы дать вам представление о том, как (один из моих) файлов данных выглядит:

0.5
0.5
0.03825
0.5
10211.0457
10227.8469
-5102.5228
0.0742
3.0944
...
коврики
источник
Вы пытались использовать --serialопцию с pasteкомандой?
Шивамс
@shivams paste --serialне объединяет файлы по столбцам ...
Стивен Китт
@StephenKitt Подождите. Я немного смущен. Имеет ли он в виду, что в выходном файле ему нужны разные столбцы для данных каждого файла? Или все данные в одном столбце?
Шивамс
@Stephen Kitt Shivams Использование paste -sдействительно работает, но вставляет отдельные файлы результатов по строкам, а не по столбцам. Тем не менее, это то, что я могу решить. Благодарность!
коврики
@shivams Я хочу другой столбец для данных каждого файла в выходном файле
маты

Ответы:

17

Если у вас есть права root на этом компьютере, вы можете временно увеличить ограничение «максимальное количество дескрипторов открытых файлов»:

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

А потом

paste res.* >final.res

После этого вы можете вернуть его к исходным значениям.


Второе решение , если вы не можете изменить ограничение:

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

Он вызывает pasteкаждый файл один раз, и в конце появляется огромный файл со всеми столбцами (это занимает минуту).

Редактировать : Бесполезное использование кошки ... Не !

Как уже упоминалось в комментариях, использование catздесь ( cat final.res | paste - $f >temp) не бесполезно. При первом запуске цикла файл final.resеще не существует. pasteзатем потерпит неудачу, и файл никогда не будет ни заполнен, ни создан. С моим решением catне удается только в первый раз No such file or directoryи pasteчитает из стандартного ввода только пустой файл, но это продолжается. Ошибка может быть проигнорирована.

хаос
источник
Благодарность! Любая идея, как я могу проверить, каковы исходные значения?
коврики
Просто ulimit -Snдля мягкого ограничения и ulimit -Hnдля жесткого ограничения
хаос
Спасибо, это частично работает. Однако, для другого набора файлов , которые я получаю следующее сообщение об ошибке: -bash: /usr/bin/paste: Argument list too long. Идеи как это решить? Извините, что беспокою вас, ребята.
коврики
@mats кажется, ваше ядро ​​не допускает больше аргументов, вы можете проверить это getconf ARG_MAX, вы можете только увеличить это значение при перекомпиляции ядра. Вы можете попробовать мое второе решение?
хаос
2
Вместо того, чтобы использовать catкаждый раз в цикле, вы можете начать с создания пустого final.resфайла. В любом случае, это хорошая идея, если там уже есть final.resфайл.
Бармар
10

Если ответ хаоса неприменим (поскольку у вас нет необходимых разрешений), вы можете разделить pasteвызовы следующим образом:

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

Этот список файлов 1000 в то время , в файлы с именами lists00, и lists01т.д., а затем вставляет соответствующие res.файлы в файлы с именамиmerge00 , и merge01т.д., и , наконец , объединяет все полученные частично объединены файлы.

Как уже упоминалось, хаос вы можете увеличить количество файлов, используемых одновременно; предел - это заданное значение ulimit -nминус сколько файлов у вас уже открыто, так что вы бы сказали

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

использовать лимит минус десять.

Если ваша версия splitне поддерживает -d, вы можете удалить ее: все, что она делает, это говорит splitиспользовать числовые суффиксы. По умолчанию суффиксы будет aa, и abт.д. , а не 01, и 02т.д.

Если существует так много файлов, которые ls -1 res.*терпят неудачу («список аргументов слишком длинный»), вы можете заменить его, findчтобы избежать этой ошибки:

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

(Как указывает don_crissti , он -1не должен быть необходим при lsвыводе данных по конвейеру ; но я оставляю его для обработки случаев, когда lsиспользуется псевдоним -C.)

Стивен Китт
источник
4

Попробуйте выполнить это следующим образом:

ls res.*|xargs paste >final.res

Вы также можете разбить партию на части и попробовать что-то вроде:

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

и в конце объединить окончательные файлы

paste final.* >final.res
Ромео Нинов
источник
@ Ромео Нинов. Это дает ту же ошибку, о которой я говорил в своем первоначальном вопросе:Too many open files
коврики
@ Mats, в таком случае вы решили разделить партию по частям. Отредактирую мой ответ, чтобы дать вам представление
Ромео Нинов
Правильно, @StephenKitt, я редактирую свой ответ
Ромео Нинов
Чтобы избежать временных файлов, рассмотрите возможность создания final.x00be-каналов - как именованных FIFO, так и неявно, используя подстановку процессов (если ваша оболочка это поддерживает - например, bash). Это не весело писать вручную, но вполне может подойти для make-файла.
Тоби Спейт
4
i=0
{ paste res.? res.?? res.???
while paste ./res."$((i+=1))"[0-9][0-9][0-9]
do :; done; } >outfile

Я не думаю, что это так сложно, как все - вы уже проделали тяжелую работу, заказав имена файлов. Только не открывайте их все одновременно, вот и все.

Другой путь:

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

... но я думаю, что это делает их в обратном направлении ... Это может работать лучше:

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

И вот еще один способ:

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

Это позволяет tarсобрать все файлы в поток с нулевым разделением для вас, проанализировать все метаданные заголовка, кроме имени файла, и преобразовать все строки во всех файлах во вкладки. Тем не менее, он полагается на то, что входные данные являются фактическими текстовыми файлами, то есть каждый из них заканчивается символом новой строки и в файлах нет нулевых байтов. Да, и это также зависит от того, что сами имена файлов не имеют перевода строки (хотя это может быть надежно обработано tarс помощью --xformопции GNU ) . Учитывая эти условия, он должен очень быстро работать с любым количеством файлов - и tarсделает почти все.

Результатом является набор строк, которые выглядят так:

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

И так далее.

Я проверил это, сначала создав 5 тестовых файлов. На самом деле я не чувствовал, что сейчас создаю 10000 файлов, поэтому я стал немного больше для каждого - и также убедился, что длины файлов сильно различаются. Это важно при тестированииtar сценариев, поскольку tarблокирует ввод данных фиксированной длины - если вы не попробуете хотя бы несколько разных длин, вы никогда не узнаете, будете ли вы обрабатывать только одну из них.

Во всяком случае, для тестовых файлов я сделал:

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

ls впоследствии сообщили:

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

... тогда я побежал ...

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

... просто чтобы показать только первые 25 полей с разделителями табуляции в строке (потому что каждый файл - это одна строка - их много ) ...

Выход был:

./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
источник
4

Учитывая количество файлов, размеры строк и т. Д., Я думаю, что оно превзойдет размеры инструментов по умолчанию (awk, sed, paste, * и т. Д.)

Я бы создал для этого небольшую программу, в которой не было бы ни 10 000 открытых файлов, ни строки длиной в сотни тысяч (10 000 файлов из 10 (максимальный размер строки в примере)). Для хранения количества байтов, считанных из каждого файла, требуется всего ~ 10000 целых чисел. Недостатком является то, что он имеет только один дескриптор файла, он используется повторно для каждого файла, для каждой строки, и это может быть медленным.

Определения FILESи ROWSдолжны быть изменены на фактические точные значения. Вывод отправляется на стандартный вывод.

#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 */
   }
}
Лоуренс Р. Угалде
источник