(awk)=
# awk

awk は、CSV や TSV ファイルなどのデータを処理する際に用いられるスクリプト言語の 1 つです。awk は [コマンド](command) や [bash](bash) と組み合わせて使用でき、手軽に利用できる点が特徴です。データ解析に先立って平均や分散を概算したい場合などに便利です。ただし、本格的な解析を行う場合には、awk ではなく [Python](python) や R などのより強力なプログラミング言語を利用する方が適しています。本章では、awk の基本的な使い方を紹介します。

## awk 基本文型

awk の基本文型は、**パターン**（**pattern**）と **アクション**（**action**）を 1 組として記述する形式です。例えば、あるファイルの各行について、指定したパターンに一致した場合に処理を行うには、次のように記述します。

```awk
pattern { action }
```

パターンとアクションが複数ある場合は、この基本文型を続けて記述します。例えば、行の内容が pattern1 に一致した場合には処理 1（action1）を行い、pattern2 に一致した場合には処理 2（action2）を行う場合は、次のように書きます。

```awk
pattern1 { action1 }
pattern2 { action2 }
```

このように複数のパターンとアクションを組み合わせることで、例えば「"apple" を含む行には処理 1 を行い、"orange" を含む行には処理 2 を行う」といった条件分岐を実現できます。

## パターン

パターンは正規表現で指定します。例えば、行全体に "apple" を含む場合に処理 1 を行い、"orange" を含む場合に処理 2 を行うには、次のように記述します。パターンマッチの対象は `/` で囲んで指定します。

```awk
$0 ~ /apple/ { action1 }
$0 ~ /orange/ { action2 }
```

ここで `$0` は組み込み変数であり、1 行全体の内容が保存されています。`$0 ~ /apple/` は、その行のどこかに "apple" という文字列が含まれているかどうかを判定しています。行中に "apple" が含まれていれば、対応するアクションが実行されます。なお、`$0 ~` は省略可能であり、次のように簡潔に記述することもできます。

```awk
/apple/ { action1 }
/orange/ { action2 }
```

CSV や TSV 形式のデータには列（フィールド）の概念があります。例えば、2 列目のデータに "apple" を含む場合に処理 1 を行い、"orange" を含む場合に処理 2 を行うには、次のように 2 列目に対してパターンマッチを行います。`$2` は組み込み変数で、2 列目の値を表します。同様に `$3`、`$4` はそれぞれ 3 列目、4 列目を意味します。

```awk
$2 ~ /apple/ { action1 }
$2 ~ /orange/ { action2 }
```

正規表現による指定以外に、行番号を用いたパターン指定も可能です。例えば、11 行目以降のデータに対して処理を行う場合は、次のように記述します。`NR` は組み込み変数の 1 つで、現在処理している行番号が保存されています。

```awk
NR > 10 { action }
```

条件が複数ある場合には、AND 演算子 `&&` や OR 演算子 `||` を用いて条件を組み合わせることができます。例えば、3 列目に "apple" を含み、かつ 10 行目以降である行に対して処理を行う場合は、次のように記述します。

```awk
$3 ~ /apple/ && NR > 10 { action }
```

## アクション

アクションでは、加算、減算、乗算、除算といった計算処理や、結果の出力処理を行うことができます。また、必要に応じて `if`、`while`、`for` などのフロー制御構文も使用できます。ただし、フロー制御を多用するような複雑な処理については、awk にこだわるよりも [Perl](perl)、[Python](python)、R などで記述した方が効率的です。そのため、本書では awk におけるフロー制御構文の詳細な説明は行いません。

アクションの書き方を単独で説明するよりも、具体例を通して理解する方が分かりやすいため、以下では実例を用いて説明します。

## 使用例

### 列の抽出

awk を用いて、条件を満たす行から特定の列のみを抽出する例を示します。この操作は `grep` や `cut` コマンドでも実現できますが、ここでは awk を用います。例として、iris.txt ファイルから "setosa" を含む行のうち、3 列目のデータを出力します。

```bash
cd ~/Desktop/takarabako
grep setosa iris.txt | cut -f 3
```

```script-output
3.5
3
3.2
...
...
```

同じ処理は awk を用いて次のように記述できます。

```bash
awk '/setosa/ { print $3 }' iris.txt
```

```script-output
3.5
3
3.2
...
...
```

awk では、1 列目、2 列目、3 列目、… のデータが、それぞれ組み込み変数 `$1`、`$2`、`$3`、… に格納されています。ここでは 3 列目を出力したいため、アクションとして `print $3` を指定しています。

次に、"setosa" を含む行のうち、2 列目から 5 列目までのデータを出力する例を示します。この場合、出力したい列をカンマで区切って指定します。カンマで区切ると、実際の出力では列と列の間に空白が 1 つ挿入されます。

```bash
awk '/setosa/ { print $2, $3, $4, $5 }' iris.txt
```

```script-output
5.1 3.5 1.4 0.2
4.9 3 1.4 0.2
4.7 3.2 1.3 0.2
...
...
```

