Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

LeapMotionでPythonを使ってジェスチャーで家電を操作する

- はじめに -

近年、VRのブームのおかげもあってモーションキャプチャー分野も発展しつつあります。

本記事は、お手軽モーションキャプチャー端末であるLeap MotionPython 3.xから利用し、様々な家電やPC上の操作をジェスチャーで行おうという記事です。

都内某企業でLTする機会があったのでネタにしたところダダ滑りしたのでこの記事もボツにしようとしましたが、現状多分多くの人が詰まるので、正しくPython3系でLeap Motionが使えるように公開しておきます。


 

- Leap Motionとは -

前述したように、人体の動きをデータ化するモーションキャプチャー分野は、近年急速に発展しています。

キャプチャー端末分野では先日のbuild2018にてProject Kinect for Azure (Microsoft,2018)が発表されたりだとか、ハンドキャプチャー特化で見てもグローブを装着して高精度に手の動きを認識するNoitom Hi5 (Noitom, 2017)が話題になったりだとか、画像認識分野ではOpenPose(CMU Perceptual, 2017)が出たりとか、あらゆる方面から進展しています。

f:id:vaaaaaanquish:20180923155542p:plain:w350
LTスライドの一部:キャプチャー分野の発展

 
Leap Motionは、2012年よりLeap Motion社が発売しているハンドモーションキャプチャーバイスです。
光センサ、赤外線センサを搭載していて、なかなかの精度で手の骨格の動きを取得する事ができます。
Windows, Mac, Linuxに対応したドライバが存在するだけでなく、Unity, Unreal Engineへの対応、以下の言語に対応するSDKとライブラリが公開されています。

(ただこれはSDK v3の話で、v4ではc++, java, pythonがメインになっています)

 
価格はAmazon最安で9,900円。

近年IT企業に増えつつある「月1万技術向上に使って良いよ〜」みたいな枠にピッタリのガジェットです。(私もその枠組で購入しました)

サイズとしてはこれくらいで扱いやすいです。USBケーブルが専用かつ1mしかないのがちょっとだけネックです。

f:id:vaaaaaanquish:20180923222724j:plain:w350
Leap Motion実物


 
Leap MotionHPよりLeap Motion搭載のPCが発売されたり、公式WebStoreではOculus RiftやHTC Viveに装着できる機器が出ています。デベロッパーギャラリーもあり、世界の開発者が作成したアプリで遊んだり、自身の作ったものを公開できる環境も存在します。

f:id:vaaaaaanquish:20180923162152p:plain:w350
LTスライドの一部:Leap Motion周辺の土壌

手軽にモーションキャプチャーの世界に入り込める、最初に遊ぶに最適な製品です。


 

- Leap Motionで取得できる情報 -

Leap Motionでは、両手における、手のひら中央及び各指、各関節のstart_point, end_pointにおける空間座標(3次元)と方向ベクトル(3次元)が取得できます。

f:id:vaaaaaanquish:20180923164036p:plain:w350
LTスライドの一部:取得可能な情報

また、SDKの提供するAPIでは以下のGestureが定義されています。

  • circle: 指で円を描く動作
  • swipe: 手を左右に動かす動作
  • key tap: 指で物を選択するような動作
  • screen taps: スクリーンをTapする動作

f:id:vaaaaaanquish:20180923171502p:plain:w350
LTスライドの一部:定義済みGesture


 

- Leap Motionに関連する開発 -

Leap Motion周りの開発について調べると、古いバージョンのSDK v3を利用した情報が多く存在します。例えばPythonであれば以下等
PythonでLeapMotionのデータを取得する。 - Qiita
PythonでLeapMotionを使ってみる - Qiita
Leap Motion を Pythonから使う方法を調べた | Futurismo
LeapMotionとpythonで遊ぶ

また、公式HPから参照できる以下のページでは、SDK v3までのReferenceしか参照することができません。
Leap Motion SDK Reference : Python SDK Documentation — Leap Motion Python SDK v3.2 Beta documentation

これは、以下のような内容からきています。

  • SDK v4からLeapCxxという名前でGithub上でライブラリが管理されるようになった
  • それまでのLeapAPIは非推奨となりLeapCなるAPIが提供されはじめた(LeapCxxの一部)
  • v4は未だbeta版であり、MacLinuxはv2の利用を推奨されている

LeapCxxではswigで多言語対応される事を前提として高速化され、LeapCSharpやUnityModulesなどのBindingを切り離す事にも成功しています。
まとめると「Windowsで利用可能な言語であればLeapCxxを使ったほうが良いが、現状使えない場面の方が多い」という事になります。

LeapCxx : GitHub - leapmotion/LeapCxx: Implementation of older C++ API using LeapC
LeapCxx documentation : Leap Motion C API: Leap Concepts

 
Windowsならv4版、Windowsじゃ無くてもv3版使えば良いじゃんとなる訳ですが、私はWindowsが扱えないのと、v3以前の開発環境は以下のようになっています。

f:id:vaaaaaanquish:20180923202240p:plain
・・・。
一目で分かるかと思いますが古いです。

行列や機械学習に突っ込む事も考慮して、せめてMacbookのPython3で動くようにしていきます。

(本記事では、Mac, Python3での利用を想定しますが、今後Windowsでv4を利用した記事もがんばって公開する予定です。)


 

Leap Motion Controllerのインストール

以下からControllerをダウンロードします。
www.leapmotion.com

ダウンロードしたdmgファイルを使ってインストールすると、MacであればメニューにLeap Motionアイコンが追加されます。

f:id:vaaaaaanquish:20180923182551p:plain
Controller
このアイコンが緑になっている場合、Leap Motionが認識しているという形です。

Visualizerが付属していますので遊んでみます。

f:id:vaaaaaanquish:20180923182352g:plain
Visualizer
この時点で、Leap Motion App Homeなるアプリもインストールされており、そのアプリ経由で公式WebStoreに公開されているアプリで遊んだり、自身のアプリを公開する事が可能です。


 

Leap Motion SDK v2のインストール

Leap Motion SDKのv2を以下よりダウンロードします。
こちらをswigを用いてPython3系でも利用できるように改修していきます。
V2 Tracking — Leap Motion Developer

zipを解凍すると、配下にdmgファイルがあるはずなので、そちらを利用してSDKをインストールします。

また、解凍したファイル群の中には「./LeapSDK/samples/Sample.py」なるサンプルが存在します。
以降このサンプルを利用してもよいですが、このSample.pyはpython 2.xベースですので今回は使いません。


 

python 3系への対応

公式がswigでPython3系に対応する方法を公開してくれています。
support.leapmotion.com

雑に調べた所、以下2つのリポジトリがpython3に対応してそうに見えますが、中身は上記の記事をスクリプトに落とし込んだだけのようですので、自身で作業した方が良さげです。
GitHub - Nagasaki45/leap_python3: Build LeapMotion binaries for python3
GitHub - BlackLight/leap-sdk-python3: Leap Motion SDK - Python 3 module builder


先程解凍したLeap Motion SDK の配下に入り ./LeapSDK/ に移動して雑なディレクトリを作ります。

cd LeapSDK
mkdir work
ls
> docs/         head_sha.txt  include/      lib/          samples/      util/         version.txt   work/

必要なファイルを全てworkにコピーしてswigに投げます

cp include/Leap.i work
cp include/LeapMath.h work
cp include/Leap.h work
cp lib/libLeap.dylib work

cd work
swig -c++ -python -o LeapPython.cpp -interface LeapPython Leap.i

この時、以下のようなエラーが出るはずです。

