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

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

큰 텍스트 파일이 있습니다(gz 이후 약 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

모든 레코드의 첫 번째 줄은 a로 시작합니다. (불행히도 모든 레코드의 세 번째 줄이 될 @수도 있습니다 . 아래 brendans의 설명을 참조하세요.)@

편집 2:

zcat file > /dev/null31분이 소요됩니다.

편집 3: 첫 번째 줄만 으로 시작합니다 @. 다른 누구도 그렇지 않을 것입니다. 바라보다여기. 기록을 순서대로 보관해야 합니다. 생성된 파일에 아무것도 추가하면 작동하지 않습니다.

답변1

나는 당신이 이것을 할 수 없다고 생각합니다. 그것은 신뢰할 수 없고 당신이 요구하는 방식도 아닙니다. 문제는 아카이브의 압축 비율이 처음부터 끝까지 균등하게 분배되지 않을 수 있다는 것입니다. 압축 알고리즘은 다른 부분보다 일부 부분에 더 잘 적용됩니다. 이것이 작동하는 방식입니다. 따라서 압축파일의 크기에 따라 분할을 나눌 수 없습니다.

게다가 gzip원래 크기의 4GB보다 큰 압축 파일 저장도 지원하지 않습니다. 처리할 수 없습니다. 따라서 신뢰할 수 있는 크기를 얻기 위해 아카이브를 쿼리할 수 없습니다. 왜냐하면 이는 사용자를 속일 수 있기 때문입니다.

4줄 - 정말 간단해요. 4개 파일 문제 - 압축되지 않은 크기를 얻기 위해 먼저 아카이브를 추출하지 않고 안정적이고 균등하게 배포하는 방법을 모르겠습니다. 나는 노력했기 때문에 당신은 할 수 없을 것이라고 생각합니다.

그러나 당신은 무엇을합니까?할 수 있는분할 출력 파일의 최대 크기를 설정하고 이러한 파일이 항상 녹음 장벽에서 파괴되도록 합니다. 쉽게 할 수 있습니다. 다음은 아카이브를 추출 하고 특정 매개변수가 있는 여러 명시적 파이프 버퍼를 gzip통해 내용을 파이프한 후 각 파일을 즉시 압축 해제/재압축하도록 전달하여 이를 수행하는 작은 스크립트입니다. 또한 각 세그먼트의 마지막 네 줄을 stderr에 인쇄하기 위해 약간의 파이프 트릭을 추가했습니다 .ddcount=$rptlz4tee

(       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
)

이는 모든 입력이 처리될 때까지 계속됩니다. 특정 비율(얻을 수 없음)로 분할하는 대신 분할당 최대 원시 바이트 수로 분할합니다. 어쨌든 문제의 가장 큰 부분은 아카이브가 너무 크기 때문에 신뢰할 수 있는 크기를 얻을 수 없다는 것입니다. 무엇을 하든 다시는 하지 마십시오. 이번 라운드에는 분할을 4GB 조각보다 작게 만드십시오. 이 작은 스크립트를 사용하면 최소한 압축되지 않은 바이트를 디스크에 쓰지 않고도 이 작업을 수행할 수 있습니다.

이는 기본만 유지하는 간단한 버전입니다. 모든 보고 내용을 추가하지는 않습니다.

(       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테스트하기 위해 임의의 데이터가 포함된 4GB 이상의 아카이브를 만들었습니다 .

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

(       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바이트의 배수인 쓰기 블록으로 버퍼링합니다. 루프는 read$line반복에서 입력이 아직 도착했는지 여부를 한 번 테스트한 다음, 해당 기간 동안 버퍼링 프로세스와 동기화 하기 위해 특히 354바이트의 배수 크기의 블록을 읽기 위해 다른 루프를 호출하기 printf전에 한 번 루프합니다. 초기화 이유로 인해 반복마다 짧은 읽기가 발생 하지만 수집기 프로세스에서 어쨌든 인쇄할 것이므로 중요하지 않습니다 .printflz4ddddread $linelz4

반복당 약 1GB의 압축되지 않은 데이터를 읽고 인스트림 데이터를 약 650Mb 정도로 압축하도록 설정했습니다. lz4거의 모든 유용한 압축 방법보다 훨씬 빠릅니다. 이것이 바로 제가 기다리는 것을 좋아하지 않기 때문에 여기서 이 방법을 선택한 이유입니다. xz그러나 실제 압축 측면에서는 더 나을 수도 있습니다. 하지만 한 가지는 lz4RAM 속도에 가까운 속도로 압축을 풀 수 있는 경우가 많다는 것입니다. 이는 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

답변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(split --filter용)가 있고 입력 파일의 약 130%가 있다고 가정합니다. 크기는 사용 가능한 디스크 공간에 있습니다. 그렇지 않은 경우 pigz/unpigz를 gzip/zcat으로 바꾸십시오. 일부 소프트웨어 라이브러리(Java?)에서는 이러한 방식으로 연결된 gzip 파일을 처리할 수 없다고 들었지만 지금까지는 아무런 문제가 없었습니다. (pigz는 병렬 압축에 동일한 트릭을 사용합니다.)

답변3

google-sphere를 확인하고 7.8 GiB 파일을 추가로 테스트한 후 수집한 내용에 따르면 .gz압축되지 않은 원본 파일 크기에 대한 메타데이터가 부정확한 것으로 보입니다(예:잘못된) 대용량 .gz파일의 경우(4GiB보다 큼(일부 버전의 경우 2GiB일 수도 있음 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줄.

파일 크기를 바이트 단위로 사용하고(pass stat) awk바이트 수(문자 아님)를 계산합니다. 줄이 |로 끝나는지 여부에 관계없이 LFCR스크립트 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

답변4

다음은 입력 파일이 진행되는 동안 출력 파일을 작성하는 Python 솔루션입니다.

사용의 한 가지 특징은 wc -l여기에 있는 모든 레코드가 동일한 크기라고 가정한다는 것입니다. 여기서는 그럴 수도 있지만 그렇지 않은 경우에도 아래 솔루션이 작동합니다. 기본적으로 wc -c파일에서 사용되거나 사용된 바이트 수 입니다 . Python에서는 다음과 같이 수행됩니다.os.stat()

프로그램 작동 방식은 다음과 같습니다. 먼저 이상적인 분할 지점을 바이트 오프셋으로 계산합니다. 그런 다음 입력 파일의 행을 읽고 해당 출력 파일에 씁니다. 최적의 다음 분할 지점을 초과한 것으로 확인된 경우그리고레코드 경계에 있습니다. 마지막 출력 파일을 닫고 다음 파일을 엽니다.

이 프로그램은 입력 파일의 바이트를 한 번 읽는다는 점에서 최적입니다. 파일 크기를 얻으려면 파일 데이터를 읽을 필요가 없습니다. 필요한 저장 공간은 행 크기에 비례합니다. 그러나 Python이나 시스템에는 아마도 I/O 속도를 높이기 위한 합리적인 파일 버퍼가 있을 것입니다.

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

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

또 다른 점은 crlf가 있는 Windows가 Unix-y 시스템에서처럼 줄 길이를 올바르게 처리하는지 잘 모르겠습니다. 여기서 len()이 1만큼 줄어들면 프로그램을 어떻게 조정해야 하는지 명확해지기를 바랍니다.

#!/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))

관련 정보