[unix] 여러 줄 레코드를 분할하지 않고 큰 텍스트 파일을 효율적으로 분할하는 방법은 무엇입니까?

큰 텍스트 파일이 있습니다 (gz ‘s ~ 50Gb). 파일은 4*N행이나 N레코드를 포함 합니다. 즉, 모든 레코드는 4 줄로 구성됩니다. 이 파일을 입력 파일의 대략 25 % 크기 인 4 개의 작은 파일로 나누고 싶습니다. 레코드 경계에서 파일을 어떻게 분할 할 수 있습니까?

순진한 접근 방식은 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 :
첫 번째 줄만으로 시작합니다 @. 다른 사람은 절대로 없습니다. 여기를 참조 하십시오 . 기록은 순서대로 유지되어야합니다. 결과 파일에 아무것도 추가하지 않아도됩니다.



답변

나는 당신이 이것을 할 수 있다고 생각하지 않습니다-확실하지 않고 당신이 요구하는 방식이 아닙니다. 문제는 아카이브의 압축 비율이 머리에서 꼬리까지 고르게 분포되지 않을 것입니다. 압축 알고리즘은 다른 부분보다 일부에 더 잘 적용됩니다. 그것이 작동하는 방식입니다. 따라서 압축 파일의 크기에 대한 분할을 고려할 수 없습니다.

무엇보다, gzip단지 크기 만 4GB보다 큰 압축 파일의 원래 크기를 저장하는 지원하지 않습니다 – 그것을 처리 할 수 없습니다. 따라서 신뢰할 수있는 크기를 얻기 위해 아카이브를 쿼리 할 수 ​​없습니다.

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
)

모든 입력을 처리 할 때까지 계속 진행됩니다. 그것은 그것을 얻을 수없는 백분율로 나누려고하지 않지만 대신 스플릿 당 최대 원시 바이트 수로 나눕니다. 어쨌든, 문제의 큰 부분은 아카이브가 너무 커서 아카이브에서 신뢰할 수있는 크기를 얻을 수 없다는 것입니다-당신이 무엇이든간에 다시하지 마십시오-4gbs 미만의 조각을 조각으로 만듭니다. , 아마도. 최소한이 작은 스크립트를 사용하면 압축되지 않은 바이트를 디스크에 쓰지 않고도이 작업을 수행 할 수 있습니다.

다음은 필수 사항을 제거한 짧은 버전입니다. 모든 보고서에 추가되지는 않습니다.

