mattintosh note

Hello Raspberry Pi!

Shell と PING でネットワークの速度を監視する

とある場所のネットワーク速度が著しく低下することがあったので監視目的で PING を使ってみることにした(iperf 使いたいけど相手が居ない)。とりあえず PING の出力をパースして SQLite に格納。GNUPLOT で可視化するけど、そのうち Zabbix とかに移して動的監視する予定。

今回は「お兄ちゃん、GREPAWK の使用を禁止します!」縛りでやります。

PING からの文字列の取り出し

ping-c {count} で回数を指定すると最後に統計を出してくれるのでそこから min、avg、max、mdev を取得します。

$ ping -c1 localhost
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.092 ms

--- localhost ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.092/0.092/0.092/0.000 ms

文字列の切り出しは grepsedawk なんかを使うのが定番ですが、冒頭の宣言に従い、ここではシェルの機能のみでやっていきます。

まず、IFS を改行のみに変更して ping の出力を行ごとに位置パラメータに格納します。

IFS=$'\n'
set -- $(ping -c1 localhost)

set -x(xtrace)を使ってシェルの動きを見てみます。

+ IFS='
'
++ ping -c1 localhost
+ set -- 'PING localhost (127.0.0.1) 56(84) bytes of data.' '64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.090 ms' '--- localhost ping statistics ---' '1 packets transmitted, 1 received, 0% packet loss, time 0ms' 'rtt min/avg/max/mdev = 0.090/0.090/0.090/0.000 ms'

eval で遅延展開を使って位置パラメータに入っている文字列を出力してみます。

