Stimulator

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

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にしていくぞ!


 

MANABIYAで「AI屋さんの1日」なるタイトルで登壇した話とその内容

- はじめに -

以下、MANABIYA techなるイベント内のAIセッションにて登壇させて頂きました。

manabiya.tech

大きなスペースでフザけたタイトルで発表するという最悪さでしたが、満員になり立ち見状態でした。
ありがとうございました。


正当な方向性でいけば登壇スライドを公開して終わりなのですが、会社のアカウントでSlideShareにアップロードするという行為に宗教上耐えきれそうにないため、会社情報を含まない範囲でここに思い出と共に書き残す形にしようかと思います。


 

- 登壇内容 -

登壇では、以下3つをテーマに話をしました。

  • AI屋さんの定義、分類は?

  • 実際AI屋さんって何やってるの?

  • 上手くAIプロジェクトを回すには?

 

AI屋さんの定義と分類

AI屋さんとは、「セッションタイトルが ‘AI’ だったので私が仕方なく付けた名称」です。

そもそも私は、機械学習や統計モデルを少々取り入れて「AI技術」だとか「人工知能技術」「AI企業」「AI部門」と声高らかに語る団体は全て嫌いです。


2012年頃から日本にも「人工知能、AIブーム」と一般的に呼ばれる"波"が来ました。

2016年後頃になると、ブーム自体が一般的に広く認知され、多くの企業が「我が社もAIをやっている」等と言うようになりました。ちょうど認知されていくタイミングと私が就職活動に勤しんでいた時期が重なり「私は機械学習がやりたいだけだ!人工知能、AIという単語を出した企業は全部こちらからお断りだ!」と躍起になっていたのを今でも覚えています。


それから数年、人工知能、AI、データサイエンティスト、機械学習エンジニアといった単語が広く普及した今になってそれらから逃れ仕事をするというのは、現代で「そもそもオタクの語源は〜」と語りながらオタクを続ける程に生きづらいでしょう。

セッションでは、生きづらい人生の歩み方を否定している訳ではなく「こうなったら言葉を潤滑油として利用してやろう」という方向性で、それぞれの言葉を実務に埋め込みながらAI屋の仕事を詳しく世に広める話をつらつら行いました。


f:id:vaaaaaanquish:20180324172357p:plain
登壇スライドより AI屋さんの分類

最初に上記の図で、AI屋の仕事というのは以下の3つに分かれる話をしました。

この分類で大事な点は「それぞれ基盤となる技術は共通」「アウトプットが違う」という所です。

ここで分析の業務とは、主に「MAU、DAUやユーザの行動履歴から広告、CM等の施策の効果を定量化、推論」「市場を分析し次の行動決定を円滑に進める」「ABテストの効果分析」等があたります。分析業務におけるアウトプットは、企業で言えば「データを分析し可視化された結果で人や施策のアクションを決定する」事です。

一方モデリングの業務とは、「データを学習できるモデルを作成する」「学習したモデルのサービスイン」といった所を指します。モデリングのアウトプットは、「実際のサービスで稼働する」となってきます。

モデリング業務は、皆サービスのユーザとしては日々感じている場合が多く、より身近かもしれません。例えば、Webサービス等において「オススメ商品がサジェストされてくる」だとか「写真にタグを自動で付けてくれる」といった類の物です。


分析、モデリングの2つのアウトプットの違いを理解せず人を雇ったり、プロジェクトメンバーを配置してしまうと悲惨な事になりがちです。

広く言われる「データサイエンティストの書くコードは汚い」といった発言は、発言者が身を置く環境が「レベルが低い」か「AI屋の認識にズレがある」という所から来ていると私は考えています。

f:id:vaaaaaanquish:20180324190557p:plain
登壇スライドより AI屋さんの分類

研究業務はさておき、上記の図には、各AI屋さんのそれぞれの中身を少し詳しく書いています。
独断と偏見も混じっていますが、概ね正しいと自負しています(刺されそうです)。


1つ目のスライド図で、分析は「一般的にデータサイエンティスト」、モデリングは「一般的に機械学習エンジニア」のお仕事であると記載しています。2つ目のスライド図では「データサイエンティスト」「機械学習エンジニア」の位置は、それぞれ分析、モデリングに寄りつつも重なった位置に書いています。

"一般的に"という説明から2つ目のスライド図を示した通り、近年における「データサイエンティスト」「機械学習エンジニア」という言葉は、揺らぎが強く、分析、モデリング業務を多岐に行っている場合が多くなっています。
分析業務とモデリング業務のアウトプットはかなり違うのに…


