株式会社クリアコード > ククログ

ククログ


Unixシェルで手軽にデータを集計する (後編)

この記事は5月16日の記事「Unixシェルで手軽にデータを集計する (前編)」の続きです。

前回に引き続いて「コマンドの実行履歴を集計する」を素材に、 Unix環境でデータを集計する方法を解説していきたいと思います。

前回のおさらい

前回はhistoryで出力した生データから、awkで実行コマンドを抽出する所まで見ました。 前編の内容を思い出すために、次のコマンドを手元で打ち込んでみましょう:

$ history | awk '{print $2}'
ls
vim
ls
vim
mv
...

ご覧いただけるように、このコマンドでデータの余計な部分を省いて、 実行したコマンドの一覧だけを取り出すことができました。 この抽出されたデータから、コマンドごとの出現回数(lsがN回、vimがM回...)をカウントできれば、 当初の目的の統計を手に入れることができます。

問題は、この集計処理をどう実現するかです。

構成要素(2): sortとuniqによる集計

結論を先取りすれば、Unixの世界ではsortuniqという二つのプログラムを組み合わせて、 カウント処理を実現します。まずは実際に次のコマンドを実行してみましょう:

$ history | awk '{print $2}' | sort | uniq -c
 3 apt
 8 bundle
13 cat
32 cd
...

データが集計されて、コマンドごとの実行回数が出力されるのがご確認いただけると思います。 ここで重要なポイントは、なぜsortuniqという(一見すると)集計処理とは何の関係も無さそうなプログラムの組み合わせで処理を実現できるのか、です。

それぞれのコマンドを簡単に見てみましょう。まず、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. 行の内容が前の行と異なっていれば出力し、同じであればスキップする。
  3. (入力データの終点に到達するまで1と2を繰り返す)

もし入力データが整列されているならば、このアルゴリズムで重複を除去することができるのは明らかでしょう。 もちろん、少し工夫すれば「整列済み」の仮定を外した、万能の重複除去プログラムを作ることもできるのですが、 uniqプログラムの最初の作者(歴史をたどると"Unixの父"のケン・トンプソンのようです)はそうしませんでした。 なぜか?というと、これは筆者の想像になるのですが:

  1. このアルゴリズムだと直前の行だけ記憶しておけばいいので、メモリ使用量が格段に少ない。巨大なデータにもスケールする。
  2. Unixには他にもcommjoinといった整列済みのデータを扱うツールがあるので、ソートする部分だけ独立のプログラムに切り出すのが自然。

というところではないかと思います。実際、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巻が発売されてますので、あわせてご参照ください。

タグ: Unix
2018-06-01

Unixシェルで手軽にデータを集計する (前編)

クリアコードの藤本です。

私たちの業務では、よく「データを集計する」という仕事が発生します。 例えば、最近の改修の効果を測定するために、過去ログからリクエストの成功率を測定したり、 お客様への業務報告のために、チケットシステムから直近の稼働状況を集計したりします。

もちろん、同じようなタスクが定型的に発生するようであれば、専用のスクリプトを作り込むのですが、 この種類のタスクは単発限りで結果が取れればよい、ということが少なくありません。

私(筆者)は、このようなタスクがある時は、Unixの標準ツール(とPython)を使って集計しています。 今回の記事では、具体的にどのように集計を行っているかを紹介したいと思います。

例: シェルでコマンドの実行回数を集計する

実際のタスクに適度に近い例として、「コマンドの実行履歴を集計する」というタスクを例に 集計のやり方を説明したいと思います。

bashであれば、集計対象のデータ(=コマンドの実行履歴)はhistoryで簡単に取得できます。

$ history
 ...
 1952  cd
 1953  ls
 1954  git add -p
 1955  git log
 1956  git commit --amend

ここから「自分が最もよく実行しているコマンド」を集計してみましょう。 大雑把には、左から二番目の列を取り出して、コマンドごとに件数を数えればよさそうです。
(注: ここではパイプやインラインのコマンドは無視しています)

この処理の実現の仕方にはいくつかあるのですが、筆者であれば次のように打ち込んで集計します:

$ 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

