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

# AWKとは

AWKはテキスト処理が得意なプログラミング言語です。UNIX/Linux環境であればデフォルトで入っていると思います。

# AWKを学ぶと何がうれしいのか

冷静に考えて、AWKなんて今更まじめに学ぶようなツールではないと感じる人が多い気がします。しかし、2018年でも、特にプログラマにとってAWKがもっとも有用なツールであるという場面があると思います。

AWKは、コマンドラインから簡単にテキストをフィルターしたり、表示を整えたり、値を集計したりできます。ポイントは「コマンドラインから簡単にテキストを」というところです。GUIから取得するデータや構造化されたファイル（jsonやyaml）を処理するのにAWKを使う必要はないですが、コマンドラインから出力したテキストをその場で処理するのにはすごく便利です。

例えば、rootが起動しているプロセスのCPU使用率の合計が知りたくなったとします。まず、`ps`コマンドで全てのプロセスとそのCPU使用率が取得できます。

```shell-session
$ 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使用率の合計を出すには、１番目の列が`root`になっているプロセスだけを抜き出して、３番目の列の値を足し合わせれば良いことがわかります。この処理をどう実行すれば良いでしょうか。`ps`の出力を保存して、rubyでテキスト処理をしても良いですし、python/pandasで集計しても便利だと思います。しかし、AWKを使えば以下の１行でやりたいことができます。スクリプトの意味はこの記事を読めばわかるようになりますが、この時点でもなんとなく察することができるのではないでしょうか。

```shell-session
$ ps aux | awk '$1 == "root" {s += $3} END {print s}'
1.6
```

AWKはコマンドラインでのテキスト処理や集計に特化したプログラミング言語なので、このようにある一定の状況下ではすごく便利なツールになり得ます。また、文法は非常に簡単で一瞬で覚えられるようなものなので、勉強しておいても損はないでしょう。

# AWKの文法

### プログラムの構造

AWKのプログラムは以下のような構造を取ります。

```awk
awk 'BEGIN {テキストを読む前に行う処理} /pattern/ {テキスト１行に対して行う処理} END {テキストを読み終わった後に行う処理}' <filename>
```

`<filename>`には処理したいテキストファイル名を書きます。もちろん、上の`ps`コマンドの例のようにコマンドの出力結果をパイプで渡すこともできます。

まず、`<filename>`で与えられたテキストを読み始める前に`BEGIN`の処理を行います。主に、変数の初期化やヘッダの出力などをすることが多いでしょう。もし特にやりたい処理がない場合は、`BEGIN`は省略可能です。

続いて、`/pattern/`にマッチするテキストの１行１行に対して行うメインの処理を書きます。ここで、`/pattern/`は正規表現で書くことが多いですが、「3列目の値が**である」など具体的な条件も指定できます。このメインの処理は複数個書くこともできます。`/pattern/`は省略可能で、省略された場合は全ての行に対して処理を行います。テキスト行の特定の列のみを出力したり、特定の列の値の集計をしたりします。

`END`に書いた処理は、全てのテキスト行に対する処理が終わった後に走ります。メインの処理で行った集計の結果の出力や、フッタの出力などをします。`BEGIN`と同様に、`END`も省略可能。

処理を書くための詳しい文法については次の節で説明しますが、まずは先ほどの`ps`コマンドの例の`$ ps aux | awk '$1 == "root" {s += $3} END {print s}'`が何をやっているかをAWKプログラムの構造をふまえて考えてみましょう。まず、`BEGIN`は省略されています。`/pattern/`では１列目（`$1`）の値が`root`であるという条件を指定しているので、そのような行に対してのみメインの処理を行います。このメイン処理ではCPU使用率の３列目（`$3`）の値を変数`s`に足し合わせていきます。最後の`END`で`s`の値を`print`することで、`root`が実行しているプロセスのCPU使用率の合計がわかります。

以上のように、一般的なプログラミング言語のpythonやrubyとは異なり、AWKでは「１行ずつファイルを読んでいきながら何かの処理をする」という流れがプログラムの構造に最初から組み込まれているので、テキスト処理が簡単に書けます。

### AWKスクリプトの文法

AWKプログラムの構造はわかったので、次に具体的な処理を書くための文法について見ていきます。文法と言っても大したものはなく、動的型付けのC言語だと思って適当に書けばだいたいうまくいく気がします。とりあえず、以下の文法を覚えておけば思い描いた処理はだいたい行えるでしょう。関数定義やループ処理などもできるのですが、使用頻度があまり高くない気がするのでこの記事では示さないことにします。

なお、以下の例ではメインの処理と入力ファイル名が省略できることを利用して`BEGIN`のみ処理を書いています。

##### 出力：`print, printf`

```shell-session
$ awk 'BEGIN {print "hello world"}'
hello world

$ awk 'BEGIN {printf "hello %s\n", "world"}'
hello world
```

##### 演算

ふつうにいろんな演算ができます。

```shell-session
$ awk 'BEGIN {print 1+2, 5*10, 3/2, 8%3, sin(1), sqrt(3)}'
3 50 2 1.5
```

##### 変数の利用

変数の型には数値型と文字列型があります。凝った処理では連想配列を使うこともありますが、とりあえずは気にしなくて良いでしょう。

```shell-session
$ 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`

```shell-session
$ 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で一番よく使う変数。一部の列のみの値を取り出したいときに使います。

