この記事は5月16日の記事「Unixシェルで手軽にデータを集計する (前編)」の続きです。
前回に引き続いて「コマンドの実行履歴を集計する」を素材に、 Unix環境でデータを集計する方法を解説していきたいと思います。
前回のおさらい
前回はhistory
で出力した生データから、awk
で実行コマンドを抽出する所まで見ました。
前編の内容を思い出すために、次のコマンドを手元で打ち込んでみましょう:
$ history | awk '{print $2}'
ls
vim
ls
vim
mv
...
ご覧いただけるように、このコマンドでデータの余計な部分を省いて、 実行したコマンドの一覧だけを取り出すことができました。 この抽出されたデータから、コマンドごとの出現回数(lsがN回、vimがM回...)をカウントできれば、 当初の目的の統計を手に入れることができます。
問題は、この集計処理をどう実現するかです。
構成要素(2): sortとuniqによる集計
結論を先取りすれば、Unixの世界ではsort
とuniq
という二つのプログラムを組み合わせて、
カウント処理を実現します。まずは実際に次のコマンドを実行してみましょう:
$ history | awk '{print $2}' | sort | uniq -c
3 apt
8 bundle
13 cat
32 cd
...
データが集計されて、コマンドごとの実行回数が出力されるのがご確認いただけると思います。
ここで重要なポイントは、なぜsort
とuniq
という(一見すると)集計処理とは何の関係も無さそうなプログラムの組み合わせで処理を実現できるのか、です。
それぞれのコマンドを簡単に見てみましょう。まず、sort
の方は比較的単純で、
名前の通り入力データを(原則はアルファベット順で)並びかえるプログラムです。
$ history | awk '{print $2}' | sort
apt
apt
apt
bundle
bundle
...
並び替えが(文字単位ではなく)行単位で行われることにご注意ください。
もう一つのuniq
は、もともとはデータから重複を除去するためのプログラムです。
実際に、引数を何もつけずに実行すると、次のような出力が得られます。
$ history | awk '{print $2}' | sort | uniq
apt
bundle
cat
cd
...
最初の実行例で見た通り、このプログラムは -c / --count
オプションを受けとり、
このオプションを指定すると、単に重複を除去するだけではなく、各行の出現回数も一緒に出力してくれます。
集計処理をsort | uniq -c
というフィルタで実現できる理由はここにあります。
このuniq
コマンドの重要なポイントは、このプログラムにはソート済みのデータを流し込む必要があるということです。
前にsort
を付けているのはこのためで、実際、整列してないデータを流し込むと意味不明な結果が返却されます。
$ history | awk '{print $2}' | uniq -c
2 ls
1 vim
1 ls
2 vim
1 mv
...
なぜこうなるかと言うと、根本的な理由はuniq
が動作する仕組みにあります。
枝葉を除いて幹の部分だけを取り出すと、uniq
は非常に単純なアルゴリズムで実装されています。
-
入力データから次の一行を読みこむ。
-
行の内容が前の行と異なっていれば出力し、同じであればスキップする。
-
(入力データの終点に到達するまで1と2を繰り返す)
もし入力データが整列されているならば、このアルゴリズムで重複を除去することができるのは明らかでしょう。
もちろん、少し工夫すれば「整列済み」の仮定を外した、万能の重複除去プログラムを作ることもできるのですが、
uniq
プログラムの最初の作者(歴史をたどると"Unixの父"のケン・トンプソンのようです)はそうしませんでした。
なぜか?というと、これは筆者の想像になるのですが:
-
このアルゴリズムだと直前の行だけ記憶しておけばいいので、メモリ使用量が格段に少ない。巨大なデータにもスケールする。
-
Unixには他にも
comm
やjoin
といった整列済みのデータを扱うツールがあるので、ソートする部分だけ独立のプログラムに切り出すのが自然。
というところではないかと思います。実際、uniq
は非常に高速に動作し、
テラバイトレベルのデータも問題なく処理できます。これは根幹のアルゴリズムの簡潔さのなせる技です。
構成要素(3): 上位N件を取り出す
ここまでの作業で得られた集計結果をランキングとして出力しましょう。
ここで再び登場するのがsort
プログラムです。
今回は-n
オプションをつけて(アルファベット順ではなく)数字の大小で並び替え、
それに加えて-r
オプションで並び順を(昇順ではなく)降順にします。
単純に出力すると結果が多すぎて画面から溢れるので、上位10件だけ取り出します。
これにはhead
というコマンドを使います。
$ history | awk '{print $2}' | sort | uniq -c | sort -rn | head
345 vim
159 python
101 ls
52 cd
48 grep
39 ll
32 cat
31 history
26 git
23 rm
これでコマンドの実行回数ランキングを出すことができました。
まとめ
この前後編の記事では、Unixシェルでデータを集計する方法を紹介しました。 このテクニックを応用すれば、例えば、次のようなタスクも手軽にこなせるようになります:
-
ApacheのアクセスログからIPアドレスごとのアクセス数を取り出す
-
SSHのログから失敗したログインの時間別統計をとる
-
雑多なテキストデータから単語の出現頻度を数える
日常の様々な管理業務をより楽にできると思いますので、ぜひともご活用ください!
なお「もっと一般的なUnixの使い方を学びたい」という方は、弊社の結城が執筆している「シス管系女子」という連載がおすすめです。
現在、第3巻が発売されてますので、あわせてご参照ください。