02 月 03 日(金)

フィルタ言語 AWK (2)

AWK の仕組みについて,より詳しく理解し, フィルタを効率的に作成できるようになろう.

AWK を活用すれば,表計算ソフト(E×cel とか)での 肉体労働的作業に煩わされる必要はないぜ. (もちろん,表計算ソフトでもある程度の自動化するための機能はあります.)

前回と同様, gawk 日本語マニュアル AWK の第一歩 等を参考にしながら,作業を進めよう.


AWK の特徴

AWK はテキストファイルのフィルタ処理に特化したプログラミング言語である. このため,テキストファイル処理に限れば, Cのような汎用(はんよう)のプログラミング言語に比べて, はるかに簡単に処理を記述できる. ではここで,AWK とCとを比較してみよう.

AWK の基本的な動作

AWK スクリプトの基本構成は次の通り:

#!/usr/bin/awk -f

BEGIN { 前処理 }

パターン1 { アクション1 }
パターン2 { アクション2 }
パターン3 { アクション3 }
...

END { 後処理 }

そして,AWK スクリプトの動作は次の通り:

  1. BEGIN ブロックの前処理が一回だけ実行される.
  2. コマンドラインで変数の初期値を指定した場合,その変数が初期化される.
  3. パターン・アクションの部分が, 入力ファイルの各レコードに対して自動的に繰り返される.
  4. なお,複数の入力ファイルを指定した場合, これらの処理は,各ファイルに対して,さらに繰り返される.

  5. 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

Cとの比較

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 の変数

AWK では,通常のユーザ変数を新規に定義できる他, 特別な意味をもつ組み込み変数があらかじめ用意されている.

なお,変数は,アクション部だけでなくパターン部でも使用できる.

ユーザ変数

Cなどの他の言語と同様に,変数・配列を定義できる. 変数名には,アルファベットと数字を組み合わせて使える. (もちろん 1 文字目はアルファベットに限定.)

AWK の変数の性質は,次の通り: (Cよりもシェルに近い.)

配列も使えるが,今回は省略,次回に紹介.

組み込み変数

有用なスクリプトを効率的に作成するためには, 組み込み変数を有効に活用する必要があるだろう.

よく使いそうなものを示しておく:

AWK の組み込み変数は,他にもたくさんある.マニュアル等を参照せよ. また,正規表現については,前々回説明済み.

OFSORS の意味がわかりづらいかも知れない. 前回のデータファイル table.txt に対して, 次の例を試してみよう:


BEGIN {
	OFS = ", ";	# カンマで区切って出力する
	ORS = "\n\n";	# ダブルスペースで(1行空けて)出力する
}
NF != 0 { $1 = $1; }	# OFSおよびORSの変更を反映するための呪文

{ print $0; }
OFS を出力に反映するには, OFS を書き換えるだけでは不十分で, レコードを書き換えてやる必要があるようだ. しかし,データを本当に書き換えてしまうと困る場合もあるので, 上の例では, $1=$1 として書き換えたフリをして,AWK をダマしてみた.

フィールド変数

$フィールド番号という形式で, レコードから特定のフィールドの内容を抽出できる. ただし,$0 はレコード全体を表わす.

フィールド変数の番号部分は,定数だけでなく,変数や式でも構わない. つまり,$変数名$(数式) という形式で, 計算結果に応じて異なるフィールドを選択できる.

たとえば:

{
	print $NF;	# 最後のフィールドだけ出力
}
シェル変数の $と混同しないこと!! 対策として, たとえば $num を C 言語的に field[num] などと 読み替えてやるとわかりやすい.

なお,フィールド変数は名前の通り変数なので,代入も可能:

{
	$1 = $1 "さんの得点:\t";
	print $0;
}

しかし,代入の後は当然, 元の入力データが失われてしまう(かもしれない)ので, 注意が必要だ. 上の例のように単に,表示を変えたいだけならば:

{ print $1, "さんの得点:\t", $2, $3, $4; }
とする方が安全だろう. フィールド変数への代入は, 本当に必要な場合だけにしておくこと.

クドいが, フィールド変数の「$変数名」と ユーザ変数の「変数名」 との違いに注意しよう.

AWK の制御構造

上述の通り,AWK では, レコードについての反復処理と選択処理とを (半自動的に)簡単に記述できる. しかし,フィールドについての反復などについては, 他の言語と同様に,(手動で)詳細に記述してやる必要がある.

そのため,AWK は制御構造用の命令 (iffor など)も備えている. なお,ほとんどのものはCと同様だが, AWK に特有のものもある.

Cと同じ制御命令:

AWK 特有の制御命令:

なお,これらの命令を使える場所は,アクション部だけ. パターン部に書くとエラー.

効率的なスクリプト作法

AWK スクリプトを効率良く書くためには, AWK 特有の制御命令と組み込み変数の活用がポイントである.

たとえば,各レコードについての処理を,C言語的な発想で, for とか if によって書くことも可能ではあるが, それだと,スクリプトは冗長になりがちだ. 制御構造 for とか if の利用については, 必要最小限にとどめるべきだ. AWK らしく,パターンをうまく利用しよう.

以下,実例として,空行を無視するスクリプトを示す.

悪い例:(AWK らしくないC言語的な AWK スクリプト)

{
	if (NF != 0) {
		...
	}
}
if 文ではなく,パターンを使うべき.

良い例:(AWK らしい AWK スクリプト)

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 の機能をより詳しく調べよう. このページの下に練習問題もある.

レポート提出 注意事項

練習問題

一次元数値データファイルの入力から 最大値・最小値を出力するような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) とか.

ヒント:

(c) 2017, yanagawa@kushiro-ct.ac.jp