複数行のレコードを分割せずに大きなテキストファイルを効率的に分割する方法


9

私は大きなテキストファイルを持っています(gzしたとき〜50Gb)。ファイルには4*N行またはNレコードが含まれています。つまり、すべてのレコードは4行で構成されます。このファイルを、入力ファイルのおよそ25%のサイズの4つの小さなファイルに分割したいと思います。ファイルをレコード境界で分割するにはどうすればよいですか?

素朴なアプローチはzcat file | wc -l、行数を取得し、その数を4で除算してからを使用することsplit -l <number> fileです。ただし、これはファイルを2回超えるため、行カウントは非常に遅くなります(36分)。もっと良い方法はありますか?

これは近いですが、私が探しているものではありません。受け入れられた回答も行数をカウントします。

編集:

このファイルには、fastq形式のシーケンスデータが含まれています。2つのレコードは次のようになります(匿名化)。

@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かかりますか?
チョロバ2015年

問題のファイルの小さなサンプルを提供できますか?
FloHimself 2015年

あなたは、すべてのレコードがで始まり、レコードごとに@4行あると言います。これらはどちらも絶対的なものですか?-そして、2、3、4行目は@?ファイルにフッター行の非レコードヘッダーがありますか?
Peter.O 2015年

1
圧縮入力を処理したり、圧縮出力を生成したりするソリューションをお探しですか?同じサイズの4つの圧縮ファイルを探していますか?
Stephen Kitt

回答:


4

私はあなたがこれを行うことができるとは思いません-確実ではなく、あなたが尋ねる方法ではありません。問題は、アーカイブの圧縮率がおそらく先頭から末尾に均等に分散されないことです-圧縮アルゴリズムは他の部分よりもある部分によく適用されます。それはまさにそれが機能する方法です。そのため、圧縮ファイルのサイズで分割を考慮することはできません。

そのうえ、 gzip GBを超えるサイズの圧縮ファイルの元のサイズの保存をサポートしていないため、処理できません。そして、信頼できるサイズを取得するためにアーカイブをクエリすることはできません-だまされてしまうからです。

4行のこと-それは本当に簡単です。4ファイルのこと-アーカイブを解凍して圧縮されていないサイズを取得せずに、均等なディストリビューションで確実にそれを行う方法を知りません。私がやってみたので、あなたができるとは思いません。

ただし、できることは、分割された出力ファイルの最大サイズを設定し、それらが常にレコードの障壁で壊れることを確認することです。簡単にできること。次の小さなスクリプトは、gzipアーカイブを抽出し、dd特定のcount=$rpt引数を持ついくつかの明示的なパイプバッファーを介してコンテンツをパイプ処理してlz4から、各ファイルをその場で解凍/再圧縮するために渡すスクリプトです。またtee、各セグメントの最後の4行を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
)

これは、すべての入力を処理するまで続きます。パーセンテージで分割しようとはしません-パーセンテージでは取得できません-代わりに、分割ごとの最大未加工バイトカウントごとに分割します。とにかく、あなたの問題の大きな部分は、アーカイブが大きすぎるために信頼できるサイズを取得できないことです-あなたが何をしても、それを再度行わないでください-このラウンドで分割を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、反復ごとに行を。私たちは、read1ので、我々は、入力が終了終了する私たちのループを必要としています。これは、レコードサイズによって異なります。たとえば、例では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バイト-バッファと同期する期間の間-プロセス。イニシャルのため、反復ごとに1回の短い読み取りがありますが、それは重要ではありません。read$lineprintfprintflz4ddddread $linelz4ますが、コレクタープロセス-とにかくん。

各反復で約1GBの非圧縮データを読み取り、そのインストリームを約650Mb程度に圧縮するように設定しました。lz4他のほとんどの有用な圧縮方法よりもはるかに高速です。これが、私が待つのが好きではないためにここで選択した理由です。xzおそらく、実際の圧縮でははるかに良い仕事をするでしょう。lz4ただし、の1つは、RAMに近い速度で解凍できることが多いということです。つまり、多くの場合、lz4アーカイブをメモリに書き込むことができるのと同じくらい高速に解凍できます。

大きなものは、反復ごとにいくつかのレポートを作成します。どちらのループもdd、転送された未加工バイト数と速度などに関するレポートを出力します。大きなループは、サイクルごとの入力の最後の4行と、そのバイトカウントも出力し、その後lslz4アーカイブを書き込むディレクトリのが続きます。出力のいくつかのラウンドはここにあります:

/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

gzip -l<2GiB非圧縮ファイルIIRC(とにかくOPのファイルよりも小さいもの)でのみ機能します。
ステファンChazelas

@StéphaneChazelas-くそー。これが、非圧縮サイズを取得するための唯一の方法です。それがなければ、これはまったく機能しません。
mikeserv

4

レコードの境界でファイルを分割することは、コードがなくても実際には非常に簡単です。

zcat your_file.gz | split -l 10000 - output_name_

