UECジャーナル

開眼☆シェルスクリプト ログを自在に加工する(1) ― awkとsedの使い方

awk(1)とsed(1)

今回はログやその他テキストをターミナルで加工する際に必ずと言ってよいほど使うことになるawk(1)とsed(1)の使い方を取り上げる。

イベントでエンジニアと話をしていると、若手のエンジニアやUNIX以外のOSで仕事をしてきた方は、awk(1)やsed(1)をあまりご存じないという印象を受ける。今回はそういった方向けの記事だ。

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

今思い返してみると、大学でLinuxは使っていたものの、実験データなどの処理にはMicrosoft Excelを使ったり、Visual C++で処理コードを記述して対処していた。考えてみれば、この原因は間違いなくawk(1)を知らなかったからだ。もし知っていたら、そんな面倒なことをする必要はなかった。

LinuxやFreeBSDなどのイメージを問われて無料のサーバOSと真っ先に答える方であれば、今回の内容は非常に有用だ。OSは強力で簡単な情報処理マシンだ。ぜひともサーバ用途に加えてそのように使っていただきたいと思う。

シンプルにいこう

今回はThe Art of UNIX Programming※1から以下を引用する。

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

大雑把にまとめると「大げさなことをするな」ということだ。awk(1)を知らなかった時の自分には耳の痛い話だ。

今回のお題 ログをさばく

CentOS 6のsecureログファイルおよびApacheのaccess_logログファイルを題材にしてテキストを処理する方法を紹介する。ログファイルは/home/ueda/LOG/ディレクトリに保存してあるとする。

$ pwd
/home/ueda/LOG
$ ls | column -c 40
access_log    secure
access_log.1  secure-20111030
access_log.2  secure-20111106
access_log.3  secure-20111113
access_log.4  secure-20111120
$ 

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

sed(1)やawk(1)はスクリプト言語であり長いコードを記述することもできる。しかしこの連載では、処理を細かく切ってパイプでつないで使っていく。こうすると、パイプラインの各ステップで行うことが明確になり、見通しのよいスクリプトを書くことができる。また、マルチコア/プロセッサの性能を活用する上でもこちらの方が好ましい。

sed(1)とawk(1)の使い方として、最初は次の3つのテキスト操作を押さえておきたい。

レコードの抽出

secureログには次のようにsshd(8)とsu(1)のログがある。

$ tail -n 5 secure | cut -c 1-50
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(8)のものだけ、あるいはsu(1)のものだけ見たいとする。grep(1)を使ってもよいが、単純にgrep(1)を使うだけでは関係ないところに記載されているsshdやsuという文字列が一致してしまう。次のようにawk(1)を使えば、そうした問題を回避することができる。

$ cat secure | awk '$5=="su:"' | cut -c 1-50
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
$ 

$5=="su:"は第5フィールドの文字列がsu:の場合にその行を出力せよ、という指定となる。フィールドはスペースで区切られたそれぞれの文字列のことで、左から第1フィールド、第2フィールド…と数える。

パターンには正規表現を使うこともできる。sshd(8)のログは「sshd[プロセス番号]」のようにプロセス番号を含むため、単純な文字列の一致では処理できない。たとえば、正規表現としてsshd[[0-9]*]:のような文字列を指定すれば、sshd[の次に数字が0個以上続きその後]:が来る文字列を含む文字列に一致するようになり、sshd(8)のログ抽出として利用できる。

対象とする文字列を含むという表現は対象をスラッシュで囲むことで行い、式と比較演算子させるには~を使う。つまり、フィールドに対して正規表現による比較演算を実施する場合、次のような使う方をすることになる。

$ cat secure	| awk '$5~/sshd[[0-9]*]:/' |
cut -c 1-50	| head -n 3
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(1)コマンドのうち、後ろのawk(1)で時刻を文字列として大小比較している。

$ cat secure				|
awk '$1=="Nov" && $2=="23"'		|
awk '$3>="08:13:40" && $3<"08:13:50"' 	|
cut -c 1-50 | head -n 3
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(1)では""で囲まれたものは文字列として扱われ、囲まないと数値扱いになる。入力されるテキストは比較対象や演算に合わせて扱いが変わる仕組みになっている。次のように操作するとその違いがよくわかる。

