AWKとは
AWKはテキスト処理が得意なプログラミング言語です。UNIX/Linux環境であればデフォルトで入っていると思います。
AWKを学ぶと何がうれしいのか
冷静に考えて、AWKなんて今更まじめに学ぶようなツールではないと感じる人が多い気がします。しかし、2018年でも、特にプログラマにとってAWKがもっとも有用なツールであるという場面があると思います。
AWKは、コマンドラインから簡単にテキストをフィルターしたり、表示を整えたり、値を集計したりできます。ポイントは「コマンドラインから簡単にテキストを」というところです。GUIから取得するデータや構造化されたファイル(jsonやyaml)を処理するのにAWKを使う必要はないですが、コマンドラインから出力したテキストをその場で処理するのにはすごく便利です。
例えば、rootが起動しているプロセスのCPU使用率の合計が知りたくなったとします。まず、ps
コマンドで全てのプロセスとそのCPU使用率が取得できます。
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.3 125452 3860 ? Ss 9月30 0:11 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
root 2 0.0 0.0 0 0 ? S 9月30 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S 9月30 0:00 [ksoftirqd/0]
...
sshd 13253 0.0 0.2 112796 2228 ? S 22:58 0:00 sshd: [net]
root 13257 0.0 0.0 0 0 ? S 22:58 0:00 [kworker/1:3]
ymr 13258 0.0 0.2 158744 2432 ? D 22:58 0:00 sshd: ymr@pts/0
ymr 13259 0.6 0.2 116240 2740 pts/0 Ss 22:58 0:00 -bash
root 13281 0.3 0.6 345904 6408 ? Sl 22:58 0:00 /usr/sbin/abrt-dbus -t133
root 13300 1.0 0.4 158680 4976 ? Ss 22:58 0:00 sshd: root [priv]
sshd 13301 0.0 0.2 112796 2228 ? S 22:58 0:00 sshd: root [net]
ymr 13302 0.0 0.1 155324 1868 pts/0 R+ 22:58 0:00 ps aux
rootが起動しているプロセスのCPU使用率の合計を出すには、1番目の列がroot
になっているプロセスだけを抜き出して、3番目の列の値を足し合わせれば良いことがわかります。この処理をどう実行すれば良いでしょうか。ps
の出力を保存して、rubyでテキスト処理をしても良いですし、python/pandasで集計しても便利だと思います。しかし、AWKを使えば以下の1行でやりたいことができます。スクリプトの意味はこの記事を読めばわかるようになりますが、この時点でもなんとなく察することができるのではないでしょうか。
$ ps aux | awk '$1 == "root" {s += $3} END {print s}'
1.6
AWKはコマンドラインでのテキスト処理や集計に特化したプログラミング言語なので、このようにある一定の状況下ではすごく便利なツールになり得ます。また、文法は非常に簡単で一瞬で覚えられるようなものなので、勉強しておいても損はないでしょう。
AWKの文法
プログラムの構造
AWKのプログラムは以下のような構造を取ります。
awk 'BEGIN {テキストを読む前に行う処理} /pattern/ {テキスト1行に対して行う処理} END {テキストを読み終わった後に行う処理}' <filename>
<filename>
には処理したいテキストファイル名を書きます。もちろん、上のps
コマンドの例のようにコマンドの出力結果をパイプで渡すこともできます。
まず、<filename>
で与えられたテキストを読み始める前にBEGIN
の処理を行います。主に、変数の初期化やヘッダの出力などをすることが多いでしょう。もし特にやりたい処理がない場合は、BEGIN
は省略可能です。
続いて、/pattern/
にマッチするテキストの1行1行に対して行うメインの処理を書きます。ここで、/pattern/
は正規表現で書くことが多いですが、「3列目の値が**である」など具体的な条件も指定できます。このメインの処理は複数個書くこともできます。/pattern/
は省略可能で、省略された場合は全ての行に対して処理を行います。テキスト行の特定の列のみを出力したり、特定の列の値の集計をしたりします。
END
に書いた処理は、全てのテキスト行に対する処理が終わった後に走ります。メインの処理で行った集計の結果の出力や、フッタの出力などをします。BEGIN
と同様に、END
も省略可能。
処理を書くための詳しい文法については次の節で説明しますが、まずは先ほどのps
コマンドの例の$ ps aux | awk '$1 == "root" {s += $3} END {print s}'
が何をやっているかをAWKプログラムの構造をふまえて考えてみましょう。まず、BEGIN
は省略されています。/pattern/
では1列目($1
)の値がroot
であるという条件を指定しているので、そのような行に対してのみメインの処理を行います。このメイン処理ではCPU使用率の3列目($3
)の値を変数s
に足し合わせていきます。最後のEND
でs
の値をprint
することで、root
が実行しているプロセスのCPU使用率の合計がわかります。
以上のように、一般的なプログラミング言語のpythonやrubyとは異なり、AWKでは「1行ずつファイルを読んでいきながら何かの処理をする」という流れがプログラムの構造に最初から組み込まれているので、テキスト処理が簡単に書けます。
AWKスクリプトの文法
AWKプログラムの構造はわかったので、次に具体的な処理を書くための文法について見ていきます。文法と言っても大したものはなく、動的型付けのC言語だと思って適当に書けばだいたいうまくいく気がします。とりあえず、以下の文法を覚えておけば思い描いた処理はだいたい行えるでしょう。関数定義やループ処理などもできるのですが、使用頻度があまり高くない気がするのでこの記事では示さないことにします。
なお、以下の例ではメインの処理と入力ファイル名が省略できることを利用してBEGIN
のみ処理を書いています。
出力:print, printf
$ awk 'BEGIN {print "hello world"}'
hello world
$ awk 'BEGIN {printf "hello %s\n", "world"}'
hello world
演算
ふつうにいろんな演算ができます。
$ awk 'BEGIN {print 1+2, 5*10, 3/2, 8%3, sin(1), sqrt(3)}'
3 50 2 1.5
変数の利用
変数の型には数値型と文字列型があります。凝った処理では連想配列を使うこともありますが、とりあえずは気にしなくて良いでしょう。
$ awk 'BEGIN {a = 4; b = 10; print a + b}'
14
$ awk 'BEGIN {s1 = "hello"; s2 = "world"; printf "%s %s\n", s1, s2}'
hello world
条件分岐:if-else
$ awk 'BEGIN {if (3 > 2) print "3 > 2"}'
3 > 2
$ awk 'BEGIN {s = 13; if (s % 2 == 0) print "even"; else print "odd"}'
odd
組み込み変数
$n
:入力行の中でn
番目の値($1
, $2
...)
おそらくAWKで一番よく使う変数。一部の列のみの値を取り出したいときに使います。
$ echo "a b c d" | awk '{print $1, $3}'
a c
$0
:入力行の全体
たまに、行全体をまるまる表示したい時があるのでその際に使います。
$ echo "a b c d" | awk '{print "alphabet:", $0}'
alphabet: a b c d
NF
:入力行に含まれる値の数
いま処理している行が何列あるのかを取得できます。
$ echo "a b c d" | awk '{print NF}'
4
$ echo "a b c d e f g" | awk '{print NF}'
7
NR
:入力行番号
いま処理している行が何行目なのかがわかります。
$ cat <<EOF | awk '{print NR, $0}'
> a b c d
> e f g h
> i j k l
> EOF
1 a b c d
2 e f g h
3 i j k l
FS
:入力の区切り文字
入力テキストの区切り文字を指定する時に使います。デフォルトではFS = " "
なのでスペース区切りの入力を処理する時には指定する必要はないのですが、例えば以下のようにカンマ区切りのテキストにAWKを使うときは明示的に区切り文字を指定してあげる必要があります。
$ cat <<EOF | awk 'BEGIN {FS = ","} {print $2}'
a,b,c,d
e,f,g,h
i,j,k,l
EOF
b
f
j
OFS
:出力の区切り文字
今度は出力するときの区切り文字です。こちらもデフォルトはスペースなので、スペース以外の文字で区切りたいときに明示的に指定してあげます。
$ cat <<EOF | awk 'BEGIN {OFS = ":"} {print NR, $2, $4}'
a b c d
e f g h
i j k l
EOF
1:b:d
2:f:h
3:j:l
OFS
を使わず、以下のようにprint
の引数に使いたい区切り文字をベタ書きしても良いです。
$ echo "a b c d" | awk '{print $1, "\t", $3}'
a c
便利なスクリプト例
AWKを使った便利なテキスト処理の例をいくつか挙げたいと思います。ここまでの記事の内容でカバーできているものが多いので、ぜひ考えてみてください。
処理するテキストファイルの例が決まっていた方がわかりやすいと思うので、以下のテキストを用いることにします。動物のリストで、1列目が種類、2列目が足の本数、3列目が匹数、4列目が生息地(陸/海)を表しています。すなわち、1行目は「犬は足が4本あり、ここに3匹いて、陸上に住む動物である」ということを表しています。ちなみに、今回調べていて知ったこととして、イカの足は10本と思われがちですが、そのうち2本は腕なので足は8本らしいです。よろしくお願いします。
inu 4 3 land
neko 4 2 land
tako 8 1 sea
ika 8 2 sea
hito 2 4 land
kani 10 3 sea
ari 6 7 land
列を抜き出す
種類だけを表示
$ awk '{print $1}' animals.txt
inu
neko
tako
ika
kani
ari
種類と生息地を順番を逆にして表示
$ awk '{print $4 "\t" $1}' animals.txt
land inu
land neko
sea tako
sea ika
sea kani
land ari
特定の条件を満たす行を抜き出す
名前にa
を含む動物のみ表示
$ awk '$1 ~ /a/' animals.txt
tako 8 1 sea
ika 8 2 sea
kani 10 3 sea
ari 6 7 land
このようにパターンを書くと、1列目の値が正規表現に/a/
にマッチする列を処理対象とします。ちなみに、awk '$1 ~ /a/ {print $0}' animals.txt
と書いても良いですが、メインの処理を省略すると入力行をそのまま出力することを利用しています。
足の本数が6本以上の動物のみ表示
$ awk '$2 >= 6' animals.txt
tako 8 1 sea
ika 8 2 sea
kani 10 3 sea
ari 6 7 land
このように、不等号を使ってパターンを書くこともできます。
偶数行目のみ表示
$ awk 'NR % 2 == 0' animals.txt
neko 4 2 land
ika 8 2 sea
ari 6 7 land
行番号NR
が偶数であるというパターンを使っています。
列の値を使った計算/集計をする
全部の動物合わせて何匹いるか表示
$ awk '{n += $3} END {print n}' animals.txt
18
変数n
に各行の匹数($3
)を足し合わせていき、最後にprint
しています。数値型の変数は0に初期化されているので、初期値が0で良いときは明示的に初期化する必要はないです。
全部の動物合わせて何本の足があるか表示
$ awk '{nlegs += ($2 * $3)} END {print nlegs}' animals.txt
116
陸上の動物合わせて何匹いるか表示
$ awk '$4 == "land" {n += $3} END {print n}' animals.txt
12
if
文を使ってawk '{if ($4 == "land") n += $3} END {print n}' animals.txt
と書いてもOK。
足の本数の最大値を表示
$ awk '{if ($2 > max) max = $2} END {print max}' animals.txt
10
足の本数に合わせて合否を表示
足が8本以上の動物しか認めない人がいた場合。
$ awk '{if ($2 >= 8) result = "OK"; else result = "NG"; print $1 "\t" result}' animals.txt
inu NG
neko NG
tako OK
ika OK
kani OK
ari NG
匹数の累積和を表示
$ awk '{cumsum += $3; print $1 "\t" cumsum}' animals.txt
inu 3
neko 5
tako 6
ika 8
kani 11
ari 18
a
のつく動物とi
のつく動物の匹数をそれぞれ計算
$ awk '$1 ~ /e/ {num_e += 1} $1 ~ /i/ {num_i += 1} END {printf "num_e: %d, num_i: %d\n", num_e, num_i}' animals.txt
num_e: 1, num_i: 4
このように、メインの処理を複数個書くこともできます。
フォーマット変更/追加
ヘッダ追加
$ awk 'BEGIN {print "kind numlegs num habitat"; print "-------------------------------"} {print $0}' animals.txt
kind numlegs num habitat
-------------------------------
inu 4 3 land
neko 4 2 land
tako 8 1 sea
ika 8 2 sea
kani 10 3 sea
ari 6 7 land
行番号追加
$ awk '{print NR "\t" $0}' animals.txt
1 inu 4 3 land
2 neko 4 2 land
3 tako 8 1 sea
4 ika 8 2 sea
5 kani 10 3 sea
6 ari 6 7 land
区切り文字変更
$ awk 'BEGIN {OFS = ","} {print $1, $2}' animals.txt
inu,4
neko,4
tako,8
ika,8
kani,10
ari,6
まとめ
- AWKはテキスト処理に特化したプログラミング言語
- 特にコマンドラインでテキストファイルを処理/集計したい場合は、短いコードでそこそこ複雑な処理が書けるので便利