AWK の仕組みについて,より詳しく理解し, フィルタを効率的に作成できるようになろう.
前回と同様, gawk 日本語マニュアルや AWK の第一歩 等を参考にしながら,作業を進めよう.
AWK はテキストファイルのフィルタ処理に特化したプログラミング言語である. このため,テキストファイル処理に限れば, Cのような汎用(はんよう)のプログラミング言語に比べて, はるかに簡単に処理を記述できる. ではここで,AWK とCとを比較してみよう.
AWK スクリプトの基本構成は次の通り:
#!/usr/bin/awk -f BEGIN { 前処理 } パターン1 { アクション1 } パターン2 { アクション2 } パターン3 { アクション3 } ... END { 後処理 }
そして,AWK スクリプトの動作は次の通り:
なお,複数の入力ファイルを指定した場合, これらの処理は,各ファイルに対して,さらに繰り返される.
AWK スクリプトのコマンドラインは,一般に次の形式となる:
$ スクリプト.awk 変数1=初期値 変数2=初期値 ... 入力ファイル1 入力ファイル2 ...
もちろん,スクリプトにはあらかじめ,実行属性を与えておく必要がある. (前回の説明も読み返しておこう.)
なお,コマンドラインで指定された変数の初期化は, 一見,スクリプト内容のコードに先立って実行されるように思えるが, 実は,そうではない. 上の「基本的な動作」で説明した通り, まず先に,BEGIN ブロックが実行され, その次に,コマンドラインでの初期化が実行されることになっている.
これを利用すると,コマンドラインでの初期値の指定/省略の切り替えが可能となる. 例として,前回の cut.awk を次のように改造してみよう:
#!/usr/bin/awk -f BEGIN { f = 1; } # デフォルトの初期値の設定 { print $f; }
$ ./cut.awk f=2 table.txt # 初期値を指定すると... 90 # 指定のフィールド $2 の抽出 80 85 $ ./cut.awk table.txt # 初期値を省略すると... Aho # デフォルトのフィールド $1 の表示 Weinberger Kernighan
AWK の基本動作をC言語的に記述すると,次のようになるだろう:
#include <stdio.h> #include <stdlib.h> #define BUFLEN 256 int main(int argc, char *argv[]) { char buf[BUFLEN]; FILE *fp; int i; { 前処理 } // BEGIN {...} { コマンドライン引数の処理 } // 変数の初期化,等 for (i = 1; i < argc; i++) { // 入力ファイルについてのループ if ((fp = fopen(argv[i], "r")) == NULL) exit(EXIT_FAILURE); while (1) { // レコードについてのループ if (fgets(buf, BUFLEN, fp) == EOF) break; { フィールド変数の処理 } // レコードをフィールドへ分解,等 if (条件式1) { アクション1 } // パターン1 {...} if (条件式2) { アクション2 } // パターン2 {...} if (条件式3) { アクション3 } // パターン2 {...} ... // 例:条件式が n == 0 のような場合,パターンもそのまま n == 0. // 条件式が strcmp(buf, 文字列) == 0 の場合,パターンは $0 == 文字列. } fclose(fp); } { 後処理 } // END {...} return (EXIT_SUCCESS); }
このように,かなり長くなってしまう. Cでは,テキスト処理に関わる本質的なコードだけでなく, そのための準備作業や後片付けなどの付随的なコードを いちいち記述しなければならないからだ. これでは,コーディングの時間・労力が多いだけでなく, それに伴い,余計な間違いも増えてしまうだろう. つまり,AWK の利点は, 開発効率が高い (やりたいことを素早く実行できる,やりたいことだけを書けばよい) ということだ.
一方,処理効率(プログラムの実行速度)については, Cの方がはるかに高い.(AWK の実行速度はとても遅い.) しかし,プログラミング作業まで含めた作業時間全体では, AWK の方が,はるかに少なくて済むかもしれない.
AWK では,通常のユーザ変数を新規に定義できる他, 特別な意味をもつ組み込み変数があらかじめ用意されている.
なお,変数は,アクション部だけでなくパターン部でも使用できる.
Cなどの他の言語と同様に,変数・配列を定義できる. 変数名には,アルファベットと数字を組み合わせて使える. (もちろん 1 文字目はアルファベットに限定.)
AWK の変数の性質は,次の通り: (Cよりもシェルに近い.)
有用なスクリプトを効率的に作成するためには, 組み込み変数を有効に活用する必要があるだろう.
よく使いそうなものを示しておく:
OFS や ORS の意味がわかりづらいかも知れない. 前回のデータファイル table.txt に対して, 次の例を試してみよう:
BEGIN { OFS = ", "; # カンマで区切って出力する ORS = "\n\n"; # ダブルスペースで(1行空けて)出力する } NF != 0 { $1 = $1; } # OFSおよびORSの変更を反映するための呪文 { print $0; }
$フィールド番号という形式で, レコードから特定のフィールドの内容を抽出できる. ただし,$0 はレコード全体を表わす.
フィールド変数の番号部分は,定数だけでなく,変数や式でも構わない. つまり,$変数名 や $(数式) という形式で, 計算結果に応じて異なるフィールドを選択できる.
たとえば:
{ print $NF; # 最後のフィールドだけ出力 }
なお,フィールド変数は名前の通り変数なので,代入も可能:
{ $1 = $1 "さんの得点\t"; print $0; }
しかし,代入の後は当然, 元の入力データが失われてしまう(かもしれない)ので, 注意が必要だ. 上の例のように単に,表示を変えたいだけならば:
{ print $1, "さんの得点\t", $2, $3, $4; }
上述の通り,AWK では, レコードについての反復処理と選択処理とを (半自動的に)簡単に記述できる. しかし,フィールドについての反復などについては, 他の言語と同様に,(手動で)詳細に記述してやる必要がある.
そのため,AWK は制御構造用の命令 (if や for など)も備えている. なお,ほとんどのものはCと同様だが, AWK に特有のものもある.
Cと同じ制御命令:
AWK 特有の制御命令:
AWK スクリプトを効率良く書くためには, AWK 特有の制御命令と組み込み変数の活用がポイントである.
たとえば,各レコードについての処理を,C言語的な発想で, for とか if によって書くことも可能ではあるが, それだと,スクリプトは冗長になりがちだ. 制御構造 for とか if の利用については, 必要最小限にとどめるべきだ. AWK らしく,パターンをうまく利用しよう.
以下,実例として,空行を無視するスクリプトを示す.
悪い例:
{ if (NF != 0) { ... } }
良い例:
NF != 0 { ... }
NF == 0 { next; } { ... }
動作はどの例でも同じだが... 開発効率(スクリプト作成・修正の手間ひま)がちがう. できるだけ,短く書こう.
テキストファイルの先頭 n 行だけを取り出すコマンド head について, AWK 版のクローン head.awk を作成せよ.
ただし,複数の入力ファイルと取り出す行数 n とを指定できること. また,行数 n を省略した場合,デフォルト値を n = 10 とすること.
実行例:
$ ./head.awk n=2 table.txt head.awk ==> table.txt <== Aho 90 100 80 Weinberger 80 100 95 ==> head.awk <== #!/usr/bin/awk -f # 説明:head のような AWK スクリプト
ヒント:
余裕のある人は,オンラインマニュアルなどを利用して, awk の機能をより詳しく調べよう. このページの下に練習問題もある.
一次元数値データファイルの入力から 最大値・最小値を出力するようなAWK スクリプト maxmin.awk を作成せよ.
ここで一次元数値データとは, 1 つのレコードに 1 つの数値フィールドだけをもつデータのことである. また,数値データには,正値だけでなく負値も含むものとする.
実行例:
$ cat data.txt -9 14 6 -234 ... # 乱数データ $ ./maxmin.awk data.txt max = ... # 最大値 min = ... # 最小値
アドバイス: データファイルを手動で作るより, 乱数のプログラムを作り,自動生成すると大変よろしい. このとき,プログラミング言語はCでも AWK でも何でもよい.
AWK では,入力データなしで動作するスクリプトも作れる. このためには,BEGIN ブロックだけを書けばよい. また,整数乱数を計算するには,int(rand()*100 - 50) とか.