(       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 바이트입니다. gzip테스트하기 위해 임의의 데이터로 4 + gb 아카이브를 만들었 습니다.

무작위 데이터는 다음과 같이 얻었습니다.

(       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

각 반복에서 약 1GB의 압축되지 않은 데이터를 읽고 인스 트림에서 약 650Mb 정도로 압축하도록 설정했습니다. lz4다른 유용한 압축 방법보다 훨씬 빠릅니다. 대기하지 않기 때문에 여기에서 선택한 이유입니다. xz그래도 실제 압축에서 훨씬 더 나은 작업을 수행 할 것입니다. lz4그러나 한 가지 중요한 점은 RAM 속도에 가깝게 압축을 풀 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


답변

레코드 경계에서 파일을 분할하는 것은 실제로 코드없이 매우 쉽습니다.

zcat your_file.gz | split -l 10000 - output_name_

출력 이름은 output_name_aa, output_name_ab, output_name_ac 등으로 각각 10000 줄의 출력 파일을 생성합니다. 입력이 최대 인 경우 많은 출력 파일이 제공됩니다. 100004의 배수로 바꾸면 원하는대로 출력 파일을 크거나 작게 만들 수 있습니다. 불행히도 다른 답변과 마찬가지로 입력에 대해 추측하지 않고 원하는 수의 동일한 크기의 출력 파일을 얻을 수있는 좋은 방법은 없습니다. (또는 실제로 전체를 통해 파이핑합니다 wc.) 레코드의 크기가 대략 같은 크기 (또는 적어도 거의 고르게 분포 된 경우)라면 다음과 같은 추정치를 시도해 볼 수 있습니다.

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

파일의 처음 1000 개 레코드의 압축 된 크기를 알려줍니다. 이를 바탕으로 각 파일에서 4 개의 파일로 끝나는 행 수를 추정 할 수 있습니다. (축소 된 다섯 번째 파일을 남기지 않으려면, 추정값을 약간 채우거나 다섯 번째 파일을 네 번째 꼬리에 붙일 준비를하십시오.)

편집 : 압축 된 출력 파일을 원한다고 가정하면 한 가지 더 트릭이 있습니다.

#!/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 (분할 필터) 및 입력 파일 크기의 약 130 %가 있다고 가정합니다. 디스크 여유 공간. gzip / zcat을 pigz / unpigz로 대체하십시오 (없는 경우). 일부 소프트웨어 라이브러리 (Java?)가 이런 방식으로 연결된 gzip 파일을 처리 할 수 ​​없다고 들었지만 지금까지 아무런 문제가 없었습니다. (pigz는 동일한 트릭을 사용하여 압축을 병렬화합니다.)


답변

google-sphere를 확인한 후 7.8 GiB .gz파일을 추가로 테스트 한 후 수집 한 내용 에서 원래 압축되지 않은 파일 크기의 메타 데이터는 큰 파일 (4GiB 이상 (일부 경우 2GiB 이상 )의 경우 정확하지 않습니다 (예 : 잘못된 ) .gz버전 gzip.)
다시 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 개의 행이 있습니다.

파일 크기를 바이트 단위 (via 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


답변

이것은 진지한 대답이 아닙니다! 나는 그냥 flex놀았는데 ~ 50Gb 인 입력 파일 (아직 테스트 파일보다 큰 입력 데이터)에서는 작동하지 않을 것입니다.

~ 1Gb 파일 input.txt 에서 작동합니다 .

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

1Gb input.txt의 실행 시간 :

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

real    2m43.640s
user    0m48.100s
sys     0m1.084s


답변

다음은 출력 파일을 작성하는 입력 파일을 한 번에 전달하는 Python 솔루션입니다.

사용에 대한 기능은 wc -l여기에서 각 레코드의 크기가 같다고 가정한다는 것입니다. 여기에는 해당 될 수 있지만, 그렇지 않은 경우에도 아래 솔루션이 작동합니다. wc -c파일에서 기본적으로 또는 바이트 수를 사용 하고 있습니다. 파이썬에서 이것은 os.stat () 를 통해 이루어집니다

프로그램이 작동하는 방식은 다음과 같습니다. 먼저 이상적인 분리 점을 바이트 오프셋으로 계산합니다. 그런 다음 해당 출력 파일에 쓰는 입력 파일의 행을 읽습니다. 당신은 당신이 최적의 다음 분리 점을 초과 한 것을 볼 때 는 가까운 마지막 출력 파일의 기록 경계하고 다음을 엽니 다.

프로그램은 이런 의미에서 최적이며 입력 파일의 바이트를 한 번 읽습니다. 파일 크기를 가져 오기 위해 파일 데이터를 읽을 필요는 없습니다. 필요한 저장 공간은 선의 크기에 비례합니다. 그러나 파이썬이나 시스템은 아마도 I / O 속도를 높이기 위해 합리적인 파일 버퍼를 가지고있을 것입니다.

분할 할 파일 수와 나중에이를 조정하려는 경우 레코드 크기에 대한 매개 변수를 추가했습니다.

그리고 이것은 분명히 다른 프로그래밍 언어로도 번역 될 수 있습니다.

다른 한 가지, crlf가있는 Windows가 Unix-y 시스템에서와 같이 줄 길이를 올바르게 처리하는지 확실하지 않습니다. 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))


답변

사용자 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게으른 목록에서 각 튜플을 ping하는 것이 중요하므로 게으른 목록이 사용됩니다. 우리는 파일의 행진에 따라 메모리가 커지므로 해당 목록의 시작에 대한 참조를 유지해서는 안됩니다.

  2. (seek-stream fo 0 :from-current)의 no-op 경우이며 seek-stream현재 위치를 반환하여 유용합니다.

  3. 성능 : 언급하지 마십시오. 사용할 수는 있지만 트로피를 가져 오지는 않습니다.

  4. 우리는 1000 튜플마다 크기 검사를 수행하기 때문에 튜플 크기를 4000 줄로 만들 수 있습니다.


답변

새 파일이 원본 파일의 연속 청크 일 필요가없는 경우 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스트림 편집에서 합리적으로 효율적입니다.