How to extract column field in shell script
シェルスクリプトで列フィールドの抽出方法をまとめた。
シェルスクリプトでCSVなど一覧表やログファイルを処理していると,先頭や末尾のフィールドを取得したいときがある。いくつか方法があるのでまとめた。
結論は以下となった。
- 記述の簡潔さではawkが一番だが,実行速度は最下位。
- 最速はシェル変数展開。
- 記述の簡潔さと実効速度を考えると,ループ処理ではcutコマンドを使うのが妥当。
シェルスクリプトでファイルを1行ずつ処理するときは,以下の形式よく使う。
cat data.dat | while read -r line
do
# field_first=$(echo "$line" | [command])
# field_last=$(echo "$line" | [command])
done
この中で,変数lineに格納された1行文のデータから,それぞれのフィールドを取得して処理していく。
先頭からN番目のフィールドを取得するには,cut
コマンドが簡単だ。しかし,cut
コマンドでは末尾からN番目のフィールドにアクセスするためのオプションなどはない。
何かよい方法がないか調査し,最後に実行速度も検討した。以下の順番で記載していく。
まずは,サンプルデータを用意する。
echo "a,b,c" > data.dat
cut
cut
コマンドは,先頭からN番目のフィールドを取得するのは簡単だが,末尾からN番目は事前に列数を取得しておかないといけない。
cut -d ',' -f 1 data.dat # a
LASTCOL=$(($(head -n 1 data.dat | grep -o ',' | wc -l) + 1))
cut -d ',' -f $LASTCOL data.dat # c
cut -d ',' -f $((LASTCOL - 1)) data.dat # b
最初にLASTCOL変数に列数を格納している。grep
で区切り文字を検索して,そのヒット数をwc
でカウントしてそれを+1している。最初に変数を用意しないといけないが,悪くない方法だ。
POSIXに含まれていないrev
コマンドを使っていいなら,もっとエレガントにできる。
rev data.dat | cut -d ',' -f 1 | rev # c
rev data.dat | cut -d ',' -f 2 | rev # b
処理内容を言葉にすると以下となる。
- revコマンドで一度行を反転(フィールドも反転)。
- cutコマンドで先頭(もともと末尾)のフィールドを抽出。
- 最後に反転した文字列を元に戻す。
参照:bash – How to find the last field using ‘cut’. Linux – Stack Overflow
awk
awk
コマンドは記述が一番簡単だ。
awk -F "," '{print 1}' data.dat # a
awk -F "," '{print $NF}' data.dat # c
awk -F "," '{print $(NF-1)}' data.dat # b
NF変数に最後のフィールドが格納されているとのこと。一時変数を用意しなくて済むので一番簡単だろう。
sed
sed
だとちょっと複雑になる。
sed 's/^\([^,]*\),.*$/\1/' data.dat # a
sed 's/^.*,\([^,]*\)$/\1/' data.dat # c
sed 's/^.*,\(.*\),.*$/\1/' data.dat # b
少し複雑でわかりにくいので説明する。sedは最短マッチのためのオプションなどはないので,正規表現を工夫して自分で最短マッチさせる必要がある。
,[^,]*$
で区切り文字から行末までの最短マッチ,つまり最後のフィールドをマッチさせている。\(\)
で,マッチした部分をグループ化し,\1
で参照している。これにより,最後のフィールドだけを出力させている。- 最後から1番目のフィールドを取得するには,マッチの部分を
,\(.*\),.*$
のように,行末の直前にフィールドを表す,.*
をその回数分記述する。
複雑で,3番目などになると,記述が長くなるのでよい方法ではない。
grep
sedと同様に,grepでもパターンマッチを使うことでフィールドを抽出できる。
grep -o '^[^,]*' data.dat # a
grep -o '[^,]*$' data.dat # c
-oオプションでマッチ部分だけ出力できるので,sedよりは簡単にかける。
ただし,grepで先頭と末尾以外のN番目のフィールドを抽出するのは難しい。正規表現のパターンが思いつかなかった。
シェル変数展開
最後に考えられるのは,シェル変数展開だ。
line=$(cat data.dat)
line_first="${line%%,*}" # a
line_last="${line##*,}" # c
line_first_1="${line#*,}" && line_first_1="${line_first_1%%,*}" # b
line_last_1="${line%,*}" && line_last_1="${line_last_1##*,}" # b
この方法だと,先頭か末尾のフィールドの取得は簡単だ。N番目は以下の手順を繰り返すことで習得できる。
- 一度先頭か末尾のフィールドだけを削除した変数(
${line#*,}
,${line%,*}
)を用意。 - その変数に対して,先頭や末尾までのフィールドまでを削除(
${line2%%,*}
,${line2##*,}
)。
シェル変数展開を使う場合,コマンドを間に挟まないため早い実行速度が期待できる。
速度比較
列からフィールドを取得する方法をみてきたが,awkが最も簡単にできそうだ。しかし,フィールドからの値の取得は,ファイルなどのループでの利用が頻出事項と考えられる。実行速度も重要な項目だろう。
そこで,実際にここで紹介した方法で5万行のファイルを処理させて処理時間を計測・比較した。
計測に使用したスクリプトは以下となる(Gist)。
cat <<- EOF > loop-time.sh
#!/bin/sh
# \file loop-time.sh
# \author SENOO, Ken
# \copyright CC0
set -u
DATA="loop-data.dat"
: > $DATA
for i in $(seq 1 50000)
do
echo "1,2,3,4,5,6,7,8,9,0" >> $DATA
done
timeit_loop(){
start=$(date +%s)
cat $DATA |
(
while read -r line
do
last_field="$(eval $1)"
done
end=$(date +%s)
dt=$((end - start))
echo "time: $dt [s], last field: ${last_field}, $1"
)
}
LASTCOL=$(($(head -n 1 $DATA | grep -o ',' | wc -l) + 1))
timeit_loop 'echo "$line" | rev | cut -d ',' -f 1 | rev'
timeit_loop 'echo "$line" | cut -d ',' -f $LASTCOL'
timeit_loop 'echo "$line" | awk -F "," "{print \$NF}"'
timeit_loop 'echo "$line" | grep -o [^,]*$'
timeit_loop 'echo "$line" | sed "s/^.*,\([^,]*\)$/\1/"'
timeit_loop 'echo ${line##*,}'
EOF
これを実行させると以下となる。
./loop-time.sh | sort
time: 37 [s], last field: 0, echo "$line" | cut -d , -f $LASTCOL time: 50 [s], last field: 0, echo "$line" | grep -o [^,]*$ time: 51 [s], last field: 0, echo "$line" | sed "s/^.*,\([^,]*\)$//" time: 54 [s], last field: 0, echo "$line" | rev | cut -d , -f 1 | rev time: 71 [s], last field: 0, echo "$line" | awk -F "," "{print \$NF}" time: 8 [s], last field: 0, echo ${line##*,}
コマンド実行が少ないシェル変数展開が8 sと最速となった。次点は37 sのcut
コマンド単体となった。それ以降は,grep,sed,cut+revが並んでおり,awkが71 sと最下位となった。
cut+revコマンドは使用するコマンド数が3個と最も多いことから,処理速度が遅くなることを心配したが,そこまで遅くなかった。用途が特化している分,実行速度が最適化されているのだろう。
まとめ
シェルスクリプトで列フィールドを取得する方法をまとめた。結論は以下となった。
- 記述の簡潔さではawkが一番だが,実行速度は最下位。
- 最速はシェル変数展開。
- 記述の簡潔さと実効速度を考えると,ループ処理ではcutコマンドを使うのが妥当。
記述の簡潔さからawkが一番かなと思ったが,実行速度をみるとそうともいえなかった。ループ中でawkでフィールドを取得するのは控え,シェル変数展開の活用を念頭に置き,難しそうならcutコマンド単体やcut+revコマンドの組み合わせが簡単だろう。
逆に,ループ外であれば実行速度はあまり問題にならないので,awkを使うのもよいだろう。
もともと,実行速度は調査するつもりはなく,awkが一番というありきたりな結論で終わりそうだった。今回速度を計測することで,予想外の結果となり勉強になった。時間の計測は大事だと思った。