これにより、10000行の出力ファイルが作成され、名前はoutput_name_aa、output_name_ab、output_name_ac、...と同じ大きさの入力で、これにより多くの出力ファイルが得られます。100004の倍数に置き換えると、出力ファイルを好きなだけ大きくしたり小さくしたりできます。残念ながら、他の回答と同様に、入力を推測せずに、(ほぼ)等しいサイズの出力ファイルを必要な数だけ取得できることを保証する良い方法はありません。(または、実際に全体をパイプしwcます。)レコードのサイズがほぼ同じ(または少なくともほぼ均等に分散されている)場合は、次のような見積もりを考えてみてください。

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

これにより、ファイルの最初の1000レコードの圧縮サイズがわかります。これに基づいて、各ファイルで必要な行数の見積もりが最終的に4つのファイルになると思います。(縮退した5番目のファイルを残しておきたくない場合は、見積もりに少しパディングするか、5番目のファイルを4番目のファイルの末尾に追加する準備をしてください。)

編集:圧縮された出力ファイルが必要であると仮定して、もう1つのトリックを示します。

#!/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も同じトリックを使用して圧縮を並列化します。)


pigzがインストールされている場合は、「zcat」を「pigz -cd」に置き換えることで、処理を少し高速化できます。
2015年

2
ああ、あなたはすでにあなたが質問の中で分割を述べたことに気づきました。しかし、実際には、ほぼすべてのソリューションが、内部で分割するのとほぼ同じことを行います。難しいのは、各ファイルに何行入れる必要があるかを理解することです。
2015年

3

私が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行が含まれます。

ファイルのサイズをバイト単位で(を介して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

2

これは真剣な答えを意味するものではありません!私は単にいじっていましたが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

ここでの実際の字句解析は非常に単純なので、字句の恩恵は実際にはありません。呼び出してgetc(stream)、いくつかの単純なロジックを適用します。また、あなたはそれを知っていますか?(ドット)(f)lexの正規表現文字は、改行以外の任意の文字と一致しますよね?一方、これらのレコードは複数行です。
Kaz

@Kazあなたの文は、一般的にcorrentですが、これは実際Q.で提供されたデータで動作します
FloHimself

偶然ですが、何も一致しない場合のデフォルトのルールがあるためです。文字を消費して出力に出力します!他の単語では、@文字を認識するルールを使用してファイルを切り替え、デフォルトのルールでデータをコピーすることができます。これで、ルールがデータの一部を1つの大きなトークンとしてコピーし、デフォルトのルールが2行目を一度に1文字ずつ取得します。
Kaz

明確にしていただきありがとうございます。どうすればこのタスクをで解決できますかtxr
FloHimself 2015年

このタスクは、大量のデータを使用して非常に単純なことを可能な限り速く実行することなので、確信が持てません。
Kaz

1

Pythonでのソリューションは次のとおりです。これは、入力ファイルを1つのパスで通過させながら、出力ファイルを書き込みます。

使用に関する特徴wc -lは、ここでの各レコードが同じサイズであると想定していることです。ここではそうかもしれませんが、そうでない場合でも以下の解決策は機能します。基本的にはwc -c、ファイル内の使用中またはバイト数です。Pythonでは、これはos.stat()を介して行われます

これがプログラムの仕組みです。最初に、理想的な分割ポイントをバイトオフセットとして計算します。次に、適切な出力ファイルに書き込む入力ファイルの行を読み取ります。次の最適な分割ポイント超えたことがわかり、レコード境界に達したら、最後の出力ファイルを閉じて次のファイルを開きます。

プログラムはこの意味で最適であり、入力ファイルのバイトを1回読み取ります。ファイルサイズを取得するために、ファイルデータを読み取る必要はありません。必要なストレージは、ラインのサイズに比例します。しかし、Pythonまたはシステムには、おそらくI / Oを高速化するための適切なファイルバッファーがあります。

分割するファイルの数、および将来これを調整する場合に備えてレコードサイズを指定するパラメーターを追加しました。

そして明らかに、これは他のプログラミング言語にも翻訳できます。

もう1つ、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))

レコードの境界で分割されていません。例えば。最初のサブファイル分割は、この入力の3行目以降に発生しますprintf %s\\n {A..Z}{A..Z}{A..Z}{A..Z}—{1..4}
Peter.O

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、タプルの遅延リストから各タプルをpingすることが重要であり、遅延リストが消費されます。そのリストの先頭への参照を保持してはなりません。ファイルを移動するにつれてメモリが増加するためです。

  2. (seek-stream fo 0 :from-current)のno-opケースでseek-stream、現在の位置を返すことで自分自身を便利にします。

  3. パフォーマンス:言及しないでください。使用可能ですが、トロフィーを持ち帰ることはありません。

  4. 1000タプルごとにサイズチェックを行うだけなので、タプルサイズを4000行にするだけで済みます。


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つまり、それぞれの次の3行と一致します。w1.txtこれらの行をすべてファイルに書き込むと言います1.txt。これは、4行ごとの4番目のグループごとに、4行の最初のグループからファイルに書き込みます。他の3つのコマンドは同じことを行いますが、それぞれ4行前にシフトされ、別のファイルに書き込みます。

これは、ファイルが指定した仕様と正確に一致しない場合はひどく壊れますが、それ以外の場合は意図したとおりに機能するはずです。まだプロファイルしていないので、どれほど効率的かはわかりませんが、sedストリーム編集はかなり効率的です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.