シェル上で横棒グラフを描画する処理をawkで書いてみる

# シェル上でデータを可視化したいだけなのですが、適当なツールが見当たらんかった

たとえば、du コマンドでサブディレクトリのディスク使用量を取得する、なんて作業はよくありがちですけど、生の数字を見比べるよりはグラフのほうがわかりやすい。

だけど、ちょいと探した範囲では適当なものが見当たらん様子。[spark](https://github.com/holman/spark) はスパークラインの描画だから自分のやりたいこととは違う。[termeter](http://qiita.com/atsaki/items/e7d2e53bac8ba6fdbce0)は興味深いけど、これもちょいと方向性が違うかも。また、その場で可視化したいだけの使い捨てのデータのために gnuplot などを使う気にもならない。

そこでサクッと awk で棒グラフを書く処理を作ってみた...のですが、色々と改良するうちにコードは少々大きくなっています。

# 作成したスクリプトでできること

## "key vaule" または "value key" のデータに横棒グラフをつけます

du で /usr/* の使用状況を取得すると、普通はこんな出力になる。

```bash
kinoue@ubuntu1604LTS:/usr$ du -s /usr/*
108200  /usr/bin
4       /usr/games
19528   /usr/include
454252  /usr/lib
100     /usr/local
10572   /usr/sbin
256432  /usr/share
245768  /usr/src
```

でも、こんなふうに棒グラフが描画できたら、ディレクトリ毎の使用量が多い、少ないという状況が感覚的に分かる。

```bash
kinoue@ubuntu1604LTS:/usr$ du -s /usr/* | bargraph.awk
108200 10% /usr/bin     ##########
     4  0% /usr/games   #
 19528  2% /usr/include ##
454252 41% /usr/lib     ########################################
   100  0% /usr/local   #
 10572  1% /usr/sbin    #
256432 23% /usr/share   #######################
245768 22% /usr/src     ######################
```

もしかすると、キーとバリューの順番が逆のデータがあるかもしれません。

```
kinoue@ubuntu1604LTS:/usr$ du -s /usr/* | awk '{ print $2, $1 }'
/usr/bin 108200
/usr/games 4
/usr/include 19528
/usr/lib 454252
/usr/local 100
/usr/sbin 10572
/usr/share 256432
/usr/src 245768
```

そういうデータでも一応扱えるようにしてあります。ただし判定ロジック的には、キーとバリューの区別がつかない行は判定を放棄して行全体をキーとして扱うようにしていますので、キー名が数値という集合は正しく扱えません。

```
kinoue@ubuntu1604LTS:/usr$ du -s /usr/* | awk '{ print $2, $1 }' | bargraph.awk

108200 10% /usr/bin     ##########
     4  0% /usr/games   #
 19528  2% /usr/include ##
454252 41% /usr/lib     ########################################
   100  0% /usr/local   #
 10572  1% /usr/sbin    #
256432 23% /usr/share   #######################
245768 22% /usr/src     ######################
```

## 入力値に同じ key が複数回出現する場合は合算集計します

もしも入力値がこんな風に重複する key を含んでいるとします。ここでは /usr/bin が2行重複しています。

```
108200  /usr/bin
108200  /usr/bin
4       /usr/games
19528   /usr/include
454252  /usr/lib
100     /usr/local
10572   /usr/sbin
256432  /usr/share
245768  /usr/src
```

すると、合算して次のように出力します。このような処理は未集計のデータを扱う場合に便利ですね。

```
216400 18% /usr/bin     ####################
     4  0% /usr/games   #
 19528  2% /usr/include ##
454252 38% /usr/lib     ########################################
   100  0% /usr/local   #
 10572  1% /usr/sbin    #
256432 21% /usr/share   #######################
245768 20% /usr/src     ######################
```

## 入力値が値だけの集合の場合は、各行のデータを key と見なした上で値 = 1 として合算集計しつつ、グラフを描画します

次の処理は、1〜6の数値をランダムに60万件出力しますが、出力されるデータは key-value で言うと key だけの集合ですね。

```
kinoue@ubuntu1604LTS:~$ seq 600000 | gawk 'BEGIN{ srand('"$RANDOM"')  } { printf "%d\n", 1 + rand() * 6 }'  | head
2
3
3
4
3
1
3
4
5
5
```

こういう場合は 各行の情報を key と見なして key:1 というデータとして集計します。ですので、実行結果は以下のように集計合算された上でグラフが生成されます。

```
kinoue@ubuntu1604LTS:~$ seq 600000 | gawk 'BEGIN{ srand('"$RANDOM"')  } { printf "%d\n", 1 + rand() * 6 }' | bargraph.awk | sort -k 3
600000
 99715 17% 1 ########################################
 99804 17% 2 ########################################
 99749 17% 3 ########################################
100518 17% 4 ########################################
100396 17% 5 ########################################
 99818 17% 6 ########################################
```

# 実装

```awk:bargraph.awk
#!/usr/bin/awk -f
BEGIN {
    column_value=0
    column_name=1
    graph_max_scale=40

    num_keys = 1;
}

NR % 100000 == 0 {
    printf "%d\r", NR > "/dev/stderr"
}

# データのフォーマット判定
{
    type="key-value"
    if ( NF == 1 ) {
        # フィールドが１つの場合は、"key":1 とみなす
        type="key"
    } else {
        param1=$1
        param2=$NF
        if ( 0 + param1 != param1 && 0 + param2 != param2 ) {
            # param1 と param2 の両方に数値が含まれない場合はスキップ
            next
        } else if ( 0 + param1 == param1 && 0 + param2 != param2 ) {
            # param1 だけがが数値の場合
            type="value-key"
        } else if ( 0 + param2 == param2 && 0 + param1 != param1 ) {
            # $2 だけがが数値の場合
            type="key-value"
        } else {
            # 判定不能の場合
            type="key"
        }
    }

    if ( type == "key" ) {
        key=$0
        value=1
    } else if ( type == "value-key" ) {
        value=$1
        key=$2
        if ( NF > 2 ) {
            for ( i = 3 ; i <= NF ; i++ ) {
                key=sprintf("%s %s", key,$i)
            }
        }
    }
    else {
        value=$NF
        key=$1
        if ( NF > 2 ) {
            for ( i = 2 ; i <  NF ; i++ ) {
                key=sprintf("%s %s", key,$i)
            }
        }
    }
}

# データの集計処理
{
    if ( key_reverseindex[key] == "" ) {
        key_reverseindex[key] = num_keys
        key_index[key_reverseindex[key]] = key
        num_keys++
    }

    valuemacher = 0 + value
    if ( valuemacher == value ) {
        valuearray[key_reverseindex[key]] = valuearray[key_reverseindex[key]] + value;

        if ( rawdata_max < valuearray[key_reverseindex[key]] ) rawdata_max = valuearray[key_reverseindex[key]]

        if ( rawdata_label_maxlength < length(key) ) rawdata_label_maxlength = length(key)

        rawdata_total = rawdata_total + value
    }
}

END {
    printf "\n" > "/dev/stderr"
    printf_formatstring = "%" int( 1 + log(rawdata_max)/log(10) ) "d %2.0f%% %s "

    for ( key = 1 ; key <= num_keys ; key ++ )  {
        if ( length( key_index[key] ) > 0 ) {
            if ( rawdata_total != 0 )
                rawdata_usage=100 * valuearray[key] / rawdata_total
            else
                rawdata_usage=0

            printf printf_formatstring, valuearray[key], rawdata_usage, key_index[key]

            for ( labelpadding = 0 ; labelpadding < rawdata_label_maxlength - length(key_index[key]) ; labelpadding++ ) {
                printf " "
            }

            if ( rawdata_max != 0 ) {
                graphchars = graph_max_scale * valuearray[key] / rawdata_max
                for ( graphlength = 0 ; graphlength < graphchars ; graphlength++ ) {
                    printf "#"
                }
            }
            printf "\n"
        }
    }
}
```

# 使いみち

例えば Web サーバのアクセスログからURLやステータスコードだけを抜き出して食わせれば、URLやステータスコード別の出現回数でグラフ化できます。

ユーザのログイン履歴のログからユーザIDだけを抜き出して食わせれば、ログイン回数での可視化ができます。

出現回数の集計だけなら、"sort | uniq -c" とやればよいのですけど、元データの母集団によっては sort すること自体がリソースを消費することになりかねないのですが。このスクリプトでは "sort | uniq -c" していないデータでも直ちに集計でき、なおかつ実行負荷も低いので、未整理のデータをオンデマンドで可視化したいときにいろいろ捗ると思います。