開眼シェルスクリプト2012年2月号

Pocket
LINEで送る

出典: 技術評論社SoftwareDesign

2. 開眼シェルスクリプト 第2回

2.1. はじめに

2.1.1. 使ってますか?awkとsed

 皆様、冷え性には辛い季節となりましたが、
寒さに負けず端末を叩いておられますでしょうか。
会社の隅、ドア近くに座っている筆者は、
コードを書いては誰かが開け放したドアを閉めるという毎日を送っております。
ドア用の close コマンドがないものか。

 今回、そして次回のお題は冷え性でもドアを閉めようということでもなく、
「ログを捌く」です。今回は、ログやその他テキストを端末で加工する際、
必ずと言っていいほど使う awk と sed の使い方を扱います。

 昔からの UNIX 使いの方には愚問になりますが、awk とsed、
ご存知でしょうか?イベントでたくさんの人と話していると、
若い人や UNIX 以外の OS で仕事をしてきた人の認識率はそんなに
高くないという印象を持っています。
自分もそうだったので、まあ、そういう状況です。

2.1.2. UNIX を情報処理装置として使うために

 筆者の場合は大学にいるときから Linux を使う機会は多く、
研究室のためにルータ作りやメールの管理に励んでいました。
が、実験データ等は excel を使うか、
Visual C++ でコードを書いて処理していました。

 この原因は間違いなく awk を端末で使う方法を知らなかったからです。
もし知っていたらそんな面倒なことをする必要は無かったのです。
excel はともかくコードを書くことは楽しかったのですが、
同時に現在と同じく多忙だったので、知っていたら楽だったのですが・・・。

 もし Linux や BSD などのイメージを問われて、
「無料のサーバ用OS」と真っ先に答える人には今回の内容は非常に有用です。
そのOSは、実は「強力で簡単な情報処理マシン」なので、
ぜひともサーバ用途に加えてそのように使っていただきたい。
awk や sed を覚えるのは、その準備です。
(脚注:perl でも一行野郎ができれば可。)

2.1.3. 今回の気構え

 今回も、お題に入る前に格言めいたものを記し、
読むときに何を意識するかの指針にしたいと思います。
今回は、The Art of UNIX Programming
(脚注: Eric S.Raymond (著), 長尾 高弘 (翻訳): The Art of UNIX Programming, アスキー, 2007.)
から、以下を引用したいと思います。

  • 他に方法がないことが実験により明らかである場合に限り、大きいプログラムを書け。
  • プログラマの時間は貴重である。プログラマの時間をコンピュータの時間より優先して節約せよ。

要は、大げさなことをするなということです。
awk を知らなかった時の自分には耳の痛い話です。

 ところで、著者の Raymond は同書で awk を否定しているのですが・・・。

2.2. 今回のお題:ログをさばく(前半戦)

 それでは、早速使ってみましょう。
題材は Linux の secure ログ、apache の access_log ログです。
これらのログは、ある程度「人間臭いデータ」なので、
テキスト操作のお題の宝庫です。
次回も secure と access_log を使って、ログの整理シェルスクリプトを書きます。

 secure ログは自宅の CentOS6 のサーバから、
access_log は CentOS5.4 のウェブサーバから採取しました。
作業する環境(CentOS6 の入った ThinkPad x41)
のホーム下に「LOG」というディレクトリを作って、
以下のように放り込んであります。

#CentOS6 は古いログに日付が入る。
[ueda@cent LOG]$ ls
httpd/access_log    secure
httpd/access_log.1  secure-20111030
httpd/access_log.2  secure-20111106
httpd/access_log.3  secure-20111113
httpd/access_log.4  secure-20111120

小ネタですが、scpを次のように使うとrootにならずにログがコピーでき、
ファイルのユーザも変更できます。
sshd の設定次第ではこの小技は使えませんので、
普通の方法でコピーしてください。

[ueda@cent ~]$ scp -r root@localhost:/var/log ./

2.2.1. awkとsedを使う取っ掛かりとコツ

 sed、awkは共にスクリプト言語なので長いコードも書けますが、
この連載では処理を細かく切ってパイプでつないで使います。
そうすることで、パイプラインの各ステップで行うことが明確になり、
見通しの良いスクリプト(あるいは端末でのコマンドライン)
を書くことができます。

 sed と awk の使い方として、
最初は次の三つのテキスト操作を押さえておきましょう。
空手の型のように身につけてください。

  • レコードの抽出
  • 文字列の置換
  • フィールドの抽出と並び替え

