1. 概要

大きなファイルを処理する場合、ファイルをパーツに分割して個別に処理する必要がある場合があります。 これを「ファイルの分割」と呼びます。

便利なsplitコマンドは、ほとんどの場合、ファイルを分割するのに役立ちます。 ただし、このチュートリアルでは、特定のファイル分割シナリオ、つまり特定の行番号でファイルを分割する方法について説明します。

2. 問題の紹介

split コマンドを使用してファイルを分割すると、サイズまたは行数でファイルを分割できます。 ただし、特定の行番号でファイルを分割したい場合があります。

サンプルファイルは、問題をすばやく理解するのに役立ちます。 input.txt:というテキストファイルがあるとします。

$ cat input.txt
01 is my line number.
02 is my line number.
03 is my line number.
04 is my line number.
05 is my line number.
06 is my line number.
07 is my line number.
08 is my line number.
09 is my line number.
10 is my line number.
11 is my line number.
12 is my line number.
13 is my line number.
14 is my line number.
15 is my line number.

ファイルには15行あります。 それでは、ファイルを 4、7、12の3行番号で分割してみましょう。 つまり、分割後、4つのファイルを取得します。

  • file1 には、 input.txt の1〜4行目(4行)が含まれます
  • file2 には、 input.txt の5〜7行目(3行)が含まれています
  • file3 は、 input.txt の8〜12行目を保持します(5行)
  • file4 には、 input.txt の13〜15行目(3行)があります

分割ファイルには異なる行数が含まれているため、splitコマンドを使用して問題を解決することはできません。

次の3つのアプローチを使用して、問題の解決策に取り組みます。

  • headおよびtailコマンドを使用するシェルスクリプト
  • sedコマンドに基づくシェルスクリプト
  • awkコマンドの使用

通常、ファイルをチャンクに分割する必要がある場合、大きなファイルに直面する可能性が非常に高くなります。 したがって、ソリューションのパフォーマンスは重要です。

ソリューションのパフォーマンスについて説明し、最も効率的なアプローチを見つけます。

3. headおよびtailコマンドの使用

headコマンドとtailコマンドを-nオプションと一緒に使用すると、入力ファイルから行を抽出できます。

input.txt:から3〜7行目を抽出してみましょう

$ tail -n +3 input.txt | head -n $(( 7-3+1 ))
03 is my line number.
04 is my line number.
05 is my line number.
06 is my line number.
07 is my line number.

したがって、tail|をラップするシェルスクリプトを作成できます。 指定された行番号でファイルを分割するheadコマンド:

$ cat head_and_tail.sh
#!/bin/bash
INPUT_FILE="input.txt"  # The input file
LINE_NUMBERS=( 4 7 12 ) # The given line numbers (array)
START=1                 # The offset to calculate lines
IDX=1                   # The index used in the name of generated files: file1, file2 ...

for i in "${LINE_NUMBERS[@]}"
do
    # Extract the lines using the head and tail commands
    tail -n +$START "$INPUT_FILE" | head -n $(( i-START+1 )) > "file$IDX.txt"
    (( IDX++ ))
    START=$(( i+1 ))
done
# Extract the last given line - last line in the file
tail -n +$START "$INPUT_FILE" > "file$IDX.txt"

次に、スクリプトを実行して、input.txtを予想されるチャンクに分割できるかどうかを確認します。

$ ./head_and_tail.sh
$ head file*
==> file1.txt <==
01 is my line number.
02 is my line number.
03 is my line number.
04 is my line number.

==> file2.txt <==
05 is my line number.
06 is my line number.
07 is my line number.

==> file3.txt <==
08 is my line number.
09 is my line number.
10 is my line number.
11 is my line number.
12 is my line number.

==> file4.txt <==
13 is my line number.
14 is my line number.
15 is my line number.

上記の出力が示すように、問題は解決されます。

4. sedコマンドの使用

sed コマンドは、指定された2つの行番号のアドレス範囲をサポートします。

たとえば、短い sed ワンライナーを記述して、 input.txt ファイルから3〜7行目を抽出できます。

$ sed -n '3,7p; 8q' input.txt 
03 is my line number.
04 is my line number.
05 is my line number.
06 is my line number.
07 is my line number.

