シェル操作課題 (cut, sort, uniq などで集計を行う) 設問編 を解いてみた

シェル操作課題 (cut, sort, uniq などで集計を行う) 設問編 - Yamashiro0217の日記

何となく暇だったので。
UNIXのテキスト操作系コマンドは非常に便利なので使いこなせると捗る。
問題の性質上、凝った解き方やログファイルの内容に依存した解き方ではなくスタンダードなやり方の方がよいと思う。

問1 このファイルを表示しろ

$ cat log.out
server1,1343363124,30,/video.php
server2,1343363110,20,/profile.php
server3,1343363115,7,/login.php
server1,1343363105,8,/profile.php
server2,1343363205,35,/profile.php
server2,1343363110,20,/profile.php
server3,1343363205,30,/login.php
server4,1343363225,12,/video.php
server1,1343363265,7,/video.php

直接見たいときは普通 more/less/vi あたりを使うけど。

問2 このファイルからサーバー名とアクセス先だけ表示しろ

$ awk -F, '{print $1","$4}' log.out
server1,/video.php
server2,/profile.php
server3,/login.php
server1,/profile.php
server2,/profile.php
server2,/profile.php
server3,/login.php
server4,/video.php
server1,/video.php

想定期待結果にカンマが使われてるので表示部分に "," が入ってる。
表示する要素が増えてくると、いちいちセパレーターを間に挟むのがめんどくさいので、出力用セパレーターOFSを指定してもよいかも。

$ awk 'BEGIN{FS=OFS=","} {print $1,$4}' log.out

問3 このファイルからserver4の行だけ表示しろ

$ awk -F, '$1=="server4"' log.out
server4,1343363225,12,/video.php

場合によってはgrepでもありかな。

$ grep '^server4,' log.out

問4 このファイルの行数を表示しろ

$ wc -l < log.out
9

別に標準入力じゃなくてもいいんだけど、ファイルを指定するとファイル名が出るので。

$ wc -l log.out
9 log.out

問5 このファイルをサーバー名、ユーザーIDの昇順で5行だけ表示しろ

$ sort -t, -k1,1 -k3,3n log.out | head -5
server1,1343363265,7,/video.php
server1,1343363105,8,/profile.php
server1,1343363124,30,/video.php
server2,1343363110,20,/profile.php
server2,1343363110,20,/profile.php

sortコマンドは awkperl と言ったスクリプト言語ではちょっと代替できない便利さがありますね。あまり使わないけど paste コマンドあたりも似たような代替不能性が。

問6 このファイルには重複行がある。重複行はまとめて数え行数を表示しろ

$ sort -u log.out | wc -l
8

"sort | uniq" でもよい。

問7 このログのUU(ユニークユーザー)数を表示しろ

$ awk -F, '{print $3}' log.out | sort -u | wc -l
6

問8 このログのアクセス先ごとにアクセス数を数え上位1つを表示しろ

$ awk -F, '{print $4}' log.out | sort | uniq -c | sort -nr | head -1
      4 /profile.php

"sort | uniq -c | sort -n" はマジ便利なので覚えましょう。チョチョイと統計取りました的結果が出せるのでよい。

問9 このログのserverという文字列をxxxという文字列に変え、サーバー毎のアクセス数を表示しろ

$ sed -e 's/^server/xx/; s/,.*//' log.out | sort | uniq -c
      3 xx1
      3 xx2
      2 xx3
      1 xx4

これは手抜きである。ちゃんとやるなら

$ awk -F, '{sub("^server", "xx", $1); print $1}' log.out

あるいは

$ awk -F, '{print $1}' log.out | sed -e 's/server/xx/'

と分けるとawkの関数とか覚えなくてもよいので楽。

問10 このログのユーザーIDが10以上の人のユニークなユーザーIDをユーザーIDでソートして表示しろ

$ awk -F, '$3>=10 {print $3}' log.out | sort -nu
12
20
30
35

awk は特定のフィールドを抜き出すのに使われることが多いけど、better grep としても使っても便利。

その他

大抵のことは grep/awk/sed/sort/uniq 辺りを駆使すればできるし、スクリプト言語ワンライナーを使うよりもタイプ数が短くなって簡単。覚えるの大変と言っても、よく使うオプションだけ覚えておけばよいし、凝ったことがしたいならperlrubyスクリプトを書いたほうが楽。

長めのテキスト処理をしたいときや、ちょっと凝った関数を使いたいときはスクリプト言語の力を借りると便利。

例えば今回の例で言えば、「UNIX epochを日付に直したい」とか、「ユーザーIDをユーザー名にしたい」とか。

$ perl -F, -anle '$t=localtime $F[1]; $n=getpwuid $F[2]; print "$t : $n" if $F[3] eq "/login.php"' log.out
Fri Jul 27 12:25:15 2012 : user7
Fri Jul 27 12:26:45 2012 : user30

login.phpにアクセスした日付とユーザー名を知りたい、みたいな?

あとは、例えばこういうファイルがあったとして、どの関数から何バイトのメモリー割り当てがされたか把握する場合とか。

$ cat log2.out
128 bytes of heap allocated from func001
665536 bytes of heap allocated from func002
1024 bytes of heap allocated from func003
32 bytes of heap allocated from func002
256 bytes of heap allocated from func003
256 bytes of heap allocated from func004
512 bytes of heap allocated from func001
256 bytes of heap allocated from func001
1024 bytes of heap allocated from func003
256 bytes of heap allocated from func002
$ perl -nle 'if(/(\d+) bytes of heap allocated from (.+)/){ $h{$2}+=$1 }; END{ print "$_ $h{$_}" foreach keys %h}' log2.out | sort -nrk2
func002 665824
func003 2304
func001 896
func004 256

あとはクソ長いログのどこかに変化点があるようなとき、ログをざっくり眺めたい場合とか。

$ perl -nle 'print "$. $_" unless $.%10000' verylongfile.txt

10000行ごとに行番号つきでprintする。awkでもNRを使って似たようなことができそうだけど、詳しくないので。

あとは grep -A/-B 辺りも非常に便利なので覚えておいて損はない。残念ながらこのオプションはGNU拡張なのですが、非GNU環境ではportしてでも使うことをオススメします。