web-dev-qa-db-ja.com

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

私は大きなテキストファイルを持っています(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:最初の行のみが@で始まることを確認します。他の誰もこれまでしません。 こちら を参照してください。レコードは整理された状態である必要があります。結果のファイルに何かを追加することはできません。

9
Rolf

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

さらに、gzipは、サイズが4gbsを超える圧縮ファイルの元のサイズの格納をサポートしていません-処理できません。そして、信頼できるサイズを取得するためにアーカイブをクエリすることはできません。

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

ただし、canを実行すると、分割された出力ファイルの最大サイズが設定され、それらが常にレコードの障壁で壊れることを確認します。簡単にできること。これは、gzipアーカイブを抽出し、特定のcount=$rpt引数を使用して、いくつかの明示的なddパイプバッファーを介してコンテンツをパイプ処理し、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=の目的は、反復ごとに1つのread行を処理することだけです。入力が終了したときにループを終了する必要があるため、read 1つです。これは、レコードによって異なります-サイズ-これは、例ごとに354バイトです。テストするために、ランダムなデータを含む4 + gb 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の倍数のサイズの書き込みブロックに出力します-バイト。ループはread a $lineを繰り返し、入力がまだ到着していることをテストします。その後、printfprintf at lz4 at another ddが呼び出されます特に354バイトの倍数のサイズのブロックを読み取る-バッファリングddプロセスと同期する-期間中。最初のread $lineがあるため、反復ごとに1回の短い読み取りがありますが、それはlz4-とにかくコレクタープロセス-で出力しているため、問題ではありません。

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

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

/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
4
mikeserv

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

zcat your_file.gz | split -l 10000 - output_name_

これにより、10000行の出力ファイルが作成され、名前はoutput_name_aa、output_name_ab、output_name_ac、...と同じ大きさの入力で、これにより多くの出力ファイルが得られます。置換10000 4の倍数で、出力ファイルを好きなだけ大きくしたり小さくしたりできます。残念ながら、他の回答と同様に、入力を推測せずに、(ほぼ)等しいサイズの出力ファイルを必要な数だけ取得できることを保証する良い方法はありません。 (または実際に全体を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%。gzip/ zcatをpigz/unpigzに置き換えてください(それらがない場合は、この方法で連結されたgzipファイルを処理できないソフトウェアライブラリ(Java?)があると聞きました)。 、しかし今のところ問題はありません(pigzは同じトリックを使用して圧縮を並列化します)。

4
Drew

Google-sphereを確認して7.8 GiB .gzファイルをさらにテストした後に収集したものから、元の非圧縮ファイルのサイズのメタデータが正確ではないようです(つまり、 間違った.gzファイルが大きい場合(4GiBより大きい(gzipの一部のバージョンでは2GiBの可能性があります)。
再。 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
3
Peter.O

これは真剣な答えを意味するものではありません!私は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
2
FloHimself

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))
1
rocky

ユーザー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)seek-streamの何もしないケースであり、現在の位置を返すことで自分自身を便利にします。

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

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

1
Kaz

新しいファイルを元のファイルの連続したチャンクにする必要がない場合は、次のように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はストリーム編集でかなり効率的です。

0
Erik