bashでファイル編集の性能改善

仕事の中でシェルスクリプトの性能改善をすることになった。そのための性能改善をするための事前調査という形で、
- 想定しうる実装
- 実装の違いによる性能差
これを確認しよう、というもの。

# Oracleの監査XMLが16万ファイル……

OracleのXML監査の集約を行う処理をシェルで実装。
で、出力されてきた監査が16万ファイルもあってえらーーーーーーーく処理に時間がかかる。出力されたファイルを転送する時間が決まっているため、その時間までに処理を終わらせなければならない。いざ、性能改善！　という次第。

# 比較する実装

- 今の実装

ファイルを入力リダイレクトして`while`と`read`でやりくりする方法

```whileとreadの合わせ技
while read LINE
do
    # あれやこれや
done < ファイル
```

- 新しい実装

もう全部、`awk`でいいんじゃね？

# 検証するコード

16万ファイルを実際に作るものあれなので、

16万ファイル × 5レコード分 + α = 100万レコード

という感じでXMLのレコードが100万件のダミーデータファイル１つを入力にする。
その中の`user`タグの中に`markus`または`jacob`という文字列を持つ情報を抽出する。

といった塩梅。詳しくは最後に載せるテスト用コードを参照。

# 結果

|パターン       |所要時間(sec) |
|-------------:|:------------:|
|`while`+`read`|15381         |
|`awk`         |8             |

**awkすげーーーーーーーー！**

# 検証用コード

上に書いていないこともいろいろやっているコード。