一方、カンマを用いずに記述した場合は、列の値が空白なしで連結されて出力されます。また、各列の値に文字列を付加したい場合には、`">"` のようにダブルクォートで囲んで指定します。

```bash
awk '/setosa/ { print ">" $2 $3 $4 $5 }' iris.txt
```

```script-output
>5.13.51.40.2
>4.931.40.2
>4.73.21.30.2
...
...
```

### 平均の計算

次に、awk を用いて平均値を計算する例を示します。iris.txt ファイルについて、"setosa" を含む行の 3 列目のデータの平均を求めます。平均を計算するために、各行の値の合計を変数 `sum` に、行数を変数 `n` に保存します。

```bash
awk '/setosa/ { sum = sum + $3; n = n + 1 } END { print sum/n }' iris.txt
```

```script-output
3.428
```

スクリプトが長くなる場合は、次のように改行して記述しても問題ありません。

```bash
awk '/setosa/ {
    sum = sum + $3; n = n + 1
}
END {
    print "sum=" sum/n
}' iris.txt
```

```script-output
sum=3.428
```

`/setosa/` に対応する処理では、`sum = sum + $3` により 3 列目の合計値を更新し、`n = n + 1` により該当行の数を数えています。

ここでは `END` という特別なパターンも使用しています。`END` は、ファイル全体の処理が終了した後に、そのアクションを 1 回だけ実行することを意味します。この例では、すべての行を処理した後で `sum` と `n` を用いて平均値 `sum/n` を計算しています。

同様に、4 列目および 5 列目についても平均値を求めることができます。

```bash
awk '/setosa/ { sum = sum + $4; n = n + 1 } END { print sum/n }' iris.txt
```

```script-output
1.462
```

```bash
awk '/setosa/ { sum = sum + $5; n = n + 1 } END { print sum/n }' iris.txt
```

```script-output
0.246
```

複数の列を同時に扱う場合には、列ごとに合計値を保存する変数を用意します。以下は、2 列目と 3 列目の平均値を同時に計算する例です。

```bash
awk '/setosa/ {
    sum2 = sum2 + $2;
    sum3 = sum3 + $3;
    n = n + 1
}
END {
    print sum2/n, sum3/n
}' iris.txt
```

```script-output
5.006 3.428
```

列数が増えるとこの方法では記述が煩雑になるため、`for` 構文を用いて繰り返し処理を行うこともできます。

```bash
awk '/setosa/ {
    for(i=2; i<6; i++){
        sum[i] = sum[i] + $i
    };
    n = n + 1
}
END {
    for(i=2; i<6; i++){
        print sum[i] / n
    } 
}' iris.txt
```

```script-output
5.006
3.428
1.462
0.246
```

このように、単純な平均計算であってもコードが複雑になりがちです。そのため、`if` や `for` などのフロー制御構文を多用する必要がある場合には、Perl、Python、R などの利用を検討するとよいでしょう。

### 行番号で行を抽出

行番号を指定して該当する行を抽出する例を示します。例えば、iris.txt ファイルの 20 行目から 30 行目までのデータを抽出する場合は、次のように記述します。この処理は `head` や `tail` コマンドでも実現可能です。

```bash
awk '20 <= NR && NR <= 30 { print $0 }' iris.txt
```

```script-output
20	5.1	3.8	1.5	0.3	setosa
21	5.4	3.4	1.7	0.2	setosa
22	5.1	3.7	1.5	0.4	setosa
23	4.6	3.6	1	0.2	setosa
24	5.1	3.3	1.7	0.5	setosa
25	4.8	3.4	1.9	0.2	setosa
26	5	3	1.6	0.2	setosa
27	5	3.4	1.6	0.4	setosa
28	5.2	3.5	1.5	0.2	setosa
29	5.2	3.4	1.4	0.2	setosa
30	4.7	3.2	1.6	0.2	setosa
```

ここで使用しているパターンは `20 <= NR && NR <= 30` であり、行番号が 20 以上かつ 30 以下の行が対象となります。アクションとして `print $0` を指定しているため、行全体が出力されます。awk では、行全体の内容は `$0` に、各列の値は `$1`、`$2`、`$3`、… にそれぞれ保存されています。

### 偶数行を抽出

処理中の行番号は組み込み変数 `NR` に保存されています。この値が奇数か偶数かを判定することで、奇数行または偶数行のみを抽出できます。偶数行のみを抽出する場合は、`NR` を 2 で割った余りが 0 であるかどうかを判定します。

```bash
awk 'NR % 2 == 0 { print $0 }' iris.txt
```

```script-output
2	4.9	3	1.4	0.2	setosa
4	4.6	3.1	1.5	0.2	setosa
6	5.4	3.9	1.7	0.4	setosa
...
...
```

### 空行を削除

空行とは、行の先頭から末尾までの間に文字が存在しない行のことです。正規表現では、行の先頭は `^`、行の末尾は `$` で表されるため、空行は `^$` と記述できます。この性質を利用して、空行パターン `^$` に一致しない行のみを出力するには、次のように記述します。なお、`!~` は「指定した正規表現に一致しない」という意味です。

```bash
awk '$0 !~ /^$/ { print $0 }' iris.txt
```