上記のコマンドでは、パフォーマンスを向上させるために、「8q」を使用して7行目を印刷した後、それ以上の処理を停止するようにsedコマンドに指示しています

ご覧のとおり、 sedコマンドのアドレス範囲を使用して行を抽出する方が、ヘッドとテールの組み合わせよりも簡単です。 したがって、問題を解決するには、各アドレス範囲の境界を計算し、それらをsedコマンドに渡す必要があります。

$ cat using_sed.sh  
#!/bin/bash
INPUT_FILE="input.txt"  # The input file
LINE_NUMBERS=( 4 7 12 ) # The given line numbers (array)
START=1                 # The start line number
IDX=1                   # The index used in the name of generated files: file1, file2 ...

for i in "${LINE_NUMBERS[@]}"
do
    # Extract the lines using sed command
    NEXT_LINE=$(( i+1 ))
    sed -n "$START, $i p; $NEXT_LINE q" "$INPUT_FILE" > "file$IDX.txt"
    (( IDX++ ))
    START=$NEXT_LINE
done

# Extract the last given line - last line in the file
sed -n "$START, $ p" "$INPUT_FILE" > "file$IDX.txt"

それでは、スクリプトを実行して、スクリプトが作成したファイルを確認しましょう。

$ ./using_sed.sh
$ head file*    
==> file1.txt <==
01 is my line number.
02 is my line number.
03 is my line number.
04 is my line number.

==> file2.txt <==
05 is my line number.
06 is my line number.
07 is my line number.

==> file3.txt <==
08 is my line number.
09 is my line number.
10 is my line number.
11 is my line number.
12 is my line number.

==> file4.txt <==
13 is my line number.
14 is my line number.
15 is my line number.

すごい! 問題は解決しました。

5. awkコマンドの使用

強力なawkスクリプト自体が配列、ループ、リダイレクト、およびその他の多くの機能をサポートしているため、問題を解決するためにawkコマンドをシェルスクリプトでラップする必要はありません。 。

awkワンライナーを使用して問題を解決することもできます。 ただし、より簡単に理解できるように、適切なインデントを使用して複数行のコードに分割します。

awk -v nums="4 7 12" '
    BEGIN {        
        c=split(nums,b)
        for(i=1; i<=c; i++) a[b[i]]
        j=1; out = "file1.txt"
    } 
    { print > out }
    NR in a {
        close(out)
        out = "file" ++j ".txt"
    }' input.txt

上記のawkコマンドを実行すると、それぞれに期待されるデータを含む4つのファイルが取得されます。

