「スクリプト入門」 の 「AWK 入門」 には、 割と長い文書や割と長いスクリプトを紹介しましたが、 本来は、簡単に使えて、短かいスクリプト (1 行なんてのもある) を使うことが多い、有益な道具です。 ここでは、そのようなものを紹介していこうと思います。
なお、短かいスクリプトの場合は、 必ずしもスクリプトファイルにしなくても コマンドラインに直接その内容を書いて実行できますので、 そのような形式で書いたりもすることにします。
かなり多くなってきたので、 「次へ」「前へ」のようなジャンプの仕組みと 仕切り線をつけてみることにしました。 目次は ... まだいいかな。
2 回のテストの点数の相関を見る、などという目的で
70 90
50 30
...
のようになっている各行 2 列のデータを扱うことがあります。
gnuplot を使えば、その散布図と回帰直線などが書けたり、
相関係数が計算できたりするのですが、
整数値のような離散的なデータの散布図を書く場合は注意が必要です。
それは、同じ x, y を持つ複数のデータがある場合、 散布図だとそれらが重なって表示されるため、 あるところにデータが固まって存在する、 という情報が見えなくなってしまうためです。
Unix であれば、そのような場合は sort と uniq を使って、
sort data1 | uniq -c > data2
などとすれば、
2 70 90
1 50 30
...
のように、2,3 列目が x,y で、1 列目はその x, y のデータの個数、
そして同じ x,y は 1 行しか出てこない (集約される) データに変換できるのですが、
それを awk でやるには、例えば以下のようにすればできます:
awk '{ z[$1 " " $2] ++ }END{ for (i in z) print z[i],i}'
data1 > data2
なお、Windows の場合は 1 行スクリプトの引用符は ' を " に、
内部の引用符の " は \" にする必要があるでしょう。
4 年も書いてなかったとは思いませんでした。 別にしばらく awk を使っていなかったわけではなく、 相変わらず日常的に awk も使っています。 先日、少し特殊な処理に使用した awk スクリプトを紹介します。
最近はコピー機で答案やレポートなどの束のデータをスキャンして PDF 化することなどが簡単にできてしまいますが、 それを分割する際に awk を使用しました。 例で説明します。
番号が 1001, 1002, 1003, ..., 1050 の 50 人の学生がいるとして、 そのレポートが番号順にスキャンされた PDF ファイル rep-1.pdf, rep-2.pdf, rep-3.pdf があるとします。 すなわち、これらはそれぞれ 50 ページのファイルで、 rep-1.pdf はレポート no.1 を 50 人分まとめたもので、 j 番目のページには j 番目の学生のレポートがある、という形です。
ここから、各学生のなんらかの属性に従って、 レポートを仕分けるという作業を行いました。 その属性は、別なファイル data に、以下のように書かれています。
1001 1
1002 2
1003 2
1004 2
1005 1
....
1050 1
2 列目の値がある属性ですが、この値は 1 か 2 で、 それによって PDF を仕分け、 そしてレポートファイルも no.1, no.2, no.3 を合わせてしまう、 ということをやりました。 つまり、作成するのは、属性が 1 の学生の、
1001 の no.1, 1005 の no.1, ...., 1050 の no.1,が順に各ページとして並んだ 1 つの PDF ファイルです。
1001 の no.2, 1005 の no.2, ...., 1050 の no.2,
1001 の no.3, 1005 の no.3, ...., 1050 の no.3
もちろん awk だけでこんなことができるわけではなく、 PDF の分割、結合には qpdf というフリーソフトを利用しました qpdf で上のようなことを行うのは実は簡単で、 コマンドラインで
qpdf --empty --pages rep-1.pdf 1,5,...,50 rep-2.pdf 1,5,...,50
rep-3.pdf 1.5,...,50 -- out.pdf
のようにするだけです。
この「1,5,...,50」の部分の「...」の部分も
もちろんちゃんと指定しないといけないのですが、
実は今回 awk を利用したのはこの「1,5,...,50」の部分の文字列の作成のみです。
この部分は、抜き出したいページ番号をカンマ (,) で区切って並べればいいだけなので、簡単にするには、
$2 == 1{ if (++N > 1) printf ","; printf "%d", NR } END { print "" }というスクリプトで data を処理してその結果をファイル (tmpf とか) に落とせばいいわけです。 あとは、シェルスクリプト (csh) で、
set s = `cat tmpf`
qpdf --empty --pages rep-1.pdf $s rep-2.pdf $s rep-3.pdf $s -- out.pdf
とすれば済みます。
これだけだと awk としてはあまり面白くありませんが、 qpdf のページ指定は、単純にカンマ区切りで並べるだけでなく、 連番を - で指定することもできます。例えば、「3,5,7-10,12」という指定は、 「3,5,7,8,9,10,12」と同じになります。 今回は、この連番指定を使って、範囲指定文字列を少し短くするような awk スクリプトを考えてみました。 だいぶ前フリが長かったですが、 結果として以下のようなスクリプトを使用しました:
$2 == 1 { if (n0 == 0) { n0 = n1 = NR; next } else if (NR == n1 + 1) { n1 ++; next } else { if (++ N > 1) printf ","; if (n0 < n1) printf "%d-%d", n0, n1; else printf "%d", n0; n0 = n1 = NR; } } END { if (++ N > 1) printf ","; if (n0 < n1) printf "%d-%d", n0, n1; else printf "%d", n0; print ""; }n0 は「7-10」のような場合の 7 を保存する変数、 n1 は 10 を保存する変数です。 まず最初に現れた行番号 NR (PDF のページ番号にあたる) を n0, n1 に保存し、 新たな NR が 前の n1 + 1 に等しい間は n1 を増やし続けます。 そうでなくなったときに表示するわけですが、n0 と n1 が等しい場合は、 「3-3」とはせずに「3」と表示し、 n1 の方が大きい場合に「3-7」のように表示するようにしています。
取り出す人数が少ない場合にはオプション指定は手作業でもできるのですが、 ほぼ半々に分けないといけなかったし、 分けるグループや、ファイルも複数ありましたので、 手ではやりたくなかったので awk を利用しました。
現在の GNU awk には、以下のような新しい関数の追加、 あるい関数の機能拡張がされています (いくつかは GNU awk 3.1.2 以降)。
array1[] を通常の比較順にソートして array2[] に代入 (array2[] を省略すると array1[] 自体に代入)。 返り値は要素数。
添え字 (常に文字列) の方を辞書順にソートして array2[] に代入 (array2[] を省略すると array1[] 自体に代入)。 返り値は要素数。
第 3 引数に配列 (array) を渡すと、array[0] = マッチした部分文字列全体、 array[n] = ( ) にマッチした部分が順に格納される。 さらに、それらのマッチ位置を示す多次元配列も使用可能 (array[n, "start"] = array[n] の開始位置、 array[n, "length"] = array[n] の長さ)
その文字列を数値文字列とみなしてその値を返す (10 進数以外にも 8 進数、16 進数を表す文字列にも使える)。
sub(), gsub() 同様の置換を行うが、how が "g" ("G") で始まる文字列なら global に (gsub)、数値なら how 番目のものを置換。 また、replacement 内で \1,\2 等により regexp の ( ) でマッチした文字列を使用できる。
日付を system() (= epoch 秒) 形式に変換
gensub(), match() などはかなり便利そうだということがすぐにわかりますが、 今まで awk になかった sort 関数 asort(), asorti() は少しわかりにくいです。
具体例で言うと、例えば
a["A"]=3; a["b"]=1; a["C"]=2; a["D"]=2;の場合、asort(a, b) とすると
b[1]=1; b[2]=2; b[3]=2; b[4]=3;asorti(a, c) とすると
c[1]="A"; c[2]="C"; c[3]="D"; c[4]="b";となります (辞書式では小文字の方が大文字の後)。 b, c を省略すると破壊的に a に強制的に作られます。 元の配列の値も消されるようで、 例えば asort(a) とすると、a[1]=1 のようになりますが、 元の a["A"] の値はなくなります。
さて、これを使ってデータのソートが可能か、ということなのですが、 awk で処理する場合は普通は他のデータも一緒にソートしたい (だから普通は awk の外部でソートをする) のが普通でしょう。 例えば、
1001 3のようなデータを、2 列目に関してソートして、 2 列目と 1 列目を入れかえる、という場合、 Unix ならば外部の sort コマンドを使って
1002 10
1003 7
1004 6
1005 3
sort +1 -2 -n data | awk '{print $2,$1}'
のようにするのが普通です。
なお、「+1 -2」は、2 番目のキーでソートする指定で、
-n は数値と見てのソート、という指定です。
-n がないと辞書式になるので、10 の方が 3 よりも先に来てしまいます。
これを、awk の asort(), asorti() を使ってやるのは実はやや面倒です。 単独で 1 列目、2 列目のみをソートするならやさしいのですが、 1 列目と 2 列目を一緒にソートするとなると工夫が必要です。
また、1 列目のデータは固定長で、しかも重複がないので、 (既にソートされていますが仮に) 1 列目でソートするのであれば、以下のようにしてできます:
{ h[$1] = $2 } END{ N = asorti(h, ind) for (j=1; j<=N; j++) printf "%s %s\n", h[ind[j]], ind[j] }
ところが、 2 列目は固定長ではないので asorti() の辞書式順のソートでは問題がありますし、 重複があるので上のような配列の添字に使うと 他のデータが上書きされてしまうことになります。
これを解消するには、2 列目にダミーのデータを追加して、 一意でかつ固定長の文字列にしてやる、という手があります:
{ h[ sprintf("%04d%03d", $2, NR) ]=$1 } END{ N = asorti(h, ind) for (j=1; j<=N; j++) { s = ind[j] printf "%3d %s\n", substr(s,1,4)+0, h[s] } }
2 列目の数字を、不足する場所には 0 を補って 4 文字固定にして、 さらにその後ろに行番号 (3 桁) を補って一意の文字列にしています。 例えば 2 列目の 10 なら "0010002" といった具合です。 その上で、出力の段階で上位 4 桁を取り出して "+0" で数値化しています。
まあ、使えないわけではないけれども、 外部のソートを使った方がまし、といったところでしょうか。 外部のソートが使いづらいような状況、 例えばソートする場所が sort コマンドのオプションでは指定できないとか、 ソートする場所の値に重複するものがある場合は、 別の場所でソートしたいなどの複雑なソートを必要とする場合には、 もしかしたら便利かもしれませんが、 その場合もなんとなく awk で前処理してから外部の sort にかけて、 それをまた awk に戻す方が楽かな、という気がします。
最近、
[インデックス 1] [順位 1]のように順位が記載されたデータからいくつかの行を除外して、 順位を再計算する、という作業を行いました。
[インデックス 2] [順位 2]
[インデックス 3] [順位 3]
....
もし、その順位の元となるデータが別な列に含まれていればいいんですが、 このデータにはそのデータ列は含まれていません。 となると、除外した列の順位分だけ順位を繰り上げる、 という作業をやることになるのですが、 Unix 上なら一番てっとりばやいのは、とりのぞいたデータを順位を元にソートして、 その行番号を順位に書き直し、ソートし直す、という方法でしょう:
% egrep -v [除外パターン] data | sort +1 -2 -n | awk '{print $1,NR}' \ | sort > newdata
ふと、これを awk だけでやるにはどうしたらいいかと考えてみました。 awk にはソートがないのが厄介ですが、 その行の順位よりも上の順位がいくつあるかを勘定して引き算すればいいので、 方法がないわけではありません:
BEGIN{ split(excS,excA) # 除外行のインデックスのリストを excS として受けとる for(j in excA) revA[excA[j]]=1 # 逆引き配列 (実際の除外に使用) exN=0 }{ if($1 in revA){ excord[++exN]=$2; next } # 除外行は順位だけ記録 indexA[++N]=$1; ordA[N]=$2 }END{ for(j=1;j<=N;j++){ x=0; for(k=1;k<=exN;k++) if(excord[k]<ordA[j]) x++ printf "%s %d\n",indexA[j],ordA[j]-x } }このスクリプト reorder.awk に対して、例えば除外するインデックスが 1005, 1020, 1135 であれば以下のように実行します:
% awk -v excS='1005 1020 1135' -f reorder.awk data > newdata
スクリプトの BEGIN 部分により
revA["1005"]=revA["1020"]=revA["1135"]=1という配列が作られ、それによって除外する行の順位を excord[] という配列に保存し、除外しない行は順番に indexA[], ordA[] にインデックスと順位を保存します。
そして、最後の END ブロックで、除外する順位 (excord[]) のうち ordA[j] よりも小さいものを x として求め、それを引き算して出力するだけで、 ごく単純なアルゴリズムです。
同点の順位 (1,2,3,3,5 のような) がある場合も、 多分これで大丈夫だと思います。
最近 gnuplot Q&A 掲示板 で質問したことなので、その話については後で gnuplot に関する情報のページ にまとめておく予定ですが、 そこで一つ awk で処理をしたものがありましたので紹介しておきます。
山賀さんが昔公開しておられた gnuplot+ では、postscript 出力では コンポジットフォントという日本語と英語を交ぜたフォントを使って出力していたため、 例えば
set title "今年は 2010 年"という文字列の出力については、 「今年は」と「年」は Ryumin-Light-EUC-H、 「 2010 」は Helvetica で自動的に出力されていました。 しかし、現在の gnuplot 4.X では、 日本語フォントが指定できてそのまま日本語文字列が出力できるようになった代わりに、 指定したフォントでしか出力しないので、
set title "今年は 2010 年" font "Ryumin-Light-EUC-H,17"(17 はサイズ指定) のように指定すると全部が Ryumin-Light-EUC-H というフォントでの出力になります。 そしてこれは、Ryumin-Light-EUC-H という名前に 実際にどんなフォントが割り当てられているかによって問題が起こる場合があります。
回避策としては、gnuplot の enhanced モードを使って、
set title "{/Rymin-Light-EUC-H 今年は} 2010 {/Ryumin-Light-EUC-H 年}" \のように、日本語の部分だけを 「{/Ryumin-Light-EUC-H }」の中に入れてしまえばいいのですが、 ちょっと面倒です。
font ",17"
そこで、awk で文字列を全角文字列部分と半角文字列部分に分けて、 それらを別々に処理することを考えました。 以下がそのスクリプトですが、gawk でないとうまく動かないかもしれません:
# set title "..." や set xlabel "..." などの行が対象 /set (title|.label)/ && match($0/\"[^\"]+\"/){ match($0,/\"[^\"]+\"/) s0=substr($0,1,RSTART-1) s1=substr($0,RSTART+1,RLENGTH-2) s2=substr($0,RSTART+RLENGTH) # s1 が title, xlabel 等に含まれる "..." 内の文字列部分 printf "%s",s0 starts=1 # 半角文字を検索 while(match(s1,/[\x20-\x7f]+/)){ # 半角文字列の前の部分の全角文字列部分の出力 (js("str")) if(RSTART>1){ if(starts) starts=0; else printf "." s3=substr(s1,1,RSTART-1) printf "js(\"%s\")",s3 } # 半角文字列の出力 (as("str")) s3=substr(s1,RSTART,RLENGTH) if(starts) starts=0; else printf "." printf "an(\"%s\")",s3 # 残りを新たな s1 とする s1=substr(s1,RSTART+RLENGTH) } # 最後に残った全角文字列部分の出力 if(length(s1)){ if(!starts) printf "." printf "js(\"%s\")",s1 } print s2 next } # それ以外の行はそのまま出力 {print}このスクリプトでは、
set title "[文字列]" font ...のような行をターゲットにしています。 その [文字列] 部分だけ変換して出力するのですが、上のスクリプトは
set xlabel "[文字列]" font ...
set ylabel "[文字列]" font ...
"今年は 2010 年"のような文字列を
js("今年は").as(" 2010 ").js("年")のように変換します。 . は gnuplot の文字列を連結するコマンドですが、 gnuplot では関数定義もできるようになっていますので、 as(), js() は、gnuplot スクリプトの先頭に
as(s) = sとでも定義しておけば、gnuplot は上の変換後の文字列を
js(s) = "{/Ryumin-Light-EUC-H ".s."}"
"{/Ryumin-Light-EUC-H 今年は} 2010 {/Ryumin-Light-EUC-H 年}"と解釈することになるわけです。しかもこのやり方なら、 「as(s) = "{/Arial ".s."}"」のような定義で 半角文字列のフォントを変更することも可能です。
よって、これで gnuplot スクリプトでは、
set term post enh postのように書いておいて、 それを上の awk スクリプトで処理したものを直接 gnuplot に食わせればよいわけです。
jfont = "Ryumin-Light-EUC-H"
as(s) = s
js(s) = "{/".jfont." ".s."}"
set title "基礎数理 III の試験結果 (2010 08/05)" font ",17"
set xlabel "問題番号" font ",14"
set ylabel "%"
plot "basic3.dat"
上に例にあるように、 私は実際に試験結果のグラフを作るスクリプト内で利用しています。 いくつかの試験で使い回すスクリプトなので、 文字列を固定にはできず、上のような awk スクリプトの処理が必要でした。
なお、上のスクリプトでは、 半角文字を 「[\x20-\x7f]+」というパターンで判断しているので、 EUC-JP では動きますが、Shift_JIS では多分だめです。
LaTeX では、equation 環境、eqnarray 環境で書いた数式は式番号がつきますが、 displaymath 環境 (または \[ \])、eqnarray* 環境の数式には式番号がつきません。 式番号をつけるかつけないかは、基本的にはそれを別な場所で参照するかどうか、 だろうと思いますが、長い文章を書いているときは、 とりあえずは後で参照する可能性があるのではないかと思って 主要な数式には一通り式番号が入るようにして、 それに \label{[名前]} のようにして名前をつけます。 後でその番号を参照するのは \ref{[名前]} のようにします。
ところが文書ができあがってみると、いくつかの数式はどこからも参照されず、 式番号が不要なことがあります。 そういう場合そのような式番号を消した方が文書は見やすくなるので、 それを探すようなスクリプトを作りました。 実際に行った方法は、実は awk だけではなく、 むしろ grep や sed などと組み合わせてシェルスクリプトを使ってやったのですが、 せっかくですから、それを awk だけでやる方法を考えてみました。 以下がそのスクリプトです:
(FILENAME ~ /.aux$/){ if(/^\\newlabel/){sub(/^\\newlabel{/,""); sub(/}.*$/,""); h[$1]=1} next } /^%/{next} /\\ref{/{ s=$0 while(s ~ /\\ref{/){ match(s,/\\ref{[^}]+}/) s1=substr(s,RSTART+5,RLENGTH-6) s=substr(s,RSTART+RLENGTH) if(s1 in h) delete h[s1] } } END{ for(s in h) print s }
これは、ファイルを latex にかけた後で以下のようにして使用します (このスクリプトファイルは script.awk, LaTeX のファイルは、file.tex と仮定します)。
awk -f script.awk file.aux file.tex最初に指定する .aux ファイルには、\newlabel コマンドとして \label コマンドなどで名前づけられたものの一覧が含まれていますから、 最初のブロックでその名前部分を取りだして連想配列 h に保存しています。
次の "/^%/" から始まる一行以下が file.tex が処理される部分ですが、 先頭の "/^%/" は、一応コメント行をはじいています。 ただし、% が行の途中にある場合にはこれは機能しませんから、 自分の LaTeX の書き方に合わせてこの部分は適当に変える必要があるでしょう。
次の "/\\ref{/" から始まるブロックで、 参照している部分を見つけ出しているわけですが、 一行に複数の \ref コマンドがある可能性も考えて、 それにマッチした行から、while ループと substr() を使って、 順に一つずつ \ref コマンドを取りだし、 その名前の配列要素を h から削除しています。
最後の END ブロックで、h に残った配列要素を全部表示させています。 これによって、使われなかった \label の名前一覧が表示されますから、 それらを LaTeX ファイルで探して、 式番号のつかないものに変えていけばいいわけです。
なお、\newlabel にはすべての \label が含まれるので、 数式の \label だけではなく、 図やセクションにも \label を振っている場合は、 余計なものも表示されてしまいますが、 私は \label を使う場合は以下のようにしていますので、 上のスクリプトを少し変更すれば、数式の \label だけを取り出すことができます。
また、このスクリプトは、 LaTeX ファイルを複数のファイルに分けている場合にも利用できます。 例えば、file.tex の中で file1.tex, file2.tex のようなものを 読みこんでいる場合は、
awk -f script.awk file.aux file*.texのようにすればいいわけです。
「gnuplot に関する情報やメモ (05/28 2009)」 に書いたネタですが、 x,y の座標が並んでいるデータがあったときに (x 座標は増加列とする)、 y 座標が減少していない部分と減少している部分で違う色のグラフを書きたい、 という話が gnuplot に関する 2ch の方に上がっていました。
それを簡単に実現するには、元のデータを加工して、 y 座標が減少する部分に空行を入れたものと、 y 座標が増加する部分に空行を入れたものとを作ってそれを描画させればいいです。
例えば、データ (data0) が
1 2のようになっているときに、
2 3
3 5
4 3
5 6
7 2
8 2
1 2というデータ (data1) と、
2 3
3 5
4 3
5 6
7 2
8 2
1 2というデータ (data2) を作成して、 gnuplot でそれを重ねて描画する、という方法です。 gnuplot は、並んでいる座標を線分で結ぶのですが、 データに空行が含まれていると、そこは線分を結ばなくなりますので、 これで目的のことができます。
2 3
3 5
4 3
5 6
7 2
8 2
このような data0 から data1, data2 を作成するスクリプト:
awk '(NR==1){y=$2; print; next}{if(y>$2) print ""; print; y=$2}' data0 > data1 awk '(NR==1){y=$2; print; next}{if(y<=$2) print ""; print; y=$2}' data0 > data2y に前の行の y 座標値を保持しておいて、 今の行と比較して空行を入れるかどうかを行っているだけです。
「gnuplot に関する情報やメモ (05/28 2009)」 にも書きましたが、 実は、現在の gnuplot などを使えば、 データの加工をしなくても gnuplot だけでもやることはできるのですが、 上の方法ならば gnuplot のバージョンによらずに目的のことが行えます。
私は awk を csh スクリプト内で利用して、 その結果を csh スクリプト変数として返してもらって次の処理に利用する、 ということをすることが多いです。
例えば、私は日頃 yahoo のニュース記事 (見出しの一覧) を日付順にダウンロードしてくるような csh スクリプトを使用しているのですが、 これまでは取得したニュースの WWW ページに次のページへのリンク (日付の新しい順なので、実際には古いページへのリンク) が続いている間は取得し続けるようにしていたのですが、 最近その WWW ページの構造が変わったようで、 延々次のページへのリンクがついていて、 ずーっと前の記事までダウンロードしてしまうようになってしまいました。
そこで、awk を利用して、指定した日付の見出しが現れた段階で ダウンロードをやめるようにしました。 WWW ページには記事の並び以外のものも沢山含まれていますので、 実際に記事の見出しのある範囲だけでその日付の検索を行う探す必要があります。
探すべき範囲は、「value="表示"」というものが含まれる行から 「<span>.*のページ」(「前のページ」、「次のページ」等のリンク行) までの部分で、探すべき行は、
<li class="ymuiArrow1"><a href="http:..."> (見出し)</a> <span ...></span><span ...> (<a href="http:..">(紙名)</a>) 27日(月)10時05分</span></li>のような行です。日付は、その日の記事の見出しの場合には入っていませんが、 1 日前以前には日付と曜日がつきます。 これを検出するようなスクリプト (内部で strftime() を利用しているので、GNU awk が必要):
set lastN = 3 awk -v lastN=$lastN \ 'BEGIN{\ date=strftime("%d日(",systime()-lastN*60*60*24); \ sub(/^0/,"",date);\ }\ /value=\"表示\"/{inp=1}\ /<span>.*のページ/{exit 0}\ (inp && /ymuiArrow1/){if(index($0,date)) exit 1}' $file set loopend = $statusこれは csh スクリプト内部での利用法ですので、 最初の「set lastN = 3」と最後の「set loopend = $status」は csh スクリプトの命令 (変数の設定) ですし、 awk の部分の改行は行末に '\' をつけて、 awk コマンド部分全体が csh スクリプト内部で 1 行として扱われるように書いています。
シェル変数に値を渡すために、 lastN (= 3) 日前の記事が含まれていたら 1 を、 ないようなら 0 を、「exit n」 を利用してシェル変数に返しています。 「exit n」で終了した場合、シェル変数 status に n の値がセットされますので、 それをシェル側で利用できます。 逆に、シェル側から lastN の値を awk に渡すために、 awk の -v オプション (Gawk or nawk) を使って渡しています。
日付は、systime() (= 現在の日付の秒数) を利用して lastN 日前の時刻 (秒数) を作り、 それで strftime() を使って「27日(」のような文字列 date を作成して、 そして行中にそれが含まれるか含まれていないかを index() で検出しています。 inp は、特定の範囲の前は調べないようにするための変数です。
最初にも書いたように、この手の短めの awk の利用例は、 csh スクリプトを探すとあちこちに出てきます。
最近は長いスクリプトか、短いけど単純なスクリプトを書くことが多かったので、 だいぶ間が空いてしまいましたが、 久しぶりに短めのスクリプトを書いたので紹介します。
あいうえお順にソートされているあるクラスの学生の名簿に対して、 その学生の名字が一人しかいないか複数人いるかの マークをつける処理をする作業を行いました。例えば、
001 阿部 太郎のようなデータに対して、その名字が複数人いる場合は 1、いない場合は 0 を 先頭につけて
002 井上 次郎
003 井上 三郎
004 鈴木 四郎
005 田中 五郎
006 田中 六郎
007 田中 七郎
008 渡辺 八郎
0 001 阿部 太郎のようなことをする、という作業です。 これは、この名簿表を第 1 フィールドの番号順ではなく、 なんらかの別な順番でソートしたときに、 その学生を名字だけで指名できるのかそうでないかを見るためのものです。
1 002 井上 次郎
1 003 井上 三郎
0 004 鈴木 四郎
1 005 田中 五郎
1 006 田中 六郎
1 007 田中 七郎
0 008 渡辺 八郎
これは、実はちょっと厄介で、その行が 0 であるか 1 であるかは、 「前の行の名字と同じか次の行の名字と同じ場合にはその行は 1、それ以外は 0」 という風になっていて、 つまり次の行を読まないとその行がどうであるかが決まりません。 よって、前の行の状態を保存しておいて、ある行を読んだときに、 その行と前の行、および前の行とそのさらに前の行との状態を用いて、 前の行の情報を出力する、といった処理をする必要があります。
そのようなことを行うには、例えば以下のようにすればよさそうです:
BEGIN{ N=2 } # $N = 比較に使用するフィールド (NR==1){ last0=$0; s0=$N; lasteq=0; next } # last0=前の行全体、s0=前の行の比較フィールド、lasteq=前の比較結果 { if($N == s0) eq=1; else eq=0 if(lasteq || eq) p=1; else p=0 print p,last0 last0=$0; s0=$N; lasteq=eq } END{ print lasteq,last0 }
1 行目は last0, s0 に $0, $N を保存しておいて、 2 行目以降は、その行と前の行の比較結果を eq に一旦保存し、 その eq と前の行の eq の結果 (= lasteq) とから 0 を出力するか 1 を出力するかを p に保存して、 p と前の行 (= last0) を出力します。 そして、その行の $0, $N, eq の値を last0, s0, lasteq に保存しておきます。 最後は、lasteq と last0 を出力すれば OK です。
なお、nawk, gawk ならば C と同様に比較結果を Bool 値として取得できますので、 if 文の部分は以下のように書くこともできるようです:
eq = ($N == s0) p = (lasteq || eq)
awk では、データの各行のフィールド要素を $1,$2 のように簡単に取り出すことができますが、 複数のフィールドの固まりを「そのまま」取り出すのはやや面倒です。 例えば、データが
[1 Taro Sato 1234] [13 Jirobe Suzuki 345] [8 Saburota Tanka 2532]であるとして (本当はデータには [ ] はなく、 データの始めと終わりを示すためにつけています)、 ここから第 2 フィールド以降のデータを「スペースの状態も保持したまま」
[Sato 1234] [Suzuki 345] [Tanka 2532]のように取り出すのはあまり簡単ではない、という話です。 単純に、$3,$4 として取りだせば、 $3 と $4 の間のスペース情報は失なわれてしまいます。
もし、第 3 フィールドの位置が最初からわかっているのなら、 substr() で $0 から切り出せばいいのですが、 それが不定の場合はそうもいきません。 そのような目的で、下の 「スクリプト集 (12/27 2006; no.2)」 に各フィールド位置を計算する方法を紹介しましたが、 それはそれで面倒です。
$1,$2 を消してしまえば、と思うかもしれませんが、残念ながらそうもいかず、 awk '{$1=$2=""; print}' にかけた結果は以下のようになります:
[ Sato 1234] [ Suzuki 345] [ Tanaka 2532]つまり、$1 や $2 への代入を行った時点で、awk は $0 をバラバラにしてしまって、 最後にスペースを 1 つだけ入れてつなぎ直してしまうようです (それはそれなりに自然な仕様だと思います) ので、 結局 $3,$4 を取り出したのと変わりません。
gawk, nawk の場合、フィールドセパレータを
awk -F '[ ]' (または script 内で FS="[ ]" とする)に変えるという手もありますが (「awk -F ' '」だと変化しないことに注意)、 これだと、一つ一つのスペースがフィールドセパレータになってしまいますので、 1 行目の "Taro" は $4、 2 行目の "Jirobe" は $3 という具合になってしまいます。 もしそれでもいいなら、次のようにする手があります (gawk か nawk が必要):
BEGIN{ FS="[ ]" N=2 } { k=0 for(j=1;j<=NF;j++) if($j!=""){ $j="" if(++k>=N) break } sub(/^ */,"") print }N=2 は、この個数の先頭フィールドを空にすることを意味しています。 このフィールドセパレータでは、例えば 1 行目に対しては、
$1 = "1", $2 = $3 = "", $4 = "Taro", ...のようになりますので、 その空のフィールド ($2,$3) 以外のフィールドの数を数えながら そのフィールドに空文字を代入することで消していって、 最後に sub() で行頭のスペースを一気に消す、ということをやっています。 ただ、面倒であることに違いはありませんし、 このフィールドセパレータの場合は、 print するだけでなくてこの後の処理もしないといけない場合は 支障が出るかもしれません。 その場合は、split() を使って、通常のフィールド分け用の正規表現 "[ \t]*" を指定して自前で分離する必要があるかもしれません。
C などのソースに (印刷用などに) 行番号をつけるスクリプト:
awk '{gsub(/\t/," "); printf "%3d: %s\n",NR,$0}'単に「printf "%3d: %s\n",NR,$0」だけだと、 タブが入っている行と入ってない行でインデントがずれることになりますので、 先にタブをスペースに展開する必要があります。
C のソースでは、タブは行頭のみに入っていることが多いので、 上のように gsub() を使ってスペースに置き換えればいいでしょう。 上ではタブをスペース 8 つに置き換えていますが、 スペース 4 つと見ている環境ではそこを修正する必要があります。
ファイルのデータを逆順にするスクリプト:
awk '{h[NR]=$0}END{for(j=NR;j>0;j--)print h[j]}'単に全データを一つの配列として保持して、 それを最後に後ろの方から出力するだけです。
日付の順に上から下に向かって書いている記録を、 逆順にしたデータにしようとして、 ふとそういうツールがない (rev は行の左右の反転) ことに気がつきました (あるけど知らないだけかも)。 awk でやるとこんな感じですね。
最近、授業アンケートの結果などを、 レーダーチャートで見ることが多くなりました。 そこでふと、gnuplot でもレーダーチャートが書けるかと考えてみたら、 極座標モード (set polar) を使えばさほど難しくなく 書けそうだということがわかりましたが、 各放射状の動径状の点を結ぶ線が、 単純なデータに対しては閉曲線になりません。
gnuplot で閉曲線にするためには、簡単にするには、 最初の行のデータを最後の行の次に追加したデータを作ればいいのですが、 それを行うスクリプト:
awk '(NR==1){s=$0}{print}END{print s}'
ただし、データにコメント行がある場合は、 それをパスするようにしなければいけません。 例えば、# で始まる行か空行を無視するようになっている場合は、 以下のようにします:
awk '(!/^\#/ && NF>0){if(N==0){s=$0; N=1}; print}END{print s}'これで、gnuplot の with linespoints の出力が閉曲線になります。
私は普段コンピュータに関するメモをコンピュータ上に置いています。 メモをコンピュータ内に置いておけば、 検索や再利用が容易なので、色々なメモをそのようにしています。 そのメモは、ほとんどすべてを以下のような書式で書いています:
08/06 2007 1) ここからメモ本文。メモ本文はタブを一つ行頭に入れて、 一行 70 or 71 文字で書く。 2) ここは、次の項目のメモ本文。 08/07 2007 3) ここは、次の日の次の項目のメモ本文。 ...つまり、最初に行頭にメモを書いている日付を書き、 メモの項目に通し番号をつけ、メモの内容は行頭にタブを入れて書いています。
通し番号をつけているのは、例えば、 後の方のメモで前のメモ項目に関連する内容を書く場合に、 「3) のあれそれは、このようにして解決できる」とか書けるからです。
ところが、集中力がないのか (^^; たまにその通し番号をつけ間違えて、 一つ前の番号と同じ番号を書いてしまう、 あるいはある桁の数字を間違えてしまったりすることがあります。 それをチェックするためのスクリプト:
BEGIN{ num0="" } ($0 ~ /^[0-9]+\)/){ num=substr($1,1,length($1)-1)+0 if(num0!="" && num!=num0+1) printf "Error: (num0,num)=(%d,%d)\n",num0,num num0=num }さらに、それを修正するためのスクリプト:
BEGIN{ num="" } ($0 !~ /^[0-9]+\)/){ print; next } (num==""){ num=substr($1,1,length($1)-1)-1 } { s=substr($0,length($1)) printf "%d%s\n",++num,s }最初のスクリプトは、番号を切り出してそれを num という変数に保存し、 一つ前の番号 num0 と比較して、num が num0+1 でなかったら出力し、 num0 に num を保存し直す、ということをやっていっています。
後者は、番号の行でなければそのまま出力、番号の行ならば、 番号の部分とそれ以降に分け、 正しい番号とそれ以降の部分を出力、としています。 正しい番号は、一番最初の番号から順に 1 ずつ足したものとするために、 num にそれを保持するようにしています。
ただし、この再番号付けは、先頭の番号だけしかつけ直さないので、 メモ本文中に出てくる参照番号は手動で直すしかありません。 Emacs の isearch-forward-regexp (M-C-s) を使えば それなりに容易に作業はできるのですが、 長くなっているメモの修正は結構大変です。
まあこれはある程度は仕方がないですが、 例えば、それらしいもの、つまりメモ本文の「数字)」の部分に 何らかのマークをつけて、それを目当てに探す、という手もあります。 また、その数字もそれなりに自動的に修正できる場合があります (例えば、ある範囲からある範囲の数字は 2 足して、 ある範囲からある範囲までは 3 足して、等) ので、 それも、単純に修正してしまうのでなく、 元の数字を残したままで変更すべき数字も追記するようにしておくと、 修正作業が容易になるかもしれません。
gnuplot QandA 掲示板 ( http://ayapin.film.s.dendai.ac.jp/cgi-bin/trees.cgi) で、MS-Windows 上で、複数のデータファイル:
file_10.txt: #x, y 1.0, 2 1.5, 1 2.0, 0 file_20.txt: #x, y 1.0, 3 1.5, 4 2.0, 5から、x (各行の最初の数字) 1.5 の所だけを取りだして、 ファイル名についている数字と合わせて、
matome.txt: # file number, y(1.5) 10, 1 20, 4のようなファイルを作ることはできないか、という質問がありました (1581 番の記事)。 もちろん、元は、そのようなファイルを作らなくても gnuplot 内でそういうデータのグラフを書くことはできないか、 という質問でしたが、 そのようなデータの作成は、 MS-Windows のバッチファイルと標準コマンドだけでやろうとすると、 FIND (や MS-Windows Xp の FINDSTR) などを使えば近いことはできそうですが、 ファイル名と y の値を並べて 1 行に書くあたりが面倒そうです。
しかし、(MS-Windows で動作する) gawk を使えばさほど問題ではなく、 次のようなスクリプト (mkmatome.awk) でそれは可能です:
BEGIN{ if(x == "") x=1.0 } (fname != FILENAME){ fname=FILENAME N=fname sub(/file_/,"",N) sub(/\.txt/,"",N) } ($1 == x){ printf "%d, %d\n",N,$2; next }最初の行は、抜き出す x のデフォルトの値の設定で、 gawk のコマンドラインで -v を使えば変更可能なようにしています。 後はファイル名を取得し、そこからその番号を切り出して、 対象となる行を抜き出しています。 実行は、コマンドプロンプト上で以下のようにします:
gawk -F "," -v x=1.5 -f mkmatome.awk file_*.txt > matome.txtデータのセパレータが ',' なので、 それに合わせて awk スクリプトもセパレータを ',' にしています (逆にそうしないと "$1 == x" の部分がうまく動きません)。
MS-Windows の標準コマンドはテキスト処理は得意ではありませんし、 バッチファイルでも変数を数字や文字列として処理するのは容易ではありません。 それらを補うために、 バッチファイル支援用のフリーソフトが昔から色々作られていますが、 gawk はそういう目的でも非常に強力なツールになると思います。 私も MS-DOS の時代に、awk を知ってからは、 そういう使い方でだいぶお世話になりました。
先日友人から、本文が
Content-Type: text/plain; charset="iso-2022-jp"のようなメールをもらいました。 これは、7 ビット JIS (ISO-2022-JP) コードの日本語に含まれる ESC (0x1b) を "=1B" のような文字列に変換するので、 よって例えば「おはよう」が「=1B$B$*$O$h$&=1B(B」のような文字列に 変わってしまっています。 調べてみたところ、だいたい以下のようなルールで変換すればよさそうでした。
Content-Transfer-Encoding: quoted-printable
ということで、それを行うスクリプト:
BEGIN{ body=0 } (NF==0){ print; body=1; next } (body==0){ print; next } { if($0 ~ /=$/){ putcr=0 sub(/=$/,"") } else putcr=1 gsub(/=1B/,"^[") # この二重引用符内は、実際は 0x1b を意味する制御文字が入る gsub(/=20/," ") gsub(/=3D/,"=") if(putcr) print $0 else printf "%s",$0 }
body はメール本文かメールヘッダかを保持する変数 (body==0 ならばメールヘッダ、body==1 ならばメール本文) で、 メールヘッダは無変換で出力するために使用しています。 メールヘッダは空行 (NF==0) で終了するので、 それに出会ったら body=1 としています。
また、putcr は改行を出力するか (1) 出力しないか (0) を保持する変数で、 行末に '=' がついている場合は putcr=0 としていて、 変換後は printf を使って改行を打たないようにしていますが、 putcr==1 のときは print で改行つきの出力にしています。
その他の個々の変換は gsub() でやっていますが、 この他にも変換が必要な制御文字が含まれてくる (含まれたメールが来る) 可能性もあります。 まあ、本来はそんなメールを送ってくれないように 送信者に設定を変えてもらえば済む話なのかもしれません。
先日、Tgif という描画ソフトで日本地図を書いて、 その折れ線データを取りだして gnuplot で利用する、 といったことをやりました。
Tgif の .obj 形式のデータはテキストファイルで、 折れ線や丸などの部品毎に座標や線の太さなどの情報が 保存されています。折れ線のデータは、 例えば以下のように保存されています (これは北海道の図形):
poly('black','',37,[ 832,164,896,224,968,252,972,260,1008,268,1048,236,1032,272,1056,304, 1024,320,972,320,928,352,912,392,804,340,768,360,744,344,728,344, 720,368,748,376,776,396,768,404,740,396,716,420,704,412,712,384, 692,368,692,344,732,320,724,300,736,296,776,312,788,296,784,272, 808,260,808,232,816,216,804,168,832,164],0,1,1,175,0,0,0,0,0,0,0,'1',0,0, "0000000000","",[ 0,8,3,0,'8','3','0'],[0,8,3,0,'8','3','0'],[ ]).poly() は折れ線を意味し、最初の [ から ] までの数字の列が x1,y1,x2,y2,... のように折れ線の各頂点の座標を順に表わしているようです (その他のデータはあまりよくは知りません (^^;))。 その座標部分だけをを取り出して、これを gnuplot で使うために
832 164のように変換する作業を awk で行いました (ただし、実際には y 座標の反転も行っています)。 そのスクリプト:
896 224
968 252
.......
832 164
($0 ~ /poly/){ printf "# %s\n",$0 getline s=$0 # 座標データを s に 1 行として保存 while(s !~ /\]/){ getline s = s $0 } sub(/\].*/,"",s) gsub(/,/," ",s) $0=s for(j=1;j<=NF;j+=2) printf "%d %d\n",$j,1120-$(j+1) print "" print "" }
まず、各折れ線を分離するために、 poly 毎に先頭にコメント行 ('#' で始まる行) を 1 行書いておいて、 その poly の次の行から ']' が含まれる行までを、 getline を利用して s に連結して保存します。 その後で ']' から最後までを sub()で削除し、 ',' の区切りを空白に変え、 あらためて $0 に代入し直すことで、座標データを $1,$2,...,$NF というフィールドに分割し、 それを for 文で順に 2 つずつ書いています。 そして最後に空行を 2 つ追加しています。
なお、Tgif のデータは、多くの画像形式がそうであるように y 軸が下向きになっていますが、 gnuplot は y 軸が上向きなのがデフォルトなので、 この画像の最大の y 座標 (より少し大きい値) である 1120 から引き算をすることで、 上向きの y 座標にしています。
実際にはこの後、都道府県単位での順番の入れかえ、 コメントのところに都道府県名や番号を追加、 などの作業を手で行って公開しました (上記 WWW page の japan_map.dat)。
2 つの文字列を重ね合わせたような文字列を作る関数:
# 文字列 s1 と文字列 s2 を重ねた文字列を返す # str2poslen(), repeatc() を必要とする。 # s1 の空白以外のフィールドと s2 の空白以外のフィールドとが # 重ならない位置にある必要がある。 # 返り値 = s1 と s2 を重ねた文字列 (エラーの場合空文字列を返す) function merge(s1,s2, p1,l1,f1,N1,p2,l2,f2,N2,s,j1,j2,n) { N1=str2poslen(s1,p1,l1,f1) N2=str2poslen(s2,p2,l2,f2) s=""; j1=1; j2=1 while(j1<=N1 && j2<=N2){ n=length(s) if(p1[j1]==p2[j2]) return "" else if(p1[j1]<p2[j2]){ if(p1[j1]+l1[j1]-1>=p2[j2]) return "" s=s repeatc(p1[j1]-n-1," ") s=s f1[j1] j1++ } else{ if(p2[j2]+l2[j2]-1>=p1[j1]) return "" s=s repeatc(p2[j2]-n-1," ") s=s f2[j2] j2++ } } n=length(s) if(j1<=N1) s=s substr(s1,n+1) else s=s substr(s2,n+1) return s } # 文字 c を n 回繰り返した文字列を返す関数 function repeatc(n,c, s,j) { s="" for(j=1;j<=n;j++) s=s c return s }
これは、下 (「スクリプト集 (12/27 2006; no.2)」) で紹介した str2poslen() を使用しています。「重ね合わせる」というのは、 例えば、
s1 = " 456 ab d ef" s2 = "123 78 c"から、
"12345678 abcd ef"のような文字列を作ることを意味します。ただし、
s2 = "AB CD" s2 = "123"のような場合は、先頭の 2 文字が重なってしまうので、 重ね合わせは失敗、と見るようにしています。 関数内部では、両方の文字列をフィールドに分割して、 単純に先に現れている物を順に書き出していく、 という方法を取っています。
つい先日、表計算データを PDF にしたようなファイルから pdftotext (xpdf 付属の PDF をテキストに変換するソフト) に -layout オプションをつけてテキスト形式のデータを取り出したのですが、 狭い行だったためか、行によってはフィールドが別の行に分かれた 2 行に分割されてしまいました。 先頭からの物理的な位置はあまり変わっていないようでしたので、 このような関数を使った複数行の重ね合わせにより 行の復元を行いました。
awk では、標準で $0 で行全体を、$j で j 番目の field を扱えますが、 その j 番目の field の先頭が、その行のどの位置にあるのかを知りたい 場合があります。 それは、例えば以下のような関数を使えば可能です:
# 文字列 str を各フィールド文字列に分解して、 # fld[1],...,fld[N] に保存する (N == フィールド数) # pos[j]: j 番目のフィールド文字列の、str の先頭から数えた位置 # len[j]: j 番目のフィールド文字列の長さ # fld[j]: j 番目のフィールド文字列自体 # 返り値 = N (フィールド数; エラーの場合 N<0) function str2poslen(str,pos,len,fld, N,p,s) { s=str p=1 N=0 while(length(s)>0){ if(match(s,/^ +/)>0){ # フィールド間のスペースをスキップ p+=RLENGTH s=substr(s,RSTART+RLENGTH) } if(length(s)==0) break if(!match(s,/^[^ ]+/)) return -1 pos[++N]=p p+=RLENGTH len[N]=RLENGTH fld[N]=substr(s,RSTART,RLENGTH) s=substr(s,RSTART+RLENGTH) } return N }
上記の関数は、split() のようにスペース区切りの文字列を フィールドに分割して配列に保存しますが、 そのときに各フィールドの先頭位置 (その文字列の先頭から数えた) と 各フィールドの長さも配列に保存して返します。 内部では、スペース部分の長さを測ってスキップして、 各フィールドの長さを測って配列に保存して、 といったことを繰り返し行っています。
ただし、この関数は、タブ (\t) 区切りには対応しておらず、 スペース区切りのものにしか正しく動作しません。 また、日本語が含まれているような文章では、 最近の GNU awk などでは、LOCALE の設定により 2byte 文字を 1 文字と数えたりするので、 length() や substr() などで期待しない動作になることがあります。 必要ならば環境変数 LANG を C に設定して使用してください (awk スクリプト内部からは設定できないようです)。
つい先日、表計算データを PDF にしたようなファイルから pdftotext (xpdf 付属の PDF をテキストに変換するソフト) に -layout オプションをつけてとりだしたテキストを整形するために 上のようなものを使用しました。
ファイルを、ある目印毎に切り分けるスクリプト:
BEGIN{ page = 0; file = sprintf("divide/file-%03d.html",page) } ($0 ~ /<h[0-9]>/){ close(file); page++ file = sprintf("divide/file-%03d.html",page) } { print > file }
上の例は、カレントディレクトリに divide/ というディレクトリを作って、 その中に <h1> や <h2> を目印に切り分けたファイル file-000.html, file-001.html, ... を作成します (MS-Windows の場合は多分修正がいるでしょう)。 つまり HTML ファイルをセクションで切り分けたようなものを作ります。
awk では print や printf の最後に「>file」をつけると、 file という変数に保存されている名前のファイルに書き出します。 もちろん、直接「>"file-001.html"」 のようにファイル名を文字列として書くこともできます。 ただし、一度に開けるファイルの数は限られていますので (多分)、 書き出す必要のないファイルは close() する必要があります。 よって、目印に当ったときに、それまでのファイルを close() して、 file を 次のファイル名に更新する、ということを行っています。
つい先日、pdftotext (xpdf 付属の PDF をテキストに変換するソフト) の出力を、'\f' (= form feed; 改ページを意味する) 毎に切り分けるときに 上のようなことをやりました。
指定したキーワード (例えば Takeno) から、 それを小文字、大文字どちらにもマッチする正規表現 (すなわちこの場合は [tT][aA][kK][eE][nN][oO] という文字列) を作成するスクリプト:
{ s="" for(j=1;j<=length($0);j++){ c=substr($0,j,1) if(c ~ /[a-zA-Z]/) s = s "[" tolower(c) toupper(c) "]" else s=s c } print s }
これは、Unix 上の有名なページャ (テキストファイルを表示するソフト) である less 用に作ったものですが、 less は "-p [pattern]" というオプションで正規表現 [pattern] を指定すると、less 起動時にその正規表現にマッチする部分を白黒反転表示し、 最初にマッチした部分にジャンプしてくれる機能があります。 そのキーワードが小文字で書かれているか大文字で書かれているか わからない場合はどうしたらいいかな、と考えて作ったものです。
文字列を 1 文字ずつ分解して、それがアルファベットなら小文字 (tolower(c)) と大文字 (toupper(c)) を [] で囲んで s に追加していく、 アルファベットでなければその文字のまま追加、ということをやっています。
これを使えば、
% less -p "`echo Takeno | awk -f script.awk`" fileとすることで
% less -p "[tT][aA][kK][eE][nN][oO]" fileと指定したことと同じことになり、 "takeno" にも "TAKENO" にも "Takeno" にも マッチするようになります (grep -i と同等)。
しかし最近、それは単に、
% less -i -p Takeno fileと less の -i オプション (ignore case) を利用すればいいだけだった ことに気がつきましたので、このスクリプトは無用になってしまいました (^^;
各行にあるデータが書かれているファイルから、 スクリプトを起動する度にランダムにその中の 1 行を出力するスクリプト:
BEGIN{srand()} # BEGIN{srand(systime())} の方がいい ? ($0 !~ /^\#/ && NF>0){ h[++N]=$0 } END{print h[int(N*rand())+1]}
これは、 「AWK の短いスクリプト集 (06/17 2006)」 に書いた srand(), rand() を使って実現できますが、 配列 h[1] ~ h[N] にそのファイルのすべての行の内容 ('#' で始まる行と空行は除いてある) を保存し、 1 ~ N までの乱数を int(N*rand())+1 で発生させて出力しています。
これを利用 (あるいは多少修正) すると、 「ログインする度に背景の壁紙をランダムに変更するスクリプト」とか、 「ランダムに教訓を表示するスクリプト」とか、 「英単語を一覧からランダムに選びだしてクイズとして出力するスクリプト」 などを実現できます。
なお、GNU awk (FreeBSD 4.11-RELEASE の /usr/bin/awk; GNU awk 3.0.6) で srand() で 1 回の rand() の生成を 1 秒置きくらいに連続して行ったところ、 あまりちらばりのよい乱数が生成せず (一つおきに同じ乱数が起きたりする)、 その srand() を srand(systime()) に変えるとなぜか多少改善されるように 見られました。 何も引数を指定しない場合、srand() は srand(systime()) をやっていると 思っていたのですが、もしかしたら違うのかもしれません。 割と短かい間隔で実行しなければいけない場合は、srand(systime()) でやってみてください。
空行区切りのデータブロック:
A1の、各ブロックのデータの個数 (行数) を数えるスクリプト:
A2
...
An
B1
B2
...
Bm
C1
C2
...
awk '(NF==0){if(N>0) print N; N=0; next}{N++}END{if(N>0) print N}'
これは、先日ある問題に対するコンピュータによる解 (かなり多い) を求めたときに使用したものです。
その解の規則性を見たかったのですが、 その解をある性質でグループ分けして、 その各グループの個数が、あるパラメータに対してどう変わるのか (何を言っているのかさっぱりわかりませんね (^^;)) を調べるのに上のスクリプトを利用しました。
その出力をさらに sort で整列化してみたのですが、 残念ながらこれでは思ったような規則性は見つけられませんでした (ま、普通そう簡単にはいかないもんです (;_;))
日付 (月日) を 1 行から 2 つ取得して、 その間の日付をすべて出力するスクリプト:
# days[m] = m 月の日数 (2 月は 29 日としている) BEGIN{ N=split("31 29 31 30 31 30 31 31 30 31 30 31",days) } (NF>=2){ m1=substr($1,1,2)+0; d1=substr($1,3,2)+0 m2=substr($2,1,2)+0; d2=substr($2,3,2)+0 if(check(m1,d1) || check(m2,d2)) next if(m1<m2 ||(m1==m2 && d1<=d2)) putout(m1,d1,m2,d2) else{ putout(m1,d1,12,31); putout(1,1,m2,d2) } } # 月日データが適切かのチェック (適切なら 0 を返す) function check(m,d) { if(m<=0 || m>12) return 1 if(d<=0 || d>days[m]) return 2 return 0 } # (m1,d1) から (m2,d2) までの日付を出力 # m1<m2 ||(m1==m2 && d1<=d2) と仮定する function putout(m1,d1,m2,d2, j,k) { if(m1==m2){ for(j=d1;j<=d2;j++) printf "%02d%02d\n",m1,j return } for(j=d1;j<=days[m1];j++) printf "%02d%02d\n",m1,j for(k=m1+1;k<m2;k++) for(j=1;j<=days[k];j++) printf "%02d%02d\n",k,j for(j=1;j<=d2;j++) printf "%02d%02d\n",m2,j }
これは、平日はほぼ毎日記録されるあるログファイルが log-MMDD (MM は月、DD は日) のような名前で保存されているのですが、 それを参照するときに「前回参照したもの以降の今日までのログ」 を参照するにあたって作ったものです。
もちろん、一度参照したものは削除するとか、 別なディレクトリに移動するとかすれば別に必要ないのですが、 参照の最後に今日の日付をファイルに残すようにすることで 前回参照した日付をファイルに記録として残しておくと、 あとはその日付と今日の日付からその間の日付を全部生成できれば、 その間のログを参照できることになりますので、それで作成しました。
このスクリプト (例えば mkdate.awk として) を、
echo "1030 1111" | awk -f mkdate.awkとすれば
1030と表示されますから、このスクリプトを少し加工するか、 またはこの出力をさらに
1031
1101
1102
...
1109
1110
1111
awk '{print "log-" $0}'に通せば
log-1030のようにできます。 よってあとはこの出力を `` (バッククォート) で囲んで less のコマンドラインにでも渡せば 必要なログを参照できることになります (Unix ならば)。
log-1031
...
log-1111
なお、このような用途用なので、年のデータは考慮せず、 日付が後の方が小さければ 12/31 をまたいでの日付とし、 2 月は 29 日あるものとしています。
順番に並んだ番号:
1に欠けがある場合 (例えば上の場合は 3)、 その欠けを抽出するスクリプト:
2
4
5
...
(NR==1){ lastn=$1; next } { n=$1 if(n>lastn+1) for(j=lastn+1;j<n;j++) print j lastn=n }
これは、あるサイトから番号つきの大量の記事をまとめてダウンロードしたとき、 最後の番号から推察される記事数と実際の記事数が合ってなかったので、 抜けでもあるのかなと調べるのに利用しました。
だから実際には、ファイル名には番号以外の部分もあるのですが、 ls -F の出力 (MS-DOS/MS-Windows でいう dir の出力) を、 sed で不要な部分を削除して、それを上のスクリプトに食わせています。 もちろん sed で削除する部分は、 awk 内で sub() を使ってカットすることもできます。
例えば htmls/ というディレクトリにあるファイルが
file0001.htmlである場合は、
file0002.html
file0004.html
file0005.html
...
{ sub(/file/,""); sub(/\.html/,"") } (NR==1){ lastn=$1+0; next } { n=$1+0 if(n>lastn+1) for(j=lastn+1;j<n;j++) printf "file%04d.html\n",j lastn=n }のようなスクリプトを nuke.awk というファイル名で作成し、
ls -F htmls | awk -f nuke.awkとかすればいいわけです。 "$1+0" のように +0 しているのは、 文字列を強制的に数値に変換するためです (この場合はいらないかも)。
Yahoo! ニュース
のニュース記事の一覧は、 大量の記事をかかえている分野 (例えば社会ニュースとか) だと 一覧自体が複数のページに分かれています。 それらを wget などで手元に持ってきて (例えば file1.html, file2.html, ... であるとします)、 そこから一覧のみを抜きだすスクリプト:awk '/<ul>/,/<\/ul>/{sub(/<\/?ul>/,""); print}' file*.html
後はこれの出力にヘッダやフッタをつければ一覧リストのできあがりです。 その手のファイルの加工を今、 「AWK 入門」 の方でいくつか書いていますが、 上のような "/pattern1/,/pattern2/" の利用に 気がついていなかったので、 getline を使ってやや面倒に書いてしまっています。
データファイルの連続する空行を 1 行の空行のみにするスクリプト:
awk '(NF>0){print; N=0; next}{N++}(N==1){print}'
gnuplot でグラフを書く場合は、 データに空行があるとそこは曲線の切れ目と思われ、曲線が切れます。 空行が 2 行あると、そこは index の切れ目 (別のデータファイルのデータ) と思われます。 つまり gnuplot では 空行の連続行が 1 であるか 2 であるかは意味が異なりますので そのような場合に使うことができます。
もし、連続する空行を 2 行以下の空行にする (2 行以下の空行はそのまま、 3 行以上の空行は 2 行に) ならば、 "N==1" の部分を "N<=2" にすればいいでしょう。
逆に 1 行以上の空行をすべて 2 行の空行にしたい場合は、 最後の "{print}" を "{print; print}" とすれば できます。
私は仕事柄、出席表の入力の読み合わせや、成績処理、 成績データの書き写し時の読み合わせなどに awk を利用することが多いのですが、 それらの多くは一度作ったものをシェルスクリプト (+ awk スクリプト) の形にしていて、それらを再利用することが多いです。
その作業中で特に awk を使うのは、
# [学籍番号] [成績 1] [成績 2] [出席]のようなデータから成績を計算するような作業です。 例えば、成績 1 が 5 割、成績 2 が 3 割、出席が 2 割 (全出席数 = 14)、 と点数付けする場合は
20069001 80 75 13
20069002 75 70 12
20069003 70 80 14
20069004 62 61 14
.....
awk '/^200/{print $1,int($2*0.5+$3*0.3+$4*0.2*100/14+0.5)}'
のようにします。最後の "+0.5" は、
int() が小数以下を切り捨てて整数化するので、
四捨五入させるためのものです
(注: 実際に私がこの比率、この数式で成績をつけている
ということではなく、あくまでひとつの例です)。
一方、後者の「他のデータベースとの連携」とは、 例えば学生の氏名等のデータベースと組み合わせたデータを作成する、 といった作業です。 特に読み合わせで利用するのですが、 例えば私は学生の学籍番号と氏名の対応表を以下のような形式で持っています (例えばファイル名を name.dat とします):
# [学籍番号] [姓 (漢字)] [名 (漢字)] [姓 (カナ)] [名 (カナ)]出席表には、例えば氏名と学籍番号を適当な順番で書いてもらうのですが、 その学籍番号の、例えば上 6 桁が全部同じで、下 2 桁だけが違う場合は、 その下 2 桁だけを書いてある順にベタに入力していきます (例えばファイル名を input1 とします):
20069001 竹野 茂治 タケノ シゲハル
20069002 竹野 茂一 タケノ シゲカズ
20069003 竹野 茂二 タケノ シゲジ
20069004 竹野 茂三 タケノ シゲゾウ
.....
(注: これもあくまで架空のデータで、こんな人が親戚にいるわけではありません)
03 04 02 01 ...この input1 と name.dat から、 以下のスクリプト (例えば mkname.awk とします) で、 学籍番号全部と氏名を復元したデータを作成します (nawk か GNU awk が必要かも):
(FILENAME==dbf){ if($0 ~ /^200/) h[$1]=substr($0,length($1)+2) } (FILENAME!=dbf){ for(j=1;j<=NF;j++){ n=top $j; if(!(n in h)) print "Error:",$j > "/dev/stderr"; else print n,h[n] } }
これを以下のように使います。
awk -v top=200690 -v dbf=name.dat -f mkname.awk name.dat input1 > out1
そうすると、out1 に
20069003 竹野 茂二 タケノ シゲジと、入力した順に学籍番号とその名前が再現されたデータが作られます。 これを読み合わせに利用するわけです。
20069004 竹野 茂三 タケノ シゲゾウ
20069002 竹野 茂一 タケノ シゲカズ
20069001 竹野 茂治 タケノ シゲハル
....
なお、実際の読み合わせは yomi (と付属ツールの shless) を使ってやっているので、 yomi に食わせる前に yomi 用に加工したり、 yomi に作らせたファイルを shless で見やすいようにさらに加工 (例えば、画面表示は "20069001 竹野 茂治" と出るようにして、 音声出力の方は "01 タケノ シゲハル" を読ませるとか) したりして使っています。
データファイルの各行に一つずつ数値データが書かれている場合、 (1 行目) - (2 行目), (2 行目) - (3 行目), ... というデータを作るスクリプト:
awk '(NR>1){print $1-x}{x=$1}'
これは、「2 行目以降ならば (1 列目のデータ)-x を出力」と 「x=(1 列目のデータ)と代入」の 2 つからできています。 もちろん if 文を使って以下のように書いても同じです:
awk '{if(NR>1) print $1-x; x=$1}'
これは、以前 gnuplot QandA 掲示板 ( http://ayapin.film.s.dendai.ac.jp/cgi-bin/trees.cgi) の、
gnuplot は、各行に保存されたテキスト形式のデータをグラフ化するソフトで、 データ処理に関する色んな機能を持っているのですが、 このような行をまたぐようなデータ処理はできません。 それをこのように awk でおぎなってやることで、 できることがかなり広がります。
ついでに、k 行前のデータとの差をとったデータを作成する、 という場合は以下のようにすればできます (GNU awk か nawk が必要かも):
awk -v k=3 '(NR>k){print $1-h[NR%k]}{h[NR%k]=$1}'
上は、例えば k=3 の例ですが、NR は行番号、 NR%k は NR を k で割った余りを意味するので、 1 行目は配列 h[1] に、2 行目は配列 h[2] に、 3 行目は配列 h[0] に保存した後、 4 行目では、NR%k=4%3=1 なので $1-h[1]、すなわち、 (4 行目) - (1 行目) を出力し、この 4 行目を h[1] に保存します。 以下同様です。
私は、研究のために偏微分方程式の数値計算をすることがありますが、 通常微分方程式の数値計算をする場合、 それに初期値やパラメータなどを与える必要があります。
固定したパラメータや初期値を与えるのではなく、 色々なパラメータ、初期値を与えて、 それらによって結果がどのように変わるか、 最適な結果を与えるようなパラメータや初期値はどのようなものか、 ということを調べるわけです。
よって、そのようなプログラムの、 微分方程式によって計算が進むは汎用的に作っておいて (通常私は C 言語で書きます)、 それに外部からパラメータや初期値を与えて 計算を開始するようにするのが普通です。
パラメータは 2, 3 個の値 (多くても 10 個以下) であることが多いので、 それはプログラムのコマンドラインオプションで与えればいいのですが、 問題は初期値です。 常微分方程式なら初期値といっても 1 つ (連立方程式なら方程式の数だけ) で済みますが、偏微分方程式となるとそうはいきません。 私の扱う、時間発展の方程式では、初期値として必要なデータの個数は、 差分の分割数の分だけあります。 だから精度を上げるために空間方向の分割数を上げれば、 その上げただけの初期値が必要になります (1000 分割なら 1000 個、5000 分割なら 5000 個のデータが必要)。
そのようなデータの生成も C のプログラムで書いてしまえばいいのですが、 初期値を変更したい場合、その C のプログラムを変更して、 別なデータを吐かせるとになりますが、 それよりは AWK を使う方が楽です。
短い AWK のスクリプトならば、シェルスクリプト内に 直接コード自体を書くことができますから、 数値計算プログラムのデータを変えて実行する、 という一連の作業をすべてシェルスクリプト内に記述できますし、 変更、改良も容易です。
例えば、0≦x≦1 上で sin(2πx) の関数を初期値としたい場合:
awk -v N=1000 'BEGIN{for(j=0;j<=N;j++) print sin(2.0*3.14159265*j/N)}'または、分割区間の真ん中の点での値を使う場合:
awk -v N=1000 'BEGIN{for(j=1;j<=N;j++) print sin(2.0*3.14159265*(j-0.5)/N)}'
ランダムな初期値が必要な場合も AWK では srand() と rand() で 簡単に作ることができます。
1 から 6 までの整数の乱数 (サイコロ) を一つ出力するスクリプト (nawk か GNU awk が必要かも):
awk 'BEGIN{srand(); print int(rand()*6)+1}'
シェルスクリプトで乱数を必要な場合には、 このように AWK を使えば容易にそれを生成できます。
もちろん、C 言語などでそれと同等のことを行うプログラムを作って それを利用してもいいのですが、 それだと、そのコードはシェルスクリプトとは別のファイルになってしまいます。 上記の AWK のコードのようにシェルスクリプト内に直接書くことができれば、 そのスクリプト内ですべて閉じていることになるので、 それらの修正や管理等が容易になるというメリットがあります。
1 日前の日付を返すスクリプト (GNU awk が必要かも):
awk 'BEGIN{print strftime("%Y%m%d",systime()-24*60*60)}'
awk の 1 行スクリプトは、シェルスクリプトで利用することも多いのですが、 ログの管理などで 1 日前の日付が欲しい場合があります。 date コマンドによっては (例えば FreeBSD の /bin/date)、 何日前の日付、というものを出力できるものもあるのですが、 GNU date ではそれはできません。 その場合、上のようにすれば (少なくとも GNU awk なら) それが得られます。 strftime() や systime() については、下の 「スクリプト集 (06/16 2006)」 も参照してください。
Netscape のブックマークファイルから、URL とタイトルを取りだすスクリプト:
/<DT><A HREF/{ match($0,/<A HREF=\"[^\"]*\"/) url=substr($0,RSTART,RLENGTH) sub(/<A HREF=\"/,"",url) sub(/\"$/,"",url) match($0,/<A HREF[^>]*\">/) title=substr($0,RSTART+RLENGTH) sub(/<\/A>.*/,"",title) printf "[%s] %s\n",title,url }
私が普段利用している Netscape 3.04 のブックマークファイル (~/.netscape/bookmarks.html) は、 入れ子の <DL> タグ (定義型リスト) で書かれています。 フォルダは
<DT><H3 FOLDED ADD_DATE="XXXX">folder1</H3>のように書かれ (XXXX は日付の epoch 秒)、URL は
<DT><A HREF="[URL]" ADD_DATE="XXXX" LAST_VISIT="XXXX" LAST_MODIFIED="XXXX">[TITLE]</A>のように書かれます。よって、この後者の行に対して、 URL の部分を match() で取り出し、不要な部分を sub() で削除し、 タイトルの部分を match() で取り出し、不要な部分を sub() で削除し、 という作業を行っています。
Netscape 7 の場合は、ブックマークファイル (~/.mozilla/user/XXXX/bookmarks.html) のフォルダは
<DT><H3 ADD_DATE="XXXX" ID="NC:BookmarksRoot#$YYYY">folder1</H3>のようになっていて、URL は
<DT><A HREF="[URL]" ADD_DATE="XXXX" LAST_VISIT="YYYY" LAST_CHARSET="[CHARSET]">[TITLE]</A>のようになっています (>A< の要素はほかにもついたり、減ったりするみたい) ので、上のスクリプトでこれにも対応していることになります。
このようにすれば、複雑なブックマークのフォルダだけ取り出して ツリー構造を見直したり、 URL だけ取り出して、複数含まれているものを削除したり、 LAST_VISIT を見てほとんどアクセスしてないものなどを削除したりするのに 使えます。
なお、LAST_VISIT 等で使われている日付は、 いわゆる epoch 秒 (1970-01-01-00:00:00 からの秒数) を指しますが、 GNU awk には、
BEGIN{ curtime=systime() # 現在の時刻 axtime=2*24*60*60 # 48 時間の秒数 } /<DT><A HREF.*LAST_VISIT/{ match($0,/LAST_VISIT=\"[0-9]+\"/) lastvtime=substr($0,RSTART,RLENGTH) sub(/LAST_VISIT=\"/,"",lastvtime) ub(/\"/,"",lastvtime) if(curtime-lastvtime>maxtime) next match($0,/<A HREF=\"[^\"]*\"/) url=substr($0,RSTART,RLENGTH) sub(/<A HREF=\"/,"",url) ub(/\"$/,"",url) match($0,/<A HREF[^>]*\">/) title=substr($0,RSTART+RLENGTH) sub(/<\/A>.*/,"",title) printf "[%s] (%s) %s\n",title,strftime("%Y/%m/%d",lastvtime),url }
C 言語のソースのコメント部分 (/* と */ の間) を削除するスクリプト:
{ s=$0 while(match(s,/\/\*/)){ printf "%s",substr(s,1,RSTART-1) s=substr(s,RSTART+RLENGTH) while(!match(s,/\*\//)){ # print "" getline s=$0 } s=substr(s,RSTART+RLENGTH) } print s }
ただし、コメントの入れ子には対応していません。 外側の (最初の) while ループがコメントの最初の検出、 内側の (2 つ目の) while ループがコメントの最後の検出を行っています。 複数行のコメント行に対応するために、 内側の while ループでは次の行の取得を行う getline を使用しています (AWK スクリプトとしては綺麗でない ?)。
関数 match(s,/pattern/) は、 文字 s が /pattern/ という正規表現にマッチすればマッチした最初の位置を、 マッチしなければ 0 を返します。 マッチした場合はさらに、組込み変数 RSTART と RLENGTH に、
よって、部分文字列を取り出す関数 substr() を使えば、s を
これは、途中で getline を利用していますが、 これを getline を使わないように書き直してみるとおもしろいかもしれません。
LaTeX ファイルに使われているマクロ等の命令部分を、 行番号とともに出力するスクリプト (GNU awk や nawk が必要かも):
{ s=$0 while(match(s,/\\[a-zA-Z]+/)){ print NR ":",substr(s,RSTART,RLENGTH) s=substr(s,RSTART+RLENGTH) } }
ここでは、マクロ等の命令とは、 アルファベットからなる名前の前に \ が置かれているもの (正規表現で言えば \\[a-zA-Z]) と考えています。 しかし、LaTeX のマクロはそうでないものもありますし、 "\begin", "\end" などは除きたい場合もあるでしょう。 そういう改良も考えてみるといいかもしれません。
学生の出席のデータが次のようにファイル file に入力されているとき:
# 学籍番号 4/01 4/08 4/15 4/22 200500001 ○ ○ × ○ 200500002 ○ × × × 200500003 ○ ○ ○ ○出席数と欠席数をカウントするスクリプト:
BEGIN{ print "# 学籍番号 出席数 欠席数" } /^2005/{ h["○"]=h["×"]=0 for(j=2;j<=NF;j++) h[$j]++ printf "%s %d %d\n",$1,h["○"],h["×"] }
これにより、
# 学籍番号 出席数 欠席数のように出力されます。
200500001 3 1
200500002 1 3
200500003 4 0
同様に、3 回目に欠席した人のみを出力するスクリプト:
awk 'BEGIN{ N=3 }/^2005/{ if($(N+1) ~ /×/) print $1,$(N+1)}' file
これは、BEGIN ブロックの N=3 を N=4 にすれば、 4 回目の欠席者を抜きだします。
各行に同じ個数のデータが書いてある場合、 そのデータを転置 (縦横を入れかえたもの) したデータを出力するスクリプト (GNU awk か nawk が必要かも):
(NR==1){ Fn=NF } { for(j=1;j<=NF;j++) h[NR , j]=$j } END{ for(j=1;j<=Fn;j++){ printf "%f",h[1 , j] for(k=2;k<=NR;k++) printf " %f",h[k , j] print "" } }
データ行数が少ない場合は、 ここで行っているように全てのデータを二重配列に読み込んで、 それを書き出すだけでできると思いますが、 データ行数がかなり多い場合は、 フィールド数だけファイルを読み直して出力する、 などの工夫が必要になるかもしれません。
各行に一つずつデータが書いてあるファイル file の、 データの個数、平均、分散 (標本分散) を計算するスクリプト:
awk '{x+=$1; xx+=$1*$1}END{print NR,x/NR,(xx-x*x/NR)/(NR-1)}' file
標本分散は、
{(x1-m)2+(x2-m)2 +...+(xN-m)2}/(N-1)と定義されますが、これは展開すれば、
(xj: データ (j=1,2,...,N)、 m=(x1+x2+...+xN)/N: その平均)
{(x12+x22 +...+xN2) -(x1+x2+...+xN)2/N}/(N-1)と変形できますので、上のように計算できます。
同様に、各行に 2 つずつスペース区切りでデータが書いてあるファイルの、 データの相関係数等を計算するスクリプト (Sxx,Syy は、x, y の平方和、Sxy は積和、r が相関係数):
{x+=$1; xx+=$1*$1; y+=$2; yy+=$2*$2; xy+=$1*$2} END{ Sxx=xx-x*x/NR; Syy=yy-y*y/NR; Sxy=xy-x*y/NR; r=Sxy/sqrt(Sxx*Syy); printf "(m_x,m_y,Sxx,Syy,Sxy)=(%f,%f,%f,%f,%f)\n",x/NR,y/NR,Sxx,Syy,Sxy; printf "r=%f\n",r; }
そして、直線相関がある場合 (上の r が 1 に近いとき)、その回帰直線は、
y-my=a(x-mx)となりますので、これも上のスクリプトを利用すればすぐに計算できます。
(mx: x の平均、my: y の平均、a=Sxy/Sxx)
ファイル file の各行にスペース (またはタブ) 区切りで 何らかのデータ (や単語など) が書いてあって、 その個々のデータを全て改行区切りにする、 つまり 1 行に一つずつのデータにするスクリプト:
awk '{for(j=1;j<=NF;j++) print $j}' file
これにより、
The /usr/bin/awk utility scans each input filename for linesという文書 (Solaris 9 の man awk より抜粋) は、
that match any of a set of patterns specified in prog.
...
Theのようになります。これを
/usr/bin/awk
utility
scans
each
input
filename
for
lines
that
match
any
of
...
sort | uniq -c
にかければ
簡単なワードカウンタになります。