Leap.i:991: Error: Line indented less than expected (line 3 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1009: Error: Line indented less than expected (line 16 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:542: Error: Line indented less than expected (line 13 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:552: Error: Line indented less than expected (line 7 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1115: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1116: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1117: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1121: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1118: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1122: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1123: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1119: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1120: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:565: Error: Line indented less than expected (line 10 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1156: Error: Line indented less than expected (line 23 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1

試しにLeap.iの991行目を見ると分かりますが、以下のようにpythonコードと言いつつdefの前にインデントが入ってます。

%extend Leap::Vector {
%pythoncode {
  def to_float_array(self): return [self.x, self.y, self.z]
  def to_tuple(self): return (self.x, self.y, self.z)
%}}

インデントが合うように、%pythoncodeで検索し、全ての行に存在する前空白2個を消していけば良いです。
1115行から1123行目のエラー原因も別箇所のpythoncodeにあります。私はvimで雑にやりました…

 
swigによって「LeapPython.cpp」なるファイルが生成出来ていれば成功です。
このファイルを利用しているPythonに紐付ける作業を行います。

clang++ -arch i386 -arch x86_64 -I/Library/Frameworks/Python.framework/Versions/3.6/include/python3.6m LeapPython.cpp libLeap.dylib /Library/Frameworks/Python.framework/Versions/3.6/lib/libpython3.6.dylib -shared -o LeapPython.so

pyenv等System以外のPythonを利用している場合は、python3.6mへのpathを、それぞれPythonの/include/pythonNNmディレクトリを指すようにします。
私は面倒なのでpyenv local systemでworkディレクトリではsystemのpython3.6を利用するようにしています。


 
LeapPython.soとLeap.pyが出力されていれば終わりです。

SampleはSwig解説記事の最後にある以下リンクからダウンロードしたものを使います。
https://leapmotion-assets-production.s3.amazonaws.com/Jsf1V_R5_kbRNHu3QKdenQ/LeapPython33.zip

解凍すると、SamplePython33.pyがあるのでそちらをworkディレクトリに移動します。(解凍したら3.3用のLeapPython.soが付いてくるけど基本無視で)
workの中でls

ls
> eap.h             Leap.i             Leap.py            LeapMath.h         LeapPython.cpp     LeapPython.h       LeapPython.so*     SamplePython33.py* libLeap.dylib*

そのままpython3.6でSamplePython33.pyを実行するとLeap.pyのimportエラーが出ます。

  File "/Users/vanquish/Documents/work/LeapDeveloperKit_2.3.1+31549_mac 2/LeapSDK/work/Leap.py", line 345
    %
    ^
SyntaxError: invalid syntax

Leap.pyを見てみると何箇所か%だけが記載されている行があるので、Leap.pyからその行を削除します。

 
これで多分動きます。

python3.6 SamplePython33

> Initialized
> Press Enter to quit...
> Connected
> Frame id: 46613, timestamp: 4268852938, hands: 0, fingers: 0, tools: 0
> Frame id: 46614, timestamp: 4268957238, hands: 0, fingers: 0, tools: 0
> Frame id: 46615, timestamp: 4269061570, hands: 0, fingers: 0, tools: 0
> Hand has 5 fingers with average tip position (38.1071, 111.769, -141.137)
> Palm position: (51.3712, 76.887, -72.069)
> Pitch: 31.787451 degrees,  Roll: 17.410023 degrees,  Yaw: -10.570597 degrees
> Hand curvature radius: 124.589699 mm
> Frame id: 49355, timestamp: 4485494804, hands: 1, fingers: 5, tools: 0
> Hand has 5 fingers with average tip position (37.3419, 113.345, -140.911)
> Palm position: (50.7582, 77.6839, -72.2254)
>  ...

後はSamplePython33.pyを見れば分かりそうですが、on_frame メソッドに手の情報が流れ込んでくるので、それに応じてGesture判定したり動作を書いてやれば良い感じです。


 

- 家電を動かす -

私の家のIoT環境です。ここにLeap Motionを足してみます。

f:id:vaaaaaanquish:20180923220036p:plain
Leap MotionをIoT環境に追加

 
実際にテレビの操作をしているGIFです。GIFに収めている関係上めっちゃ反応がよく見えます(実際は30秒くらいの動画です)。

f:id:vaaaaaanquish:20180923220340g:plain:w450
Leap Motionによるテレビの操作

 
電気やクーラー、テレビも「OK, Google全部消して」等で消せるようにしていますので、そちらのトリガーをLeap Motionにしてみます。

f:id:vaaaaaanquish:20180923220526g:plain:w450
家電の操作
これ引っ越し直前の家で実験してるのでちょっと汚いんですが、この端末が玄関にあればサッとジェスチャーキメるだけで全て消せるようになりそうではあります。


 
まあここまで書いてなんですが、実際他にも「ターミナルやTweetDeckを起動する」とか「じゃんけんアプリを作ってみる」とかやったものの、あんまりウケそうなものができずコレに落ち着きました…

実際やってみる中で、モーションキャプチャーで何するかと考えてもどれも微妙で「妻がキレてる時に声出してオーケーグーグルって言えない時」とかに使えるかも知れないなレベルの感想しか得られませんでした。


このクックパッドを手で操作するのは公開されているアプリで最初に試したんですが、唯一これが料理中とかに使い物になるかもしれないなあ…

f:id:vaaaaaanquish:20180923221012g:plain
クックパッドの操作


 

- おわりに -

普通にデバイスとしては面白いですし、多分VR機器買ったらもっと遊べるんだと思います。

高専に居た頃にKinectを触った事がありますが、手の認識に関してはKinectよりは精度良く安く出来て暇つぶしに良い端末です。


 

複数ノードDockerでChainerMNを動かすためのTips

- はじめに -

ChainerMNがついに本家Chainerにマージされました。分散深層学習への本気度が伺えます。

github.com


節目という事で、Dockerを利用して複数ノードでChainerMNするために行った事のメモをTips形式で残しておこうという記事です。

私は半年くらい前からこの記事の内容を使っているのでアップデートがあるかもしれません。
加えて、最近はkubernetesを使うのが流暢で、PFNさんも公式ブログ書いてるし多分k8sが良いと思います。
ChainerMN on Kubernetes with GPUs
(私もk8s挑戦してるけどあんまり上手くいってなくて放置中です…)

この記事はk8s使えないけどDocker使えるような分散環境はあるみたいなニッチな需要を満たすかもしれないなあというレベル感の記事です。

 

 

- 前提と参考文献 -

記事前提として以下を想定しています。

  • docker、nvidia-docker、cuda周りがインストールされており、1ノード1GPUでChainerのtrainを回せる環境がある
  • 複数ノード、複数GPU環境がある

また、私が環境構築した際に読んだ参考文献を示します。

ひとまず、上の5つに目を通せば動くような気がします。
加えて、細かなエラーでStackOverflowやOpenMPIの公式リファレンスを読んでいますがそちらは随時記載します。


Deep Learning分散の仕組みについては、秋葉さん、鈴木さんの記事とスライドに任せます。


 

- Docker環境構築 -

cuda周り以外はまっさらのUbuntu 16.04 のDocker想定。

apt-get upgrade
apt-get update
apt-get install python3-pip python3-dev
apt-get install python-pip python-dev
apt-get install wget git

バックエンドにOpenMPI、NCCLを利用する想定でインストールを進める。

OpenMPIのインストール

apt-get install infiniband-diags opensm ibverbs-utils infiniband-diags perftest

wget https://download.open-mpi.org/release/open-mpi/v3.0/openmpi-3.0.1.tar.gz
tar -zxvf openmpi-3.0.1.tar.gz
cd openmpi-3.0.1

./configure --with-cuda --prefix=$HOME/local/openmpi --with-openib
make -j4
make install

touch ~/.bashrc
echo 'export LD_LIBRARY_PATH=$HOME/local/openmpi/lib:${LD_LIBRARY_PATH}' >> ~/.bashrc
echo 'export PATH=$HOME/local/openmpi/bin:${PATH}' >> ~/.bashrc


NCCLなるGPU通信ライブラリのインストールは、以下のリンクからダウンロードする。
NVIDIA Developerの登録を済ませておく必要がある)
https://developer.nvidia.com/nccl/nccl-download

ライセンス読んで「I Agree~」をチェックした後、debファイルを以下からダウンロードしてくる。
f:id:vaaaaaanquish:20180917213514p:plain

Docker内にダウンロードしてきたファイルを設置する。

# コンテナ内に送る
sudo docker cp nccl-repo-ubuntu1604-2.2.12-ga-cuda9.0_1-1_amd64.deb [CONTAINER ID]:/hogehoge

# docker attach後
dpkg -i nccl-repo-ubuntu1604-2.2.12-ga-cuda9.0_1-1_amd64.deb
apt update
apt install libnccl2 libnccl-dev
echo 'export NCCL_ROOT=/usr/local/nccl' >> ~/.bashrc
echo 'export CPATH=$NCCL_ROOT/include:$CPATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=$NCCL_ROOT/lib/:$LD_LIBRARY_PATH' >> ~/.bashrc
echo 'export LIBRARY_PATH=$NCCL_ROOT/lib/:$LIBRARY_PATH' >> ~/.bashrc
source ~/.bashrc

追記:2018/09/19
エゴサしてたらNCCL情報が


 
この後一番陥りやすいのが相互にsshする環境の構築である。
複数ノードの場合「ノード間はssh-keyでパスワードなしでssh可」「dockerに入るにはパスワードなしで」を実現しなければならず、通信できない場合にOpenMPIが無反応だったりしてわからんってなるのでちゃんとやる。

apt-get install -y build-essential libssl-dev libreadline-dev zlib1g-dev language-pack-ja
apt-get -y install openssh-server ufw curl
mkdir /var/run/sshd

# ユーザを作って、そのユーザでsshできるようにする(vaaaaanquishの所をよしなに)
useradd -m vaaaaanquish && echo "vaaaaanquish:vaaaaanquish" | chpasswd && gpasswd -a vaaaaanquish sudo
mkdir -p /home/vaaaaanquish/.ssh
chmod 700 /home/vaaaaanquish/.ssh
passwd -d root
passwd -d vaaaaanquish

鍵はDockerコンテナ内では生成せず、ノードからマウントしてくる形を取る(結局ノード間自体もsshできないと意味ないので)。

 
/etc/ssh/sshd_configを以下のように変更する(なければ作る)
ここで指定するPortは、Dockerからもアクセスするので今後統一して選べる所にする。

Port 2223
PermitEmptyPasswords yes
PasswordAuthentication no
PubkeyAuthentication yes
UsePAM no

また、rootから~/.ssh/configの最期の方に以下も追記する(なければ作る)

Host *
    Port 2223
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null

 
最後にcythonとChanierMNをインストールする。
cupyのcudaNNのバージョン等を絶対間違えないよう確認する

pip3 install cython
pip3 install cupy-cuda90
pip3 install chainermn

# Chainer試すためのMNISTパッケージもここで
pip3 install python-mnist


 
dockerコンテナからデタッチする前に、以下のファイルをルート配下辺りに「init.sh」のような名前で置いておく。

#!/bin/bash

NCCL_SOCKET_IFNAME=^docker0
/usr/sbin/service ssh start
/bin/bash

NCCLがDockerの作る仮想ソケットを利用してしまうらしいのでNCCL_SOCKET_IFNAME=^docker0は必須
参考:http://tabisurubiker.hatenadiary.jp/entry/2017/10/02/092952
Dockerコマンドでrunする際に実行するプログラムとして、このinit.shを実行、sshサーバを立ち上げて、bashで待機させるようにする。

待機しているdockerコンテナに対してOpenMPIがプロセス振ってくれるイメージ。

 
これで多分できたのでcommitしてsaveしてDockerコンテナは終了。


 

- ノード間のsshの実現 -

前述したようにノード間はsshできないとダメ。

実現する方法はいくつかあるがが、私はアホなので全ノードでssh-keygenして全ノードに配ればええやんと思ってスクリプト書きました。これで作ったkeyをマウントする形を取っている。ssh_generator.pyなる名前で以下のファイルを作って実行するだけ。自身をfabricで読んで各サーバに送る動作をpexpectで実行するアホみたいなスクリプトです。

# -*- coding: utf-8 -*-
# must : pip install pexpect, fabric3
from fabric.api import *
import pexpect
import sys

HOSTS = ['hogehoge', piyopiyo]    # 利用予定のサーバ
env.user = 'vaaaaanquish'          # localからサーバにアクセスするユーザ
env.key_filename = 'local key'    # localからサーバにアクセスするためのkey
env.password = PASSWORD      # localからサーバにアクセスするためのkey pass


def ssh_gen():
    run('ssh-keygen -t rsa')

def ssh_copy_id(y):
    run('ssh-copy-id -i /home/{}/.ssh/id_rsa.pub {}'.format(USER, y))


if __name__ == '__main__':
    for x in HOSTS:
        # 作るやつ
        print('\n[Making key : {}]'.format(x))
        cmd= "fab -f ssh_generator.py -H {} ssh_gen".format(x)
        child = pexpect.spawn(cmd, encoding='utf-8')
        child.logfile = sys.stdout
        while 1:
            i=child.expect([
                r'^.*(Enter file in which to save the key).*',
                r'.*(Enter passphrase).*',
                r'.*(Enter same passphrase again).*',
                r'.*(Overwrite).*\?',
                pexpect.EOF, pexpect.TIMEOUT], timeout=5)
            if i in [0,1,2]:
                child.sendline('')
            if i == 3:
                child.sendline('y')
                child.sendline('')
            if i in [4, 5]:
                break

        # 配るやつ
        print('\n[Sending key : {}]'.format(x))
        for y in HOSTS:
            cmd= "fab -f ssh_generator.py -H {} ssh_copy_id:{}".format(x,y)
            child = pexpect.spawn(cmd, encoding='utf-8')
            child.logfile = sys.stdout
            while 1:
                i = child.expect([
                    r".*(password).*",
                    pexpect.EOF, pexpect.TIMEOUT], timeout=5)
                if i == 0:
                    child.sendline('{}'.format(PASSWORD))
                    child.sendline('')
                if i in [1,2]:
                    break

これをローカルPCで実行し、鍵配り作業とする。各サーバの~/.ssh/配下に鍵がそれぞれ登録されます。
(基本的に使い終わったらこの鍵を削除しておかないと危ういのでちゃんとやりましょう)

踏み台等を利用する環境でも「env.gateway = [GATEWAY_SERVER]」のようにする事で解決可能です。


 

- mpiexecを用いたChainerMNの実行 -

以下ChinerMNのチュートリアルpython-mnistで試していく。
Step 1: Communicators and Optimizers — ChainerMN 1.3.1 documentation

 

Dockerのrun

nvidia-docker runするがこの際には以下のようにオプションを付ける。

sudo nvidia-docker run -it -d \
    --net=host -p 2223:2223 \
    -v /tmp:/tmp \
    -v /etc/hosts:/etc/hosts:ro \
    -v /home/vaaaaanquish/.ssh:/home/vaaaaanquish/.ssh \
    -e LD_LIBRARY_PATH=/usr/local/nccl/lib/:/root/local/openmpi/lib:/usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/local/cuda/lib64 \
    -e PATH=/root/local/openmpi/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
    -e NCCL_ROOT=/usr/local/nccl \
    -e CPATH=/usr/local/nccl/include:/usr/local/cuda/targets/x86_64-linux/include \
    -e LIBRARY_PATH=/usr/local/nccl/lib/:/usr/local/cuda/lib64/stubs \
    -e PYTHONPATH=/usr/bin/python3\
    [docker_image_name] sh /init.sh

dockerのポートはssh_configに書いたものを指定する。
/etc/hostsはマウントしないとdockerから各ノード間で通信できないので必須。
また前述の通り、配布したsshもノード上の物をマウントして利用する。
環境変数はdocker内でbashrcに書いても良いし、個々で-eで指定しても良いですが今回はinit.shを最後に動かすようにしてるのでここで。

あとコレはHorovodなるpackageを使った時に起こりがちな症状ですが/tmpにファイル作る場合もあるので、/tmpも大人しくマウントしておくと良いです。

 
何か雑にこんなんで全ノードでdocker実行すればええんちゃうんですかね

from fabric.api import *

env.hosts = [HOSTS]
env.user = USER
env.key_filename = KEY_PATH
env.password = PASSWORD

DOCKER_ENV = '-e LD_LIBRARY_PATH=/usr/local/nccl/lib/:/root/local/openmpi/lib:/usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/local/cuda/lib64 -e PATH=/root/local/openmpi/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -e NCCL_ROOT=/usr/local/nccl -e CPATH=/usr/local/nccl/include:/usr/local/cuda/targets/x86_64-linux/include -e LIBRARY_PATH=/usr/local/nccl/lib/:/usr/local/cuda/lib64/stubs -e PYTHONPATH=/usr/bin/python3'

@parallel
def run_docker():
        run("sudo nvidia-docker run -it -d --name {} --net=host -p 2223:2223 -v {}:{} -v /etc/hosts:/etc/hosts:ro -v /home/{}/.ssh:/home/{}/.ssh {} {} sh {}".format(P_NAME, LUSTERFS_MAUNT, LUSTERFS_HOME, USER, USER,DOCKER_ENV, DOCKER, INIT_SH))


 

mpiexecでChainerMNの実行

どこか1つのノードに対して「ssh root@hogehoge -p 2223」等としてdockerに入る。

docker内でmpiexecを実行する時、オプションで別のノードを指定することもできるが、 hostfile が あると便利なのでこれを用意する。
host.txtみたいな雑な名前で良い。書き方は以下の通り。

hogehoge_server_001 port=2223 cpu=2
hogehoge_server_002 port=2223 cpu=2
hogehoge_server_003 port=2223 cpu=2
hogehoge_server_004 port=2223 cpu=2

cpu=2って書いてるけど、これ実際はgpuの数っぽい。(プロセス数という意なのかも)
各サーバでport=2223とport=2224をそれぞれGPU割り当てて、Dockerマウントして使いたい!みたいなのはhostfileでは書けない。


いざ実行。前述example内のtrain.pyの画像path等をちょっと変更して動かしてみる。

mpiexec --allow-run-as-root --mca btl_tcp_if_include ib0 -x PATH=$PATH -x PYTHONPATH=$PYTHONOATH -x LD_LIBRARY_PATH=$LD_LIBRARY_PATH -x CPATH=$CPATH -x LIBRARY_PATH=$LIBRARY_PATH -x NCCL_ROOT=$NCCL_ROOT --hostfile host.txt -np 8 python3 train.py

dockerのrootで走ってもらう必要があるので--allow-run-as-rootが必須。
また、Infinibandを使っている時は--mca btl_tcp_if_include ib0して指定しないと掴んでくれなくて遅いので注意。
npの後ろにGPUと同じ数を書いて実行コマンドを叩くだけ。


学習が走ってそうだったら終わりです。


 

- エラー対応とか -

結構つまり所なエラーについて書いておきます。
OpenMPI、クソデコレーションされたエラー文吐く時もあれば、うんともすんとも言わないときもあるのでイライラせず対応していきましょう。

mpiexec実行しても全く何も表示されない!

→ 相互sshが上手くいってないので普通にsshで別ノードの指定ポートにパスワードなしで飛んでdockerコンテナ内に入れるかチェック

orterun was unable to launch the specified application as it could not access or execute an executable ~~~

→ 相互sshが上手くいってないので普通にsshで別ノードの指定ポートにパスワードなしで飛んでdockerコンテナ内に入れるかチェック

mpiexecが動いてそうだけど学習前に止まる!

→ chainerのサンプルでいうとGPU掴むところで止まってるんだと思います。
hostfileの書き方が怪しい場合と環境変数が上手く渡せてない場合が多いです。
この場合エラーも出ず止まったままになるのでサッサとctrl+cで抜けて確認。

ORTE does not know how to route a message to the specified daemon located on the indicated node ~~~

sshsshdのconfig間違えてデーモン起動できてないんだと思います

クソ遅い!received unexpected process identifier!

→ infiniband掴めてないのでmpiexec時にこれ忘れてる --mca btl_tcp_if_include ib0
もしくは環境とオプションが合ってない

It appears as if there is not enough space for (the shared-memory backing

→ /tmpとかファイルシステムに書き込む物を実行した時にそこマウントしてないと起こりがち
エラー文を読めばなんとか。

Failed, NCCL error nvidia-sample.cu:88 'unhandled system error

→ NCCLがDockerの作る仮想ソケットを利用してしまうので、上記init.shもしくはそれと同等のものが動いてるか確認


 

- おわりに -

最初にも書きましたが多分k8sとか色んなツールが出てきてるので、こんなコツコツやる必要もなくなって行くと思います。

ただこれ貯めといても仕方ないし書いときました。何かしらの参考になれば幸いです。


最近uberが出しているTF, Keras, PyTorchのOpenMPIトレーニングのwrapperとして扱えるhorovodも、以上の方法で使えます。
uber/horovod: Distributed training framework for TensorFlow, Keras, and PyTorch.
github.com

horovodとかdistTF、pytorch.distributedの知見も徐々にアウトプットしていこうと思っています。がんばります。


 

PyTorchで学習済みモデルを元に自前画像をtrainしてtestするまで

- はじめに -

最初のステップとなる「学習済みのDeep Learningモデルをpre-train modelとして自分が用意した画像に対して学習」する時のメモ。

多分これが一番簡単だと思います。

 

- 準備 -

バージョンはtorch (0.4.1)、torchvision (0.2.1)の話をする。

pip install torch
pip install torchvision

学習済みモデルはpytorchの画像向けパッケージとなるtorchvisionでもサポートされている。
torchvisionで扱えるモデルは以下(2018/09/15 時点)

  • AlexNet
  • VGG
  • ResNet
  • SqueezeNet
  • DenseNet
  • Inception v3

参考:torchvision.models — PyTorch master documentation

 
最近はすごいスピードで他の高精度モデルや、仕組みの違う学習済みモデルが出てきてるので、pytorchのpretrainモデルを使う場合のサポートpackageを使うと良さそう。
以下のどちらでも良い。
GitHub - creafz/pytorch-cnn-finetune: Fine-tune pretrained Convolutional Neural Networks with PyTorch
GitHub - Cadene/pretrained-models.pytorch: Pretrained ConvNets for pytorch: NASNet, ResNeXt, ResNet, InceptionV4, InceptionResnetV2, Xception, DPN, etc.

pip install cnn_finetune
pip install pretrainedmodels

上記のtorchvisionに加えて以下が簡易に扱えるようになる(2018/09/15 時点)

  • ResNeXt
  • NASNet-A Large
  • NASNet-A Mobile
  • Inception-ResNet v2
  • Dual Path Networks
  • Inception v4
  • Xception
  • Squeeze-and-Excitation Networks
  • PNASNet-5-Large
  • PolyNet

モデルは「どのモデルがどんな感じの精度なん?」というのは以下READMEにimagenetでの精度比較表が載ってるので参考に。
https://github.com/Cadene/pretrained-models.pytorch#evaluation-on-imagenet

それぞれのモデルへのリンクも以下に存在する。
https://github.com/Cadene/pretrained-models.pytorch#documentation


 

- pretrainモデルで簡易に学習する -

cnn_finetuneの方がちょっとばかり楽できるので今回はcnn_finetuneベースで薦める。

分類するクラス数とモデルの入力となる画像サイズ、pretrained=Trueを指定して実行すると、学習済みデータがダウンロードされて読み込まれる。

from cnn_finetune import make_model
import torch

# cnn_futureを使う場合
model = make_model('pnasnet5large', num_classes=2, pretrained=True, input_size=(384, 384))
# pretrainedmodelsを使う場合
# model = pretrainedmodels.__dict__[model_name](num_classes=10, pretrained='imagenet')

# 'cuda' or 'cpu'
device = torch.device('cuda')
model = model.to(device)

  
学習のためのデータセットとしては、header付きのhogehoge.csvなる「学習画像の名前(ImageName)」と「学習画像に対するラベル(ImageLabel)」がある想定。こんなん。

ImageName,ImageLabel
0001.jpg,1
0002.jpg,1
0003.jpg,0
0004.jpg,1

また「学習画像は/hogehoge/train/なる配下に全て入っている」想定。

学習にはDataset、DataLoaderというクラスを利用する必要がある。
今回は雑に2クラスのデータセットを想定して書く。

# must: pip install pillow, pandas
from PIL import Image
from torch.utils.data import Dataset
import pandas as pd
import os
import torchvision.transforms as transforms


class MyDataSet(Dataset):
    def __init__(self, csv_path, root_dir):
        self.train_df = pd.read_csv(csv_path)
        self.root_dir = root_dir
        self.images = os.listdir(self.root_dir)
        self.transform = transforms.Compose([transforms.ToTensor()])
        
    def __len__(self):
        return len(self.images)
    
    def __getitem__(self, idx):
        # 画像読み込み
        image_name = self.images[idx]
        image = Image.open( os.path.join(self.root_dir, image_name) )
        image = image.convert('RGB') # PyTorch 0.4以降
        # label (0 or 1)
        label = self.train_df.query('ImageName=="'+image_name+'"')['ImageLabel'].iloc[0]
        return self.transform(image), int(label)

train_set = MyDataSet('hogehoge_train.csv', '/hogehoge/train/')
train_loader = torch.utils.data.DataLoader(train_set, batch_size=32, shuffle=True)

シンプルに画像とラベルを返す__getitem__とデータの大きさを返す__len__を実装するだけです。

バッチサイズは画像の大きさに合わせて調整すると良いです。
デカすぎると皆大好き「RuntimeError: cuda runtime error: out of memory」になります。

 
Optimizerを選ぶ。学習済みモデル使うならSGDでええんちゃうんと思ってるけどベストプラクティスは謎。

import torch.nn as nn
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

 
これで後は学習回すだけ。

import datetime

def train(epoch):
    total_loss = 0
    total_size = 0
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        total_loss += loss.item()
        total_size += data.size(0)
        loss.backward()
        optimizer.step()
        if batch_idx % 1000 == 0:
            now = datetime.datetime.now()
            print('[{}] Train Epoch: {} [{}/{} ({:.0f}%)]\tAverage loss: {:.6f}'.format(
                now,
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), total_loss / total_size))

for epoch in range(1, 10 + 1):
    train(epoch)

以下examplesを参考にした。
https://github.com/creafz/pytorch-cnn-finetune/blob/master/examples/cifar10.py


 

- modelを保存する -

state_dictはモデルの構造だけ保存。
普通にsaveするとGPU等device関連情報も一緒に保存するため、別環境で動かす時面倒らしい。

torch.save(model.state_dict(), 'cnn_dict.model')
torch.save(model, 'cnn.model')

参考:https://pytorch.org/docs/master/notes/serialization.html


 

- predictする -

モデルを読み込む。
modelをそのままsaveした場合はloadで簡易に読み込めるが、state_dictした場合は以下のように。

import torch
from cnn_finetune import make_model

# モデル定義
model = make_model('pnasnet5large', num_classes=2, input_size=(384, 384))
# パラメータの読み込み
param = torch.load('cnn_dict.model')
model.load_state_dict(param)
# 評価モードにする
model = model.eval()

 
テスト時もDataLoaderが必要になるが、今回は上記train時に作成したMyDataSetクラスをそのまま使う。

test_set = MyDataSet('hogehoge_test.csv', '/hogehoge/test/')
test_loader = torch.utils.data.DataLoader(test_set, batch_size=32)

 
torch.no_gradとした上でmodelにデータを入力するだけ。
皆大好きclassification_reportを出す。

# must: pip install scikit-learn
from sklearn.metrics import classification_report

pred = []
Y = []
for i, (x,y) in enumerate(test_loader):
    with torch.no_grad():
        output = model(x)
    pred += [int(l.argmax()) for l in output]
    Y += [int(l) for l in y]

print(classification_report(Y, pred))

出力はSoftmax使えばクラス数分 [クラス1の予測値, クラス2の予測値, ...] となってるので、argmax取ってやれば予測クラスを出すことができる。


 

- おわりに -

最近インターン生にオススメされてPyTorch触り始めて「ええやん」ってなってるので書いた。

ちょっと複雑なモデル書く時の話や torch.distributed 使う話も気が向いたら書くと思うけど、TensorFlow資産(tensorbordとか)にも簡単に繋げられるし、分散時もバックエンド周りを意識しながら書きやすいので結構良い感じする。


 
追記:2018/09/16

以下の部分でtransforms内のメソッドを利用して入力正規化とかaugumentationも出来るのですが、今回省いています。

self.transform = transforms.Compose([transforms.ToTensor()])

そしたら「正規化はpretrainに合わせてやった方がいいのでは?」みたいな話がTwitterで発生しました。

結論としては、多分やったほうが良いみたいになったのですが、確証が今の所ないのでtransforms.Normalizeとかtransforms内の色々試して比較すべきみたいな感じです。

もしpretrainに合わせて正規化をしたい場合は以下にmean, stdが載っているので使うと良いと思います。
pretrained-models.pytorch/pretrainedmodels/models at master · Cadene/pretrained-models.pytorch · GitHub

 

xonshにおけるpexpectを利用した対話コマンド自動化

- はじめに -

shellでsshのパスワード入力などの対話行動を自動化するには、expectコマンドを使うのが一般的である。

対して、xonshではPythonの記述やパッケージの利用が可能となる。

本記事は、pythonを利用しながら筆者が利用しているssh周りのコードを例にとってshell対話自動化を行う際のtipsをまとめるものである。


 

- pexpect -

pythonを利用して、対話自動化を行うパッケージとしてpexpectがある。

github.com

pipで導入する。

pip install pexpect

 

xonshでpexpectを利用する

xonshrcにでもそれっぽく書く。注意すべきは、pexpectの画面サイズで、デフォルトでは画面サイズが80, 120でセッションが作られるため、ssh先でtmuxやvimを開くと画面が小さいみたいな現象が発生する(iterm2やプロンプトで確認)。

sshとscpのパスワード自動入力の例を以下に示す。

import pexpect
import curses

passwd = 'password'

def _ssh(x):
    # 画面サイズ指定しながらssh
    p = pexpect.spawn("ssh " + x[0])
    curses.setupterm()
    term_lines = int(curses.tigetnum("lines"))
    term_cols = int(curses.tigetnum("cols"))
    p.setwinsize(term_lines,term_cols)
    # password
    p.expect('ssword:*')
    p.sendline('passwd')
    # 操作できるように
    p.interact()
aliases["ssh"] = _ssh


def _scp(x):
    p = pexpect.spawn("scp " + x[0] + ' ' + x[1])
    p.expect('ssword:*')
    p.sendline('passwd')
    p.interact()
aliases["scp"] = _scp

パスワードだけなら普通に暗号鍵作っておけば問題ないが、キーパスフレーズや後述する2段階認証も同じ方法で取り繕える。

 

xonshでpxsshを利用する

最初にpexpectを利用した方法を記載したが、pexpectにはpxsshなるssh専用モジュールが付いてくる。
pxssh - control an SSH session — Pexpect 4.6 documentation


sshのみであれば、簡易に以下のように実装できる。

from pexpect import pxssh

username = 'root'
passwd = 'password'

def _ssh(x):
    p = pxssh.pxssh()
    p.login (x[0], username, password)
    p.prompt()
aliases["ssh"] = _ssh

簡易に書ける分、2段階認証や踏み台等の特殊な環境に対応するのが少しばかり難しいため、筆者は前述のpexpect実装を利用している。


 

ssh configと周辺関数

筆者のssh configは大体以下のように書かれている

HostKeyAlgorithms +ssh-dss
AddKeysToAgent yes
host *
    ForwardAgent yes

// 踏み台なし
Host hoge
    HostName hoge.jp

// 踏み台あり
Host piyo
    HostName piyo.aws.piyopiyo
    IdentityFile ~/.ssh/id_rsa
    LocalForward 8888 localhost:8888
    ProxyCommand ssh step.piyo.co.jp ncat %h %p
    IdentitiesOnly yes

これで踏み台にKeyを送りつつ、主にポートフォワーディングして作業を行っている形である。

ssh piyo

仕事柄、数十のサーバにアクセスするので、「Host名教えてくれ」と言われた時のためにxonshrcに以下を記載している。

def _get_host():
    cat ~/.ssh/config | grep Host
aliases['get_host']=_get_host

Hostの略称とポートを一括で引っ張ってきている。


また、ssh先のホスト名をpecoに流し、ctrl+sでssh先をよしなに検索できるようにしている。

import re
from prompt_toolkit.keys import Keys
from prompt_toolkit.filters import (Condition, IsMultiline, HasSelection, EmacsInsertMode, ViInsertMode)

# sshをpecoに流す
def get_ssh():
    items = re.sub(r'(?i)host ', '', $(cat ~/.ssh/config /etc/ssh/ssh_config | grep -i '^host'))
    return items

@events.on_ptk_create
def custom_keybindings(bindings, **kw):
    # ptk 2.xでは不要
    handler = bindings.registry.add_binding

    # ptk 2.xでは @bindings.add('c-v') とする
    @handler(Keys.ControlS)
    def select_sh(event):
        hosts = '\n'.join([x for x in get_ssh().split('\n') if x!='*' and 'HostKey' not in x])
        selected = $(echo @(hosts) | peco)
        if selected:
            if 'kukai' in selected:
                event.current_buffer.insert_text('ssh ' + selected.strip())
            else:
                event.current_buffer.insert_text('ssha ' + selected.strip())


xonshrcの書き方については以下に書いてある。
vaaaaaanquish.hatenablog.com


 

- パスワード管理と2段階認証 -

上記方法だとxonshrcにパスワードやユーザ名を書くことになってあまり嬉しくない。
もちろん一般的に鍵を用意してAddKeysToAgentする方法を取るが、それでもパスフレーズを定期的に要求されるわけで、それらを全ての環境でよしなにやりたいのでパスワード管理ソフトウェアに一任する。

筆者は普段からパスワード管理に1password、2段階認証管理にAuthyを利用しており、それぞれCLIから呼び出して扱えるようにしている。
(※ 1passwordもAuthy同様に2段階認証管理が行えるが、一身上の技術的都合によりAuthyを利用している)

若干設定して使えるようになるまでがダルいので以下記事に設定を託す。

- 1password
support.1password.com
dev.classmethod.jp
- Authy
qiita.com
 

1password-cli

1password-cliの設定が終わってopコマンドが利用できるようになっていれば、1passwordにCLIからログインした上で、以下のように記載する事でssh先のHost名と同じパスワードを取得しssh時に利用できる。後はよしなにパスフレーズを要求されたら〜とかHost死んでたら〜みたいな分岐を好みで追加していく。

import pexpect
import curses

def _ssh(x):
    # 1passwordからパスワード取得 
    PASSWORD = $(op get item x[0] | jq '.details.fields[] | select(.designation=="password").value')
    p = pexpect.spawn("ssh " + x[0])
    curses.setupterm()
    term_lines = int(curses.tigetnum("lines"))
    term_cols = int(curses.tigetnum("cols"))
    p.setwinsize(term_lines,term_cols)
    p.expect('ssword:*')
    p.sendline(PASSWORD.strip())
    p.interact()
aliases["ssh"] = _ssh

 

Authy

Authyで生成されるtokenをmfacodegenコマンドで取得できるよう設定していれば、以下のようにVerification codeとして入力する事ができる。2段階認証が必要なsshもこれで通す。

import pexpect
import curses

def _ssh(x):
    # 二要素tokenをクリップボードにコピー
    mfacodegen -c -s HOGE
    p = pexpect.spawn("ssh " + x[0])
    curses.setupterm()
    term_lines = int(curses.tigetnum("lines"))
    term_cols = int(curses.tigetnum("cols"))
    # クリップボードからtoken取得
    PS = $(pbpaste)
    p.expect("Verification code:")
    p.sendline(PS)
    p.interact()
aliases["ssh"] = _ssh

 

おわりに

大体これでなんとかなると思います。

pexpectもexpectも毎回書き方忘れるけど、結構便利なので使うと良いです。

以下も参考になります。
qiita.com

 

Python製シェルxonshを半年使った所感や環境設定のまとめ

- はじめに -

Pythonにおけるpython-prompt-toolkit(以下ptk)を使って作られたシェルである「xonsh」を同僚にオススメされて、大体半年くらい使ったので設定とかxontribとか所感を晒していく。

前半でxonshのメリット、デメリットの概要を記載し、後半に自身が利用する設定やxontribについて記載する。

この記事は、xonsh導入に至る人もしくは、環境設定について広く知りxonshを扱える人を増やす事が目的である。


追記:2018/07/18
xonsh 0.7.0が出ていますが、現在prompt-toolkit2.0の各機能対応中という感じです。
これは、ptk1.x -> 2.xにおいて結構な破壊的変更があるためです。
現状はpip install xonshする時に
pip install prompt_toolkit==1.0.15
pip install xonsh==0.6.10
としておくのが良いと思います。
この記事の内容も0.6.10のものであり、0.7.0でptk2.0を利用した場合にxonshrcが動作しない箇所があります。

追記:2018/08/03
xonsh 0.7.2になり大分ptk周りのバグが気にならなくなってきました。
jupyterが未だptk2.xで起動しない問題がありますが、そちらが気にならなければ、もう普通にpipでxonsh入れても良い頃合いだと思います。
jupyterで普段作業する方は先にpip install prompt_toolkit==1.0.15

追記:2018/09/22
ipythonもjupyter周りもptk2.xになったので、これからは心おきなく以下の1コマンドでOK!
pip install xonsh


追記:2018/12/05
アドベントカレンダー2018で書いたので以下も参考に

バージョンの違いに対応したり、xonshのprint出力color設定を設定したりする記事:
vaaaaaanquish.hatenablog.com

xonshの2018年のptk, カラー, 設定, bug fixなどの変遷:
vaaaaaanquish.hatenablog.com

2018年最後のxonshrc:
vaaaaaanquish.hatenablog.com




 

- xonshについて -

xonshはPythonのptkを用いて作成されたShellである。
bash, zshを除くシェルで代表的な物としてfishがあるが、「fishに比べ、よりPythonに寄ったシェル」という位置付けになる。

github.com

読み方についてはゾンシュ、カンク、コンク、エックスオンエスエッチ等諸説あるが、公式ページでランダムに表示される文言の中に [コンシュ] が入っている事からコンシュが正解であると思われる。
f:id:vaaaaaanquish:20180520181037p:plain

導入は以下だけ

pip install xonsh

追記 2018/07/03:
今ちょうどxonsh 0.6.8でprompt-toolkitが2.0対応中なのでpip install xonshする前にpip install prompt_toolkit==1.0.15 しておくと良いです
バージョンの違いを鑑みるとbrew等でなはくpipが良いと私は思います

 

xonshの良さ

筆者は、何周か回って「xonshの良さ」は端的に以下に集約されると考えている。

  1. python資産の活用とワンライナーシェルコマンドからの開放
  2. defaultで利用可能なリッチな補完と履歴
  3. 中身も全てPythonでOSに依存しない高速な環境構築が簡易

 
1の「python資産の活用とワンライナーシェルコマンドからの開放」は最も筆者に得を与えている。「シェル芸」と呼ばれる技術は、テクニカルで高速で最も我々の生活に馴染んでいるものであるが、時に複雑な処理を書きたい場合において人類にはやや理解し難い構文が発生してしまう。何より、各コマンドを中度以上に知る人間が読まなければ、何をどういう原理で処理しているのかワンライナーの理解でさえ時間を要する。

xonshでは以下の例のように、既存のシェルコマンドの入出力を利用しながら、Pythonで行いたい処理に流したり、逆流する事も可能である。もちろん、これらを関数としてコマンド化する事もxonshであれば簡易に行える。

# lsコマンドの結果を利用しながらpythonでの処理を行う例
for x in ![ls ~/work/]:
    print(x)
# > hoge.txt
# > piyo.md
# > ....

# pythonスクリプトの結果をシェルコマンドに流す例
import sys
echo @(sys.version_info) | sed 's/ /\\\x0A/g'
# > 3
# > 6
# > 3
# > candidate
# > 1

日々Pythonを書き慣れている人間であれば、manやhelpを読み込む必要無く複雑な処理を記述し、スクリプトやコマンドとして再利用する事ができるであろう。

 
2の「defaultで利用可能なリッチな補完と履歴」はxonshの魅力の1つである。コマンドの補完は履歴からhistoryからの補完やMan-pageを参照した補完が(ptkがinstallされていれば)defaultで扱える。ちなみにこの補完は、fishを参考に作られている。
f:id:vaaaaaanquish:20180622142703g:plain:w350

 
3の「中身も全てPythonでOSに依存しない高速な環境構築が簡易」が筆者が最もxonshを多く利用している理由である。永らくzshを利用してきた筆者だが、様々な管理ソフトウェア、パッケージをインストールして利用するため、環境構築スクリプトは幾度もの修正によって異様な形相となっている。oh-my-zsh、Antigen、prezto、zplugを経てこそ短くなったrcファイルも、管理できているとは言えないようなものだ。

xonshは、本体がpythonパッケージ管理であるpipでインストールできるだけでなく、拡張となるxontribもpipで導入できる。「サーバにDockerは導入できないがpythonは入っている」といった状態であれば、作業用Dcokerコンテナが無くともpipであらかたの環境が整う。これは、初学者にも効能があると考えていて、簡易な導入で高度な機能が使えるのCLIというのは魅力になり得る。

また、シェル自体もPythonである故、コマンドだけでなくシェル自体の動作もOverrideがPythonによって記述でき、configもrc fileもPythonで記述できる。Pythonを習熟していれば、CLIを習熟できるようなものだ(言い過ぎ)。


余談だが、bashやfishとの比較に関しては以下のツイートは結構すき。
https://twitter.com/LinSocist/status/971904816505958400

メイン開発者のscopatzは強めの比較がすきっぽい。


まあ http://xon.sh/#comparison の比較もなかなかパワフルだしそういうものか。


 

xonshのまだまだなところ

xonshがまだまだな点も同様に挙げておく。

  1. 完全なbash互換ではない
  2. 特定条件下で落ちる
  3. Pythonが生理的に無理な人にはオススメできない

1つに、完全にbash互換ではない事が挙げられる。コマンドはPythonのサブプロセスを利用して走るため権限的に触れない部分があったり、Pythonとの競合部分故OSコマンドやブレース展開は正しく扱えない場合がある。Hacker News内のXonsh, a Python-ish, Bash-compatible shell language and command promptでも、fishのリードエンジニアとxonshの作者が「bash互換と言うのはやめた方がええんちゃうか」と話しており、完全なPOSIXサポートシェルではなく「Pythonが扱える者であれば、気軽に高度なシェル操作が行える」辺りを目指しているようだ。

 
そして「たまに落ちる」。普段の作業中に落ちる事はほぼ無いと言っていいのだが、設定書き換えてロードした直後や、xonsh上でゴリゴリにメモリを使うとシェル自体が落ちてしまう。同僚はxonshを数週間使い続けるとPC全体が重くなってしまうとも言っており、Pythonのガベージコレクタや膨大になった履歴jsonとの戦いがたまに見られる。こちらに対しては後述するが、筆者は簡易に設定したzshの上でxonshを走らせ、いつ死んでも良いようにしている。

追記2018/06/26:xonsh 0.6.7でpromptがゾンビ化してCPU持っていく現象が解消されたのでだいぶ良くなったと思います

 
加えてPythonシェルが生理的に無理という人にはオススメできない。ただ、こればかりはどうしようもない。Pythonはそもそもシェル向きの言語かと聞かれれば筆者も口を噤むし、Pythonじゃなくて〇〇だったら?と聞かれれば簡単に心が揺れるだろう。コマンドシェル - ArchWikiですら、xonshはレトロなシェルとして紹介されているし、もうすまんそれならzshを使ってくれという感じである。


 

- 筆者のxonsh環境 -

以降は、筆者が利用しているxonsh環境について記載する。
2018/06/22、xonsh 0.6.1 時点であり、絶対最強という訳でもないと思う。

 

zshの上で動かす

これ大事。xonshが死んで作業できなくなったら終わり。
もちろんbashでも良い。

色々試した結果、~/.zshrcの最後にxonsh起動をコマンド書いてデフォルトシェルをzshにする形に落ち着いた。起動が若干遅くなるが、どうせシェルは常時起動しているので心を広く保つ事でカバーしている。

Mac向けの設定なのでPATHはよしなに。bashだとalias以降は動くはず。

# 人類最低限zshrc
autoload -U compinit; compinit
setopt auto_cd
setopt auto_pushd
setopt pushd_ignore_dups
setopt histignorealldups
setopt always_last_prompt
setopt complete_in_word
setopt IGNOREEOF
export LANG=ja_JP.UTF-8
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
autoload -Uz colors
colors
alias l='ls -ltr --color=auto'
alias ls='ls --color=auto'
alias la='ls -la --color=auto'
PROMPT="%(?.%{${fg[red]}%}.%{${fg[red]}%})%n${reset_color}@${fg[blue]}%m${reset_color} %~ %# "


# vim
export VISUAL='/usr/local/bin/vim'

# pyenv
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

# gcloud
if [ -f '/Users/shukawai/google-cloud-sdk/path.zsh.inc' ]; then source '/Users/shukawai/google-cloud-sdk/path.zsh.inc'; fi
if [ -f '/Users/shukawai/google-cloud-sdk/completion.zsh.inc' ]; then source '/Users/shukawai/google-cloud-sdk/completion.zsh.inc'; fi

# xonsh起動
alias x='xonsh'
x

自分が作業していく中で見たxonshから利用しづらいものとしてpyenv、gcloudコマンドがある。
こればかりは仕方ないのでzshで操作するようにしている。

また、LD_LIBRARY_PATHPATHもxonshrcではなく上層のシェルで書いておかないと動かなかったりする(動く時もある)。
$PATH.append("/usr/local/bin")のようにxonshrcに記載するのが正解だと考えているが、試してできた記憶は今の所ない。


なんか「他にこれもダメだった」というのがあれば、はてブTwitterにでも書いてくれればエゴサします。


 

xonshrc

設定ファイル。~/.xonshrcに書く。
hoge.xshファイルを作って~/.xonshrc内でfrom hoge import *としてやる事でファイル分割もできる。
~/.config/xonsh/rc.xshでも多分大丈夫。
最低限な部分だけ以下に抜粋。

python環境以外に特別に入れているのはvimとpeco。vimはよしなに。
pecoはMacならbrewLinuxなら以下のスクリプトを実行するだけですのでgoとか入れる必要なしです。Windowsgithubのreleaseからバイナリ取ってきてPATH通せば動く。
Linux に最新版の peco をインストールするシェルスクリプト - Qiita

vim, pecoとpipでインストールできるものを導入した上で下記のようにしています。

# -*- coding: utf-8 -*-
# エディタ
$EDITOR = '/usr/local/bin/vim'
$VISUAL = '/usr/local/bin/vim'
# vi風の操作がシェル上で直感的でないのでFalse
$VI_MODE = False
# 補完をEnterで直接実行しない
$COMPLETIONS_CONFIRM = True
# Ctrl + D で終了しない
$IGNOREEOF = True
# tabではなく空白4つ
$INDENT = "    "
# 補完時に大小区別しない
$CASE_SENSITIVE_COMPLETIONS = False
# 連続重複コマンドを保存しない
$HISTCONTROL = "ignoredups"
# 括弧を補完
$XONSH_AUTOPAIR = True
# ディレクトリ名を入力でcd
$AUTO_CD = True
# エラー全て吐くように
$XONSH_SHOW_TRACEBACK = True
# サブプロセスタイムアウトのメッセージ抑制
$SUPPRESS_BRANCH_TIMEOUT_MESSAGE = True
# キー入力即評価(サイコー)
$UPDATE_COMPLETIONS_ON_KEYPRESS = True
# プロンプトの表記
$PROMPT = "{INTENSE_RED}{user}{INTENSE_GREEN}@{INTENSE_BLUE}{hostname}{INTENSE_YELLOW} [ {cwd} ] {GREEN}$ "
# lsコマンドの結果の見た目
$LS_COLORS="di=34:ln=35:so=32:pi=33:ex=31:bd=46;34:cd=43;34:su=41;30:sg=46;30:tw=42;30:ow=43;30"

# alias
# OS判別をplatformで
import platform
if platform.system() == 'Darwin':
    # Mac(iTerm2のimgcat)
    aliases['icat'] = 'imgcat'
else:
    aliases['f'] = 'free -h'
    aliases['wf'] = 'watch free -h'
    aliases['tm'] = 'tmux'
aliases['ls'] = "ls --color=auto"
aliases["l"] = "ls -l"
aliases["lf"] = "ls -f"
aliases["ld"] = "ls -d"
aliases["la"] = "ls -la"
aliases["ll"] = "ls -l"
aliases["v"] = "vim"
aliases["vi"] = "vim"
aliases["vx"] = "vim ~/.xonshrc"
aliases["vz"] = "vim ~/.zshrc"
aliases["vv"] = "vim ~/.vimrc"
aliases["vs"] = "vim ~/.ssh/config"


# 履歴をpecoに流す
# https://qiita.com/riktor/items/4a90b4e125cd091a9d07
# pecoのinstall : https://qiita.com/ngyuki/items/94a7e638655d9910971b
import json
from collections import OrderedDict
from operator import itemgetter
def get_history(session_history=None, return_list=False):
    hist_dir = __xonsh_env__['XONSH_DATA_DIR']
    files = [ os.path.join(hist_dir,f) for f in os.listdir(hist_dir)
              if f.startswith('xonsh-') and f.endswith('.json') ]
    file_hist = [ json.load(open(f))['data']['cmds'] for f in files ]
    cmds = [ ( c['inp'].replace('\n', ''), c['ts'][0] )
                 for cmds in file_hist for c in cmds if c]
    cmds.sort(key=itemgetter(1))
    cmds = [ c[0] for c in cmds[::-1] ]
    if session_history:
        cmds.extend(session_history)
    # dedupe
    zip_with_dummy = list(zip(cmds, [0] * len(cmds)))[::-1]
    cmds = list(OrderedDict(zip_with_dummy).keys())[::-1]
    if return_list:
        return cmds
    else:
        return '\n'.join(cmds)

# sshをpecoに流す
def get_ssh():
    items = re.sub(r'(?i)host ', '', $(cat ~/.ssh/config /etc/ssh/ssh_config | grep -i '^host'))
    return items


# キーバインド
from prompt_toolkit.keys import Keys
from prompt_toolkit.filters import (Condition, IsMultiline, HasSelection, EmacsInsertMode, ViInsertMode)
@events.on_ptk_create
def custom_keybindings(bindings, **kw):
    # ptk 2.xでは不要
    handler = bindings.registry.add_binding

    # ptk 2.xでは @bindings.add('c-v') とする
    # コマンド入力中にctrl+vでvim編集
    @handler(Keys.ControlV)
    def edit_in_editor(event):
        event.current_buffer.tempfile_suffix = '.py'
        event.current_buffer.open_in_editor(event.cli)

    # ctrl+rで履歴をpecoに流して検索
    @handler(Keys.ControlR)
    def select_history(event):
        sess_history = $(history).split('\n')
        hist = get_history(sess_history)
        selected = $(echo @(hist) | peco)
        event.current_buffer.insert_text(selected.strip())

    # ctrl+sでssh先をpeco
    @handler(Keys.ControlS)
    def select_ssh(event):
        hosts = '\n'.join([x for x in get_ssh().split('\n') if x!='*' and 'HostKey' not in x])
        selected = $(echo @(hosts) | peco)
        if selected:
            event.current_buffer.insert_text('ssh ' + selected.strip())


# 直近のxonshjobころすマン
# https://github.com/zardus/xonshrc/blob/master/xonshrc
def _kill_last(args, stdin=None):
	if __xonsh_active_job__ is None:
		print("No active job. Aborting.")
		return
	cmd = 'kill %s %d' % (''.join(args), __xonsh_all_jobs__[__xonsh_active_job__]['pgrp'])
	os.system(cmd)
aliases['kill_last'] = _kill_last

# diskutil infoを見る
# https://github.com/asmeurer/dotfiles/blob/master/.xonshrc
def _free(args, stdin=None):
    disk_info = $(diskutil info /)
    return [i for i in disk_info.splitlines() if "Free" in i][0] + '\n'
aliases['fr'] = _free

# gc
import gc
def _gc(args, stdin=None):
    gc.collect()
aliases['gc'] = _gc

# ライブラリの実行時import
# https://vaaaaaanquish.hatenablog.com/entry/2017/12/26/190153
# xonsh上で使うときがありそうなライブラリはlazyasdで補完時、実行時に読み込み
from xonsh.lazyasd import lazyobject
import importlib
lazy_module_dict = {
    'sys': 'sys',
    'random': 'random',
    'shutil': 'shutil',
    'pd': 'pandas',
    'np': 'numpy',
    'requests': 'requests',
    'os': 'os',
    'plt': 'matplotlib.pyplot',
    'Path': 'pathlib.Path',
        }
for k,v in lazy_module_dict.items():
    t = "@lazyobject\ndef {}():\n    return importlib.import_module('{}')".format(k, v)
    exec(t)

設定可能な環境変数値は日本語含めて以下にまとめてあるが、ver6.0の時に書いたものなので、公式の http://xon.sh/envvars.html をウォッチしておくと良い。
vaaaaaanquish.hatenablog.com

またGithubでxonshrcで検索する他、Xonsh Advent Calendar 2017 - Qiita でも設定周りの記事を書いている人がいるので参考になると思います。xonshrc書く上でのオススメは以下辺り。
qiita.com
qiita.com

また手前味噌ですが、xonshの各種eventについても書いてます。
vaaaaaanquish.hatenablog.com

またまた手前味噌ですが、自分もxonshの記事結構書いています。PROMPTに画像出したりdatetime出したり、対話的な選択コマンド作ったり、matplotlibで遊んだりしています。
xonsh カテゴリーの記事一覧 - Stimulator



 

xontrib

xonshを使う上で楽しい拡張について記載する。
基本的には以下にまとめています。
vaaaaaanquish.hatenablog.com


自分が使っている物だけ抜粋。

# xonshrc - xontrib

# Docker周りの補完 pip install xonsh-docker-tabcomplete
xontrib load docker_tabcomplete

# tracebackを省略し見やすくする pip install xontrib-readable-traceback
xontrib load readable-traceback
$READABLE_TRACE_STRIP_PATH_ENV=True
$READABLE_TRACE_REVERSE=True

基本的にdockerがが動作する環境では上記を利用している。
zコマンド、fzfコマンドのcontribも存在するが、環境によって日本語文字化けが発生する事があり、xonshrcにある通りpecoを利用している。
peco使い始めて以降zコマンドもあまり使ってないので省略。


最後のreadable-tracebackは手前味噌ですが、筆者が作っています。
GitHub - vaaaaanquish/xontrib-readable-traceback: xonsh readable traceback


 

config.json

より外側の設定ファイル。~/.config/xonsh/config.jsonに書く。
一応以下の記事にまとめてあるが、config.jsonはサポートされなくなり、xonshrcになったので不要。
Xonshのconfigを書く - Stimulator

WARNING! old style configuration
(/Users/xxx/.config/xonsh/config.json)
is no longer supported. 
Please migrate to xonshrc.

 

基本的な操作と編集

xonsh独特なやつだと補完候補を確定させたい時はctrl+e、ctrl+↑↓で複数行のコマンドまたいで履歴移動、複数行にまたがるコマンドを途中で実行したければesc, enterくらい覚えておけば良さそう。ctrl+cでコマンドキャンセル、ctrl+a,eで行頭尾に移動とか、ctrl+←→で移動とか基本的なやつは身体で感じて覚えるかカスタマイズする。

複数行の記述は基本vimでやっている。xonshrcでconsole入力中にctrl+vでvim呼び出せるようにしており、vimで複数行コマンドを編集、quitすると入力されている状態となる。
f:id:vaaaaaanquish:20180622202308g:plain:w350

また、以下のようにvimrcを設定し、SyntaxHighlightや補完をxonsh周りのファイルでも効くようにしておけば便利。

" vimrc
autocmd BufRead,BufNewFile *.xonshrc setfiletype python
autocmd BufRead,BufNewFile *.xsh setfiletype python

検索はctrl+rだが、peco等に流せるなら設定しておくと便利。


 

おわりに

xonshのメリット、デメリットの概要と、自身が利用する設定やxontribについて記載した。

Pythonは(無理な所ももちろんあるが)生理的に嫌いという程ではないので、できるだけ多くの人にxonsh使って欲しいし、みんなxontrib作って公開して欲しい。

ブログも書いて欲しい。

いやほんとマジで。


 

Pythonでyahoo画像検索した結果をimgcatに流して表示してURLをクリップボードにコピーするやつ

- はじめに -

近年では、チャットツールの発展が睦まじく、グループ内、企業内においてもチャットツールによるコミュニケーションが盛んとなっている。

チャットツールでのコミュニケーションにおいて欠かせないのが、画像によるハイコンテクストなやり取りである。
互いに同じレベルでの前提知識を持ち合わせている時、「有名な漫画のコマ」や「その場の状況を風刺する画像」を共有するコミュニケーションは、一般的な文字でのやり取りよりも時に頑強となる事が多い。

本記事では、有名な画像を検索しチャットに貼るために必要な工程である、画像検索、選択、コピーを簡略化するため、xonshを利用したPythonによる画像検索スクリプトを提示する。


つまるところコンソールだけで以下のように画像検索、URLコピーまでを扱えるようにする。
f:id:vaaaaaanquish:20180613153439p:plain
書いたスクリプトimgsearch_on_xonsh · GitHub



 

- 画像検索先 -

なんかGoogleは普通にクロールしようとするとすぐBANされるしAPIも画像検索は全然ダメなので、Yahoo!画像検索を利用する。


 

- yahoo画像検索の結果を取得する -

あるワードでYahoo!の画像検索をかけ、結果のURLを取得するPythonスクリプトを書く。
BeautifulSoupでHTMLを解析し、画像のURLだけ取ってくる。

from mimetypes import guess_extension
from urllib.request import urlopen, Request
from urllib.parse import quote
from bs4 import BeautifulSoup

def _request(url):
    # requestを処理しHTMLとcontent-typeを返す
    req = Request(url)
    try:
        with urlopen(req, timeout=5) as p:
             b_content = p.read()
             mime = p.getheader('Content-Type')
    except:
        return None, None
    return b_content, mime

def _yahoo_img_search(word):
    # yahoo!画像検索の結果から画像のURLのlistを返す
    url = 'http://image.search.yahoo.co.jp/search?n=60&p={}&search.x=1'.format(quote(word))
    byte_content, _ = _request(url)
    structured_page = BeautifulSoup(byte_content.decode('UTF-8'), 'html.parser')
    img_link_elems = structured_page.find_all('a', attrs={'target': 'imagewin'})
    # 順番守りつつset取る
    seen = set()
    seen_add = seen.add
    img_urls = [e.get('href') for e in img_link_elems if e.get('href') not in seen and not seen_add(e.get('href'))]
    return img_urls

print(_yahoo_img_search('hoge piyo'))

画像の検索結果のURLが取得できた。
これだけでも使えるけど、コンソールで選択的にコピーしたいのでもうちょっとがんばる。


 

- URLから画像をローカルに保存する -

取得したURLから、画像を一旦ローカルに保存してやる。

今回最終的な目的がコンソール上での処理なので、10枚の画像を想定。

この処理が最も重たくなるのでmultiprocessingでよしなにやる。

先述のスクリプトの関数を利用して以下。

import os
import sys
from PIL import Image
from multiprocessing import Pool
from multiprocessing import cpu_count

def _save_img(t):
    # (id+'\t'+url)を受け取って/tmp/img配下にid名で画像を保存する
    img, mime = _request(t.split('\t')[1])
    if mime is None or img is None:
        return ''
    # 拡張子
    ext = guess_extension(mime.split(';')[0])
    if ext in ('.jpe', '.jpeg', '.png', '.gif'):
        ext = '.jpg'
    if not ext:
        return ''
    # 保存
    result_file = os.path.join('/tmp/img', t.split('\t')[0] + ext)
    with open(result_file, mode='wb') as f:
        f.write(img)
    # multiprocessingからprintするにはこう
    sys.stdout.write('.')
    sys.stdout.flush()
    return result_file


def _img_d(word):
   # tmp/img無かったら作る
    if not os.path.exists('/tmp/img'):
        os.makedirs('/tmp/img')
    # wordに対して検索結果の画像URL取得し10個に絞る
    t = _yahoo_img_search(word)
    if len(t)<10:
        print('Not Found 10 IMG.')
        return [], []
    t = t[:10]
    # id付ける
    urls = [str(i)+'\t'+x for i,x in enumerate(t)]
    # multiprocessで画像ダウンロード
    cpu = cpu_count()
    p = Pool(cpu-1)
    a = p.map(_save_img, urls)
    p.close()
    print('saved images.')
    return a, t

上記スクリプトで/tmp/imgにYahoo!画像検索の結果が保存される。
あとでけす。


 

- 画像を並べてxonshを利用してimgcat -

プロンプト上で画像を表示するにはimgcatを使う。
私はiterm2を使っているので以下を利用する。
www.iterm2.com

Linuxでimgcatを使いたい場合は以下。
github.com


先述のスクリプトで保存された画像をそれぞれ読み込んで、サイズを250*250に加工する。
横に5つずつ並べてtmp/h.jpgを作成し、imgcatに流す。

imgcatに流した画像に対して、番号入力を待ち受ける形にし、番号に相当する画像URLをpbcopyによってPCのクリップボードにコピーする。

def _imgs(word):
    # 引数をつなげて画像検索、ダウンロード
    word = ' '.join(word)
    paths, urls = _img_d(word)
    if not paths or not urls:
        print('Bad input.')
    else:
        # 画像をリサイズし、縦2*横5で並べた画像を生成
        img = Image.new('RGB', (250 * 5, 500))
        for j in range(10):
            im = Image.open(paths[j]).resize((250, 250))
            if j >= 5:
                img.paste(im, ( 250*(j-5), 250))
            else:
                img.paste(im, ( 250*j, 0))
        img.save("/tmp/h.jpg")
        # imgcatで表示
        imgcat /tmp/h.jpg
        # input待ち受け
        img_num = input('image number(1~10) : ')
        # inputした結果に対応するURLをシェルコマンドを利用してクリップボードに流す
        try:
            echo -n @(urls[int(img_num)+1]) | pbcopy
        except:
            print('Bad input.')

aliases['imgs'] = _imgs

多分ここだけ編集すればxonshでなくてもPythonスクリプトとして使える。

 
このPythonによる画像加工とimgcat、pbcopy、peco辺りの組み合わせが癖になるので皆さんxonsh使うと良いと思います。

github.com



 

- おわりに -

ひどいもんだ

f:id:vaaaaaanquish:20180613172047p:plain


 
適当に書いたスクリプトなので以下gistにまとめた。
多分何かもっとスピード上げられるけど現状不満はないです。

SyntaxHighlightつけるために.xsh.pyになってるのでhoge.xshにしてxonshrcから from hoge import * してやれば良いです。
imgsearch_on_xonsh · GitHub

 

CLI上でtableを綺麗に見たくて各言語のtable表示について調べた

- はじめに -

データ分析、機械学習という仕事柄、csvやtsvを見る機会が多い。

処理する時は大体はpythonのpandasで読み込んで〜とするのだが、コンソール上で作業する時、どうしても「このcsvなんだっけ…」という事が発生する。

cat hoge.csv | head

等として上部だけ見たり、jsonならjqコマンドに流すのだが、いかんせん見栄えの問題で一瞬で判断出来なかったりする。
https://stedolan.github.io/jq/


そこで、table形式にしてコンソール上で表示して見れると嬉しいなと思って調べた事をまとめる記事。
端的に結果を最初に言うと、一般的なコンソールで使うだけならcsvkitなるCLIツールがかなり機能的で便利。tty-tableコマンドとしてjqコマンドのように扱えるtty-tableも綺麗に見れて良い。
CLIツールを今から自前で作るならGoでtablewriterpython上でも資産として利用するならpython-tabulateが便利そうという事が分かった。


追記:

ん〜!わかる!


 

- CLIにtable表示するパッケージのまとめ -

CLIツールとして有名どころではNode.js製のcli-tableがある。
また、cli-tableのAPIと互換性を持ちつつリメイクされたcli-table2や、BoxやProgressBar等の実装も含まれるpixl-cliを使う選択肢がある。

Goだとtablewriter、Pythonだとpython-tabulateが良い。

 

Node.js実装

cli-table

Node.jsで書かれたCLI向けのテーブルビジュアライザ。

github.com

npmが入ってればすぐインストールできる。

npm install cli-table

簡単に扱えて

var Table = require('cli-table');

var table = new Table({head: ['X (train)', 'Y (label)'], colWidths: [20, 20]});
table.push(['hogehoge', 'piyo1'], ['fugafuga', 'piyo2']);
console.log(table.toString());

f:id:vaaaaaanquish:20180503201742p:plain

中身でcolor.jsで色付けしてくれるので見やすい。
Horizontalにするにはlist、Verticalならdictを入れるだけ。
かなり簡易。

 

cli-table2

cli-tableの機能拡張リメイク

github.com

cli-tableのAPIを踏襲している上、セルごとに文字、色、paddingを設定できたり、行列にまたがるセルを生成できるのが強み。

npmで入れる

npm install cli-table2

基本的にはcli-tableと同じ感じで扱える。

var Table = require('cli-table2');
var table = new Table({head:['X', 'Y'], colWidths: [20, 20]});
table.push(
[{colSpan:2,content:'None'}],
['hogehoge','piyo1'],
['fuga', 'piyo2'],
['fugefuga2',{rowSpan:2,content:'piyo4\npiyo5'}],
['fugafuga3']
);
console.log(table.toString());

f:id:vaaaaaanquish:20180503205647p:plain

なんか趣向と外れてきてる気がするが面白い。

以下のAdvanced-usageに面白そうな例が沢山あるので参考に。
cli-table2/advanced-usage.md at master · jamestalmage/cli-table2 · GitHub

 

pixl-cli

上記2つとは少し違って、コマンドラインでNode.jsを作る時のユーティリティが詰まったやつ。
ユーザinputやgraphical info boxes(boxだけでなくtableも扱える)、ProgressBar等が含まれている。

github.com

boxもしくはtableを使うと良さげに表示できる

var cli = require('pixl-cli');
cli.print( cli.box("This is Example!! :D") + '\n');

f:id:vaaaaaanquish:20180503211430p:plain
良さそう。

var cli = require('pixl-cli');
var rows = [
    ['X (data)', 'Y (label)'],
    ['hoge', 'piyo1'],
    ['fuga', 'piyo2']];
cli.print(cli.table(rows)+'\n');

f:id:vaaaaaanquish:20180503211840p:plain

tableも複雑な事はできないが、list投げるだけなので所感は変わらず。

今回は割愛したがProgressBarも便利感あるし、Node.jsでCLIツール作る時はこれ使うと良さそう。

 

Ruby実装

terminal-table

ASCIIでtableをよしなに表示するやつ。

github.com

Rubyなのでgemでインストールする

gem install terminal-table

サンプルを適当に動かす。

require 'terminal-table'
rows = [['hoge', 1], ['piyo', 2], ['fuga', 'test']]
table = Terminal::Table.new :title => "Header Sample", :headings => ['X', 'Y'], :rows => rows
puts table

f:id:vaaaaaanquish:20180503213004p:plain

色も良いけど、やっぱりASCIIが可愛い。
よしなに文字幅等調整するようになってるので、tableのline変えても結構自由に動く。

でも個人的には、RubyCLIツール作る機会がほぼ無くなってしまったので使う機会があれば。

 

tty-table

rubyのTTY toolkit用に作られているtableビジュアライザ。

他のパッケージと違って、インストール時点でコマンドとして使えるのが良さ。
言語関係なくCLIで使うだけなら一番楽だと思う。

また、どの形式でtableを形成するか選べて、unicodeやASCIIが選べる。

www.npmjs.com

gemで入れる

gem install tty-table

TTY::TableやTTY::Table::Rendererが用意されているので、それぞれinitializeして表示。

require 'tty-table'
rows = [['hoge', 1], ['piyo', 2], ['fuga', 'test']]
table = TTY::Table.new rows
renderer = TTY::Table::Renderer::ASCII.new(table)
puts renderer.render

f:id:vaaaaaanquish:20180503214830p:plain

試しにASCIIで出したが良い感じ。

コマンドでも出せて、これが便利感がある。
例えば以下みたくcsvを表示してみる。

cat titanic.csv | head | tty-table

f:id:vaaaaaanquish:20180503215742p:plain
みんな大好きtitanicデータ。

コマンドだと--formatでjsonも指定できたり、--csv-delimiterで'\t'指定すればtsvをtable表示したりもできるので使い勝手がすごい。

 

Go実装

tablewriter

Goだとtablewriter一択だと勝手に思っている。

github.com

知らない間に以下も吸収されていた。
GitHub - crackcomm/go-clitable: Command line (ASCII) and Markdown table for Golang. You probably want to take a look at: https://github.com/olekukonko/tablewriter


機能面では一番使い勝手が良い。
CSVや他Separatorが扱えるだけでなく、行列内のセル、Markdown Formatまで実装されている。
Captionをつける機能もあって、多分CLIでtable作る時は一番便利に扱える。

go get  github.com/olekukonko/tablewriter

以下見たくgoファイル作ってツール化。

package main
import "os"
import "github.com/olekukonko/tablewriter"
func main() {
  data := [][]string{
    []string{"hoge", "String", "SAMPLE"},
    []string{"piyo", "Int", "10"},
    []string{"fuga", "Int", "10"},
  }
  table := tablewriter.NewWriter(os.Stdout)
  table.SetHeader([]string{"X", "TYPE", "VALUE"})
  for _, v := range data {
    table.Append(v)
  }
  table.Render()
}

f:id:vaaaaaanquish:20180503221950p:plain

inputにCSVも使える上、Docも充実。
tablewriter - GoDoc


自前でtable見るツール作るならベストだし、割りとGoでCLIツール作られている事が多くなってきたので絶対使いそう。

 

Python実装

python-prettytable

redhatのdprince氏が作っているprettytable。

github.com

READMEに気合いが入っていている。

pythonなのでpipで導入する。

pip install prettytable

READMEにはset_field_namesなるmethodがあると書いてあったが見当たらず以下のように

from prettytable import PrettyTable
x = PrettyTable()
x.field_names = ['X', 'Y', 'VALUE']
x.add_row(['hoge', 'piyo1', '1'])
x.add_row(['fuga', 'piyo2', '2'])
print(x.get_string())

f:id:vaaaaaanquish:20180503224618p:plain

prettytableの良いところとして、get_html_stringなるmethodがあり、html形式のtable取得ができるので、Webアプリ等に直接返す時にちょっと使えそう。

 

csvkit

CLIツールとして利用するなら最も機能が多い。
コマンドとして入るのですぐ使える。

github.com

pipで導入していく

pip install csvkit

コマンドとして使えるようになってるはず

csvlook titanic.csv

f:id:vaaaaaanquish:20180505103957p:plain

この他、csvgrepやcsvsort、csvjoin、csvstatといったサッとコマンドとして使いたい機能が全てまとまっている。

以下ドキュメントを見るのが最も分かりやすい。
csvkit 1.0.3 — csvkit 1.0.3 documentation

csvlook hoge.csv | head としたり、csvstat見たりすることで、やりたい事は大体実現されている気がする。
良い。

 

python-tabulate

多分多くのPythonパッケージではこれが利用されてると思う。

github.com

pipで導入していく

pip install tabulate

普通にリスト投げるだけで良いので扱いやすい点でユーザが多いのだと思う。

from tabulate import tabulate
table = [["hoge",1],["piyo",2],["fuga",3]]
headers = ["X (data)", "Y (label)"]
print(tabulate(table, headers, tablefmt="grid"))

f:id:vaaaaaanquish:20180503225745p:plain

outputの形式が充実していて、一般的なtableのgrid形式から、PHP MarkdownのpipeやEmacs org-mode、wikiLaTeX markup形式としても出せる。
また、入力としてpandasも対応している。
つよい。


 

でどれが良さそうなのよ

最初にも書いた通り、「すぐコマンドとして使いたい」ならcsvkitがベスト。
tty-tableも良い。cat csvしてtty-tableコマンドに流すだけ。

CLIツールを今から作るならtablewriterでコマンド作って流す形にするのが良さそう。
機能が一番充実しているし、Docも充実している。
Goは良いぞ。

python上でも資産として利用するならpython-tabulateが便利そう。
本当の事を言うと、コンソール上で使いたいというよりxonsh上で使いたいという気持ちが強いので、個人的にはこれを使っていく事になると思う。
python-tabulateにhtml形式のoutput実装してmergeしてもらえば今の所満足そう。

rubyのとnodeは使って見た上で保留。
pixl-cliはめっちゃ使えそうだけどCLIツール最近はGoで書いてて、Node.jsはもっぱらGCP周りで使うくらいなのでう〜ん…

他言語も保留。


 

おわりに

csv簡易table閲覧コマンド作ってxontribにしていくぞ!