シェルで回答してみる「シェル操作課題 (cut, sort, uniq などで集計を行う) 設問編」

毛利です。

最近「とりあえず出来ればいいか」ぐらいでレベルが止まってると感じるので何か工夫(短くする、コマンドの回数を減らす、高速なコマンドを使う)できないかを考えながら解いてみる。

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

データファイルはdata.csvとしてます。OSはMountain Lion。シェルはbash

$ uname -a
Darwin iMac.local 12.2.0 Darwin Kernel Version 12.2.0: Sat Aug 25 00:48:52 PDT 2012; root:xnu-2050.18.24~1/RELEASE_X86_64 x86_64
$ echo $SHELL
/bin/bash
$ cat <<EOF > data.csv
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
EOF

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

$ cat data.csv

Man page of CAT

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

cut -d, -f1,4 data.csv 

Man page of CUT

ちなみに -f オプションは以下の様な使い方も出来る。

# 1~3列目
$ cut -d, -f1-3 data.csv | head -1
server1,1343363124,30
# 3列目以降
$ cut -d, -f3- data.csv | head -1
30,/video.php
思いつく限り他のコマンドで(ある程度実用的と思える範囲で)
$ awk -F, '{printf("%s,%s\n",$1,$4)}' data.csv 
$ sed -e 's/^\([^,]*\),\([^,]*\),\([^,]*\),\([^,]*\)$/\1,\4/' data.csv 
$ perl -F, -a -nle 'print join(",",$F[0],$F[3])' data.csv 

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

$ egrep "^server4," data.csv 
思いつく限り他のコマンドで(ある程度実用的と思える範囲で)
$ awk -F, '/^server4,/{print}' data.csv
$ awk -F, '{if("server4"==$1){print}}' data.csv
$ sed -ne '/^server4,/p' data.csv
$ perl -ne 'print if (/^server4,/)' data.csv 

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

$ wc -l data.csv 
       9 data.csv

Man page of WC

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

「サーバー名,unixtime,ユーザーID,アクセス先」とのことなので1,3列目。文字列としてではなく数字としてソート。

$ sort -t, -k1,1 -k3,3n data.csv | 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
  • NG例

ハマッてしまった。-kではコンマ区切りでフィールド複数指定すると思ってた。

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

※ユーザID列でソートされない

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

$ sort data.csv | uniq | wc -l
       8

Man page of UNIQ

sortやuniqのオプションで短く出来ないかと思って調べた
$ sort -u data.csv | wc -l
       8

こんなもん?

-u
デフォルトの動作と -m オプションの動作では、等しいとされた行のうちの最初のものだけを表示する。

perlだとこんな感じ?
$ perl -nle '$lc++ if(! $d{$_}); $d{$_}=1; END{print $lc}' data.csv 
8

こうか。

$ perl -nle '$d{$_}++; END{print scalar keys %d}' data.csv 
8

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

$ cut -d, -f3 data.csv | sort | uniq | wc -l
       6
さっき覚えたsort -uを使う
$ cut -d, -f3 data.csv | sort -u | wc -l
       6
perlだとこんな感じ?
$ perl -F, -a -nle '$d{$F[2]}++; END{print scalar keys %d}' data.csv 
6

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

$ cut -d, -f4 data.csv | sort | uniq -c | sort -k1n | tail -1
   4 /profile.php
perlだと…どんな感じ?

ちょっとワンライナーって感じのロジックは思いつかず。

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

"このログの"とあるので、とりあえず全部置換して集計。

$ sed -e 's/server/xxx/g; s/,.*$//' data.csv | sort | uniq -c
   3 xxx1
   3 xxx2
   2 xxx3
   1 xxx4
思いつく限り他のコマンドで(ある程度実用的と思える範囲で)
$ cut -d, -f1 data.csv | sed -e 's/server/xxx/' | sort | uniq -c
$ cut -d, -f1 data.csv | sort | uniq -c | sed -e 's/server/xxx/'
$ perl -nle 'BEGIN{%d=()}s/server/xxx/g; @F=split /,/; $d{$F[0]}++; END{print join(" ", $d{$_}, $_) for keys %d}' data.csv 
1 xxx4
3 xxx1
2 xxx3
3 xxx2

最後のperlはソートしてない。設問にソートは入ってないからいいかなと。

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

$ cut -d, -f3 data.csv | egrep '\d\d+' | sort -u

※"01"というIDがあったら表示されてしまうが。

別解
$ awk -F, '{if($3 > 10){print $3}}' data.csv | sort -u

終わりに

  • 大した工夫は出来なかった感じ。他の人の回答があれば勉強しよう。
  • 問8のperl版は宿題かなぁ。他の人の回答見てみよう。

追記(当日)

perl -aF, -nle '$m{$F[3]}++;eof&&print join" ",@{(sort{$b->[0]<=>$a->[0]}map{[$m{$_},$_]}keys%m)[0]}' test.txt

@{(ほにゃらら)}[0]かぁ。なるほど。


いじょ。