awkの便利な使い方 / How to use awk nicely

こんにちは マネックスラボのMです。

今日はawkについて語っていきたいと思います。
マネックスラボではMONEX VIEWMONEX VISONMONEX 投資力診断などのサービスを開発していますが
開発の過程でデータ確認を行うことがよくあります。そして私はデータ確認にLinuxコマンドを多用します。

Linuxコマンドの中で最も優秀なのってなんでしょうね。
というソシャゲの最強キャラを言い争うような発言はしたくありませんが、
私はawkが好きです。優秀かどうかというより好きって表現が似合いますねawkには。

実は別にawkに詳しいわけでもありませんので、語るというほどの事もなく、
今日はこの使い方便利だよねっていう例をいくつか紹介させて頂きます。

区切り文字指定の-F、指定文字に該当する行抽出、該当行と出力項目を指定して出力

最も基本的な使い方になるでしょうか。data1のようなデータが並んでいるとして
「区切り文字(デフォルトはスペースかタブ)をカンマに指定」して 、「orangeを含んでいる行の」「1カラム目と3カラム目を抜き出す」だけです。
ついでに「行数をNRで出力」しています。

apple,shop1,100
orange,shop2,90
grape,shop3,200
$ awk -F, '/orange/ {print NR,$1","$3}' data1
2 orange,90

行数を指定して出力

ファイルのフォーマットによっては「特定の行だけ抜き出し」たい。なんてこともよくあります。
FNRを使用して行数を指定しています。
ついでに「printfで出力フォーマットの指定」や「ARGVによる引数の表示」をしています。
%sは文字列、%02dは2桁の整数をゼロ埋めしてください。という意味になっています。

特定の行を出力

apple,shop1,100
orange,shop2,90
grape,shop3,200
potato,shop1,50
carrot,shop2,40
onion,shop3,60
$ awk 'FNR==2{++i; printf "%02d: %s %s\n", i, ARGV[i], $0}' data2-{1,2}
01: data2-1 orange,shop2,90
02: data2-2 carrot,shop2,40

特定の行から特定の行までを出力

「この行からこの行まで抜き出し」たい。なんてこともよくありますよね。

111
222
333
444
555
666
777
888
999
000
$ awk 'FNR>4&&9>FNR{print NR, $0}' data2-3
5 555
6 666
7 777
8 888

特定のカラムを足し算

「特定のカラム(金額など)を合計したい」場合はこうです。私はよく使います。
BEGINは前処理、ENDは後処理を表しています。

apple,shop1,100
orange,shop2,90
grape,shop3,200
$ awk -F, 'BEGIN {a=0} {a+=$3} END {print a}' data1
390

特定の条件の行だけで足し算

発展型で、「特定のカラムが一致するものだけで合計したい」なんていう時もあります。
例えばこんな風に2カラム目がいくつかのパターンに分かれていて、同じパターン(shop1だけ等)のやつだけで足したい場合とかですね。

apple,shop1,100
orange,shop2,90
grape,shop3,200
banana,shop3,70
peach,shop1,300
melon,shop1,1000

ifで分岐させる

泥臭いですが分かりやすいです。

$ awk -F, 'BEGIN {{s1=0, s2=0, s3=0}} {if($2=="shop1"){s1+=$3} else if($2=="shop2"){s2+=$3} else{s3+=$3}} END {print "shop1="s1", shop2="s2", shop3="s3}' data3
shop1=1400, shop2=90, shop3=270

配列をパターン名で作成する

こうするとよりオシャレかもしれません。

$ awk -F, '{a[$2] += $3} END { for (i in a) print i, a[i] }' data3
shop1 1400
shop2 90
shop3 270

スラッシュ区切りの日付をスラッシュ外して数値としてソート

フォーマットの異なる困ったデータを扱わないといけない時もあります。
例えば、同じ日付でもゼロ埋めされていたりされていなかったりするこんなデータ。

2021/1/1
2021/10/2
2021/03/30
$ sort data4
2021/03/30
2021/1/1
2021/10/2
$ sort -n data4
2021/03/30
2021/1/1
2021/10/2
でもawkなら大丈夫。
$ $ awk -F/ '{printf $1} {printf "%02d", $2} {printf "%02d\n", $3}' data4 | sort -n
20210101
20210330
20211002

特定のカラムが現在日時と一致するか判定

「現在日付等を処理」することも出来ます。
例えば、今日が2021/09/13だとしてその日付が含まれる行だけを処理したい場合は
strftimeで日付を取得して、matchをかけるとRSTARTが一致箇所になります。
0でない=一致箇所があるということになるのを利用して判定しています。

2021/09/13 00:00:00 aaa
2021/09/14 00:00:00 bbb
2021/09/15 00:00:00 ccc
$ awk -F" " '{{match($1, strftime("%Y/%m/%d",systime()))} {if(RSTART!=0){print "OK "$0} else{print "NG "$0}}}' data5
OK 2021/09/13 00:00:00 aaa
NG 2021/09/14 00:00:00 bbb
NG 2021/09/15 00:00:00 ccc

長い1行の中から目的文字列に一致する部分だけ出力

1行の困った形式のファイルから目的の箇所だけを抜き出したい場合にも使えます。
「><部分でsplitしたデータでmatchさせてRSTARTで判定して表示」しています。
(もっと簡単に出来そうですが)

<product><name>apple</name><shop>shop1</shop><price>100</price><name>orange</name><shop>shop2</shop><price>90</price><name>grape</name><shop>shop3</shop><price>200</price></product>
$ awk '{num=split($0,p,/></)}{for(i=0;i<num;i++) {{match(p[i],"name>.*</name" )} {if(RSTART != 0) {print p[i]}} }}' data6
name>apple</name
name>orange</name
name>grape</name

おわりに

まだまだawkには色々な事が出来ると思いますが、
日常的によく使いそうなものを紹介させて頂きました。