$ echo 9.9 | awk '$1>88'
$ echo 9.9 | awk '$1>"88"'
9.9
$ 

数値で比較した場合には9.9は88よりも小さいと評価され、文字列で比較した場合には先頭の"9"と"8"が比較されるため"9.9"の方が"88"よりも大きいと評価される。

置換

sed(1)を使ってNovを11に置換するには次のようにすればよい。

$ tail -n 1 secure | cut -c 1-64
Nov 23 16:35:05 cent su: pam_unix(su:session): session
$ tail -n 1 secure | sed 's/^Nov/11/' | cut -c 1-64
11 23 16:35:05 cent su: pam_unix(su:session): session
$ 

s/^Nov/11/はsが置換を意味しており、1つ目のスラッシュと2つ目のスラッシュの間の文字列が置換対象を表現する正規表現、2つ目のスラッシュと3つ目のスラッシュの間の文字列が置換後の文字列を表している。この指定では1行に対して1回だけ、左側からこの処理が実行される。

1行で何回も置換したければ最後のスラッシュの後に文字gを付ければよい。^Novは行頭にあるNovという文字列という意味になる。区切り文字は必ずしもスラッシュである必要はなく、sの後の最初の文字列を区切り文字として統一して利用すればよい。つまりs/^Nov/11/はs,^Nov,11,と書いてもよいし、s+^Nov+11+と書いてもよい。

正規表現でマッチした文字列を再利用することもできる。次のように、正規表現にマッチした文字列を&で呼び出したり、( )で範囲指定して後から1,2,3,...という記号で呼び出すこともできる。この機能は後方参照と呼ばれている。

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

正規表現中の.は任意の一字という意味になる。このあたりは環境変数LANGの値で次のように処理が変わるので注意が必要。処理するテキストの文字コードに合わせて環境変数LANGの値は適切なものに設定しておく必要がある。

$ echo $LANG
ja_JP.UTF-8
$ echo 大岡山 | sed 's/^.//g'
岡山
$ echo 大岡山 | LANG=C sed 's/^.//g'
��岡山
$ 

awk(1)で置換を実施するなら次のようにgsub関数を使えばよい。

$ tail -n 1 secure			|
awk '{gsub(/Nov/,"11",$1);print $0}'	|
cut -c 1-64
11 23 16:35:05 cent su: pam_unix(su:session): session
$ tail -n 1 secure			|
awk '{if($1=="Nov"){$1="11"};print $0}'	|
cut -c 1-64
11 23 16:35:05 cent su: pam_unix(su:session): session
$ 

awk(1)ではプログラムに相当する部分は{}の中に記述する。先の例で書いたのはパターン部分であり、{}の部分は出力するというデフォルトの処理が動いていた。書いていなかったが、実際には{print $0}という処理(アクション)を指定していたことになる。$0は行全体を指している。たとえばawk '$5=="su:"'というのはawk '$5=="su:"' '{print $0}'と書いているのと同じだ。{print $0}は{print}のように省略することもできる。

gsubの例では逆にパターンの指定を省略している。パターンを省略した場合には、入力されるすべての行が処理の対象となる。上記では$1に格納されている文字列のうち、Novをgsubという関数で11へ置換している。gsubの3つの引数は、それぞれ置換対象となるパターン、置換後の文字列、処理対象となる変数となっている。

awk(1)ではフィールド変数に文字列を代入すると、そのフィールドの内容が書き換わるという仕組みになっている。上記の2つ目の例では代入によって置換を実施している。次のようにawk(1)を実行してみれば、それぞれどのような動作をするものかがよくわかる。

$ echo 1 2 3 | awk '{print $1,$2,$3}'
1 2 3
$ echo 1 2 3 | awk '{print $0}'
1 2 3
$ echo 1 2 3 | awk '{print}'
1 2 3
$ echo 1 2 3 | awk '{$2="二";print $0}'
1 二 3
$ echo 1 2 3 | awk '{$2="二";print}'
1 二 3
$ 

awk(1)では{}で囲った部分をアクションと呼び、アクションの前に指定されている式をパターンと呼んでいる。アクション内の各文はセミコロンで区切られ、左から右に処理が実行される。基本的にawk(1)はこのように「パターン {アクション} パターン {アクション} …」と記述することで動作する仕組みを採用している。