それでは、それがどのように機能するかを理解しましょう。

  • -v nums =” 4 7 12” :指定された行番号を変数numsに割り当てます。
  • BEGIN{…} BEGIN ブロックのコードは、入力ファイルから最初の行を読み取る前に1回だけ実行されます。
    • c = split(nums、b) split()関数を使用して、3つの数値を配列( b [] )に分割し、変数cは、配列の長さを保持します( 3
    • for(i = 1; i <= c; i ++)a [b [i]] :別のものを作成します連想配列 a [] の要素を保持します b [] キーとして。 例: b [1] = 4-> a [4] ; b [2] = 7-> a[7]など
    • j = 1; out =“ file1.txt” :ここでは、出力ファイルのファイル名を含む変数( out )と、のインデックスを保持する変数( j )を初期化します。各出力ファイル
  • {print> out} :現在の行を出力ファイルに出力します
  • NR in a {close(out); out =“ file” ++ j“ .txt”} :現在の行番号が連想配列 a [] に存在する場合、現在の出力ファイルを閉じて、インデックスをインクリメントする必要があります。ファイル名

6. パフォーマンス

これまで、問題を解決するための3つの異なる方法を学びました。 それでは、彼らのパフォーマンスについて話し合いましょう。

スクリプトのベンチマークを行う前に、3つのアプローチを確認し、結果を見積もりましょう。

入力ファイルをnチャンクに分割する必要があるとしましょう。

  • head_and_tail.sh 2n プロセス( tail | head )が必要で、入力ファイルをn回処理します
  • using_sed.sh n プロセス( sed )を開始し、入力n回処理します
  • awk コマンド–単一のプロセス( awk )を作成し、入力を1回だけ処理します

上記の分析に基づくと、 awk ソリューションのコストは最も低く、最高のパフォーマンスが得られるはずです。 逆に、head_and_tail.shが最も遅いはずです。

次に、見積もりが正しいかどうかを確認しましょう。

6.1. 大きな入力ファイルの作成

input.txt は15行しかないため、パフォーマンステストには適していません。 1億行のbig.txt入力ファイルを作成しましょう。

$ seq 100000000 > big.txt
$ du big.txt 
848M	big.txt

$ wc -l big.txt
100000000 big.txt

パフォーマンスベンチマークの入力ファイルとしてbig.txtを使用します。

6.2. パフォーマンスのベンチマーク

time コマンドを使用して各スクリプトまたはコマンドをテストし、そのパフォーマンスをベンチマークします。

テストを開始する前に:

  • INPUT_FILE変数をbig.txtを指すように変更しました
  • また、入力ファイルの行数が100 Mになっているため、 LINE_NUMBERS 配列を「(400000 50000000 70000000)」に変更しました。
  • ファイルシステムのキャッシュの影響を回避するために、tmpfsファイルシステムの/tmpディレクトリですべてのテストを実行します

まず、head_and_tail.shスクリプトをテストしてみましょう。

$ time ./head_and_tail.sh 
real 1.40
user 1.11
sys 1.00

次に、using_sed.shスクリプトの実行速度を確認します。

$ rm file* ; time ./using_sed.sh
real 10.80
user 10.08
sys 0.68

最後に、awkスクリプトをテストしてみましょう。

$ rm file* ; time awk -v nums="400000 50000000 70000000" ' .... '  big.txt
real 18.73
user 18.33
sys 0.38

6.3. 結果を理解する

結果は驚くべきものです!

head_and_tail.sh スクリプトは8つのプロセスを開始し、大きな入力ファイルを4回読み取りますが、これが最速のソリューションです。

ただし、 awk コマンドは、最速のソリューションであると考えられ、 head_and_tail.sh スクリプトよりも約16倍遅く、3つのアプローチの中で最も遅いものでした。

sed ソリューションはその中間にありますが、それでもhead_and_tail.shよりも約8倍低速です。

ここで、質問が出てきます。入力ファイルを1回だけ読み取る awk コマンドが、head_and_tail.shよりもはるかに遅いのはなぜですか。

その理由は awkコマンドは、ファイルのすべての行を読み取り、フィールド、NF、レコードなど、指定されたFSおよびRSに応じていくつかの内部属性を初期化します。 次に、それは私たちを読みます awk スクリプトを作成し、テキストで何かを実行する必要があるかどうかを確認します。 この場合、行をファイルにリダイレクトするだけです。 次に、awkコマンドがテキストをファイルに書き込みます。 したがって、目前の問題には必要のない多くのオーバーヘッドが発生します。

一方、 headおよびtailコマンドは、何もせず、 line のテキストを保持せずに、改行文字のみを読み取ります。 彼らは目標の行番号を見つけるまで探します。 繰り返しになりますが、彼らは行を読んだり保持したりしません。 代わりに、コンテンツを出力にダンプするだけです。

sed コマンドは、入力ファイルのすべての行も読み取り、保持します。 したがって、head_and_tailソリューションよりもはるかに低速です。

ただし、sedコマンドはawkコマンドよりも初期化が少ないため、sedスクリプトはawkソリューションよりも高速です。

さらに、sedコマンドの「q」アドレスコマンドはそのパフォーマンスを向上させます。 using_sed.shスクリプトから「$NEXT_LINEq 」を削除して再度テストすると、遅くなります:

$ time ./using_sed_without_q.sh
real 15.69
user 14.69
sys 0.99

 7. 結論

この記事では、特定の行番号でファイルを分割する3つの異なる方法について説明しました。

sedコマンドに基づくソリューションは最も簡単です。

ただし、いくつかの大きなファイルで作業する必要がある場合は、ヘッドとテールのソリューションで最高のパフォーマンスが得られます

awk コマンドは、ワンライナーで問題を解決できる非常に強力なテキスト処理ユーティリティです。 ただし、これは3つのアプローチの中で最も遅いソリューションです。