読者です 読者をやめる 読者になる 読者になる

tumblr

tumblr(タンブラー)は、メディアミックスブログサービス。ブログとミニブログ、そしてソーシャルブックマークを統合したマイクロブログサービスである。アメリカのDavidville.inc(現: Tumblr, Inc.)により2007年3月1日にサービスが開始された。

話題のシェル操作課題をrubyで解いてみる

シェル操作課題 (cut, sort, uniq などで集計を行う) 設問編
もうなんか話題に乗り遅れた感が半端ないですが、シェルスクリプトとか全く覚えてないし1ビットたりとも書けないので、1バイトくらいは辛うじて書くことのできるrubyで挑戦してみました。
ものすごくどうでもいいことなのですが、元の設問ブログのヘッダー画像のフランスパンがどうしても卑猥なものに見えてしまって、問題解くのに集中できませんでした。


rubyワンライナー

挑戦の前にrubyワンライナーを書く方法を覚える。

ワンライナーで書いてみる

まずはhello worldを出力するワンライナーを書いてみる

$ ruby -e 'p "hello world"'

rubyコマンドとともに後述の起動オプションをいくつか指定し、さらにシングルクオートで実行したいスクリプトを囲む。
スクリプトを囲むのはシングルクオートじゃないといけないみたい。ダブルクオートだとなんでダメなんだろうか。詳しい人教えてください。

-e : コマンドラインからスクリプトを指定

引数からスクリプトファイル名を取らない。ワンライナーで書くときは必須。

-n : スクリプトをwhileで囲む

このオプションが指定されるとスクリプト

while gets
...
end

で囲まれた状態になる。


ちなみに-pオプションをつけると、-nのようにwhileで囲んだのと同じ状態になった上で、$_(getsで読み込んだ文字列)をループの最終行で出力するようになる。なので

$ echo matz | ruby -p -e '$_.tr! "a-z", "A-Z"'
MATZ

という感じでわざわざ p とかしなくてもよくなる。例はrubyリファレンスから。

-a : 読み込んだ行をスプリットする

各ループの先頭で

$F = $_.split

が行われる。スプリットされた行は$Fで参照できる。
ループが必要なので、-nや-pオプションが同時に指定されてないといけない。

-l : 行末の自動処理

pした時に自動で改行してくれる。-pとか-nをつけた場合は逆にgetsで読まれた各行の最後に来る改行を消してくれる。chop!してくれるというわけ。


参考:
http://d.hatena.ne.jp/mickey24/20110310/ruby_one_liner
http://doc.ruby-lang.org/ja/1.9.3/doc/spec=2frubycmd.html


解いてみよう

とりあえずリファレンスとかざっと見ながら起動オプションをある程度頭に入れたところで、解いてみる
ログ名はhoge.logとした。
7/31時点の問題のログは以下。

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

問1 このファイルを表示しろ
ruby -pe '' hoge.log

シングルクオートのあとにファイル名をいれると、そのファイルがgetに渡されたことになる。

  • pをつけておけば自動でhoge.logの行ごとに出力してくれる。
問2 このファイルからサーバー名とアクセス先だけ表示しろ
ruby -nle 'p $_.split(",").values_at(0, 3).join(",")' hoge.log 

結果

"server1,/video.php"
"server2,/profile.php"
"server3,/login.php"
"server1,/profile.php"
"server2,/profile.php"
"server2,/profile.php"
"server3,/login.php"
"server4,/video.php"
"sever1,/video.php"

行末に\nが入っちゃうので-lをつけてそれを削除する。
あとは行ごとにカンマ区切りで配列にしてvalues_atでサーバ名とアクセス先だけ抜き出す。

問3 このファイルからserver4の行だけ表示しろ
ruby -ane '$F.each{|l|p l if l.index("server4")}' hoge.log

結果

"server4,1343363225,12,/video.php"

  • aで行を配列化してeachでぶん回す。
問4 このファイルの行数を表示しろ
ruby -e 'p open("hoge.log").readlines.length' 

結果

9

  • nとか-pとかだとぶん回しちゃうので、起動オプションに頼らずopenした。
問5 このファイルをサーバー名、ユーザーIDの昇順で5行だけ表示しろ
ruby -e 'open("hoge.log").readlines.sort{|a, b| a, b = a.split(","), b.split(","); (a[0] <=> b[0]) == 0 ? a[2].to_i <=> b[2].to_i : a[0] <=> b[0]}.slice(0,5).each{|l| p l.chomp!}' 

大分長くなってしまった...
ソートしたあとに出力しなければいけなそうなので、問4と同じくopenするようにした。
sortの中では比較する行のサーバ名が同じならばユーザーIDでソートし、サーバ名が別ならばサーバ名でソートする(のを三項演算子でやってる)。

問6 このファイルには重複行がある。重複行はまとめて数え行数を表示しろ
ruby -e 'p open("hoge.log").readlines.uniq.length' 

結果

8

問7 このログのUU(ユニークユーザー)数を表示しろ
ruby -e 'p open("hoge.log").readlines.collect{|l| l.split(",")[2]}.uniq.length'

結果

6

各行をカンマ区切りで配列にするとユーザーIDがとれるので、collectで全行をぶん回してそれを一気に取得する

問8 このログのアクセス先ごとにアクセス数を数え上位1つを表示しろ
ruby -e 'p open("hoge.log").readlines.collect{|l| l.split(",")[3].chomp}.inject(Hash.new(0)){|r, i| r.key?(i) ? r[i] += 1 : r[i] = 1; r}.max{|a, b| a[1] <=> b[1]}[0]'

結果

"/profile.php"

これまた長くなってしまった。
問7と同じくまずはcollectでアクセス先を集める。でそれをinjectを使ってアクセス先をキーに、値をアクセス数としてハッシュに集めて、そのハッシュを昇順にソートしてる。

問9 このログのserverという文字列をxxxという文字列に変え、サーバー毎のアクセス数を表示しろ
ruby -e 'open("hoge.log").readlines.collect{|l| l.split(",")[0].sub("server", "xxx")}.inject(Hash.new(0)){|r, i| r.key?(i) ? r[i] += 1 : r[i] = 1; r}.each{|k, v| p "#{k} #{v}"}'

結果

"xxx1 3"
"xxx2 3"
"xxx3 2"
"xxx4 1"

問8とほとんど変わらない。サーバーを抜き出してそれぞれのアクセス数をinjectで集める。

問10 このログのユーザーIDが10以上の人のユニークなユーザーIDをユーザーIDでソートして表示しろ
ruby -e 'open("hoge.log").readlines.collect{|l| l.split(",")[2].to_i}.select{|n| n > 10}.uniq.sort.each{|n| p n}'

結果

12
20
30
35

ユーザーIDだけ抜き出して、さらにその中から10以上のものをselectで抜き出す。