glances で windows-curses を入れたついでというか。まあ相当残念なネタであることは覚悟しといたらいいよ。
「コンソールにまつわるもの」としてもう一つ ANSI シーケンスそのもの、と colorama のネタも持っているんだけど、今回はcurses のほうだけにしとく。
一連の COVID19 ネタは「良い見える化とそれの共有」の話で、「ほらね今やこういうのって簡単なのさぁ」みたいなことで pygal にも触れたわけなんだけれど、その、「かつてはそんなに簡単なことではなかった」の一例だと思うのよね、「テキストオンリーベースのターミナルだけを使ってグラフを表現する」というタスクは。
その「かつてはそんなに簡単なことではなかった」の中身は、もちろん「数学的なサポート」などの欠如なんかも多分にあるけれど、それ以前の問題として「可視化表現に使えるインフラの欠如による自由度のなさ」なわけね。決して「そのプアな表現をするためのプログラミングも困難」だったわけじゃない。たとえばこういうことな:
1 # -*- coding: utf-8 -*-
2 # pygal を紹介する際に「pygal は簡単なんだけど numpy 部分がこ難しくみえちゃうのが歯痒い」
3 # かったので、あえて numpy を使わずに書いてみた。
4 import io
5 import os
6 import sys
7 import datetime
8 import csv
9 import curses
10
11
12 def _main(stdscr, tkoprefdat):
13 _COLSNM = (
14 "testedPositive",
15 "peopleTested",
16 "hospitalized",
17 "serious",
18 "discharged",
19 "deaths",
20 "effectiveReproductionNumber",
21 )
22 reader = csv.reader(io.open(tkoprefdat, encoding="utf-8-sig"))
23 next(reader)
24 def _f(d):
25 if not d or d == "-":
26 return 0
27 return float(d)
28 trgprefecdat = {"testedPositive": [], "deaths": [], "serious": []}
29 tpn = "大阪府"
30 for line in reader:
31 pn = line[3]
32 data = list(map(_f, line[5:]))
33 if pn == tpn:
34 for k in trgprefecdat.keys():
35 trgprefecdat[k].append(data[_COLSNM.index(k)])
36 #
37 stdscr.clear()
38 curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK)
39 curses.init_pair(3, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
40 curses.init_pair(4, curses.COLOR_RED, curses.COLOR_BLACK)
41 height, width = stdscr.getmaxyx()
42 height -= 4
43 width -= 2
44 for k in trgprefecdat.keys():
45 trgprefecdat[k] = trgprefecdat[k][-width:]
46 maxv = max(trgprefecdat[k])
47 trgprefecdat[k] = [(height - int((v / maxv) * height)) for v in trgprefecdat[k]]
48 for i in range(len(trgprefecdat["testedPositive"])):
49 # addstr の x, y が矩形 [width, height] 範囲外を指すと即(なんの説明もない
50 # 不親切な)エラーが返るので注意。
51 stdscr.addstr(trgprefecdat["testedPositive"][i], i, "#", curses.color_pair(2))
52 stdscr.addstr(trgprefecdat["serious"][i], i, "#", curses.color_pair(3))
53 stdscr.addstr(trgprefecdat["deaths"][i], i, "#", curses.color_pair(4))
54 tit = "(" + tpn + "の感染流行波の形 ) "
55 for i, c in enumerate(tit):
56 stdscr.addstr(height + 1, i * 2, c)
57 tit = "「東洋経済オンライン「新型コロナウイルス 国内感染の状況」」を加工 "
58 for i, c in enumerate(tit):
59 stdscr.addstr(height + 2, i * 2, c)
60 stdscr.getch()
61
62
63 if __name__ == '__main__':
64 import urllib.request
65 import shutil
66 if os.path.exists("prefectures.csv"):
67 ctm = datetime.datetime.utcfromtimestamp(os.stat("prefectures.csv").st_mtime)
68 now = datetime.datetime.utcnow()
69 if (now - ctm).total_seconds() > 60 * 60:
70 os.remove("prefectures.csv")
71 if not os.path.exists("prefectures.csv"):
72 cont, msg = urllib.request.urlretrieve(
73 "https://toyokeizai.net/sp/visual/tko/covid19/csv/prefectures.csv")
74 shutil.move(cont, "prefectures.csv")
75 curses.wrapper(_main, "prefectures.csv")
curses であることのメリットは、色付けのこともあるけどカーソル位置を自由にコントロール出来て好きな場所に文字印字出来ること、ね。実際一切 curses 使わずに全く同じグラフは書ける、でしょ、わかるよね?
結果はワタシのコンソールだとこんな感じ:
直近の「トレンド」の雰囲気をつかむだけなら、こんなんでも結構十分なんではないかという気はするよね。例えばこれを毎日監視してたりすれば、「上がりバナ」にはすぐに気付くであろうよ。
さて。
こういうやたらにレガシーなスキルを身につけることが何か役に立つのか、と言えば、「わざわざ身につけるつもりなら見合わない」とは思う。そうではなくて、「いざとなったらこういうやり方もある」という事実を「頭の片隅に入れておく」ことが、柔軟性の源となる、かもしれない、てハナシね。以前 curses の話をした際には「いまでも curses (や termcap/terminfo)の需要は結構ある」と書いたが、これはおそらく主として「サーバメンテナンスをリモートで行う」などの用事がある際にのみ頻出となりうるわけね、それ以外のシチュエーションはさすがにこれだけに固執する理由はほとんどなくて、ほかのもっとリッチなものを選ぶべき、となるだろうと思う。
ただ、「手っ取り早くコストをかけずに」の一つのやり方として、ということであれば、このアプローチは実は技術的には「vt100 的コンソール」にしかほとんど依存していない点が割と優れていて、特に Unix 系ならほぼ 100% 通用するという点は、強いといえば強い。ので、やはり「こういう考え方/やり方もある」と知っとくのは悪くないと思う。
2022-01-26追記:
どういう動機でワタシのこのページに辿り着くのかを想像してみるに、きっと「python で」はあまり関係なく、なおかつ「cueses」にもさほどこだわりなく、シンプルに「vt100 的コンソールでグラフを描きたい」という人もいるんではないかと思うのね。そういう人以外にはちょっと雑念になっちゃうかもしれない追記。
今想像した動機よりはきっと Sparklines の方が近いんではないかという、だけれども今のワタシの動機のほうにむしろ相応しい asciichart というプロジェクトがある。GitHub ページの構造の都合「Python プロジェクト」にみえるけれど、ドキュメントの方を読めばわかる通り、これは node.js、つまり javascript のプロジェクトで、Python は「ports のひとつ」という扱いである。Sparklines のほうが近いのでは、といった意味はわかるよね?
今回の追記では、この Python ports で遊んでみてもまぁいいと思ったんだけれど、ここ数日 Go で遊んでたもんで、なので、ちょっと Go のほうの ports である asciigraph の方でやってみようかと。
1 [me@host: ~]$ # Goモジュールとして使いたいなら:
2 [me@host: ~]$ go get github.com/guptarohit/asciigraph
3 [me@host: ~]$ # 完成品としての CLI ツールとしても欲しければ
4 [me@host: ~]$ go install github.com/guptarohit/asciigraph/cmd/asciigraph
後者の CLI だけで既に結構オモロイと思うよ:
1 [me@host: ~]$ echo {1..10} | ~/../../go/bin/asciigraph.exe -h 10 -c "-h 10 of {1..10}"
2 10.00 ┤ ╭
3 9.10 ┤ ╭╯
4 8.20 ┤ ╭╯
5 7.30 ┤ ╭╯
6 6.40 ┤ ╭╯
7 5.50 ┤ ╭╯
8 4.60 ┤ │
9 3.70 ┤ ╭╯
10 2.80 ┤ ╭╯
11 1.90 ┤╭╯
12 1.00 ┼╯
13 -h 10 of {1..10}
14 [me@host: ~]$ echo {1..10} | ~/../../go/bin/asciigraph.exe -h 1 -c "-h 1 of {1..10}"
15 10.00 ┼ ╭─────
16 1.00 ┼───╯
17 -h 1 of {1..10}
18 [me@host: ~]$ echo {1..10} | ~/../../go/bin/asciigraph.exe -h 20 -c "-h 20 of {1..10}"
19 10.00 ┤ ╭
20 9.55 ┤ │
21 9.10 ┤ ╭╯
22 8.65 ┤ │
23 8.20 ┤ ╭╯
24 7.75 ┤ │
25 7.30 ┤ ╭╯
26 6.85 ┤ │
27 6.40 ┤ │
28 5.95 ┤ ╭╯
29 5.50 ┤ │
30 5.05 ┤ ╭╯
31 4.60 ┤ │
32 4.15 ┤ ╭╯
33 3.70 ┤ │
34 3.25 ┤ ╭╯
35 2.80 ┤ │
36 2.35 ┤ │
37 1.90 ┤╭╯
38 1.45 ┤│
39 1.00 ┼╯
40 -h 20 of {1..10}
tail -f みたいに継続入力に追従して描画することも出来るようなので、リアルタイム監視用にも使えるみたいね。公式では ping -i を追いかける例を載っけてる。
自分で Go プログラムを作るならば、の例を自作しようかとも思ったが、まぁ公式の例で十分ろ。とても簡単。
このプロジェクトのアプローチは curses のようなもの、つまり「端末の機能」を使うという発想ではなくて、「某ちゃんねらー」が好む、まさにアスキーアートね。このプロジェクトだけでなく、他にも近いのは見たことがある。こういうのも場合によってはアリだと思うよ、てことで。(ただし「文字通りの文字」を使ったお絵描きなので、ロケールとかの繊細な問題の影響をモロに受けることに注意。)