アウトプットまでの課程が変われば、そこで利用するデータや手法、工数割り振り、考え方が変わってきてしまいます。
つまり「分析で出た結果が良いのでそのままサービスインしよう!」「モデリングで出来た物が良いので」というのはかなり厳しい訳です。
これは、分析、モデリングの工程における統計に基づくデータの加工や、機械学習モデルの選択が変わってくるからです。

業務においてプロジェクトを成功に導くには、「データサイエンティスト」「機械学習エンジニア」という言葉から更に深掘りし、どういったアウトプットを要求するかという点を考え、それぞれの業務の経験、得意不得意から適材適所に人を配置しなければいけません。



最後に研究業務ですが、Yahoo! Japanという企業を例に出すと、Yahoo! JAPAN研究所を作り「学会での論文発表」「大学との共同研究」「オープンデータ、ソフトウェアの公開」という業務を行っているようです。

f:id:vaaaaaanquish:20180324184638p:plain
Yahoo! Japan研究所論文発表数:research-lab.yahoo.co.jp/gaiyo/index.htmlより引用

企業で利用できるデータを用いた国内外のトップカンファレンスでの発表、オープンデータの公開といった業務以外にも多分社内向けの分析基盤を作ったりもしているでしょう。
参考:
ソフトウェア/データ - Yahoo! JAPAN研究所 - ヤフー株式会社
研究領域 - Yahoo! JAPAN研究所 - ヤフー株式会社


研究はモデリング、分析とは違い、直接的に事業に貢献できる物ではない場合が多い業務です。
アウトプットも「論文」「機械学習アルゴリズム」「分析ソフトウェア」等になってきます。


AI屋さんのアウトプット基準で分類を定義しました。
大事なのは研究、分析、モデリング全てにおいて機械学習、統計、自然言語処理、画像認識、音声認識、情報検索…といった専門分野が基盤にあるのに対して、アウトプットで見ると全く違うという点です。

皆さんが行いたい「AI」って何ですか?
専門性ももちろんなのですが、「データサイエンティスト」「機械学習エンジニア」という言葉から更に深掘りし、どういったアウトプットを要求するかという点を考え、それぞれの業務の経験、得意不得意から適材適所に人を配置しなければいけません。(大事なので2回言っています)


ただただ「できる機械学習エンジニアが欲しい!」と言ってる人は、「フロントエンド、バックエンドの何がやりたいのか定義せずJavaScriptエンジニアが欲しい!」と言っている人と同レベルです。


 

実際のAI屋さんの業務

AI屋さんの1日を見て、どのように業務を進めているかについても話しました。

実際に私の平均的な1日を図として示しています。

f:id:vaaaaaanquish:20180324191530p:plain:w400
登壇スライドより AI屋さんの1日

よくわからない人間とのミーティングや雑務を除いて、Qiitaやはてなブログでバズってくるような「機械学習ライブラリを使ってパラメータをイジって学習〜」「データを可視化〜」という部分はかなり上澄みで、実際の業務で使われる時間はどんどん少なくなっています。

いくつかの要因がありますが、主には以下です。


統計、機械学習において最も効果的なのが学習データ、タスクの修正であるという話は、AI屋さんの中では以前より存在するかなり当たり前の話です。

実際に学習データが綺麗になればなるほど、機械学習モデルの精度は上がっていきます。
また、精度が上がりきらなければタスク自体をより機械学習に適したタスクに落とし込むという作業も有効です。

例えば、私は前職現職でもひたすら学習用の画像を見て数日を過ごす事もありました。
また、タスク自体を変更するために、年度やユーザ、特定のルールに基づいてデータを収集しなおしたり、問題設定自体を見直すために正解ラベルが入ったExcelファイルと数週間戦う事もあります。

こういった「泥臭い作業」は、クラウドソーシングで〜という場合もあるにはありますが、基本的には統計、機械学習に関する知見が必要で「これは機械学習で判断できるであろう」という前提を持たなければ出来ない作業です。

