01 月 30 日(月)

フィルタ言語 AWK (1)

AWK(おーく)は, 任意の機能をもつフィルタプログラムを手軽に作成するためのスクリプト言語である. 文法はC言語に似ているので,3J 学生にとっては,なじみやすいだろう.

AWK の名前は,3人の作者 Aho(エイホ),Weinberger(ワインバーガー), そして Kernighan(カーニハン)の頭文字から命名された. Kernighan はC言語の作者でもある.

オンラインマニュアル gawkや 参考文献 AWK の第一歩 も参考にしながら, 作業を進めよう.

なお,前回の内容(フィルタ,正規表現) を理解していないと,今回の内容も理解できない. まずは,前回の復習が必要.

AWK のコンセプト

とにかく,AWK を使えば簡単に, フィルタプログラムを構成できることを体験してみよう.

入力データ

AWK が得意とする入力データは, 表(テーブル,table)形式のテキストデータである. なお,表データの各行(横方向)のデータ集合をレコード(record), 各列(縦方向)のデータ集合をフィールド(field)と呼ぶ.

データの例として,成績表を考えよう.

NameEngMathPhys
Aho9010080
Weinberger8010095
Kernighan8595100

表データをテキストファイルとして記録する場合, 各フィールドの間は,通常,スペースやタブによって区切る. たとえば,上の成績表をテキストデータとして表わすと次のようになる. サンプルデータファイル table.txt

Aho		 90	100	 80
Weinberger	 80	100	 95
Kernighan	 85	 95	100
もちろん,フィールド名(Name,Eng,等)を データファイル内(の先頭行とか)に記述しておいてもよい. しかし,処理が少々複雑になってしまうので, 今回は省略しておく.

レコード単位の処理

AWK はレコード単位に処理を実行する. つまり,1 行のデータを読み,そのレコードに対して処理を実行する. そして,すべてのレコードに対して,この処理を繰り返す.

各レコードに対する処理は,Cのようなブロック構造をもち, 一般に次のような形式で記述される:

パターン { アクション }		# パターンに一致した行だけに対してアクションを実行

パターン(正規表現や条件式)にマッチしたレコードに対してだけ, アクション(処理)を実行する.

また,デフォルトのパターンの場合,すべてのレコードを処理対象とする:

{ アクション }		# すべての行に対してアクションを実行
以上の動作をC言語的に表わすと:
while (レコード文字列を入力) {
	if (レコード文字列 == パターン) { アクション }
	{ アクション }
}

AWK では,whileif は不要であり, コードを非常に短く書ける.

アクションの部分には, 計算式や制御構造などをCと同様な文法で記述できる.

レコード内の各フィールドを参照するには, フィールド変数$番号」を使う. 上の成績表の例では,$1 が Name, $2 が Eng,... のフィールドを表わす. また,$0 はレコード全体を表わす.

なお,普通の変数には $ を付けない.

記号 $ の使い方がシェルとは異なる. 微妙に似ている部分もあり混同しやすいので,注意しよう.

例:簡単なフィルタコマンドのクローン

では, サンプルデータ table.txt に対して, 単純な AWK コマンドを実行してみよう:

$  cat table.txt
Aho		 90	100	 80
Weinberger	...
...

$  awk '{ print $0 }' table.txt		# すべてのフィールドを表示
Aho		 90	100	80
...

$  awk '{ print $2 }' table.txt		# 2 番目のフィールドだけを表示
90
...

これらは,cat コマンドおよび cut コマンドと同様な機能をもつフィルタの例だ.

次は,正規表現のパターンを使ってみよう:

$  awk '/n.*n/ { print $0 }' table.txt
Kernighan	 85	 95	100

これは,grep 'n.*n' table.txt みたいなフィルタだ. なお,記号「/」でクォートされたパターンが正規表現とみなされる.

そして,条件式のパターンも使ってみよう:

$  awk '$4 >= 90 { print $0 }' table.txt
Weinberger	 80	100	 95
Kernighan	 85	 95	100

第4フィールド(Phys)が 90 点以上のレコードだけを抽出した.

さて,以上のような機能をもつフィルタプログラムを「C言語で実装せよ」 と言われると大変そうだ. はたしてソースコードは何行になってしまうのだろうか...? AWK なら 1 行で出来た!!


AWK のスクリプト

AWK は,前のセクションのようにコマンドライン上で実行することもできるが, 処理が複雑な場合やその処理を何度も使い回したい場合には, スクリプトファイルに記述しておくと便利である.

最も単純な AWK スクリプトの例

まず,cat コマンドのクローンを例として, AWK スクリプトファイル cat.awk を作ってみよう:

#!/usr/bin/awk -f
# 説明:cat のような AWK スクリプト
# 使い方:cat.awk ファイル名 ...

{
	print $0;        # レコード全体を出力
}

なお,シェルスクリプトの場合と同様, 記号 # から行末まではコメントであり省略してもよいが, 第1行だけは単なるコメントではなく,実行に必要なものなので削除しないこと. そして,実行許可についてもシェルと同様:

$  chmod  +x  cat.awk

では,table.txt を入力として実行してみよう:

$  ./cat.awk  table.txt
Aho	 90	100	80
...
	# cat table.txt と同じ結果

このように,AWK スクリプトのコマンドライン引数は, 自動的に入力ファイルとして処理される.