実行したコマンドの中身については後ほど解説しますので、まずは出力の方をご覧ください。 左側がコマンドの出現回数で、右側が対応するコマンドの名前です(出現回数が多い順に並んでます)。 この端末では、vimが最も多く実行されていることがご覧いただけると思います。

皆さんも、試しに上のコマンドを自分の環境で実行してみると、 自分が普段どのコマンドを使ってるのかが分かって楽しいと思います。

全体構成: パイプによるプログラムの連結

まずはコマンドの全体の構成から説明します:

$ history | awk '{print $2}' | sort | uniq -c | sort -rn | head

すぐに見てとれるように、このコマンドは6つのプログラムを | 記号で連結したものです。 この縦棒記号でプログラムをつなげる仕組みをパイプ(あるいはパイプライン)と呼びます。 パイプで連結すると、前のプログラムの結果を、後のプログラムに流すことができます。

より単純なechotrというコマンドを例に仕組み説明します。 echoは入力として与えられた内容をそのまま出力するプログラムです:

$ echo aiueo 
aiueo

これをtrという「文字列を置換するコマンド」に流してみましょう:

$ echo aiueo | tr a x  # "a"を"x"に置換する
xiueo

2つのコマンドがパイプを通じて接続されているのが見て取れると思います。 もちろん、この後にさらにコマンドを連ねていくことも可能です:

$ echo aiueo | tr a x | tr o x  # さらに"o"を"x"に置換する
xiuex

同じ要領でコマンドをいくらでもつなげていくことができます。 このように何段も連結するのは現実の場面でもそう珍しいことではありません (実際、先ほどの集計コマンドでは6つのプログラムを接続しています)。

普段何気なく使うことが多いのですが、パイプはUnixの偉大な発明の一つです。

構成要素(1): awkによるテキスト処理

集計コマンドの全体像がつかめたところで、パイプラインの最初に登場するawkに注目しましょう。

このプログラムは平たく言えば「テキストを行単位で処理するツール」です。 雑多なテキストから必要なデータを抽出するのに活躍してくれるプログラムで、 今回のケースでは「コマンド履歴の2列目を取り出す」という処理に利用してます:

$ history | awk '{print $2}'
...
cd
ls
git
git
git

awkの引数の中身をあれこれいじってみると、感覚がつかめると思います。 例えば$2$1に変えると、一列目が取り出せます。

$ history | awk '{print $1}'
...
1952
1953
1954
1955
1956

紙幅の関係で省略しますが、実はawkの正体は非常に高度な機能を備えた本物のプログラミング言語です。 処理の中でループ構文や関数を使うこともでき、他の一般的なスクリプト言語に匹敵する表現力を持っています。 したがって、指示の与え方を工夫すれば、大抵の処理はawkだけで実現することができます。

実際、本記事のコマンド集計の処理も、やろうと思えばawkだけで実現することができます:

$ history | awk '{A[$2]+=1} END{PROCINFO["sorted_in"]="@val_num_desc";for(i in A) print A[i],i}'
345 vim
159 python
101 ls
52 cd
48 grep
39 ll
32 cat
...

こう話を進めると、最初からawkだけで全ての処理を済ませれば良いように思えてくるのですが、 実際にはawkだけで処理を完結させるのは稀で、最初に示したコマンド集計の例のように、 より単純なコマンドとの組み合わせで処理を実現することの方が圧倒的に多いです。

この理由は、おそらく上のコマンド集計の実装例を見れば、何となく察しがつくのではないか と思います。このawkプログラムはわずか80字に満たない短い実装ですが、 既に一目で何が行われているのかを把握できる水準を(書いた本人の筆者にとってすら)越えてきています。

もちろん、インラインで収めようとせずに、テキストエディタで整形しながら書けば、 コードの見通しは俄然よくなるのですが、そもそもコマンドラインで集計しようとしている発端が 「テキストエディタで本式のプログラムを書くのが面倒」という点にあったこと考えると、 本末転倒の感が強くなってきます。

本記事で、あくまでawkを「2列目を取り出す」という単純な処理にしか用いず、 実際の集計処理を後続のコマンドに委ねているのは、このような理由によるものです。

(後編に続きます)

つづき: 2018-06-01
タグ: Unix
2018-05-16

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|
タグ: