LoginSignup
1
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-02-25

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

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

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

比較する実装

  • 今の実装

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

while read LINE
do
    # あれやこれや
done < ファイル
  • 新しい実装

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

検証するコード

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

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

という感じでXMLのレコードが100万件のダミーデータファイル1つを入力にする。
その中の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--------------------"

1
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3