```test_performance.sh
#!/bin/bash
#
gen_records=$1

# 第1引数の出力レコード数がなかったら、10万レコードとする。
if [ -z ${gen_records} ]; then
    gen_records=100000
fi

TEST_FILE="testdata.xml"
TEST_TARGET="testTarget.dat"

# 検証に使用するデータを用意する。
function prepare(){
    
    max_gen=$1

    # create test XML data
    echo '<?xml version="1.0" encoding="utf-8">' > ${TEST_FILE}
    for i in $(seq 1 ${max_gen})
    do
        printf "%s%s%s%s%s%s%s%s%s\n" \
        "<record>" \
        "<keyset><key>${i}</key><key>none</key></keyset>" \
        "<user>kevin</user>" \
        "<sequence>" \
        "<data>fix</data><data>fix</data><data>fix</data><data>fix</data><data>fix</data><data>fix</data>" \
        "<data>value_${i}</data><data>value_2</data>" \
        "</sequence>" \
        "<Action>${i}</Action>" \
        "</record>" >> ${TEST_FILE}


    done
    i=100001
        printf "%s%s%s%s%s%s%s%s%s\n" \
        "<record>" \
        "<keyset><key>${i}</key><key>none</key></keyset>" \
        "<user>markus</user>" \
        "<sequence>" \
        "<data>fix</data><data>fix</data><data>fix</data><data>fix</data><data>fix</data><data>fix</data>" \
        "<data>value_${i}</data><data>value_2</data>" \
        "</sequence>" \
        "<Action>100</Action>" \
        "</record>" >> ${TEST_FILE}
    i=100002
        printf "%s%s%s%s%s%s%s%s%s\n" \
        "<record>" \
        "<keyset><key>${i}</key><key>none</key></keyset>" \
        "<user>markus</user>" \
        "<sequence>" \
        "<data>fix</data><data>fix</data><data>fix</data><data>fix</data><data>fix</data><data>fix</data>" \
        "<data>value_${i}</data><data>value_2</data>" \
        "</sequence>" \
        "<Action>101</Action>" \
        "</record>" >> ${TEST_FILE}

    # create test target user list
    {
        echo "markus"
        echo "jacob"
    } > ${TEST_TARGET}

}

# whileを使ったループで全件抽出
function perform_while_all(){


    local -r output="testresult_for.xml"
    echo -n "" > ${output}

    while read file_line
    do

        printf "%s\n" ${file_line} >> ${output} 

    done < ${TEST_FILE}

}

# whileを使ったループでuserタグのテキストがmarkusのみを抽出
function perform_while_markus(){


    local -r output="testresult_for.xml"
    echo -n "" > ${output}

    while read file_line
    do
    
        availablity=$(echo "${file_line}" | grep '<user>\(.*\)</user>' | wc -l )

        if [ 0 -eq ${availablity} ]; then
            continue
        fi

        user=$(echo "${file_line}" | sed -e 's|^.*<user>\(.*\)</user>.*$|\1|g' )

        isMarkus=$(grep ${user} ${TEST_TARGET} | wc -l )

        if [ 0 -eq ${isMarkus} ]; then
            continue
        fi

        printf "%s\n" ${file_line} >> ${output} 

    done < ${TEST_FILE}

}

# awkを使ったループで全件抽出
function perform_awk_all(){

    local -r output="testresult_awk.xml"
    echo -n "" > ${output}


    awk -v output=${output} '
    {
        print $0 >> output
    }
    ' ${TEST_FILE}

}

# awkを使ったループでuserタグのテキストがmarkusのみを抽出
function perform_awk_markus(){

    local -r output="testresult_awk.xml"
    echo -n "" > ${output}


    awk -v matchfile=${TEST_TARGET} '
        # 対象ユーザ一覧を取得
        BEGIN {
            matchlist = "|";
            CAT = "cat " matchfile;
            while ((CAT | getline) > 0) {
                matchlist = matchlist "" $0 "|";
            } 
            close(CAT)
        }
        # メイン処理
        {
            user = $0
            sub(/^.*<user>/, "", user)
            sub(/<\/user>.*/,"", user)

            item = "|" user "|"

            if (index(matchlist, item) > 0) {
                print $0
            }
        }
    ' ${TEST_FILE} >> ${output}

}

# awkを使ったループでuserタグのテキストがmarkusのみを抽出。
# しかもパターンとしてActionタグのテキストが100または101の場合のみ処理する。
function perform_awk_markus_withAction(){

    local -r output="testresult_awk2.xml"
    echo -n "" > ${output}


    awk -v matchfile=${TEST_TARGET} '
        # 対象ユーザ一覧を取得
        BEGIN {
            matchlist = "|";
            CAT = "cat " matchfile;
            while ((CAT | getline) > 0) {
                matchlist = matchlist "" $0 "|";
            } 
            close(CAT)
        }
        # メイン処理
        $0 ~ /<Action>10[01]<\/Action>/{
            user = $0
            sub(/^.*<user>/, "", user)
            sub(/<\/user>.*/,"", user)

            item = "|" user "|"

            if (index(matchlist, item) > 0) {
                print $0
            }
        }
    ' ${TEST_FILE} >> ${output}

}


# gawkを使ったループでuserタグのテキストがmarkusのみを抽出。
function perform_gawk_markus(){

    local -r output="testresult_gawk.xml"
    echo -n "" > ${output}


    gawk -v matchfile=${TEST_TARGET} '
        # 対象ユーザ一覧を取得
        BEGIN {
            matchlist = "|";
            CAT = "cat " matchfile;
            while ((CAT | getline) > 0) {
                matchlist = matchlist "" $0 "|";
            } 
            close(CAT)
        }
        # メイン処理
        {
            user = $0
            sub(/^.*<user>/, "", user)
            sub(/<\/user>.*/,"", user)

            item = "|" user "|"

            if (index(matchlist, item) > 0) {
                print $0
            }       
        }
    ' ${TEST_FILE} >> ${output}

}

# gawkを使ったループでuserタグのテキストがmarkusのみを抽出。
# しかもパターンとしてActionタグのテキストが100または101の場合のみ処理する。
function perform_gawk_markus_withAction(){

    local -r output="testresult_awk2.xml"
    echo -n "" > ${output}


    gawk -v matchfile=${TEST_TARGET} '
        # 対象ユーザ一覧を取得
        BEGIN {
            matchlist = "|";
            CAT = "cat " matchfile;
            while ((CAT | getline) > 0) {
                matchlist = matchlist "" $0 "|";
            } 
            close(CAT)
        }
        # メイン処理
        $0 ~ /<Action>10[01]<\/Action>/{
            user = $0
            sub(/^.*<user>/, "", user)
            sub(/<\/user>.*/,"", user)

            item = "|" user "|"

            if (index(matchlist, item) > 0) {
                print $0
            }
        }
    ' ${TEST_FILE} >> ${output}

}


echo "-------------PERFORMANCE TEST--------------------"
echo "##PREPARE##"
prepare ${gen_records}
echo "records: $(wc -l ${TEST_FILE}) lines"
echo "kevin records: $(grep kevin ${TEST_FILE} | wc -l)"
echo "markus records: $(grep markus ${TEST_FILE} | wc -l)"
echo "Action 100 OR 101 records: $(grep -E '<Action>10[01]<\/Action>' ${TEST_FILE} | wc -l)"
echo "##PREPARE FINISH##"

echo "##EXAMINE##"
SECONDS=0
perform_while_all
echo "perform_while_all:Performance time: ${SECONDS} sec"

SECONDS=0
perform_awk_all
echo "perform_awk_all:Performance time: ${SECONDS} sec"

SECONDS=0
perform_while_markus
echo "perform_while_markus:Performance time: ${SECONDS} sec"

SECONDS=0
perform_awk_markus
echo "perform_awk_markus:Performance time: ${SECONDS} sec"

SECONDS=0
perform_gawk_markus
echo "perform_gawk_markus:Performance time: ${SECONDS} sec"

SECONDS=0
perform_awk_markus_withAction
echo "perform_awk_markus_withAction:Performance time: ${SECONDS} sec"

SECONDS=0
perform_gawk_markus_withAction
echo "perform_gawk_markus_withAction:Performance time: ${SECONDS} sec"


echo "-------------PERFORMANCE TEST--------------------"

```