awkの魅力を伝える: 「重複排除」「前行と比較しながらの条件分岐」
はじめに
数カ月前までawkに対して苦手意識があり、技術書籍を購入して色々勉強しました。その結果、ある程度は触れるようになってきたので、awkが活用できる実践例をいくつか紹介して、少しでも魅力を伝えられればと思っています。
本当は10ケースくらい一度に紹介するつもりだったのですが、どうにもうまくまとめきれないので、2,3ケース程度を小出しに公開していくことにしました。膨大になるようならbookにしたほうが良いのと、執筆を長引かせるとモチベーションが下がりそうなため。
この記事の対象読者
- パターン・アクション・組み込み変数・関数などある程度の知識がある
- grep/sed等のLinuxコマンドはある程度使えるが、awkを使う場面にピンと来ない
学習に使用した書籍
- 「シェル芸」に効く!AWK処方箋
Amazon: http://www.amazon.co.jp/dp/B01MYDZ55O
awkの文法をきちんと学ぶのに必要な知識が一通り詰まっています。
長所・短所もよくまとまっている印象です。
使用するテストデータ
実際に文字列処理の練習をする際、ある程度大きい適当なデータが欲しくなります。
ダミーデータや疑似個人情報などを活用したり、自分の端末から適当なOSログを引っ張ってきたりしても良いと思います。[1]今回は、集計などの説明をする際に以下のファイルを使用します。少し長いので、折りたたんでいます。このデータは次回以降でも使うことにします。
クリックして表示
Windows
Mac
Windows
Mac
Mac
Linux
Windows
Windows
Mac
Mac
Windows
Linux
Mac
Linux
日付,時刻,天気,説明,気温,最高気温,最低気温,湿度
2022-03-05,03:00:00,Clouds,曇りがち,8.21,9.04,8.21,75
2022-03-05,06:00:00,Clouds,雲,8.09,8.24,8.09,72
2022-03-05,09:00:00,Clouds,雲,9.51,9.51,9.51,57
2022-03-05,12:00:00,Clear,晴天,13.28,13.28,13.28,32
2022-03-05,15:00:00,Clear,晴天,17.62,17.62,17.62,27
2022-03-05,18:00:00,Clear,晴天,15.99,15.99,15.99,30
2022-03-05,21:00:00,Clear,晴天,13.77,13.77,13.77,35
2022-03-06,00:00:00,Clear,晴天,9.61,9.61,9.61,35
2022-03-06,03:00:00,Clouds,薄い雲,7.41,7.41,7.41,32
2022-03-06,06:00:00,Clouds,曇りがち,7.31,7.31,7.31,27
2022-03-06,09:00:00,Clouds,曇りがち,9.04,9.04,9.04,26
2022-03-06,12:00:00,Clouds,曇りがち,10.71,10.71,10.71,21
2022-03-06,15:00:00,Clouds,曇りがち,10.88,10.88,10.88,20
2022-03-06,18:00:00,Clouds,雲,8.24,8.24,8.24,26
2022-03-06,21:00:00,Clouds,薄い雲,7.25,7.25,7.25,29
2022-03-07,00:00:00,Clear,晴天,6.7,6.7,6.7,30
2022-03-07,03:00:00,Clear,晴天,5.89,5.89,5.89,33
2022-03-07,06:00:00,Clear,晴天,5.49,5.49,5.49,36
2022-03-07,09:00:00,Clouds,雲,7.92,7.92,7.92,24
2022-03-07,12:00:00,Clouds,厚い雲,10.48,10.48,10.48,24
2022-03-07,15:00:00,Clouds,厚い雲,11.74,11.74,11.74,38
2022-03-07,18:00:00,Clouds,厚い雲,9.66,9.66,9.66,59
2022-03-07,21:00:00,Rain,小雨,8.2,8.2,8.2,60
2022-03-08,00:00:00,Rain,小雨,7.56,7.56,7.56,69
2022-03-08,03:00:00,Rain,適度な雨,5.55,5.55,5.55,89
2022-03-08,06:00:00,Rain,小雨,4.87,4.87,4.87,87
2022-03-08,09:00:00,Rain,小雨,4.76,4.76,4.76,86
2022-03-08,12:00:00,Clouds,厚い雲,6.36,6.36,6.36,76
2022-03-08,15:00:00,Clouds,曇りがち,9.18,9.18,9.18,58
2022-03-08,18:00:00,Clouds,薄い雲,9.5,9.5,9.5,53
2022-03-08,21:00:00,Clouds,薄い雲,7.75,7.75,7.75,60
※Open Weather Map API[2]を利用して取得したデータをスクリプトで加工したものです。
実行環境
これも重要かと思います。awkはGNU実装のものになります。Macなどで検証をされる際は挙動がおかしくなる場合がありますので、ご注意ください。
- bash / GNU bash, version 4.4.20(1)-release (x86_64-redhat-linux-gnu)
- awk / GNU Awk 4.2.1, API: 2.0 (GNU MPFR 3.1.6-p2, GNU MP 6.1.2)
実践例1. 重複排除
awkの連想配列を上手く利用して、テキストの重複行の排除をすることが出来ます。
oslist.txt
に登場するOSの種類を列挙したいとします。
cat oslist.txt | awk '!a[$0]++'
Windows
Mac
Linux
実行結果の解釈
awkの連想配列をカウンターとして使用しています。$0
で行全体をキーとして、変数aのキー$0
に対応する値をインクリメントします。後置インクリメントのため、最初はa[$0]==0
すなわちFalse
として評価され、!
によって反転し、パターンがTrue
となります。
2回目以降はカウンターの値は1以上となっているので、(1以上の数値)
すなわちTrue
、!
によって反転し、パターンがFalse
となります。
アクションにはprint $0
が省略されており、最初の一回のみ出力されるといった挙動となります。この仕組みのおかげで、タイプ数が非常に少なく、スリムに見えるのが素晴らしいですね。
sort | uniq
、sort -u
との比較
Linuxコマンドでの重複行排除といえば以下が有名です。重複排除と聞いたとき、こちらのコマンドが先に思い浮かんだ方も少なくないでしょう。
sort | uniq
sort -u
uniqコマンドがそもそも重複排除のためのコマンドですが、このコマンドでは「直前の行と同じ値が出てきたら出力をしない」といった動きをします。そのため、事前にsortをしていることが必要とされます。[3]
試しに、oslist.txt
に対してuniqを直接適用してみます。
cat oslist.txt | uniq
Windows
Mac
Windows
Mac
Linux
Windows
Mac
Windows
Linux
Mac
Linux
今回は連続していなくても重複を排除してほしいので、事前にsortをした上でuniqを実行する必要があります。しかし、sortコマンドは一般的にはΟ(NlogN)の時間計算量がかかるとされています。[4]
ソートの必要がない場合はawkを使用した方が計算量の面で効率が良いです。同じ「重複排除」が目的の場合でも、以下の状況ではawkを使う価値が大きいと言えると思います。
- sortを意識する必要がない
- 行の登場順序を崩したくない
sort | uniq
と sort -u
では後者の方が速度が速いですが、
sort | uniq
を用いると
# 登場回数の多い順にランキングを出力する
cat oslist.txt | sort | uniq -c | sort -nr
6 Mac
5 Windows
3 Linux
といった使い方が出来ます。[5]
状況に応じて、awk
、sort -u
、sort | uniq
を上手く使い分けられると良いと思います。
発展: 特定フィールドのみの重複排除
awkでは特定フィールドに絞っての重複排除なども可能です。
この例ではopenweatherdata.csv
を使用します。3列目は天気の分類、4列目は具体的な天気の説明が記述されているので、4列目を元に「データ内に存在する天気の詳細説明のパターン」を抽出してみましょう。awkではデフォルトの区切り文字を半角スペースとしているので、,区切りのcsvデータを対象にする場合は-F,
と記述します。
cat openweatherdata.csv | awk -F, 'NR>1&&!a[$4]++{print $4}'
曇りがち
雲
晴天
薄い雲
厚い雲
小雨
適度な雨
最初の行はヘッダのため、無視します。(NR>1)
あとは行全体版とほとんど一緒で、$0
と行全体にしていたものを$4
に変更しただけです。
筆者はawkをある程度使えるようになるまでこのような処理を行う際は、grep -o
でフィールドをうまく抽出して(またはsed -E
で後方参照を利用して置換するなど…)、そこからsort -u
として結果を得ていました。
awkだと非常にシンプルで無駄がなく、awk1つで完結します。
実践例2. 前行と比較しながらの条件分岐
前行の情報をもとに判定を行う…という例を2つ紹介します。
ここでもopenweatherdata.csv
を使用します。
2-1. 天気が変化するレコードのみを出力する(変化がない行を削除する)
出力結果は3時間置きに出力されていますが、例えば以下について。
…
2022-03-05,12:00:00,Clear,晴天,13.28,13.28,13.28,32
2022-03-05,15:00:00,Clear,晴天,17.62,17.62,17.62,27
2022-03-05,18:00:00,Clear,晴天,15.99,15.99,15.99,30
2022-03-05,21:00:00,Clear,晴天,13.77,13.77,13.77,35
2022-03-06,00:00:00,Clear,晴天,9.61,9.61,9.61,35
2022-03-06,03:00:00,Clouds,薄い雲,7.41,7.41,7.41,32
…
3/5 12時~3/6 0時まではずっとClear,晴天となっていますね。そして天気が変わるのは3/6 3時のタイミングです。3列目の情報が大まかな天気を表しているので、変化がない場合は出力をスキップすることにしましょう!
cat openweatherdata.csv | awk -F',' 'NR>1&&tmp!=$3{print $0}{tmp=$3}' | column -s, -t
出力結果は見やすく表示するために| column -s, -t
を付け足しています。
-s
で出力の区切り文字を指定して、-t
でテーブル表示に整形します。
2022-03-05 03:00:00 Clouds 曇りがち 8.21 9.04 8.21 75
2022-03-05 12:00:00 Clear 晴天 13.28 13.28 13.28 32
2022-03-06 03:00:00 Clouds 薄い雲 7.41 7.41 7.41 32
2022-03-07 00:00:00 Clear 晴天 6.7 6.7 6.7 30
2022-03-07 09:00:00 Clouds 雲 7.92 7.92 7.92 24
2022-03-07 21:00:00 Rain 小雨 8.2 8.2 8.2 60
2022-03-08 12:00:00 Clouds 厚い雲 6.36 6.36 6.36 76
この処理そのものに実用性があるかはさておき、余計な情報の出力を削りたいといったケースはそれなりに需要があると思います。例えばログファイルの監視において、ステータスの変化がない部分を削るなど。
さすがにgrep/sedでこのような処理をするのは苦しいのではないでしょうか。そもそもgrep/sedは改行を跨いでの処理が苦手という弱点があります。[6]複数行を跨いでの判定が得意というのはまさにawkの強みではないでしょうか。
2-2. 日付表記が↑と同じ場合は空白とする
Excelでよくあるやつです。誰もがExcel初心者の頃に調べた経験があるのではないでしょうか。
cat openweatherdata.csv | awk -F',' 'BEGIN{OFS=","}NR==1{tmp=$1}NR>1{if(tmp==$1){$1=""}else{tmp=$1}}{print $0}' | column -s, -t
BEGIN{OFS=","}
ではなく、オプション-v
を使用して-v OFS=,
としても良いです。
日付 時刻 天気 説明 気温 最高気温 最低気温 湿度
2022-03-05 03:00:00 Clouds 曇りがち 8.21 9.04 8.21 75
06:00:00 Clouds 雲 8.09 8.24 8.09 72
09:00:00 Clouds 雲 9.51 9.51 9.51 57
12:00:00 Clear 晴天 13.28 13.28 13.28 32
15:00:00 Clear 晴天 17.62 17.62 17.62 27
18:00:00 Clear 晴天 15.99 15.99 15.99 30
21:00:00 Clear 晴天 13.77 13.77 13.77 35
2022-03-06 00:00:00 Clear 晴天 9.61 9.61 9.61 35
03:00:00 Clouds 薄い雲 7.41 7.41 7.41 32
06:00:00 Clouds 曇りがち 7.31 7.31 7.31 27
09:00:00 Clouds 曇りがち 9.04 9.04 9.04 26
12:00:00 Clouds 曇りがち 10.71 10.71 10.71 21
15:00:00 Clouds 曇りがち 10.88 10.88 10.88 20
18:00:00 Clouds 雲 8.24 8.24 8.24 26
21:00:00 Clouds 薄い雲 7.25 7.25 7.25 29
2022-03-07 00:00:00 Clear 晴天 6.7 6.7 6.7 30
03:00:00 Clear 晴天 5.89 5.89 5.89 33
06:00:00 Clear 晴天 5.49 5.49 5.49 36
09:00:00 Clouds 雲 7.92 7.92 7.92 24
12:00:00 Clouds 厚い雲 10.48 10.48 10.48 24
15:00:00 Clouds 厚い雲 11.74 11.74 11.74 38
18:00:00 Clouds 厚い雲 9.66 9.66 9.66 59
21:00:00 Rain 小雨 8.2 8.2 8.2 60
2022-03-08 00:00:00 Rain 小雨 7.56 7.56 7.56 69
03:00:00 Rain 適度な雨 5.55 5.55 5.55 89
06:00:00 Rain 小雨 4.87 4.87 4.87 87
09:00:00 Rain 小雨 4.76 4.76 4.76 86
12:00:00 Clouds 厚い雲 6.36 6.36 6.36 76
15:00:00 Clouds 曇りがち 9.18 9.18 9.18 58
18:00:00 Clouds 薄い雲 9.5 9.5 9.5 53
21:00:00 Clouds 薄い雲 7.75 7.75 7.75 60
2022-03-09 00:00:00 Clouds 曇りがち 7.33 7.33 7.33 55
03:00:00 Clouds 雲 6.47 6.47 6.47 50
06:00:00 Clouds 薄い雲 5.98 5.98 5.98 53
09:00:00 Clouds 雲 7.6 7.6 7.6 46
12:00:00 Clouds 曇りがち 9.82 9.82 9.82 40
15:00:00 Clouds 曇りがち 11.26 11.26 11.26 36
18:00:00 Clouds 雲 11.11 11.11 11.11 41
21:00:00 Clouds 雲 10.35 10.35 10.35 51
2022-03-10 00:00:00 Clouds 薄い雲 9.6 9.6 9.6 59
かなり見やすくなった気がしますね。
awk内のコマンドで行っている処理が少し複雑だと思うので、注釈とインデントを付けてみました。[7]
# 出力時の区切り文字OFSを,に設定する。デフォルトだとスペース1つ分。
BEGIN{
OFS=","
}
# 最初の行は空白にはせず、tmpを更新する
NR==1{
tmp=$1
}
# 前行の$1と同じかどうかの判定を行うブロック
NR>1{
if(tmp==$1){
# 同じ場合は$1を空白にする
$1=""
}else{
# 異なる場合は日付が変わったということなので、tmpを更新する
# 空白の設定はしない
tmp=$1
}
}
# 明示的に出力を指定。場合によっては出力内容のフィールドを指定する。
{
print $0
}
ちなみにこのcsvデータにはヘッダがあるので、本来は行番号2からスタートさせるのが適切だと思います。今回のケースはどちらでも問題ないので横着しています。
個人的にはLinuxコマンドのワンライナーに自然に混ぜるような使い方が出来ればと思っているので、可能な限りfor文・if文のネストはしたくないですが…、このくらいなら許容範囲でしょうか。重複排除に比べるとスマートさには欠けるかもしれません。
まとめと次回予告
awkを知らなくてもgrep/sed/cutコマンドであったり、テキストエディタや表計算ソフトなど、様々なもので代用が効く場合が多いです。逆に言うと、awkを知っていればそれらの処理をすべて1本でまとめることが出来るかもしれませんし、他のコマンドでやろうとすると手間取る処理がスマートに書けるかもしれません。
元々1つの記事で書こうとしていた内容を小分けにしたので、一気に統一感というかコンセプトがなくなったような気がしています…。難しいですね。
次回は、awkならではの集計処理などの観点で執筆したいと思います。
-
事故が起きたら危ないので、機密情報や仕事用のデータなどを使用するのは避けましょう。 ↩︎
-
Open Weather Map API https://openweathermap.org/ ↩︎
-
一見不便なようにみえますが、状況によっては敢えてそのままuniqを実行した方が良い場面もあると思います。 ↩︎
-
ソートアルゴリズムの実装による違いもあり、対象データの状態によってはO(N)に近い時間で処理をしたり、最悪の場合はΟ(N^2)かかったりする場合もあります。 ↩︎
-
少し面倒ですが、awkでも連想配列を用いて集計することが出来ます。 ↩︎
-
どちらも-zオプションを用いて処理することが可能です。 ↩︎
-
awkのフォーマットは自力でやっているので、おかしいところがあるかもしれません。 ↩︎
Discussion