```shell-session
$ echo "a b c d" | awk '{print $1, $3}'
a c
```

##### `$0`：入力行の全体

たまに、行全体をまるまる表示したい時があるのでその際に使います。

```shell-session
$ echo "a b c d" | awk '{print "alphabet:", $0}'
alphabet: a b c d
````

##### `NF`：入力行に含まれる値の数

いま処理している行が何列あるのかを取得できます。

```shell-session
$ echo "a b c d" | awk '{print NF}'
4
$ echo "a b c d e f g" | awk '{print NF}'
7
```

##### `NR`：入力行番号

いま処理している行が何行目なのかがわかります。

```shell-session
$ 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を使うときは明示的に区切り文字を指定してあげる必要があります。

```shell-session
$ cat <<EOF | awk 'BEGIN {FS = ","} {print $2}'
a,b,c,d
e,f,g,h
i,j,k,l
EOF
b
f
j
```

##### `OFS`：出力の区切り文字

今度は出力するときの区切り文字です。こちらもデフォルトはスペースなので、スペース以外の文字で区切りたいときに明示的に指定してあげます。

```shell-session
$ 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`の引数に使いたい区切り文字をベタ書きしても良いです。

```shell-session
$ echo "a b c d" | awk '{print $1, "\t", $3}'
a 	 c
```

# 便利なスクリプト例

AWKを使った便利なテキスト処理の例をいくつか挙げたいと思います。ここまでの記事の内容でカバーできているものが多いので、ぜひ考えてみてください。

処理するテキストファイルの例が決まっていた方がわかりやすいと思うので、以下のテキストを用いることにします。動物のリストで、１列目が種類、2列目が足の本数、3列目が匹数、4列目が生息地（陸/海）を表しています。すなわち、1行目は「犬は足が4本あり、ここに3匹いて、陸上に住む動物である」ということを表しています。ちなみに、今回調べていて知ったこととして、イカの足は10本と思われがちですが、そのうち2本は腕なので足は8本らしいです。よろしくお願いします。

```text: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
```

### 列を抜き出す

##### 種類だけを表示

```shell-session
$ awk '{print $1}' animals.txt
inu
neko
tako
ika
kani
ari
```
##### 種類と生息地を順番を逆にして表示

```shell-session
$ awk '{print $4 "\t" $1}' animals.txt
land    inu
land    neko
sea     tako
sea     ika
sea     kani
land    ari
```

### 特定の条件を満たす行を抜き出す

##### 名前に`a`を含む動物のみ表示

```shell-session
$ awk '$1 ~ /a/' animals.txt
tako    8       1       sea
ika     8       2       sea
kani    10      3       sea
ari     6       7       land
```

このようにパターンを書くと、１列目の値が正規表現に`/a/`にマッチする列を処理対象とします。ちなみに、`awk '$1 ~ /a/ {print $0}' animals.txt`と書いても良いですが、メインの処理を省略すると入力行をそのまま出力することを利用しています。

##### 足の本数が6本以上の動物のみ表示

```shell-session
$ awk '$2 >= 6' animals.txt
tako    8       1       sea
ika     8       2       sea
kani    10      3       sea
ari     6       7       land
```

このように、不等号を使ってパターンを書くこともできます。

##### 偶数行目のみ表示

```shell-session
$ awk 'NR % 2 == 0' animals.txt
neko    4       2       land
ika     8       2       sea
ari     6       7       land
```

行番号`NR`が偶数であるというパターンを使っています。

### 列の値を使った計算/集計をする

##### 全部の動物合わせて何匹いるか表示

```shell-session
$ awk '{n += $3} END {print n}' animals.txt
18
```

変数`n`に各行の匹数（`$3`）を足し合わせていき、最後に`print`しています。数値型の変数は0に初期化されているので、初期値が0で良いときは明示的に初期化する必要はないです。

##### 全部の動物合わせて何本の足があるか表示

```shell-session
$ awk '{nlegs += ($2 * $3)} END {print nlegs}' animals.txt
116
```

##### 陸上の動物合わせて何匹いるか表示

```shell-session
$ awk '$4 == "land" {n += $3} END {print n}' animals.txt
12
```

`if`文を使って`awk '{if ($4 == "land") n += $3} END {print n}' animals.txt`と書いてもOK。

##### 足の本数の最大値を表示

```shell-session
$ awk '{if ($2 > max) max = $2} END {print max}' animals.txt
10
```

##### 足の本数に合わせて合否を表示

足が8本以上の動物しか認めない人がいた場合。

```shell-session
$ 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
```

##### 匹数の累積和を表示

```shell-session
$ awk '{cumsum += $3; print $1 "\t" cumsum}' animals.txt
inu　　　　　　　　　　3
neko　　　　　　　　5
tako　　　　　　　　6
ika　　　　　　　　　　8
kani　　　　　　　　11
ari　　　　　　　　　　18
```

##### `a`のつく動物と`i`のつく動物の匹数をそれぞれ計算

```shell-session
$ 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
```

このように、メインの処理を複数個書くこともできます。

### フォーマット変更/追加

##### ヘッダ追加

```shell-session
$ 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
```

##### 行番号追加

```shell-session
$ 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
```

##### 区切り文字変更

```shell-session
$ awk 'BEGIN {OFS = ","} {print $1, $2}' animals.txt
inu,4
neko,4
tako,8
ika,8
kani,10
ari,6
```

# まとめ

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