for ((i = 1; i <= ${#}; i++))
do
    eval echo LINE ${i}: \${${i}}
done

行ごとに位置パラメータに格納されているのがわかります。この中で必要になるのは最後の行だけです。

LINE 1: PING localhost (127.0.0.1) 56(84) bytes of data.
LINE 2: 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.090 ms
LINE 3: --- localhost ping statistics ---
LINE 4: 1 packets transmitted, 1 received, 0% packet loss, time 0ms
LINE 5: rtt min/avg/max/mdev = 0.090/0.090/0.090/0.000 ms

なお、遅延展開には以下のような方法があります。1番目と2番目は書式が違うだけで同じ動きをしますが、(自分の備忘録によると)3番目は ZSH では使えないようなので注意が必要です。位置パラメータが二桁を超えると {} が必要となりますので常につけておく方が無難だと思います。

eval echo \${${#}}
eval echo '$'{${#}}
echo ${!#}

今度は最後の行だけを取り出して再度位置パラメータにセットします。現在の IFS は改行のみになっているので、予め IFS を変更して単語の分割も一緒にやってしまいます。最後の行で使われている区切り文字は space/= なのでこの3つを IFS にセットします。

IFS=' /='

そして、先程と同じように eval を使って遅延展開で最終行を展開します。

eval set -- \${${#}}

for で位置パラメータを見てみます。

for ((i = 1; i <= ${#}; i++))
do
    eval echo ITEM ${i}: \${${i}}
done

これで最終行の分割ができました。

ITEM 1: rtt
ITEM 2: min
ITEM 3: avg
ITEM 4: max
ITEM 5: mdev
ITEM 6: 0.090
ITEM 7: 0.090
ITEM 8: 0.090
ITEM 9: 0.000
ITEM 10: ms

通常は ITEM 6〜ITEM 9までを取り出して変数に代入するのでしょうが、ITEM 2〜ITEM 5までの文字列が変数名に使えるのでそのまま使います。

${2}=${6} # -> min=0.090
${3}=${7} # -> avg=0.090
${4}=${8} # -> max=0.090
${5}=${9} # -> mdev=0.000

これを eval で実行します。

eval ${2}=${6} ${3}=${7} ${4}=${8} ${5}=${9}
echo MIN : ${min}
echo AVG : ${avg}
echo MAX : ${max}
echo MDEV: ${mdev}
MIN : 0.090
AVG : 0.090
MAX : 0.090
MDEV: 0.000

for 文を消して set -vx で見てみると位置パラメータが eval によってどのように展開されているかわかると思います。

IFS=$'\n'
+ IFS='
'
set -- $(ping -c1 localhost)
++ ping -c1 localhost
+ set -- 'PING localhost (127.0.0.1) 56(84) bytes of data.' '64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.046 ms' '--- localhost ping statistics ---' '1 packets transmitted, 1 received, 0% packet loss, time 0ms' 'rtt min/avg/max/mdev = 0.046/0.046/0.046/0.000 ms'

IFS=' /='
+ IFS=' /='
eval set -- \${${#}}
+ eval set -- '${5}'
set -- ${5}
++ set -- rtt min avg max mdev 0.046 0.046 0.046 0.000 ms

eval ${2}=${6} ${3}=${7} ${4}=${8} ${5}=${9}
+ eval min=0.046 avg=0.046 max=0.046 mdev=0.000
min=0.046 avg=0.046 max=0.046 mdev=0.000
++ min=0.046
++ avg=0.046
++ max=0.046
++ mdev=0.000

echo MIN : ${min}
+ echo MIN : 0.046
MIN : 0.046
echo AVG : ${avg}
+ echo AVG : 0.046
AVG : 0.046
echo MAX : ${max}
+ echo MAX : 0.046
MAX : 0.046
echo MDEV: ${mdev}
+ echo MDEV: 0.000
MDEV: 0.000

とりあえずまとめると以下の数行で ping コマンドの出力から変数の代入までができます。

save_IFS=${IFS}
IFS=$'\n'
set -- $(ping -c1 localhost)
IFS=' /='
eval set -- \${${#}}
eval ${2}=${6} ${3}=${7} ${4}=${8} ${5}=${9}
IFS=${save_IFS}

IFS をバックアップしたりするのが面倒な場合はサブシェルで実行してもいいと思います。

eval $(
    IFS=$'\n'
    set -- $(ping -c1 localhost)
    IFS=' /='
    eval set -- \${${#}}
    echo ${2}=${6} ${3}=${7} ${4}=${8} ${5}=${9}
)

なお、IFS は以下のコマンドで初期状態になります。

IFS=$' \t\n'

PING タイムスタンプの取得

ping-D でタイムスタンプが拾えます。これは2行目から拾えばよさそうです。

$ ping -c1 -D localhost
PING localhost (127.0.0.1) 56(84) bytes of data.
[1511621731.988689] 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.063 ms

--- localhost ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.063/0.063/0.063/0.000 ms

文字列の取り出しの前にタイムスタンプ部分の説明をしておきます。-D オプションで表示される書式は UNIX 時間なので、読みやすくするためには変換が必要になります。これは date コマンドを使います。

$ LC_TIME=C date -d@1511621731.988689
Sat Nov 25 23:55:31 JST 2017

SQLite の日付型の書式は yyyy-mm-dd HH:MM:SS のようになっているので以下のように変換をします。ただ、SQLiteUNIX時間を整数型か浮動小数点数型で追加してもあとから変換はできるので、ここで無理して日付型に変換する必要はないかもしれません。

$ LC_TIME=C date -d@1511621731.988689 +'%F %T'
2017-11-25 23:55:31

さて、ping コマンドの出力からタイムスタンプ部分を抜き出す必要がありますが、少々余計なものがいくつかあります。

PING localhost (127.0.0.1) 56(84) bytes of data.
[1511621731.988689] 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.063 ms
(以下略)

まずは2行目を取り出します。ping-D オプションを忘れずに。

IFS=$'\n'
set -- $(ping -c1 -D localhost)
echo ${2}

2行目が取り出せました。ここからタイムスタンプ部分だけを取り出したいのですが、[] が邪魔ですね。

[1511622657.836489] 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.042 ms

[] を区切り文字として IFS にセットします。位置パラメータがどうなってるかわかりやすくするために途中に set -x を入れます。

IFS=' []'
set -x
set -- ${2}
echo ${2}

先頭の [ の左には何もないので空になっています。タイムスタンプの右側にあった ] は区切り文字として扱われたので無くなりました。

+ set -- '' 1511622852.338428 64 bytes from localhost '(127.0.0.1):'
+ echo 1511622852.338428

date コマンドに渡せる書式になったので変換します。

date -d@${2} +$'%F %T'

これで yyyy-mm-dd HH:MM:SS 形式で取り出せました。

2017-11-26 00:15:33

ここまでを続けて書くと以下のようになります。

IFS=$'\n'
set -- $(ping -c1 -D localhost)
IFS=' []'
set -- ${2}           # ここの ${2} は「2""目」
date -d@${2} +'%F %T' # ここの ${2} は「2"列"目」

PING コマンドの2行目からタイムスタンプを取得して最終行から結果を取得する

ここまでで最終行の取り出しとタイムスタンプの取り出しができましたが、実は問題があります。最終行の取り出し、もタイムスタンプの取り出しも、2回目の set で最初に取得した ping コマンドの結果を上書きしてしまっているためどちらかしか取り出せません。

最初に取得した ping コマンドの結果を使い回す方法はいくつかあると思いますが、

  • ping コマンドの結果を変数に格納しておく
  • 関数を作って位置パラメータを渡す
  • サブシェルで処理する

の3つくらいでしょうか。(シェルに詳しい人なら他にも思いつきそうですが)

ここまで位置パラメータだけでやってきて今更 ping コマンドの結果を変数に入れるのはスマートな方法ではないような気がするので関数かサブシェルにします。

まずは関数版から。カレントシェルの IFS が書き換わるのが嫌なのですべてサブシェル内で実行しています。カレントシェルでやる場合は適当な変数に最初の IFS を保存しておくとよいです。

# as = Assignment Statements
time_as(){
    (
        IFS=' []'
        set -- ${2}
        date -d@${2} +'time="%F %T"'
    )
}
stat_as(){
    (
        IFS=' /='
        eval set -- \${${#}}
        echo ${2}=${6} ${3}=${7} ${4}=${8} ${5}=${9}
    )
}

## IF CURRENT SHELL
#save_IFS=${IFS} IFS=$'\n'
#set -- $(ping -c1 -D localhost)
#IFS=${save_IFS}

eval $(
    IFS=$'\n'
    set -- $(ping -c1 -D localhost)
    time_as "${@}"
    stat_as "${@}"
)

echo ${time} ${min} ${avg} ${max} ${mdev}

それぞれの関数に "${@}" で一番最初に取得した位置パラメータ群を渡すと代入分を返してくれるので eval で実行させて代入まで済ませます。

set -x
return_time_as(){
    (
        IFS=' []'
        set -- ${2}
        date -d@${2} +'time="%F %T"'
    )
}
return_stat_as(){
    (
        IFS=' /='
        eval set -- \${${#}}
        echo ${2}=${6} ${3}=${7} ${4}=${8} ${5}=${9}
    )
}
eval $(
    IFS=$'\n'
    set -- $(ping -c1 -D localhost)
    return_time_as "${@}"
    return_stat_as "${@}"
)
++ IFS='
'
+++ ping -c1 -D localhost
++ set -- 'PING localhost (127.0.0.1) 56(84) bytes of data.' '[1511627944.923406] 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.043 ms' '--- localhost ping statistics ---' '1 packets transmitted, 1 received, 0% packet loss, time 0ms' 'rtt min/avg/max/mdev = 0.043/0.043/0.043/0.000 ms'
++ return_time_as 'PING localhost (127.0.0.1) 56(84) bytes of data.' '[1511627944.923406] 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.043 ms' '--- localhost ping statistics ---' '1 packets transmitted, 1 received, 0% packet loss, time 0ms' 'rtt min/avg/max/mdev = 0.043/0.043/0.043/0.000 ms'
++ IFS=' []'
++ set -- '' 1511627944.923406 64 bytes from localhost '(127.0.0.1):' icmp_seq=1 ttl=64 time=0.043 ms
++ date -d@1511627944.923406 '+time="%F %T"'
++ return_stat_as 'PING localhost (127.0.0.1) 56(84) bytes of data.' '[1511627944.923406] 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.043 ms' '--- localhost ping statistics ---' '1 packets transmitted, 1 received, 0% packet loss, time 0ms' 'rtt min/avg/max/mdev = 0.043/0.043/0.043/0.000 ms'
++ IFS=' /='
++ eval set -- '${5}'
+++ set -- rtt min avg max mdev 0.043 0.043 0.043 0.000 ms
++ echo min=0.043 avg=0.043 max=0.043 mdev=0.000
+ eval 'time="2017-11-26' '01:39:04"' min=0.043 avg=0.043 max=0.043 mdev=0.000
time="2017-11-26 01:39:04" min=0.043 avg=0.043 max=0.043 mdev=0.000
++ time='2017-11-26 01:39:04'
++ min=0.043
++ avg=0.043
++ max=0.043
++ mdev=0.000

echo ${time} ${min} ${avg} ${max} ${mdev}
+ echo 2017-11-26 01:39:04 0.043 0.043 0.043 0.000
2017-11-26 01:39:04 0.043 0.043 0.043 0.000

長いですね。

続いてサブシェル版。

## IF CURRENT SHELL
#save_IFS=${IFS} IFS=$'\n'
#set -- $(ping -c1 -D localhost)
#IFS=${save_IFS}

eval $(
    IFS=$'\n'
    set -- $(ping -c1 -D localhost)
    (
        IFS=' []'
        set -- ${2}
        date -d@${2} +'time="%F %T"'
    )
    (
        IFS=' /='
        eval set -- \${${#}}
        echo ${2}=${6} ${3}=${7} ${4}=${8} ${5}=${9}
    )
)

echo ${time} ${min} ${avg} ${max} ${mdev}

set -vx で実行してみます。

eval $(
    IFS=$'\n'
    set -- $(ping -c1 -D localhost)
    (
        IFS=' []'
        set -- ${2}
        date -d@${2} +'time="%F %T"'
    )
    (
        IFS=' /='
        eval set -- \${${#}}
        echo ${2}=${6} ${3}=${7} ${4}=${8} ${5}=${9}
    )
)
++ IFS='
'
+++ ping -c1 -D localhost
++ set -- 'PING localhost (127.0.0.1) 56(84) bytes of data.' '[1511627682.894487] 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.048 ms' '--- localhost ping statistics ---' '1 packets transmitted, 1 received, 0% packet loss, time 0ms' 'rtt min/avg/max/mdev = 0.048/0.048/0.048/0.000 ms'
++ IFS=' []'
++ set -- '' 1511627682.894487 64 bytes from localhost '(127.0.0.1):' icmp_seq=1 ttl=64 time=0.048 ms
++ date -d@1511627682.894487 '+time="%F %T"'
++ IFS=' /='
++ eval set -- '${5}'
+++ set -- rtt min avg max mdev 0.048 0.048 0.048 0.000 ms
++ echo min=0.048 avg=0.048 max=0.048 mdev=0.000
+ eval 'time="2017-11-26' '01:34:42"' min=0.048 avg=0.048 max=0.048 mdev=0.000
time="2017-11-26 01:34:42" min=0.048 avg=0.048 max=0.048 mdev=0.000
++ time='2017-11-26 01:34:42'
++ min=0.048
++ avg=0.048
++ max=0.048
++ mdev=0.000

echo ${time} ${min} ${avg} ${max} ${mdev}
+ echo 2017-11-26 01:34:42 0.048 0.048 0.048 0.000
2017-11-26 01:34:42 0.048 0.048 0.048 0.000

わざわざ関数作るまでもないのでサブシェルでよさそうです。

PING が失敗したときの例外処理

まずは PING の終了ステータスを確認します。テストは下記の4種類としました。

  1. デフォルトゲートウェイ[0]
  2. 宛先が存在しない[1]
  3. 宛先がネットワークアドレス[2]
  4. 宛先がブロードキャストアドレス[2] ※Linux では -b オプションが必要
  5. 宛先が範囲外[2]
$ for i in 1 254 0 255 256; do ping -c1 -W1 192.168.1.$i >/dev/null 2>&1; echo $?; done
0
1
2
2
2

サブシェル内の ping がエラーで終了してもカレントシェルでは set の終了コードで上書きされてしまいます。サブシェル内の ping が異常終了した場合のみ位置パラメータの最後に終了コードを入れるようにして、あとから参照するようにしてみます。

save_IFS=${IFS} IFS=$'\n'
set -- $(ping -c${ping_count} -D ${host} || echo $?)
case ${_} in
0)
    :
;;
*)
    exit 1
;;
esac
IFS=${save_IFS}

(あ、なんかめんどくさい…)

もう無難に PING の出力を変数に入れることにします…。

ping_result=$(ping -c${ping_count} -D ${host}) || exit $?
save_IFS=${IFS} IFS=$'\n'
set -- ${ping_result}
IFS=${save_IFS}

データベースの作成(SQLite

データを格納するデータベースを作成します。ここでは SQLite を使用しますが、特に貯めておく必要がないのであれば揮発性 KVS を使うのもアリだと思います。

データベースファイル名とテーブル名は下記の通りです。

  • SQLite ファイル(データベース)名:ping.sqlite
  • SQLite テーブル名:PING

テーブルの構成です。今回は複数ホストのデータを格納するため HOST 列を用意しています。TIMESTAMPHOST は同時に存在しないはずなので Primary key を組みます。

TIMESTAMP HOST MIN AVG MAX MDEV
2017-11-26 13:00:28 192.168.1.103 59.789 95.716 128.351 24.985
2017-11-26 13:00:30 192.168.1.1 1.367 3.428 8.866 3.145
2017-11-26 13:00:33 192.168.1.103 44.499 87.418 122.071 29.737
2017-11-26 13:00:35 192.168.1.1 1.403 1.49 1.537 0.059
2017-11-26 13:00:38 192.168.1.103 5.982 106.414 218.366 80.11
2017-11-26 13:00:40 192.168.1.1 1.438 1.521 1.671 0.098

SQLite3 でテーブルを作成する際、DEFAULT CURRENT_TIMESTAMP で現在時刻を自動的に挿入するようにもできるのでテーブルにレコードを追加したときの時間がよいという場合は ping コマンドでタイムスタンプを取得する必要はありません(ただし SQLiteUTC なので日本時間に合わせる場合は datetime() 関数などを使う必要があります)。値を格納する列はミリ秒なので REAL にしておきます。

データベースファイルを作成します。

$ sqlite3 ping.sqlite

テーブルを作成します。SQLite の Primary key は null が許可されているので NOT NULL にしておきます。

create table
PING
(
    TIMESTAMP   DATE NOT NULL
,   HOST        TEXT NOT NULL
,   MIN         REAL
,   AVG         REAL
,   MAX         REAL
,   MDEV        REAL
,   primary key (
        TIMESTAMP
    ,   HOST
    )
);

下記はデータを挿入する場合の例です。

insert into
PING
values (
    '2017-11-25 18:18:00'
,   '192.168.1.1'
,   0.2
,   0.2
,   0.2
,   0.2
)

DEFAULT CURRENT_TIMESTAMP を使う場合は TIMESTAMP 列に何も入れないため、列を指定してデータを挿入します。

create table
PING
(
    TIMESTAMP   DATE PRIMARY KEY DEFAULT CURRENT_TIMESTAMP
,   HOST        TEXT
,   MIN         REAL
,   AVG         REAL
,   MAX         REAL
,   MDEV        REAL
);
insert into PING (
    HOST
,   MIN
,   AVG
,   MAX
,   MDEV
) values (
    '192.168.1.1'
,   0.2
,   0.2
,   0.2
,   0.2
);

これでデータベースの作成は完了です。

PING 実行から結果をデータベースに追加するまでをスクリプト化する

一通り準備ができたのでスクリプト化していきます。

CREATE TABLEINSERT 文は予めシェルの変数に入れておきます。INSERT 文は値をあとから eval で更新するようにしています。(SQLiteOracle の引数みたいなのあったかどうか覚えてません)

#!/bin/bash
 set -e
 set -u
#set -x

### ENVIRONMENT ###
host=${1:?}
ping_count=4
sqlite_db_file="ping.sqlite"
sqlite_table_name="PING"

### SQL Query ###
## CREATE TABLE
sqlite_sql_create_table="\
create table
${sqlite_table_name}
(
    TIMESTAMP   DATE NOT NULL
,   HOST        TEXT NOT NULL
,   MIN         REAL
,   AVG         REAL
,   MAX         REAL
,   MDEV        REAL
,   primary key (
        TIMESTAMP
    ,   HOST
    )
);
"

## INSERT (Shell's delayed expansion)
sqlite_sql_insert="\
insert into
${sqlite_table_name}
values (
   '\${timestamp}'
,  '\${host}'
,   \${min}
,   \${avg}
,   \${max}
,   \${mdev}
);
"


if ! test -e "${sqlite_db_file}"
then
    sqlite3 "${sqlite_db_file}" <<!
${sqlite_sql_create_table}
!
fi

ping_result=$(ping -c${ping_count} -D ${host}) || exit $?
save_IFS=${IFS} IFS=$'\n'
set -- ${ping_result}
IFS=${save_IFS}

eval $(
    (
        IFS=' []'
        set -- ${2}
        date -d@${2} +'timestamp="%F %T"'
    )
    (
        IFS=' /='
        eval set -- \${${#}}
        echo ${2}=${6} ${3}=${7} ${4}=${8} ${5}=${9}
    )
)

echo "\
${host} (${timestamp})
    MIN : ${min}
    AVG : ${avg}
    MAX : ${max}
    MDEV: ${mdev}
"

eval sqlite_sql_insert=\""${sqlite_sql_insert}"\"

sqlite3 "${sqlite_db_file}" <<!
${sqlite_sql_insert}
!

ルータ宛と Raspberry Pi Zero 宛で試してみます。

$ watch -pn 10 './ping.sh 192.168.1.1; ./ping.sh 192.168.1.103'

Every 10.0s: ./ping.sh 192.168.1.1; ./ping.sh 192.168.1.103

192.168.1.1 (2017-11-26 21:57:11)
    MIN : 1.493
    AVG : 1.607
    MAX : 1.904
    MDEV: 0.176

192.168.1.103 (2017-11-26 21:57:14)
    MIN : 47.586
    AVG : 114.384
    MAX : 207.937
    MDEV: 61.446

データの可視化

GNUPLOT を使います。が、なんか色々忘れたのですごく簡素なグラフです。(一時期は毎日やってたのにすっかり忘れてしまいました)

$ sqlite3 -separator , ping.sqlite 'select * from PING where HOST = "192.168.1.103" order by TIMESTAMP;" > data.csv
$ gnuplot graph.gnu

途中、データが無い部分は PING を止めていたところになりますが、死活監視としても使えそうです。

f:id:mattintosh4:20171127001613p:plain

SQLite-html オプションで HTML 出力もしてくれるので表はすぐに作成できます。

$ sqlite3 -html -header ping.sqlite 'select * from (select * from PING where HOST = "192.168.1.103" order by TIMESTAMP desc LIMIT 10) order by TIMESTAMP;'
TIMESTAMP HOST MIN AVG MAX MDEV
2017-11-27 00:21:36 192.168.1.103 5.973 123.851 230.968 97.036
2017-11-27 00:21:46 192.168.1.103 7.398 75.579 211.868 80.311
2017-11-27 00:21:56 192.168.1.103 6.885 41.134 142.876 58.741
2017-11-27 00:22:06 192.168.1.103 79.394 112.944 145.748 24.741
2017-11-27 00:22:16 192.168.1.103 3.778 78.472 137.308 51.999
2017-11-27 00:22:26 192.168.1.103 5.991 158.897 358.325 134.201
2017-11-27 00:22:36 192.168.1.103 33.446 123.946 215.256 68.784
2017-11-27 00:22:46 192.168.1.103 5.815 69.027 137.482 59.065
2017-11-27 00:22:56 192.168.1.103 33.585 99.668 212.551 67.971
2017-11-27 00:23:06 192.168.1.103 35.1 126.315 217.04 68.714

というわけで今回はここまで。

前に書いた GNUPLOTスクリプト整理しないと…。