Posted at

テキスト処理にたまに便利なAWK入門


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に足し合わせていきます。最後のENDsの値を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本らしいです。よろしくお願いします。


animal.txt

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はテキスト処理に特化したプログラミング言語

  • 特にコマンドラインでテキストファイルを処理/集計したい場合は、短いコードでそこそこ複雑な処理が書けるので便利