また,複数のファイル名を指定してもよい. その場合,すべてのファイルに対して処理が繰り返される. 同じファイルを3個使った場合:

$  ./cat.awk  table.txt  table.txt  table.txt
Aho	 90	100	80
...
Aho	 90	100	80
...
Aho	 90	100	80
...
	# cat table.txt table.txt table.txt と同じ結果

コマンドライン引数の使用例

cut クローンな AWK スクリプト cut.awk は次のようになる:

#!/usr/bin/awk -f
# 説明:cut のような AWK スクリプト
# 使い方:cut.awk f=フィールド番号 ファイル名 ...

{
	print $f;        # f 番目のフィールドだけ出力
}

このバージョンでは, 切り出すフィールドをコマンドラインで指定できる. たとえば,第2フィールドを抽出したければ:

$  ./cut.awk  f=2  table.txt
90
80
85

処理内容的には,変数の使い方が特殊なので, ちょっと理解しづらいかもしれない. コマンドライン引数に,たとえば f=2 が指定されると, スクリプト内の変数 f2 が代入される. そのため,$f$2 に置換され, 結果的に print $2 を実行することになる.

記号 $ の意味が, シェルとは異なることに注意. すでに説明した通り, AWK では,$n は,通常の変数ではなくフィールド変数であり, レコード内の n 列目のデータを指す. シェルではどうだった?再確認しよう.

パターンの使用例

同様に grep クローンを作ってみよう:

#!/usr/bin/awk -f
# 説明:grep のような AWK スクリプト
# 使い方:grep.awk  p=正規表現パターン  ファイル名 ...

$0 ~ p {		# 正規表現に一致したレコードに対して...
	print $0;	# レコードを出力
}

$  ./grep.awk  p='n.*n'  table.txt
Kernighan	 85	 95	100

なお,条件演算子「~」は,「==」の正規表現版である. 正規表現パターンが文字列定数の場合, 前出のとおり,「/正規表現/」と書いていた. 一方,正規表現パターンが変数の場合には, このスクリプトのように,条件式として記述する必要がある.

もし,パターン部に「/p/」と書いてしまうと, 文字列「"p"」にマッチするか?という意味に... 一方,「$0 ~ p」であれば, 変数 p の内容(正規表現)にマッチするか?となる.

複数のブロックの例

もう少し複雑にしてみよう. 次は,wc コマンドのクローン:

#!/usr/bin/awk -f
# 説明:wc のような AWK スクリプト
# 使い方:wc.awk  ファイル名 ...

BEGIN {		# 前処理
	n = 0;		# 単語数を初期化(この行は省略可)
}

{		# 各レコードに対して...
	n += NF;	# 単語数をカウント
}

END {		# 後処理
	print NR, n;	# 行数と単語数を出力
}
AWK では,変数の宣言や初期化を省略してよい. 初めて使う変数には初期値として 0(ゼロ)や ""(空文字列)が 自動的に代入される.

このように複数個のブロックを設置できる. これは,Cプログラムで if ブロックを複数個書き並べていることと同じだ.

なお,パターン BEGIN および END のブロックは, それぞれ,最初のレコードの前および最後のレコードの後に処理される. (つまり,Cプログラムの場合,メインループの外部の処理.) その他のブロックは各レコードに対して処理される. (つまり,Cプログラムの場合,メインループの内部の処理.)

また,変数 NR には, データ全体のレコード数(Number of Records), 変数 NF には, そのレコードのフィールド数(Number of Fields)が自動的に代入されている.

なお,変数 NR は, 空行(NF == 0 のレコード)も数えてしまう. wc の場合には,これでも構わない. しかし,場合によっては(他のプログラムでは), NR をそのまま使うだけでは, 意図しない結果になるかもしれない. 注意しよう.

空行を取り除くスクリプトの断片:

NF > 0 {
	print $0;
	nr++;		# nr:空行以外の行数(ユーザ変数)
}

なお,これらの組み込み変数NRNF)の値は, 自動的に設定される. これらも変数なので,ユーザ変数と同様, 書き換え可能ではあるが,それはトラブルの元だ.やめておこう.

AWK のオンラインマニュアルには, 他にも多くの機能や使用例が紹介されている. 目を通しておこう:

$  man gawk
熟読する必要はないが,機能などを大まかに把握しておくとよい.

本日の課題

サンプルデータ table.txt を入力として, 各レコード内の平均点(個人の平均)および 全レコードの平均点(科目の平均)の欄を追加して出力する AWK スクリプト average.awk を作成せよ.

実行例:

$  ./average.awk  table.txt
Aho		 90	100	 80 90
Weinberger	 80	100	 95 91.6667
Kernighan	 85	 95	100 93.3333

average 85 98.3333 91.6667 91.6667

なお,フィールドの表示位置の整列については難しいので, 多少は,表示が乱れてもよい.

もし余裕があれば,HTML の <table> へ変換して出力してみよう. そうすれば,ブラウザが出力データを整列表示してくれる. HTML 版の実行例:

$  ./average.awk  table.txt > average.html
$  firefox  !$  &
Aho 90100 8090
Weinberger 80100 9591.6667
Kernighan 85 9510093.3333
average8598.333391.666791.6667

ヒント:

レポート提出 注意事項

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