sedとawkの性質や誕生の経緯については、wikipediaに詳しいので割愛します。

2.2.2. レコードの抽出(grep の拡張版としての awk )

 まずはレコードの抽出から。
 サンプルの secure ログには次のように sshd と su のログがあります。

#レコードの後半は長いので省略
[ueda@cent LOG]$ tail -n 5 secure
Nov 23 08:56:13 cent sshd[32743]: pam_unix(sshd:se
Nov 23 16:34:55 cent su: pam_unix(su-l:auth): auth
Nov 23 16:34:59 cent su: pam_unix(su-l:session): s
Nov 23 16:35:03 cent su: pam_unix(su-l:session): s
Nov 23 16:35:05 cent su: pam_unix(su:session): ses

これを sshd のものだけ、あるいは su のものだけ見たいとします。
grep を使ってもよいのですが、この際いつも問題になるのは、
関係ないところに sshd や su という文字列が混ざっているかもしれず、
きっちり抽出できない懸念があることです。

 awk を使えば、そのような心配なく su のレコードだけ抽出できます。

[ueda@cent LOG]$ cat secure | awk '$5=="su:"'
Nov 23 16:34:55 cent su: pam_unix(su-l:auth): auth
Nov 23 16:34:59 cent su: pam_unix(su-l:session): s
Nov 23 16:35:03 cent su: pam_unix(su-l:session): s
Nov 23 16:35:05 cent su: pam_unix(su:session): ses

awk ‘$5==”su:”’は、
「第5フィールドの文字列が『su:』の場合」抽出しろということです。
フィールドというのは、スペースで区切られた文字列のことで、
左から第1、第2、・・・と数えます。
このようにawkは、位置指定付きのgrepのように使えます。

 正規表現も使えます。sshdのレコードだけ見たければ、
例えば次のように打ちます。
スラッシュで囲まれた部分が正規表現で、第5フィールドに適用しています。
正規表現 sshd\[[0-9]*\]: は、

sshd[ の次に数字が0個以上続き、その後 ]: が来る文字列

という意味になります。もう少し補足すると、
[0-9] は0から9のどれか一字という意味になります。
[, ] は正規表現で使う記号なので、
[, ] という文字そのものを書く時は\記号でエスケープし、
\[\] と記述します。

[ueda@cent LOG]$ cat secure | awk '$5~/sshd\[[0-9]*\]:/'
Nov 23 08:44:49 cent sshd[32686]: pam_unix(sshd:se
Nov 23 08:56:13 cent sshd[32743]: Accepted publick
Nov 23 08:56:13 cent sshd[32743]: pam_unix(sshd:se
(以下略)

 さらに、文字列や数値の大小比較でレコードを抽出することも可能です。
次の例では、11月23日の8時13分40秒台のレコードを抽出しています。
2つあるうちの後ろのawkで、時刻を文字列として大小比較しています。

#いろいろ攻撃されてますが、
#鍵認証しか許可していないので大丈夫です。多分。
[ueda@cent LOG]$ cat secure | awk '$1=="Nov" && $2=="23"' | awk '$3>="08:13:40" && $3<"08:13:50"'
Nov 23 08:13:40 cent sshd[32578]: Invalid user cro
Nov 23 08:13:40 cent sshd[32579]: Received disconn
(中略)
Nov 23 08:13:49 cent sshd[32601]: Received disconn

 awkでは文字列を”“で囲むと文字列扱い、囲まないと数値扱いになります。
入力されるテキストは比較対象や演算に合わせて扱いが変わります。
したがって、以下のように出力に違いが出ます。

#9.9は数字88と比較されるので数字扱い。抽出されない。
[ueda@cent ~]$ echo 9.9 | awk '$1>88'
#9.9は文字列88と比較されるので文字列扱い。
#辞書順で比較され、抽出される。
[ueda@cent ~]$ echo 9.9 | awk '$1>"88"'
9.9

以上がレコード抽出で最初に知っておけばよいことです。
awkをちょっと気の利いたgrepとして使ってみようという気になったら
後は自然に上達すると思います。

2.2.3. 置換

 次に文字列の置換をしてみましょう。
例えばsedを使ってNovを11に置換するには、次のように書きます。

#置換前
[ueda@cent LOG]$ tail -n 1 secure
Nov 23 16:35:05 cent su: pam_unix(su:session): session (略)
#置換後
[ueda@cent LOG]$ tail -n 1 secure | sed 's/^Nov/11/'
11 23 16:35:05 cent su: pam_unix(su:session): session (略)

sedのオプション「s/^Nov/11/」は呪文めいてますが、左からsが「置換」、
スラッシュの前が置換対象の正規表現、スラッシュの後が置換後の文字列です。
一行に一回、この変換が適用されます。
もし一行で何回も置換したければ、最後のスラッシュの後に文字gを付けます。
正規表現「^Nov」は、行頭にあるNovという意味になります。
区切り文字は必ずしもスラッシュである必要はありません。
正規表現や置換後の文字列にスラッシュが含まれる場合は、
セミコロンなどを使います。あとからそのような例が出てきます。

 正規表現でマッチした文字列を再利用することもできます。
次の例のように、正規表現にマッチした文字列を&で呼び出したり、
\( \) で範囲指定して \1,\2,\3,... という記号で呼び出すことができます。

[ueda@cent LOG]$ echo 1140003 | sed 's/.../〒&-/'
〒114-0003
[ueda@cent LOG]$ echo 09012345678 | sed 's/^\(...\)\(....\)/tel:\1-\2-/'
tel:090-1234-5678

正規表現中の . は、任意の一字という意味です。
かな漢字も正しく一字と数えてくれますが、
LANGの指定によっては次のように動作が変わります。
この例では、LANG=C としてマルチバイト文字を意識しないようにすると、
「大」の先頭1バイトだけが削れてしまいます。

#文字コードがUTF-8
[ueda@cent ~]$ echo $LANG
ja_JP.UTF-8
[ueda@cent ~]$ echo 大岡山 | sed 's/^.//g'
岡山
#LANGをCとすると動作が変わる。
[ueda@cent ~]$ echo 大岡山 | LANG=C sed 's/^.//g'
��岡山

 awk を使っても置換ができます。
secure ログの Nov を11に置換するには次のように打ちます。

#置換の関数 gsub を使う
[ueda@cent LOG]$ tail -n 1 secure | awk '{gsub(/Nov/,"11",$1);print $0}'
11 23 16:35:05 cent su: pam_unix(su:session): session (略)
#条件文を使う
[ueda@cent LOG]$ tail -n 1 secure | awk '{if($1=="Nov"){$1="11"};print $0}'
11 23 16:35:05 cent su: pam_unix(su:session): session (略)

この場合、awkはレコード抽出ツールではなくて文字置換ツールになっています。
抽出以外のawkプログラムは、{}の中に書きます。
この例の上の方は、$1に自動に入った”Nov”をgsubという関数で操作しています。
gsubの三つの引数は、それぞれ正規表現、置換後の文字列、変数です。
下の方は、if文を使って$1を”11”に置き換えています。

  print $0 の$0は、レコード一行全体を表します。
awk の面白いところは、$1や$2を書き換えると$0も変わるところです。
print $0 は、 print と省略できます。
この規則のおかげで、端末に書く文字が短くなります。
以下は例です。

#入力された全フィールドをそのまま出力する方法
[ueda@cent ~]$ echo 1 2 3 | awk '{print $1,$2,$3}'
1 2 3
[ueda@cent ~]$ echo 1 2 3 | awk '{print $0}'
1 2 3
[ueda@cent ~]$ echo 1 2 3 | awk '{print}'
1 2 3
#フィールドの値の変更
[ueda@cent ~]$ echo 1 2 3 | awk '{$2="二";print $0}'
1 二 3
[ueda@cent ~]$ echo 1 2 3 | awk '{$2="二";print}'
1 二 3

 {}に囲まれた部分は「アクション」と呼ばれます。
囲まれていない、抽出の部分は「パターン」と呼ばれます。
アクション内の各文はセミコロンで区切られ、左から右に処理が流れます。
C言語の影響が強いので、記号類の使い方はC言語に似ています。

 ところで先ほどからログの「Nov」を「11」に変換していますが、
他の月も変換するにはどうすればよいでしょうか。
sed や awk を12個つなげばできますが
(脚注:マルチコアの場合、12個つなぐと並列処理になるのでバカにしてはいけません。)、
awk や sed のスクリプトを用意することもできます。
月の変換では、次のMONTHファイルを準備して sed で使えばよいでしょう。

[ueda@cent LOG]$ cat MONTH
s/^Jan/01/
s/^Feb/02/
s/^Mar/03/
(以下略)
[ueda@cent LOG]$ sed -f ./MONTH secure | tail -n 1
11 23 16:35:05 cent su: pam_unix(su:session): sess(略)

2.2.4. フィールドの抽出と並び替え

 あるフィールド$iの抽出をしたい場合は print $i と記述します。
次の例では、access_logから第4フィールドを抽出しています。
ただ、access_logの区切り文字は複雑なので、
この場合の第4フィールドは単に空白区切りで見たときの4番目のデータということになります。

[ueda@cent LOG]$ head -n 1 httpd/access_log
114.80.93.71 - - [20/Nov/2011:06:47:54 +0900] "GET / HTTP/1.1" 200 1429 (略)
[ueda@cent LOG]$ cat httpd/access_log | awk '{print $4}'
[20/Nov/2011:06:47:54
(以下略)

並び替えは、並べたい順にフィールドを指定してprintを適用します。
例えば前の例に続けて、抽出した日付、時刻のデータを8桁の日付、
6桁の時刻で正規化するには次のように操作します。

[ueda@cent LOG]$ cat httpd/access_log | awk '{print $4}' | sed 's;[:/\[]; ;g' | awk '{print $2,$1,$3,$4$5$6}' | sed -f ./MONTH | awk '{print $3$1$2,$4}'
20111120 064754
20111120 064805
(略)

長いので各段階でパイプを切って出力を観察しましょう。
file1 から file2 への変換では、sed で [, :, / を空白に変換しています。
正規表現にスラッシュが含まれるので、区切り文字にセミコロンを使っています。
file2 から file3 への変換では、
print を使って日付の年月日の並び替えと時分秒の間のスペースを除去しています。
この例では、 print する変数の間にカンマがあったりなかったりしますが、
カンマを入れると空白区切りで出力、カンマを入れないと連結して出力という意味になります。
カンマを入れずに連結する場合は、 $4 $5 $6 と間に空白を入れても連結されます。
あとは月を数字表記に変えて、年月日を連結して目標の出力を得ています。

[ueda@cent LOG]$ cat httpd/access_log | awk '{print $4}' | head -n 1 > file1
[ueda@cent LOG]$ cat file1
[20/Nov/2011:06:47:54
[ueda@cent LOG]$ cat file1 | sed 's;[:/\[]; ;g' > file2
[ueda@cent LOG]$ cat file2
 20 Nov 2011 06 47 54
[ueda@cent LOG]$ cat file2 | awk '{print $2,$1,$3,$4$5$6}' > file3
[ueda@cent LOG]$ cat file3
Nov 20 2011 064754
[ueda@cent LOG]$ cat file3 | sed -f ./MONTH > file4
[ueda@cent LOG]$ cat file4
11 20 2011 064754
[ueda@cent LOG]$ cat file4 | awk '{print $3$1$2,$4}'
20111120 064754

2.2.5. おまけ

 awk については、他にも改行を取ったり、行をまたいで数値を集計したりと、
まだちょっと覚えなければならないことがありますが、
次回のログ処理で使うにはこの程度で十分です。
もうちょっと勉強したい人のために、文法的に凝ったコードを示します。
このコードは、西暦年の情報が入っていない secure に、
無理やり年を付加することを想定したものです。

#MMDDの4桁で月日を表現
#3行目で年明け
[ueda@cent LOG]$ cat hoge
1230
1231
0101
0102
#データをひっくり返し、年をまたいだら年を一つ減らす。
#年を入れて8桁にしたら再びデータをひっくり返す。
[ueda@cent LOG]$ tac hoge | awk 'BEGIN{y='$(date +%Y)';md='$(date +%m%d)'}{if(md<$1){y--};md=$1;print y md}' | tac
20111230
20111231
20120101
20120102

2.3. おわりに

 今回は、ログの加工を題材に awk と sed の使い方を説明しました。
今回は端末で awk, sed を使う話で、シェルスクリプトは書きませんでした。
sed で月の英語表記を数字表記に変換する sed スクリプトが一つ出てきました。

 今回行った端末での awk, sed の使い方は、空手の「型」のようなものです。
自在にテキストを加工するためには、
このような型を組み合わせて端末で使いこなすことが必要で、
少し慣れる必要があります。
また、端末という限られたスペースで必要な処理を行うことで、
きれいなシェルスクリプトを書くことができるようになります。
そこまで苦労して身につけるべきかというところですが、
あまり深く考えず、
grep の代わりに awk を使ってみてから考えてもらえれば幸いです。

Pocket
LINEで送る