Unixシェルで手軽にデータを集計する (前編) - 2018-05-16 - ククログ

ククログ

株式会社クリアコード > ククログ > Unixシェルで手軽にデータを集計する (前編)

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列目を取り出す」という単純な処理にしか用いず、 実際の集計処理を後続のコマンドに委ねているのは、このような理由によるものです。

(後編に続きます)