月をすべて数字に変換するのであれば、sed(1)やawk(1)を12個パイプでつなげて処理させてもよいし、ひとつのコマンドラインで書くこともできる。メニーコア時代に突入しつつある現在では、処理を細かくパイプでつなげた方が高い性能が期待できるため、今後はパイプで接続する方法の方が有益になるとみられる。しかし、ワンラインで記述する方法も覚えておいて損はない。たとえば次のように記述できる。

$ cat MONTH
s/^Jan/01/
s/^Feb/02/
s/^Mar/03/
s/^Apr/04/
s/^May/05/
s/^Jun/06/
s/^Jul/07/
s/^Aug/08/
s/^Sep/09/
s/^Oct/10/
s/^Nov/11/
s/^Dec/12/
$ sed -f ./MONTH secure | tail -n 1 | cut -c 1-60
11 23 16:35:05 cent su: pam_unix(su:session): sess
$ 

上記例では置換命令をファイルにまとめている。この手の使い方はほかの処理に応用が効くので便利だ。別ファイルを用意せず、ワンライナーで記述するなら次のようになる。

$ sed -e s/^Jan/01/ -e s/^Feb/02/ -e s/^Mar/03/ -e s/^Apr/04/ -e s/^May/05/ -e s/^Jun/06/ -e s/^Jul/07/ -e s/^Aug/08/ -e s/^Sep/09/ -e s/^Oct/10/ -e s/^Nov/11/ -e s/^Dec/12/ secure	|
tail -n 1 | cut -c 1-60
11 23 16:35:05 cent su: pam_unix(su:session): sess
$ 

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

あるフィールドを取り出したい場合にはprint $iと記述する。iはフィールド番号を指定する。次の例では、access_logから第4フィールドを抽出している。

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

並び替えは、並べたい順にフィールドを指定してprintを実行すればよい。たとえば前の例に続けて日付、時刻のデータを8桁の日付、6桁の時刻で正規化するには次のようにコマンドを使えばよい。

$ 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}'		|
head -n 2
20111120 064754
20111120 064805
$ 

まずsed(1)で[, :, /の3つの文字をぞれそれを空白に変換している。正規表現にスラッシュが含まれるので、区切り文字にセミコロンを使っている※2。次に、printを使って日付の年月日の並び替えと時分秒の間のスペースを除去している。printする変数の間にカンマがあると空白区切りで出力され、カンマを入れないと連結して出力される。あとは月を数字表記に変えて、年月日を連結して目的とする出力を得ている。処理を個別に確認していくと次のようになる。

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

もうひとつ、awk(1)を使用例を掲載しておきたい。これは西暦年が入っていないログデータに年データを付加するという処理をしている。BEGINというのは処理をはじめるもっとも最初に自動的に一致する特殊なパターン、awk(1)の2行目にはパターンが指定されずにアクションのみが書いてあるが、パターンの指定がないときはすべての行に一致するというパターンとなり、2つ目のアクションはすべての行に対して適用されることになる。

$ cat log
1230
1231
0101
0102
$ cat log						|
awk 'BEGIN	{y='$(date +%Y)';md='$(date +%m%d)'}
		{if(md<$1){y--};md=$1;print y md}'
20111230
20111231
20120101
20120102
$ 

awk(1)は行をまたいだ処理やカウントなどさまざまな処理を実現できる。ただし、ログを処理する程度であれば、今回紹介した程度の機能を押さえておけば十分だ。

おわりに

今回はログの加工を題材にawk(1)とsed(1)の使い方を説明した。awk(1)とsed(1)はテキスト処理をするうえでとても有益なコマンドなので、これを機にawk(1)およびsed(1)に慣れてもらえればと思う。

※1 The Art of UNIX Programming, Eric S.Raymond(著), 長尾高弘(翻訳), アスキー, 2007.

※2 セミコロンではなくて、別にカンマでもコロンでも何でもよい。

Software Design 2012年2月号 上田隆一著、「テキストデータならお手のもの "開眼☆シェルスクリプト" 【2】ログを自在に加工する(1) ― awkとsedの使い方」より加筆修正後転載

last modified: 2014-01-13 16:01:13