加えて、そういった「泥臭くない作業以」はフレームワークやマシンの充実で自動化が進んでいます。
例えば、パラメータの調整手法の充実、機械学習ライブラリや可視化ツール、分散処理技術、GPUスパコンの存在がそれらを支えています。
Yahoo! Japanにはどうやらkukaiと呼ばれるスパコンもあるらしいです(https://about.yahoo.co.jp/pr/release/2017/06/19b/)。
(もちろんこれら基盤にお金が必要だったりという場合もありますがその話はここでは取り上げません)。


適切な環境やツール、手法さえ選び使う事ができれば、泥臭く最も効果のある作業に集中しアウトプットの質を上げる事が可能なのです 。
AI屋さんの仕事のサイクルというのは「泥臭い作業をしながら、泥臭くない作業をガンガン自動化して効率化していく」という所に尽きます。


作業の効率化の方法は、研究業務にも近く、研究業務のアウトプットが社内外の分析、モデリング業務を支えているというイメージです。


 

AIプロジェクトを上手く回すには

AIプロジェクトを回すコツを3つ紹介しました。

  • 使いたいのは誰か決める
  • 泥臭くない作業を自動化する
  • 要件を順に詳しく決定していく

上2つは、今までの話と関連しています。

分析、モデリング、研究とそれぞれ利用する技術は同じでも、アウトプットの形が違います。
「データサイエンティスト」「機械学習エンジニア」といった言葉も良いですが、より深めて見ないとプロジェクト毎倒れてしまいます。

また泥臭くない作業というのは積極的に自動化したい所です。
もちろん自動化しやすい最適なフレームワークやツール、手法の選定はAI屋さんが行うのですが、インフラ屋さんやDB屋さん、果てはハード屋さんの協力も必要でしょう。
時にはお金も必要でしょう。
「AI屋さんにGPU買ったけど日の半分画像フォルダとにらめっこしてる」という事が多くあると思いますが、そういった時に特に自動化を進められるような協力体制が出来ているとプロジェクト全体が良く回るようになるでしょう。


3つ目の要件の決定ですが、目的が決まる毎にAI屋さんと相談すると良さそうです。
大きな理由は以下の2つです。

  • ある目的のために集められたデータが
他目的に使えるとは限らない

  • サービス要件を満たせるかどうかわからない

例えば、泥臭く集めたデータというのはAI屋さんが目的に応じて特定のルールで集め、目的に応じてクレンジング、加工してある物である場合が多いです。
それらのデータをそのまま利用して、他タスクでも成果を出すというのは非常に困難です。
別の目的で利用した時に考えられる、エラーや課題を許容できる場合であれば良いですが、それらが想定しきれないようであればでデータ集めからやり直しとなってしまう場合もありますので相談はお早めに。

また目的によっては、せっかく作ったモデルがサービス要件を満たせないという事もあります。
シンプルな例としてDeep Learningがあります。
Deep Learningにおける巨大なニューラルネットワークモデルでは、GPU推論で数秒かかってしまう場合があります。毎日数万アクセスがあるサービスで、1ユーザの利用で数秒消費するというモデルを利用するのは、企業として相当な体力が必要です。AI屋さんの端くれである私としては、別モデルの利用、転移学習、枝刈り、蒸留…といった回避策が思い付くのですが、目的が曖昧であるとそれら選択肢から選ぶ事すらできません。
目的は徐々に相談しながら決めていくと、プロジェクト全体を見た時に成功しやすいでしょう。


登壇中も言いましたが、拡大解釈していくとこれらはAIプロジェクトに限った話ではない部分も多いかと思います。
しかしながら、泥臭い作業が多い等、AI屋さんならではの話も含んでいるため、それらを考慮し調整しながらサイクルを回す事で、プロジェクト全体が効率よく回るようになるでしょう。


 

- 登壇の感想とか -

実際の登壇はもう少し実例を出して話をしたとは言え、かなり広い話になってしまいました。
より詳しく技術の話をしても良かったのですが、会社のアレコレが面倒なのと、他登壇者、参加者の様子を見て、初めてAIという広い表題を扱いました。

個人的には今後の登壇について考えさせられる事が多く、良い経験となりました。
会社名義で登壇するのは面倒事多すぎるので、自身の技術を自身で強く磨いていかないとと思いました。


クロスセッションでは、AI関連の質問を受けて他登壇者とセッションしたのですが、夢も希望もない話をしてしまったので、夢と希望が持てる話もネタとして持っておかないとなと思いました。


 

- 他のAIセッションの登壇 -

他のAIセッションの登壇を紹介しておきます。
大体全部オススメです。
ブームもあってか他の部屋も盛況だったようです。

Aidemyのハンズオンは多分Aidemyを登録してやれよという事でしょう!
(あとフューチャーアーキテクトの登壇もあったような…)


 

- おわりに -

MANABIYAというイベントですが、最初に貰った資料がペラペラで信用できず、突然のクラウドファンディングで登壇予定の人がキレてTwitterで辞退宣言したり、連絡のやり取りがめちゃくちゃ遅かったり、懇親会の2次募集すると言ってされなかったりと、「本当にこれだけのエンジニア集めて成功に持っていけるのか・・・?」という不安がめちゃくちゃありました。

当日もバタバタしたスタッフとタイトなスケジュールが組まれていたのですが、何とか全てのタスクが回っていて、大炎上するような人は何とか居なかったようです。

イベント初回でこの規模を、レバレジーズという会社の規模で回しきったというのは、素直にすげえなあと思います。
元学校という会場で学祭のようなわちゃわちゃ感で、それなりのクオリティのイベントに収まっていました。
(クオリティが高いのは登壇者がすごいからか…)

 
このイベントが本当にteratailなるサービスの宣伝になっているのか本当に謎ですが、実際にフォロワーに声をかけてもらったり、AIセッション登壇者内でも交流する事ができ、自分としてはそれなりに良い時間を過ごさせて頂きました。





ありがとうレバレジーズ。ありがとうteratail。
teratail登録はしてみたけど、質問する事がないのでこれから考えます。


追記:
より正しい情報があった

追記2:
その場で質問を壁に貼って誰かが答える面白コンテンツが思い出深かったです。


 

xonshのPROMPTにdatetimeを表示する

- はじめに -

xonshで作業をしているとつい時間を忘れてしまうので、時間を表示してやるメモ。

POWERLINEで良くみるやつをxonshrcで実装。


 

- timeを表示する -

コンソール上の右側に表示するにはこんな感じ

from time import strftime
$RIGHT_PROMPT = lambda: strftime('[ %H:%M:%S ]')

f:id:vaaaaaanquish:20180316235141p:plain
こんな感じ


xonshrcを書く - Stimulatorでも書いた通り、コマンド前は$PROMPT、右は$RIGHT_PROMPT、下にバー状に出しておきたければ$BOTTOM_TOOLBARに関数を突っ込む。


上記をlambda使わずに書くと以下
文字列を返す関数をPROMPT系の変数に突っ込んでおけば、キー入力時に評価される。

from time import strftime
def get_time():
    def prompt():
        return strftime('[ %H:%M:%S ]')
    return prompt
$RIGHT_PROMPT = get_time()
$UPDATE_PROMPT_ON_KEYPRESS=True

文字色や背景色などのフォーマットは、xonshのprompt-toolkitの普通のやつと似たような感じにすれば良い。


 

- datetimeを出す -

右にあっても見なさそうだったのでPROMPTに出す。

from datetime import datetime as dt
prompt = " {INTENSE_RED}{user}{INTENSE_GREEN}@{INTENSE_BLUE}{hostname}{INTENSE_YELLOW} [ {cwd} ] {GREEN}$ "
$PROMPT = lambda: dt.now().strftime('[ %Y-%m-%d %H:%M:%S ]')  + prompt
$UPDATE_PROMPT_ON_KEYPRESS=True

キー入力ごとに更新される。
f:id:vaaaaaanquish:20180317003150g:plain


以下参考
https://github.com/xonsh/xonsh/blob/adcd20f72fcbe6962533cb3ad78a4a9ec396e150/xonsh/readline_shell.py
https://github.com/xonsh/xonsh/blob/72f3bc0d089ea91d4e5288bb1c44ebfbe81db43e/xonsh/ptk/shell.py

 

- おわりに -

xonshにはそもそもxonsh.tools内に時間表記のメソッドがあったりするが、イマイチだったのでtime、datetimeを入れてきた方が早そう。
(Tools (xonsh.tools) — xonsh 0.8.3 documentation)


リアルタイムに表示したくなって数日試したけど、あんまり良い方法がなかったのでアイデアが欲しい。
xonshではなくprompt-toolkit側を見ないとダメそうなのでちょっとしんどそう


 

Qiitaの特定記事やタグ付記事をいいね、ストックしているユーザを見るPythonスクリプト

- はじめに -

Qiitaは、プログラミングに関する知識を記録・共有するためのサービスです。

Qiitaアカウントには企業情報が紐付いている場合があり、Qiitaの様々な記事から情報を取得し分析する事で「機械学習を記事を多くストックしている勉強熱心な会社はここだ!」等といった事が分かるのではないかという思いから、Qiitaのアカウント情報を取得するスクリプトを書きました。

その時のただのメモです。


 

- Qiita API -

QiitaにはAPIが存在します。

以下のQiita記事でAPIPythonクライアントを書いている人が既に居たので、こちらを利用します。
Qiita API v2のPythonラッパー実装した - Qiita

pipで導入します。

pip install qiita_v2

以下から「個人用アクセストークン」にread権限を付けて発行します。
https://qiita.com/settings/applications

その際表示されるtokenをメモしておきます。

これで後はいいね数を取得するだけなのですが、実際にこのPythonクライアントの中を見てみると、APIに存在する「いいね数の取得」に対応していませんでした。
qiita_py/client.py at master · petitviolet/qiita_py · GitHub

仕方ないので以下のようにメソッドを追加しつつ、記事のID(stock_id)に対していいねしたユーザとストックしたユーザを取得するスクリプトを書きました。

from qiita_v2.client import QiitaClient

def list_item_likes(self, item_id, params=None, headers=None):
    return self.get("/items/{}/likes".format(item_id), params, headers)
QiitaClient.list_item_likes = list_item_likes

stock_id = ''
token = ''
client = QiitaClient(access_token=token)

# いいねしたユーザ
res = client.list_item_likes(stock_id)
like_users = [x["user"]['id'] for x in res.to_json()]
for i in range(int(int(res.result_count)/20)):
    res = client.list_item_likes(stock_id, params={"page":i+2})
    like_users += [x["user"]['id'] for x in res.to_json()]

# ストックしたユーザ
res = client.list_item_stockers(stock_id)
stock_users = [x["id"] for x in res.to_json()]
for i in range(int(int(res.result_count)/20)):
    res = client.list_item_stockers(stock_id, params={"page":i+2})
    stock_users += [x['id'] for x in res.to_json()]

APIの制限は認証している状態で1000回/時、そうでなければIPアドレスごとに60回/時です。
数記事であれば大丈夫でしょう。


 

- ユーザ情報の取得 -

list_item_likes、list_item_stockersのresponseとなるjsonには""organization""なるキーが含まれています。

しかし、Qiitaプロフィールで表示されるものには「organization」と「Organizations」が存在しており、Qiita Organizationに登録されていない会社は前者、登録されている会社は後者に含まれ、APIでは前者しか取得できないのが現状です。

データ分析のため、後者のOrganizationsを取得したい事からユーザ情報取得のスクリプトを書きました。

import requests
from bs4 import BeautifulSoup
import time

def get_accounts_org(accounts):
    data = {}
    for account in accounts:
        res = requests.get("https://qiita.com/{}".format(account))
        bs = BeautifulSoup(res.text, "lxml")
        organization = [y.attrs["content"] for y in [x.find("meta") for x in bs.findAll("li", attrs={"itemprop":"memberOf"})]]
        host = [x.text.strip() for x in bs.findAll("div", attrs={"class": "newUserPageProfile_info_body"}) if x.find("i", attrs={"class": "fa-building-o"}) is not None]
        data.update({account: {"org":organization, "host":host}})
        time.sleep(5)
    return data

like_orgs = get_accounts_org(like_users)

like_usersリストに入っているユーザ情報から「organization(host)」「Organizations(org)」を取得する事ができました。


 

- 実際に見てみる -

実際に「記事にいいねしたユーザ」について見てみました。

テストとして以下の記事を選びました。
技術共有サービスQiitaで開催されていた2017年Advent Calenderにて、いいね獲得数ランクによってIBM、Greenという会社から表彰された記事ですので、いいね数もそれ相応かと思います。
qiita.com

集計は適当にCounterで

from collections import Counter

d = Counter()
for y in [(k,v) for k,v in like_orgs.items() if v["org"] or v["host"]]:
    for x in y[1]["org"]:
        d[x]+=1
    for x in y[1]["host"]:
        if x not in y[1]["org"]:
            d[x]+=1
for x in d.most_common(10):
    print(x)

結果は以下のようになりました

('株式会社リブセンス', 21)
('株式会社ミクシィ', 5)
('フューチャーアーキテクト株式会社', 4)
('フリーランス', 4)
('スタディプラス株式会社', 4)
('株式会社オールアバウト', 3)
('株式会社モバイルファクトリー', 3)
('アイレット株式会社(cloudpack)', 3)
('IBM Japan', 3)
('株式会社ゆめみ', 3)

記事の著者もリブセンスという会社ですが、やはり同僚の記事はいいねしたくなる傾向にあるのでしょうか。
私も社内SNSに「アイツの記事バズってるよww」みたいに同僚の記事を貼る人間を見たことがあるので、そういう影響もあるでしょう。

いかんせん、データ数が少ないので何も言えませんが、取得して見るまではできました。


 

- Qiitaの特定タグのついた記事を集める -

Qiitaには、記事にタグを付ける機能があります。

特定のタグがついているAPIを叩くため、上記と同じくQiitaClientに新しくメソッドを追加して実行します。

from qiita_v2.client import QiitaClient

def get_tag_items(self, item_id, params=None, headers=None):
    return self.get("/tags/{}/items".format(item_id), params, headers)
QiitaClient.get_tag_items = get_tag_items

tag_name = '機械学習'
token = 'hoge'
client = QiitaClient(access_token=token)

res = client.get_tag_items(tag_name)


ひとまず機械学習タグのついた記事を100 * 20 件分取得してみます。
QiitaのAPI制限を考えると、一度取得した情報はcacheしておくのが正解だと思います(以下では一切やっていません)。

ids = []
users = []
for i in range(100):
    res = client.get_tag_items("機械学習", params={"page":i+1})
    ids += [x["id"] for x in res.to_json()]
    users += [x["user"]["id"] for x in res.to_json()]

上記スクリプトにより、機械学習タグのついた記事を投稿しているユーザ、投稿された記事のidリストが取得できました。


 

- 機械学習記事を投稿、いいねしたユーザ情報を見てみる -

前述したget_accounts_org関数を利用して、機械学習タグ付き記事を投稿したユーザが属する企業、機械学習タグ付き記事をいいねしたユーザが属する企業について、Countをとってみます。

以下が直近2000記事で見た「機械学習タグのついた記事」を多く投稿する社員のいる企業です。

('TIS株式会社', 6)
('株式会社トップゲート', 5)
('株式会社ブレインパッド', 4)
('Team AI', 4)
('株式会社リブセンス', 4)
('株式会社 ドワンゴ', 4)
('Nuco Inc.', 4)
('株式会社Nextremer', 4)
('株式会社アカツキ', 3)
('株式会社クラウドワークス', 3)

Qiitaで機械学習記事といえば、あのアカウントとあのアカウントとあのアカウントかな…?という一覧になりました。
データが増加してもQiita活動家が社内に居るか居ないか、に寄ってしまいそうです。

これ以下はほぼ1~2だったので面白い結果を得るまで数を増やすのは少し大変そうです。
 
 
以下は直近2000記事で見た「機械学習タグのついた記事」にいいねを多くした社員のいる企業です。

('株式会社リブセンス', 31)
('Fringe81株式会社', 24)
('株式会社クラウドワークス', 22)
('TIS株式会社', 21)
('株式会社VASILY', 16)
('株式会社リクルートライフスタイル', 12)
('Retty株式会社', 12)
('フューチャーアーキテクト株式会社', 11)
('P&D – Planning and Development – ', 11)
('株式会社アカツキ', 11)
('Shinonome, inc.', 11)
('株式会社LIFULL', 10)
('株式会社トップゲート', 10)
('株式会社オールアバウト', 10)
('株式会社ゆめみ', 9)
('フリーランス', 8)
('株式会社 ドワンゴ', 8)
('株式会社Nextremer', 8)
('株式会社ACCESS', 8)
('株式会社エイチームライフスタイル', 8)
('株式会社エイチームブライズ', 8)
('株式会社BitStar', 8)
('The University of Tokyo', 7)
('株式会社div(ディブ).', 7)
('株式会社WACUL', 6)
('株式会社Fusic', 6)
('Kyoto University', 6)
('株式会社Rosso', 6)
('株式会社サイバーエージェント', 6)
('エムスリー株式会社', 6)

リブセンスという会社はQiitaに相当な時間を使っているという事がわかりました。
こちらも上位は「あのアカウントかな…」と個人が見え隠れする結果となりました。

こちらは、東大、京大やフリーランスがトップ30に入ってきた辺りを見るに、データが集まれば少し改善しそうです。


 

- おわりに -

APIの制限数によって1日ではこの程度でした。

多くデータを集めた上で、「機械学習タグのついた記事にいいねするユーザ」の数を企業単位で集計することで、特定の技術に興味関心が強い企業が見られるかも…という結果になりました。

Qiita APIではユーザに紐付いたLinkedinやFacebookTwitterのIDも取得できるため、次回はそれらを含めた集計を出せればと思います。