Как эффективно разделить большой текстовый файл без разделения многострочных записей?

9

У меня большой текстовый файл (~ 50Gb, когда gz'ed). Файл содержит 4*Nстроки или Nзаписи; то есть каждая запись состоит из 4 строк. Я хотел бы разделить этот файл на 4 файла поменьше, каждый размером примерно 25% от входного файла. Как я могу разделить файл на границе записи?

Наивным подходом было бы zcat file | wc -lполучить количество строк, разделить это число на 4 и затем использовать split -l <number> file. Тем не менее, это происходит по файлу дважды, и счетчик строк очень медленный (36 минут). Есть ли способ лучше?

Это близко, но это не то, что я ищу. Принятый ответ также делает счетчик строк.

РЕДАКТИРОВАТЬ:

Файл содержит данные последовательности в формате fastq. Две записи выглядят так (анонимно):

@NxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGCGA+ATAGAGAG
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxTTTATGTTTTTAATTAATTCTGTTTCCTCAGATTGATGATGAAGTTxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
AAAAA#FFFFFFFFFFFFAFFFFF#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF<AFFFFFFFFFFAFFFFFFFFFFFFFFFFFFF<FFFFFFFFFAFFFAFFAFFAFFFFFFFFAFFFFFFAAFFF<FAFAFFFFA
@NxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGCGA+ATAGAGAG
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCCCTCTGCTGGAACTGACACGCAGACATTCAGCGGCTCCGCCGCCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
AAAAA#FFFFF7FFFFFFAFFFFA#F7FFFFFFFFF7FFFFFAF<FFFFFFFFFFFFFFAFFF.F.FFFFF.FAFFF.FFFFFFFFFFFFFF.)F.FFA))FFF7)F7F<.FFFF.FFF7FF<.FFA<7FA.<.7FF.FFFAFF

Первая строка каждой записи начинается с @.

EDIT2:

zcat file > /dev/null занимает 31мин.

EDIT3: только первая строка начинается с @. Ни один из других никогда не будет. Смотрите здесь . Записи должны оставаться в порядке. Нельзя добавлять что-либо в полученный файл.

Рольф
источник
Как долго длится один zcat file > /dev/null?
Чороба
Можете ли вы предоставить небольшой образец рассматриваемого файла?
FloHimself
Вы говорите, что каждая запись начинается с @того, что в каждой записи есть 4 строки. Являются ли оба эти абсолютными? - а строки 2,3,4 могут начинаться с @? и есть ли в файле какие-либо не записываемые заголовки строк нижнего колонтитула?
Peter.O
1
Вы ищете решение, которое обрабатывает сжатый ввод и / или производит сжатый вывод? Вы ищете четыре сжатых файла одинакового размера?
Стивен Китт

Ответы:

4

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

Более того, gzipпросто не поддерживается сохранение исходного размера сжатых файлов размером более 4 ГБ - он не может с этим справиться. И поэтому вы не можете запросить архив, чтобы получить надежный размер - потому что он вас обманет.

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

Однако, что вы можете сделать, это установить максимальный размер для разделенных выходных файлов и убедиться, что они всегда нарушаются при барьерах записи. Это вы можете легко сделать. Вот небольшой скрипт, который сделает это путем извлечения gzipархива и передачи содержимого через несколько явных ddконвейерных буферов с конкретными count=$rptаргументами, прежде чем передать его lz4для распаковки / повторного сжатия каждого файла на лету. Я также добавил несколько маленьких teeхитростей, чтобы напечатать последние четыре строки для каждого сегмента в stderr.

(       IFS= n= c=$(((m=(k=1024)*k)/354))
        b=bs=354xk bs=bs=64k
        pigz -d </tmp/gz | dd i$bs o$b |
        while   read -r line _$((n+=1))
        do      printf \\n/tmp/lz4.$n\\n
        { {     printf %s\\n "$line"
                dd count=$c i$b o$bs
        }|      tee /dev/fd/3|lz4 -BD -9 >/tmp/lz4.$n
        } 3>&1| tail -n4 |tee /dev/fd/2 |
                wc -c;ls -lh /tmp/[gl]z*
        done
)

Это будет продолжаться до тех пор, пока не будет обработан весь ввод. Он не пытается разделить его на некоторый процент - который он не может получить - но вместо этого он разделяет его на максимальное количество необработанных байтов за разделение. И в любом случае, большая часть вашей проблемы заключается в том, что вы не можете получить надежный размер для вашего архива, потому что он слишком большой - что бы вы ни делали, не делайте этого снова - сделайте сплиты менее 4 ГБ на кусок, так что , может быть. Этот маленький скрипт, по крайней мере, позволяет вам делать это без необходимости записывать несжатый байт на диск.

Ниже приведена более короткая версия, которая не включает в себя все элементы отчета:

(       IFS= n= c=$((1024*1024/354))
        pigz -d | dd ibs=64k obs=354xk |
        while   read -r line _$((n+=1))
        do {    printf %s\\n "$line"
                dd count=$c obs=64k ibs=354xk
        }  |    lz4 -BD -9  >/tmp/lz4.$n
        done
)  </tmp/gz

Он делает все то же самое, что и первый, в основном, ему просто нечего сказать об этом. Кроме того, там меньше беспорядка, так что легче понять, что происходит, может быть.

Все IFS=дело в том, чтобы обрабатывать одну readстроку на одну итерацию. Мы readодин, потому что нам нужно, чтобы наш цикл заканчивался, когда ввод заканчивается. Это зависит от размера вашей записи - который, по вашему примеру, составляет 354 байта на. Я создал 4 + ГБ gzipархив с некоторыми случайными данными, чтобы проверить его.

Случайные данные были получены следующим образом:

(       mkfifo /tmp/q; q="$(echo '[1+dPd126!<c]sc33lcx'|dc)"
        (tr '\0-\33\177-\377' "$q$q"|fold -b144 >/tmp/q)&
        tr '\0-\377' '[A*60][C*60][G*60][N*16][T*]' | fold -b144 |
        sed 'h;s/^\(.\{50\}\)\(.\{8\}\)/@N\1+\2\n/;P;s/.*/+/;H;x'|
        paste "-d\n" - - - /tmp/q| dd bs=4k count=kx2k  | gzip
)       </dev/urandom >/tmp/gz 2>/dev/null

... но, может быть, вам не нужно беспокоиться об этом, так как у вас уже есть данные и все такое. Вернуться к решению ...

В основном pigz- который, кажется, распаковывает немного быстрее, чем делает zcat- передает несжатый поток и ddбуферы, которые выводят в блоки записи, размер которых определенно кратен 354 байтам. Цикл будет один раз в каждой итерации теста , что ввод еще прибывающего, который он будет потом на перед другим , называется для чтения размеров блоков конкретно на кратна 354-байт - для синхронизации с буферным процесса - в течение всего срока. Будет одно короткое чтение за каждую итерацию из-за начального - но это не имеет значения, потому что мы печатаем это в нашем процессе сбора - в любом случае.read$lineprintfprintflz4ddddread $linelz4

Я настроил его так, что каждая итерация будет считывать примерно 1 ГБ несжатых данных и сжимать их в потоке примерно до 650 МБ или около того. lz4гораздо быстрее, чем любой другой полезный метод сжатия - вот почему я выбрал его здесь, потому что я не люблю ждать. xzвозможно, будет гораздо лучше работать при фактическом сжатии. Но есть одна вещь lz4, которая заключается в том, что он часто может распаковываться со скоростью, близкой к скорости ОЗУ, а это означает, что вы можете распаковывать lz4архив так же быстро, как и в любом случае.

Большой делает несколько отчетов за одну итерацию. Оба ddцикла напечатают отчет о количестве переданных необработанных байтов, скорости и т. Д. Большой цикл также будет печатать последние 4 строки ввода за цикл и количество байтов для него, а также lsкаталог, в который я записываю lz4архивы. Вот несколько раундов вывода:

/tmp/lz4.1
2961+1 records in
16383+1 records out
1073713090 bytes (1.1 GB) copied, 169.838 s, 6.3 MB/s
@NTACGTANTTCATTGGNATGACGCGCGTTTATGNGAGGGCGTCCGGAANGC+TCTCTNCC
TACGTANTTCATTGGNATGACGCGCGTTTATGNGAGGGCGTCCGGAANGCTCTCTNCCGAGCTCAGTATGTTNNAAGTCCTGANGNGTNGCGCCTACCCGACCACAACCTCTACTCGGTTCCGCATGCATGCAACACATCGTCA
+
I`AgZgW*,`Gw=KKOU:W5dE1m=-"9W@[AG8;<P7P6,qxE!7P4##,Q@c7<nLmK_u+IL4Kz.Rl*+w^A5xHK?m_JBBhqaLK_,o;p,;QeEjb|">Spg`MO6M'wod?z9m.yLgj4kvR~+0:.X#(Bf
354

-rw-r--r-- 1 mikeserv mikeserv 4.7G Jun 16 08:58 /tmp/gz
-rw-r--r-- 1 mikeserv mikeserv 652M Jun 16 12:32 /tmp/lz4.1

/tmp/lz4.2
2961+1 records in
16383+1 records out
1073713090 bytes (1.1 GB) copied, 169.38 s, 6.3 MB/s
@NTTGTTGCCCTAACCANTCCTTGGGAACGCAATGGTGTGANCTGCCGGGAC+CTTTTGCT
TTGTTGCCCTAACCANTCCTTGGGAACGCAATGGTGTGANCTGCCGGGACCTTTTGCTGCCCTGGTACTTTTGTCTGACTGGGGGTGCCACTTGCAGNAGTAAAAGCNAGCTGGTTCAACNAATAAGGACNANTTNCACTGAAC
+
>G-{N~Q5Z5QwV??I^~?rT+S0$7Pw2y9MV^BBTBK%HK87(fz)HU/0^%JGk<<1--7+r3e%X6{c#w@aA6Q^DrdVI0^8+m92vc>RKgnUnMDcU:j!x6u^g<Go?p(HKG@$4"T8BWZ<z.Xi
354

-rw-r--r-- 1 mikeserv mikeserv 4.7G Jun 16 08:58 /tmp/gz
-rw-r--r-- 1 mikeserv mikeserv 652M Jun 16 12:32 /tmp/lz4.1
-rw-r--r-- 1 mikeserv mikeserv 652M Jun 16 12:35 /tmp/lz4.2
mikeserv
источник
gzip -lработает только для несжатых файлов <2GiB IIRC (что-то меньше, чем файл OP в любом случае).
Стефан Шазелас
@ StéphaneChazelas - блин. Это единственный способ получить несжатый размер. Без этого это не работает вообще.
mikeserv
4

Разделить файлы по границам записи на самом деле очень легко, без какого-либо кода:

zcat your_file.gz | split -l 10000 - output_name_

Это создаст выходные файлы по 10000 строк каждый с именами output_name_aa, output_name_ab, output_name_ac, ... При таком большом размере ввода, вы получите много выходных файлов. Замените 10000на любое число, кратное четырем, и вы можете сделать выходные файлы такими большими или маленькими, как вам нравится. К сожалению, как и в случае с другими ответами, нет хорошего способа гарантировать, что вы получите желаемое количество (приблизительно) одинакового размера выходных файлов без каких-либо предположений о вводе. (Или, на самом деле, все это проясняет wc.) Если ваши записи примерно одинакового размера (или, по крайней мере, примерно равномерно распределены), вы можете попытаться составить такую ​​оценку:

zcat your_file.gz | head -n4000 | gzip | wc -c

Это скажет вам сжатый размер первых 1000 записей вашего файла. Исходя из этого, вы, вероятно, можете составить оценку того, сколько строк в каждом файле вы хотите получить в результате из четырех файлов. (Если вы не хотите, чтобы вырожденный пятый файл был оставлен, обязательно немного увеличьте свою оценку или будьте готовы прикрепить пятый файл к хвосту четвертого.)

Изменить: Вот еще один трюк, если вы хотите сжатые выходные файлы:

#!/bin/sh

base=$(basename $1 .gz)
unpigz -c $1 | split -l 100000 --filter='pigz -c > _$FILE.gz' - ${base}_

batch=$((`ls _*.gz | wc -l` / 4 + 1))
for i in `seq 1 4`; do
  files=`ls _*.gz | head -$batch`
  cat $files > ${base}_$i.gz && rm $files
done

Это создаст много небольших файлов, а затем быстро объединит их. (Возможно, вам придется настроить параметр -l в зависимости от длины строк в ваших файлах.) Предполагается, что у вас относительно свежая версия GNU coreutils (для split --filter) и около 130% размера входного файла в свободное место на диске. Замените gzip / zcat на pigz / unpigz, если у вас их нет. Я слышал, что некоторые программные библиотеки (Java?) Не могут обрабатывать сцепленные таким образом файлы gzip, но у меня до сих пор не было проблем с этим. (pigz использует тот же трюк для распараллеливания сжатия.)

Нарисовалась
источник
Если у вас установлен pigz, вы можете немного ускорить процесс, заменив «zig» на «pigz -cd».
Дрю
2
Ах, я только что заметил, что вы уже упомянули разделение в вопросе. Но на самом деле, практически любое решение будет делать то же самое, что и разделение под капотом. Сложная часть - выяснить, сколько строк нужно поместить в каждый файл.
Дрю
3

Из того, что я собрал после проверки google -phere и дальнейшего тестирования .gzфайла размером 7,8 ГБ , кажется, что метаданные исходного размера несжатого файла не являются точными (то есть неправильными ) для больших .gzфайлов (больше 4 ГБ (может быть 2 ГБ для некоторых). версии gzip).
Re. мой тест метаданных gzip:

* The compressed.gz file is  7.8 GiB ( 8353115038 bytes) 
* The uncompressed  file is 18.1 GiB (19436487168 bytes)
* The metadata says file is  2.1 GiB ( 2256623616 bytes) uncompressed

Таким образом, кажется, что невозможно определить несжатый размер, не распаковав его (что немного грубовато, если не сказать больше!)

Во всяком случае, вот способ разбить несжатый файл на границах записи, где каждая запись содержит 4 строки .

Он использует размер файла в байтах (через stat) и с awkподсчетом байтов (не символов). Являются ли окончание строки LF| CR| CRLFэтот скрипт обрабатывает длину конца строки через встроенную переменную RT).

LC_ALL=C gawk 'BEGIN{"stat -c %s "ARGV[1] | getline inSize
                      segSiz=int(inSize/4)+((inSize%4)==0?0:1)
                      ouSplit=segSiz; segNb=0 }
               { lnb++; bytCt+=(length+length(RT))
                 print $0 > ARGV[1]"."segNb
                 if( lnb!=4 ) next
                 lnb=0
                 if( bytCt>=ouSplit ){ segNb++; ouSplit+=segSiz }
               }' myfile

Ниже приведен тест, который я использовал для проверки количества строк в каждом файле mod 4 == 0

for i in myfile  myfile.{0..3}; do
    lc=$(<"$i" wc -l)
    printf '%s\t%s\t' "$i" $lc; 
    (( $(echo $lc"%4" | bc) )) && echo "Error: mod 4 remainder !" || echo 'mod 4 ok'  
done | column -ts$'\t' ;echo

Тестовый вывод:

myfile    1827904  mod 4 ok
myfile.0  456976   mod 4 ok
myfile.1  456976   mod 4 ok
myfile.2  456976   mod 4 ok
myfile.3  456976   mod 4 ok

myfile был сгенерирован:

printf %s\\n {A..Z}{A..Z}{A..Z}{A..Z}—{1..4} > myfile
Peter.O
источник
2

Это не должно быть серьезным ответом! Я просто играл, flexи это, скорее всего, не будет работать с входным файлом с ~ 50 Гб (если вообще, с большими входными данными, чем мой тестовый файл):

Это работает для меня на входном файле ~ 1 ГБ :

Учитывая flexвходной файл splitter.l :

%{
#include <stdio.h>
extern FILE* yyin;
extern FILE* yyout;

int input_size = 0;

int part_num;
int part_num_max;
char **part_names;
%}

%%
@.+ {
        if (ftell(yyout) >= input_size / part_num_max) {
            fclose(yyout);
            if ((yyout = fopen(part_names[++part_num], "w")) == 0) {
                exit(1);
            }
        }
        fprintf(yyout, "%s", yytext);
    }
%%

int main(int argc, char *argv[]) {

    if (argc < 2) {
        return 1;
    } else if ((yyin = fopen(argv[1], "r")) == 0) {
        return 1;
    } else if ((yyout = fopen(argv[2], "w")) == 0) {
        fclose(yyin);
        return 1;
    } else {

        fseek(yyin, 0L, SEEK_END);
        input_size = ftell(yyin);
        rewind(yyin);

        part_num = 0;
        part_num_max = argc - 2;
        part_names = argv + 2;

        yylex();

        fclose(yyin);
        fclose(yyout);
        return 0;
    }
}

генерировать lex.yy.c и компилировать его в splitterдвоичный файл с помощью:

$ flex splitter.l && gcc lex.yy.c -ll -o splitter

Применение:

$ ./splitter input.txt output.part1 output.part2 output.part3 output.part4

Продолжительность 1Гб input.txt :

$ time ./splitter input.txt output.part1 output.part2 output.part3 output.part4

real    2m43.640s
user    0m48.100s
sys     0m1.084s
FloHimself
источник
Фактический лексизм здесь настолько прост, что вы действительно не получаете выгоды от лекса. Просто позвоните getc(stream)и примените простую логику. Кроме того, вы знаете, что. (точка) регулярное выражение в (f) lex соответствует любому символу, кроме новой строки , верно? Тогда как эти записи многострочные.
Каз
@Kaz В то время как ваши заявления, как правило, ошибочны, на самом деле это работает с данными, представленными в Q.
FloHimself
Только случайно, потому что есть правило по умолчанию, когда ничто не соответствует: потреблять символ и выводить его на вывод! Другими словами, вы можете просто переключить файл с помощью правила, которое распознает @символ, а затем позволить правилу по умолчанию копировать данные. Теперь ваше правило копирует часть данных в виде одного большого токена, а затем правило по умолчанию получает вторую строку по одному символу за раз.
Каз
Спасибо за разъяснение. Интересно, как бы вы решили эту задачу txr?
FloHimself
Я не уверен, что смог бы, потому что задача состоит в том, чтобы сделать очень простую вещь с большим объемом данных как можно быстрее.
Каз
1

Вот решение в Python, которое делает один проход по входному файлу, записывая выходные файлы по мере продвижения.

Особенность использования wc -lзаключается в том, что вы предполагаете, что все записи здесь имеют одинаковый размер. Это может быть правдой здесь, но решение ниже работает, даже если это не так. Это в основном использование wc -cили количество байтов в файле. В Python это делается через os.stat ()

Итак, вот как работает программа. Сначала мы вычисляем идеальные точки разделения как смещения байтов. Затем вы читаете строки записи входного файла в соответствующий выходной файл. Когда вы увидите, что вы достигли оптимальной следующей точки разделения, и вы находитесь на границе записи, закройте последний выходной файл и откройте следующий.

Программа оптимальна в этом смысле, она считывает байты входного файла один раз; Получение размера файла не требует чтения данных файла. Необходимое хранилище пропорционально размеру строки. Но Python или система предположительно имеют разумные файловые буферы для ускорения ввода-вывода.

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

И, очевидно, это можно перевести и на другие языки программирования.

Еще одна вещь, я не уверен, что Windows с ее crlf правильно обрабатывает длину строки, как в Unix-системах. Если len () отключен на единицу, я надеюсь, что это очевидно, как настроить программу.

#!/usr/bin/env python
import os

# Adjust these
filename = 'file.txt'
rec_size = 4
file_splits = 4

size = os.stat(filename).st_size
splits = [(i+1)*size/file_splits for i in range(file_splits)]
with open(filename, 'r') as fd:
    linecount = 0
    i = 0 # File split number
    out = open('file%d.txt' % i, 'w')
    offset = 0  # byte offset of where we are in the file: 0..size
    r = 0 # where we are in the record: 0..rec_size-1
    for line in fd:
        linecount += 1
        r = (r+1) % rec_size
        if offset + len(line) > splits[i] and r == 1 :
            out.close()
            i += 1
            out = open('file%d.txt' % i, 'w')
        out.write(line)
        offset += len(line)
    out.close()
    print("file %s has %d lines" % (filename, linecount))
скалистый
источник
Это не расщепление на границе записи. например. Первое разделение printf %s\\n {A..Z}{A..Z}{A..Z}{A..Z}—{1..4}
подфайла
1

Пользователь FloHimself, казалось, интересовался решением TXR . Вот тот, который использует встроенный TXR Lisp :

(defvar splits 4)
(defvar name "data")

(let* ((fi (open-file name "r"))                 ;; input stream
       (rc (tuples 4 (get-lines fi)))            ;; lazy list of 4-tuples
       (sz (/ (prop (stat name) :size) splits))  ;; split size
       (i 1)                                     ;; split enumerator
       (n 0)                                     ;; tuplecounter within split
       (no `@name.@i`)                           ;; output split file name
       (fo (open-file no "w")))                  ;; output stream
  (whilet ((r (pop rc)))  ;; pop each 4-tuple
    (put-lines r fo) ;; send 4-tuple into output file
    ;; if not on the last split, every 1000 tuples, check the output file
    ;; size with stat and switch to next split if necessary.
    (when (and (< i splits)
               (> (inc n) 1000)
               (>= (seek-stream fo 0 :from-current) sz))
      (close-stream fo)
      (set fo (open-file (set no `@name.@(inc i)`) "w")
           n 0)))
  (close-stream fo))

Ноты:

  1. По той же причине pop- важно, чтобы каждый кортеж из ленивого списка кортежей был важен, так что ленивый список используется. Мы не должны сохранять ссылку на начало этого списка, потому что тогда память будет расти по мере продвижения по файлу.

  2. (seek-stream fo 0 :from-current)это безоперационный случай seek-stream, который делает себя полезным, возвращая текущую позицию.

  3. Производительность: не упоминайте об этом. Можно использовать, но не принесет домой никаких трофеев.

  4. Так как мы проверяем размер только через 1000 кортежей, мы можем просто сделать размер кортежа 4000 строк.

Kaz
источник
0

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

sed -n -e '1~16,+3w1.txt' -e '5~16,+3w2.txt' -e '9~16,+3w3.txt' -e '13~16,+3w4.txt'

Он -nостанавливает печать каждой строки, и каждый из -eсценариев по сути делает одно и то же. 1~16соответствует первой строке и каждой 16-й строке после. ,+3означает сопоставлять следующие три строки после каждой из них. w1.txtговорит, что записать все эти строки в файл 1.txt. Это берет каждую 4-ю группу из 4 строк и записывает ее в файл, начиная с первой группы из 4 строк. Другие три команды делают то же самое, но каждая из них сдвинута вперед на 4 строки и записывает в другой файл.

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

Erik
источник