termgraph を自分のスクリプトから使いたい
ターミナルに簡単にグラフを書きたいと思って PyPI を探してみたら termgraph というのを見つけたものの、README.md を見ると何やらコマンドとして使うのが想定されているようで Python スクリプトから呼び出す方法については書かれていなかった。 仕方ないので termgraph の中を見てみて、こんな感じでやれば自分の Python スクリプトから termgraph 内の関数を呼び出してグラフを書けるよ、という内容のメモ。
termgraph に付属のサンプルデータにある data/ex4.txt を例にする。
# Example Data Set 4 with 2 Categories
@ Boys,Girls
2007,183.32,190.52
2008,231.23,5.0
2009,16.43,53.1
2010,50.21,7
2011,508.97,10.45
2012,212.05,20.2
2014,30.0,20.0
- 2007 年から 2014 年までの、Boys と Girls についてのなんらかの値が入っている。
- # で始まる行はコメント
- @ で始まる行は、データ行の列名だと思っておけば良い。termgraph の中では categories と呼ばれている
- 以降の行は、label,boys,girls という並びでカンマ区切りでデータが置かれている
お手本
termgraph コマンドを使うと、こんなグラフが書ける。
% termgraph ~/src-misc/termgraph/data/ex4.dat
▇ Boys ▇ Girls
2007: ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 183.32
▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 190.52
2008: ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 231.23
▏ 5.00
2009: ▇ 16.43
▇▇▇▇▇ 53.10
2010: ▇▇▇▇ 50.21
▏ 7.00
2011: ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 508.97
▇ 10.45
2012: ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 212.05
▇ 20.20
2014: ▇▇ 30.00
▇ 20.00
%
コマンドを使うのではなく、自分の Python スクリプトから termgraph の中のものを使って同じことをやろう、というのが目標。
概要
データを用意して、以下の termgraph 内の関数を呼び出す。
- termgraph.check_data
- termgraph.print_categories
- termgraph.chart
termgraph.chart 関数
最終的に termgraph.chart 関数を呼んでグラフを書くことになる。
def chart(colors: List, data: List, args: Dict, labels: List) -> None:
"""Handle the normalization of data and the printing of the graph."""
colors, data, args, labels を用意する必要がある。
- colors - チャートの色の list
- data - チャートのデータの list
- args - termgraph.chart 関数のオプションを指定する dict
- labels - 各要素の名前
colors リスト
上の例では Boys / Girls ともに端末の文字の色でグラフが描かれるが、termgraph コマンドに --color {blue,red} とオプションを付け加えると、Boys は青で Girls は赤でグラフが描かれる。
この colors リストは自分で用意する必要はない。termgraph.check_data 関数の返り値を colors にする。
colors = tg.check_data(labels, data, args)
では、どのように色を指定するのかというと、args['color'] に値を入れておく。端末の文字色でよければ None を入れる。categories の要素ごとに色を変えたい場合リストに色を指定する、例えば Boys は青で Girls は赤にしたいという場合には、args['color'] = ['blue', 'red'] という感じ。このリストの要素数は、categories リストの要素数と等しくなくてはならない。
指定できる色は、termgraph.AVAILABLE_COLORS に定義されている。
# ANSI escape SGR Parameters color codes
AVAILABLE_COLORS = {
"red": 91,
"blue": 94,
"green": 92,
"magenta": 95,
"yellow": 93,
"black": 90,
"cyan": 96,
}
色を追加で定義したいのなら、この AVAILABLE_CHARTS に追加すれば良い。例えば白なら tg.AVAILABLE_COLORS = {'white': 97} と書く。 wikipedia の ANSI escape code のページを見ると、30 から 37 の値も使えそうではあるが試してはいない。
labels リスト
2007 から 2014 までの文字列のリストを作っておけば良い。
labels = ['2007', '2008', '2009', '2010', '2011', '2022', '2014']
data リスト
[boysの値,girlsの値] のリストを作っておけば良い。こんな感じ。
data = [
# Boys, Girls
[183.32, 190.52],
[231.23, 5.0 ],
[ 16.43, 53.1],
[ 50.21, 7 ],
[508.97, 10.45],
[212.05, 20.2 ],
[ 30.0, 20.0 ],
]
args dict
termgraph コマンドのヘルプ (termgraph --help) を参考に dict を作っていけば良いのだが、termgraph.chart 関数や termgraph.check_data 関数を呼ぶだけなら不要だったりするものや、そもそも使われていないものもある。。。
termgraph.chart を呼ぶために必須なもの
オプション名 | タイプ | オプションの意味 |
---|---|---|
width | int | グラフの幅。デフォルト 50。 |
format | str | 各棒の後に数値を表示する際の形式。デフォルト {:<5.2f} |
suffix | str | 各棒の後に表示した数値の後につける文字列。 % とかをつけたいなら。 |
no_labels | bool | 各棒の先頭にラベルを表示するかどうか。 |
no_values | bool | 各棒の後に値を表示するかどうか。 |
color | list | 色名をリストで指定。 ["blue", "red"] とか。指定がなければ None を入れておけば良い。 |
vertical | bool | 横ではなく縦にグラフを書きたい! |
stacked | bool | 各カテゴリを合わせたグラフにしたい! |
histogram | bool | ヒストグラムでお願い! |
bins | int | ヒストグラムにしたときの bin の数。 デフォルト 5。 |
different_scale | bool | Categories have different scales らしいが調べてない |
termgraph コマンドのオプションとして定義されているが、chart や check_data を使うにあたっては不要なもの
- title
- calendar
- start-dt
- custom-tick
- delim
タイトルは自分で書けばいい。
calendar と start-dt は、カレンダーヒートマップを書きたい場合に使う。termgraph.chart 関数ではなく、termgraph.calendar_heatmap 関数を呼ぶ。
custom-tick は、グラフを描くときの文字を変更したい場合に使う。
delim は、データ入力をファイルから行うときに区切りもじを指定するために使う。(termgraph.DELIM の値を変える。)
termgraph コマンドのオプションとして定義されているが使われていないもの...
- space-between
- label-before
というわけで、こんな感じで args を用意すればよい。
args = {
'width': 50,
'format': '{:<5.2f}',
'suffix': '',
'no_labels': False,
'no_values': False,
'color': None,
'vertical': False,
'stacked': False,
'histogram': False,
'bins': 5,
'different_scale': False
}
categories リスト
凡例を表示するには、termgraph.print_categories(categories, colors) を呼ぶ。 categories リストは ['Boys', 'Girls'] として用意する。
組み合わせてみる
全部合わせてみると、こんな感じのスクリプトになる。
from termgraph import termgraph as tg
# data/ex4.dat
categories = ['Boys', 'Girls']
labels = ['2007', '2008', '2009', '2010', '2011', '2022', '2014']
data = [
# Boys, Girls
[183.32, 190.52],
[231.23, 5.0 ],
[ 16.43, 53.1],
[ 50.21, 7 ],
[508.97, 10.45],
[212.05, 20.2 ],
[ 30.0, 20.0 ],
]
args = {
'width': 50,
'format': '{:<5.2f}',
'suffix': '',
'no_labels': False,
'no_values': False,
'color': None,
'vertical': False,
'stacked': False,
'histogram': False,
'bins': 5,
'different_scale': False
}
colors = tg.check_data(labels, data, args)
if categories:
tg.print_categories(categories, colors)
tg.chart(colors, data, args, labels)
hoge.py として保存して、実行してみる。
% python hoge.py
▇ Boys ▇ Girls
2007: ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 183.32
▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 190.52
2008: ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 231.23
▏ 5.00
2009: ▇ 16.43
▇▇▇▇▇ 53.10
2010: ▇▇▇▇ 50.21
▏ 7.00
2011: ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 508.97
▇ 10.45
2022: ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 212.05
▇ 20.20
2014: ▇▇ 30.00
▇ 20.00
%
できあがり。
やっぱりお腹が痛くなる
#C言語クイズ
— Yutaka Hirata (@yutakakn) 2023年1月3日
実行結果はどうなるでしょう?#include <stdio.h>#include <stdbool.h>
int main(void)
{
char buf[5] = "2023";
char c1 = buf[0];
char c2 = 2[buf];
bool cmp = (c1 == c2);
printf("%s\n", cmp ? "true" : "false");
return 0;
}
この 2[buf] みたいな書き方もできちゃうというのが記憶の片隅にあり、true と印字されるんだろうなと思ったので最初の選択肢をポチッとした。 言語仕様で決まっているのかどうだったかまでは覚えてないけれども、とりあえずコンパイラがどんな機械語を生成するのか確認してみた。
環境
コンパイル&実行
やはり true が印字される。
[yusuke@albirex foo]$ gcc -g foo.c -o foo [yusuke@albirex foo]$ file ./foo ./foo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=12cde68dd82759dc3f81ce929093e43480f3c6f7, not stripped [yusuke@albirex foo]$ ./foo true [yusuke@albirex foo]$
disas main
やはり 2[buf] は buf[2] のように処理されている。。。
(gdb) disas main Dump of assembler code for function main: 0x000000000040052d <+0>: push %rbp 0x000000000040052e <+1>: mov %rsp,%rbp 0x0000000000400531 <+4>: sub $0x10,%rsp スタックを16バイト伸長する 0x0000000000400535 <+8>: movl $0x33323032,-0x10(%rbp) ベースポインタから -0x10 のところから "2" "0" "2" "3" 0x000000000040053c <+15>: movb $0x0,-0xc(%rbp) nullターミネート "\0" 0x0000000000400540 <+19>: movzbl -0x10(%rbp),%eax buf[0] の値をバイト幅で eax へ 0x0000000000400544 <+23>: mov %al,-0x1(%rbp) eax の下位のバイトを rbp-1 へ 0x0000000000400547 <+26>: movzbl -0xe(%rbp),%eax 2[buf] の値をバイト幅で eax へ 0x000000000040054b <+30>: mov %al,-0x2(%rbp) eax の下位のバイトを rbp -2 へ 0x000000000040054e <+33>: movzbl -0x1(%rbp),%eax rbp-1に取っておいた値をバイト幅で eax へ 0x0000000000400552 <+37>: cmp -0x2(%rbp),%al eax の下位バイトと rbp-2 の値を比較 0x0000000000400555 <+40>: sete %al ...以下省略... 0x0000000000400558 <+43>: mov %al,-0x3(%rbp) 0x000000000040055b <+46>: cmpb $0x0,-0x3(%rbp) 0x000000000040055f <+50>: je 0x400568 <main+59> 0x0000000000400561 <+52>: mov $0x400610,%eax 0x0000000000400566 <+57>: jmp 0x40056d <main+64> 0x0000000000400568 <+59>: mov $0x400615,%eax 0x000000000040056d <+64>: mov %rax,%rdi 0x0000000000400570 <+67>: callq 0x400410 <puts@plt> 0x0000000000400575 <+72>: mov $0x0,%eax 0x000000000040057a <+77>: leaveq 0x000000000040057b <+78>: retq End of assembler dump. (gdb)
メモリ上の配置
分かりにくいけど、こんな感じ。
+------ rbp-0x10。つまり buf[0] | +---- rbp-0xe。 つまり buf[2] であり 2[buf] | | +-- rbp-0xc。 つまり buf[4] | | | +-- rbp | | | | v v v v --------+-+-+-----------+------- 2023n 22 // n は \0。2, 0, 2, 3 は、数値ではなく char。 --------+-------------+++------- | ^^ +-- rsp || |+-- buf[0] からのコピー。つまり char c1 +--- 2[buf] からのコピー。つまり char c2
gcc オプション
ちなみに、-g をつけない場合も、main の disas は同じだった。-g はデバッグ情報を持つセクションを用意するだけで生成するマシン語には影響しないと思っておいてよさそう。 -O1 だとこんな感じ。もはや true を印字するのみ。
(gdb) disas main Dump of assembler code for function main: 0x000000000040052d <+0>: sub $0x8,%rsp 0x0000000000400531 <+4>: mov $0x4005e0,%edi 0x0000000000400536 <+9>: callq 0x400410 <puts@plt> 0x000000000040053b <+14>: mov $0x0,%eax 0x0000000000400540 <+19>: add $0x8,%rsp 0x0000000000400544 <+23>: retq End of assembler dump. (gdb) (gdb) x/s $rdi 0x4005e0: "true" (gdb)
そして、gcc がどんなアセンブラを作るのかだけみたいのなら、gdb など使わずとも gcc -S foo.c とやれば良い。数値が 10 進数で表記されている。
[yusuke@albirex foo]$ gcc -S foo.c [yusuke@albirex foo]$ cat foo.s .file "foo.c" .section .rodata .LC0: .string "true" .LC1: .string "false" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $858927154, -16(%rbp) movb $0, -12(%rbp) movzbl -16(%rbp), %eax movb %al, -1(%rbp) movzbl -14(%rbp), %eax movb %al, -2(%rbp) movzbl -1(%rbp), %eax cmpb -2(%rbp), %al sete %al movb %al, -3(%rbp) cmpb $0, -3(%rbp) je .L2 movl $.LC0, %eax jmp .L3 .L2: movl $.LC1, %eax .L3: movq %rax, %rdi call puts movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)" .section .note.GNU-stack,"",@progbits [yusuke@albirex foo]$
str.split() の maxsplit
maxsplit という引数があるのを知らなかったので、確認。
>>> 'a-b-c-d-e'.split('-', -5) ['a', 'b', 'c', 'd', 'e'] >>> 'a-b-c-d-e'.split('-', -4) ['a', 'b', 'c', 'd', 'e'] >>> 'a-b-c-d-e'.split('-', -3) ['a', 'b', 'c', 'd', 'e'] >>> 'a-b-c-d-e'.split('-', -2) ['a', 'b', 'c', 'd', 'e'] >>> 'a-b-c-d-e'.split('-', -1) ['a', 'b', 'c', 'd', 'e'] >>> 'a-b-c-d-e'.split('-', 0) ['a-b-c-d-e'] >>> 'a-b-c-d-e'.split('-', 1) ['a', 'b-c-d-e'] >>> 'a-b-c-d-e'.split('-', 2) ['a', 'b', 'c-d-e'] >>> 'a-b-c-d-e'.split('-', 3) ['a', 'b', 'c', 'd-e'] >>> 'a-b-c-d-e'.split('-', 4) ['a', 'b', 'c', 'd', 'e'] >>> 'a-b-c-d-e'.split('-', 5) ['a', 'b', 'c', 'd', 'e'] >>> 'a-b-c-d-e'.split('-', 6) ['a', 'b', 'c', 'd', 'e'] >>>
バックグラウンドでコマンドを実行して、そのコマンドの stdout/stderr をファイルに記録する
以下のように複数のコマンドの出力を保存したかった。
- コマンドを実行してその標準出力と標準エラー出力をファイルに書き出したい
- 長い時間かかるコマンドがあるので、複数同時に実行したい
例えば、複数のホストに対する ping コマンドの出力とか。
とりあえず、こんな感じ。。。?
import subprocess def do_pings(ip_addrs, count): procs = [] for ip_addr in ip_addrs: p = subprocess.Popen( f'ping -c {count} {ip_addr}', shell=True, text=True, # this "text" argument is valid since 3.7. # For earlier releases, use # universal_newlines=True, or # encoding='utf-8'. stdout=open(f'ping_{ip_addr}_stdout.txt', 'w'), stderr=open(f'ping_{ip_addr}_stderr.txt', 'w'), ) procs.append(p) [p.wait() for p in procs] def main(): IP_ADDRS = ('192.168.0.1', '192.168.0.2') PING_COUNT = 30 do_pings(IP_ADDRS, PING_COUNT) if __name__ == '__main__': main()
O'REILY 入門 Python 3 - 12.5 復習問題 12.9 の文字列
O'REILY 「入門 Python 3」の 12 章「データの自在な操作」にある復習課題 12.9 は、84 文字の文字列を入力しなければならない。 もう二度と 0 とか f を目で数えたくはないので、ここにメモしておくことにする。
'47494638396101000100800000000000ffffff21f9' '0401000000002c000000000100010000020144003b'
asciitree の AttributeTraversal を使う
pypi に asciitree というパッケージがある。 ターミナルにツリー表示を描くために使用することができるのだけど、README をパッと見る限りではツリー構造を dict で用意しなければならないようで、なんだか不便そう。ただ、ドキュメントを見ると children という属性を見てツリー構造を解釈してくれそうな AttributeTraversal というクラスがあるが、使用例がない。
というわけで、その使用例を書いてみる。書き出すツリーは、README と同じ以下のもの。
asciitree
+-- sometimes
| +-- you
+-- just
| +-- want
| +-- to
| +-- draw
+-- trees
+-- in
+-- your
+-- terminal
まずは、import。
from asciitree import LeftAligned
from asciitree.traversal import AttributeTraversal
そして、children を属性に持つ Node クラスを用意してみる。(とりあえず tuple にしているけど、list でも set でも良い。) この Node クラスのオブジェクトに対して str() が行われた結果が、ツリーの各ノードに表示される文字列になるので、__repr()__ でカスタマイズできる。
class Node:
def __init__(self, data):
self.__data = data
self.children = tuple()
def __repr__(self):
return str(self.__data)
ノードを作っていく。
ast = Node('asciitree')
sts = Node('sometimes')
you = Node('you')
jst = Node('just')
wnt = Node('want')
to = Node('to')
drw = Node('draw')
trs = Node('trees')
inn = Node('in')
yr = Node('your')
tnl = Node('terminal')
各ノードが持つ子どもを tuple にまとめて、children に入れていく。(list とか set にする場合には、それに応じて適当に。)
ast.children = (sts, jst, trs, inn)
sts.children = (you,)
jst.children = (wnt,)
wnt.children = (to, drw)
inn.children = (yr,)
yr.children = (tnl,)
traverse 引数に AttributeTraversal() を指定して LeftAligned のオブジェクトを作り、call する。
tr = LeftAligned(traverse=AttributeTraversal())
print(tr(ast))
これで、ツリー構造をターミナルに書くことができる。 dict で表現するよりはかなり楽。
ちなみに、asciitree はデフォルトで children という名前の属性を見てくれるのだけど、これを変更することも可能。AttributeTraversal オブジェクトを作る際に、attribute 引数で属性名を文字列で指定すれば良い。例えば、子どもがひとつしかない構造で属性名が child があるというような場合には、AttributeTraversal(attribute='child') と書けば良い。
tar の -a オプションを知らなかった話
macOS (BSD) の tar には -a (--auto-compress) というオプションがあるのを知った。
c (create) のときに指定できるオプションで -f に指定したアーカイブファイル名の拡張子部分によって、どの圧縮形式を使うのかを判断してくれる。
macOS の man tar にはこんな風に書いてある。
-a, --auto-compress
(c mode only) Use the archive suffix to decide a set of the for-
mat and the compressions. As a simple example,
tar -a -cf archive.tgz source.c source.h
creates a new archive with restricted pax format and gzip com-
pression,
tar -a -cf archive.tar.bz2.uu source.c source.h
creates a new archive with restricted pax format and bzip2 com-
pression and uuencode compression,
tar -a -cf archive.zip source.c source.h
creates a new archive with zip format,
tar -a -jcf archive.tgz source.c source.h
ignores the ``-j'' option, and creates a new archive with
restricted pax format and gzip compression,
tar -a -jcf archive.xxx source.c source.h
if it is unknown suffix or no suffix, creates a new archive with
restricted pax format and bzip2 compression.
.tar.bz2 にするために、 -j オプションを知らずに、
tar cf - aaa bbb ccc | bzip2 -c > xxx.tar.gz2
とか書いていた私。
それが、
tar acf xxx.tar.bz2 aaa bbb ccc
で済むというのは、とても楽だと思う。 というか、.zip も作れちゃうのね。tar さん。ステキ。
でも、parallel bzip2 (pbzip2) を使いたいときには、" --use-compress-program pbzip2" かな。
tar cf xxx.tar.bz2 --use-compress-program pbzip2 aaa bbb ccc