Stimulator

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

XonshのException発生時のtracebackを見やすくする

- はじめに -

この記事は、Xonsh Advent Calendar 2017 - Qiita 15日目の記事です。

完全に遅刻しています。

3日間xonsh本家のコードを読みながら「あーでもないこーでもない」とやっており遅れました。

結論を先に述べてから、後半でその経緯も話します。

 

Xontribにしておいたので簡単に使えると思います。



アジェンダ

 

- PythonのStackTraceを見やすくする -

PythonのStackTraceを見やすくする方法を以下の記事に書きました。

vaaaaaanquish.hatenablog.com

この中でも、私が最も見やすいと感じたbacktraceを使って、xonshのTraceBack表示を短くします。

他のパッケージや、自作のシンタックスハイライト用parserでも上手くいくと思います。


 

- xonshのstderrにbacktraceを適応する -

結論から言うと以下のコードを~/.xonshrcに差し込むのが早いと思います。

import xonsh.tools
import backtrace
import sys

# backtraceパッケージの_flush()をOverride
# 元コード : https://github.com/nir0s/backtrace/blob/f2c8683ec53e4fa48ea8c99c196b201bf22fda3e/backtrace.py#L36
def __flush(message):
    st = message + '\n'
    sys.stderr.buffer.write(st.encode(encoding="utf-8"))
backtrace._flush=__flush

# xonshのprint_exception()をOverride
# 元コード : https://github.com/xonsh/xonsh/blob/230f77b2bc64cbc3e04837377252793f5d09b9ba/xonsh/tools.py#L798
def _print_exception(msg=None):    
    tpe, v, tb = sys.exc_info()
    backtrace.hook(tb=tb, tpe=tpe, value=v)
    if msg:
        msg = msg if msg.endswith('\n') else msg + '\n'
        sys.stderr.write(msg)
xonsh.tools.print_exception = _print_exception

※上記xonshrcは最小限のコードです


こんな感じになり、コンソール汚染が減ってハッピーです。
一部ユーザ名を加工しています。
f:id:vaaaaaanquish:20171218231935p:plain

特に後者のpandasのkey errorなどは、本来30行近いエラーが出力されるのですが、半分程度に収まっていますし、かなり作業がしやすいかと思います。

 
backtrace._flush()は元のソースコードが以下のようになっています。

def _flush(message):
    sys.stderr.write(message + '\n')
    sys.stderr.flush()

後述しますが、xonshはsys.stderr.bufferが使える環境であればsys.stderr.buffer.write、使えなければsys.stderr.writeを利用してエラー内容を出力するようになっています(ここの調査に1日半かかりました)。
よって、backtraceの_flushをOverrideする時は、適切にPython環境に合わせてどちらかを選択して書き換えてやるのが正解です。

上記のxonshrcより丁寧に、sys.version_infoでバージョンチェックをして書いてやると良いです。
私はPython2以下は小学生に馬鹿にされるので使わないため利用してません。

 
また、xonshではエラー発生時に「tracebackを取得して、ログに保存して、メッセージを表示する」といった内容をまとめたxonsh.tools.print_exceptionを必ず呼ぶようになっています。
上記xonshrcコードでは、そのxonsh.tools.print_exceptionの」ほぼ全てをそぎ取ってbacktraceに任せるという形を取っていますが元のソースコードはそこそこ長くちゃんとしています。

元の機能をなるべく壊さないようOverrideする場合は、元ソースでtraceback.print_exc()となっている所をsys.exc_infoからのbacktrace.hockに書き換えてやるのが一番良いと思います。


 

- さらに見やすくするためにcoloramaのStyleを記述する -

backtraceパッケージでは、coloramaというANSIカラーコードを利用したカラーリングパッケージを使って、さらにStyleを変更する事が可能です。

上記xonshrcのbacktrace.hookする部分で、reverse(逆順表示)やstrip_path(ファイル名のみ表示)、styleのパラメータを設定してやれば良いです。
私は以下のようにしています。

import backtrace
from colorama import init, Fore, Style
STYLES = {
    'backtrace': Fore.YELLOW + '{0}',
    'error': Fore.RED + Style.BRIGHT + '{0}',
    'line': Fore.RED + Style.BRIGHT + '{0}',
    'module': '{0}',
    'context': Style.BRIGHT + Fore.GREEN + '{0}',
    'call': Fore.RED + '--> ' + Fore.YELLOW + Style.BRIGHT + '{0}',
}
backtrace.hook(reverse=True, strip_path=True, styles=STYLES, tb=tb, tpe=tpe, value=v)

これで以下のようにさらに情報量が減り、順番もトップを見るだけという感じになりました。
f:id:vaaaaaanquish:20171218230912p:plain

やったね。


 

- どのようにprint_exceptionが呼ばれているか -

ここからは蛇足です。
数日詰まっていた理由と解決に至った流れを書いておくものです。

 
以下のtools.print_exception()があらゆるException発生時に呼ばれる事はDocを見ても明確である。
xonsh/tools.py at 230f77b2bc64cbc3e04837377252793f5d09b9ba · xonsh/xonsh · GitHub
Tools (xonsh.tools) — xonsh 0.6.0.dev151 documentation

そして、xonshのshell上で入力される全てのコマンドやPythonスクリプトは、base_shell.default()にてコンパイルされ実行される。
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/base_shell.py#L313

default内3行目のpushメソッドが入力されたコードをcompileするものである。
ptk構成のshellの場合、以下_pushでOverrideされている。
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/ptk/shell.py#L123

正確にpushがOverrideされている箇所を示すと以下cmdloop。
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/ptk/shell.py#L145

 
pushは降ってきたスクリプトPythonのexecer.compileに投げてコンパイルして返すものである。

push時に@(x=1/0)とすればSyntaxErrorをコンパイラが検知してExceptionを返す。
しかし、@(1/0)は算術なので実行時にExceptionが出る(ここでかなり躓いた)。

 
実行時のExceptionというのは、ここ(run_compiled_code)で走った結果起こったものである
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/base_shell.py#L330

xonsh.codecache.run_compiled_codeはただのexecするだけの関数である
https://github.com/xonsh/xonsh/blob/f544e63699a19d3990d2abf1ff082c9b5f48176d/xonsh/codecache.py#L58


pushもrun_compiled_codeもException発生時、tools.print_exceptionが呼ばれており、一見違いが分かりにくい。

ここで出てくるのが独自のstreem io用のClassであるTee。
このTeeは、Pythonのバージョンの違い(sys.hoge.bufferのあるなし)を考慮しつつ、stdoutとstderrを同じIOで処理しながら、エラー前に$XONSH_STDERR_PREFIX、後に$XONSH_STDERR_POSTFIXを付けて、ログに保存しながら出力するためのものである。
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/base_shell.py#L217

以下で_TeeStdインスタンス作った時sys.stderrやsys.stdoutを関連付けしてる。
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/base_shell.py#L238
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/base_shell.py#L132


この_TeeStdがsys.stderr.buffer.write(byte入力)を利用しているため、backtraceが利用するsys.stderr.write(string入力)とで噛み合わない上、Teeのインスタンス生成タイミングはコードをcompileした後なので「@(x=1/0)の時はcoloramaのcolorが適応されるけど、@(1/0)の時は適応されない何で…」という事象が発生し、xonshの動作を逐一追うことになった。

これが全貌。


 

- おわりに -

最終的にbacktrace._flushもsys.stderr.buffer.writeに合わせてやる事で解決したけど、絶対もっとスマートに出来ると思う。
というかTeeのインスタンス作るタイミングは、普通にissue立てても良さそう。


Xonshアドベントカレンダーも空きの日のネタを用意してたのに書けてないしなかなか…という感じ。

誰か書いて下さい…

qiita.com

 

PythonのException発生時のTracebackを綺麗に見る

- はじめに -

PythonOSSパッケージ等を利用していると、Exceptionが発生した際に表示されるTraceback(正確にはスタックトレース)がかなり長い場合がある。

例えば、以下の簡易なコード実行で表示されるTracebackの行数は30近くなる。

import pandas as pd
df = pd.DataFrame(dict(a=[1,2,3]))
df['b']

引用 : python - Shorten large stack traces when using libraries - Stack Overflow


より複雑なプログラムにおいては、この比ではない。
にも関わらず、記述ミスのようにTraceback上位部にエラーの重要な内容がある場合もあれば、パッケージ内部のValidationで下位部が重要な場合もある。

得てしてPython開発環境として利用されるxonsh等の対話コンソール上やJupyter notebook等で100行近いエラーが出てきた時は、少なからず気持ちが折れる。

これらは、様々な言語の資産の利用などから引き起こされる長さである。
また設計上、過度なWrappingが行われた結果長くなっている場合も多々ある。

本記事は、主にPython3系において、Exceptionをcatchした時のTracebackを出来る限り綺麗に表示させ、あくまで開発のために人に優しい環境にするためのTipsやパッケージをまとめるものである。


※ 長く書いたが要約すると「xonshで作業してる時にクソ長いエラーが出てきてコンソール汚染されて腹立つから何とかしたい」と思っていたが調べてるうちに知見が溜まったのでまとめるという話


結論から言うとbacktraceかTBVaccineが良さそう。
 
もくじ:

 

- Tracebackの表示 -

そもそもPythonにおけるStackTraceは、Exception発生時にsys.last_tracebackに変数にtracebackオブジェクトとして格納される。

その中身をよしなに参照するためにsys.exc_info()が用意されている。
sys.exc_info()は返り値が(value, type, traceback)となっている長さ3のtupleなので、それらをtraceback.format_hogeに投げて以下のように直接中身を参照する事ができる。
(もちろんこんな実装を実際にしている人は居ないと思うが)

import sys
import traceback

try:
    x = 1 / 0    # ゼロ除算
except Exception as e:
    t, v, tb = sys.exc_info()
    print(traceback.format_exception(t,v,tb))
    print(traceback.format_tb(e.__traceback__))

# >>> ['Traceback (most recent call last):\n', '  File "<stdin>", line 2, in <module>\n', 'ZeroDivisionError: division by zero\n']
# >>> ['  File "<stdin>", line 2, in <module>\n']


tracebackモジュールには、traceback.format_exctraceback.print_excが用意されているので実際はこちらで十分である。

import traceback

try:
    x = 1 / 0    # ゼロ除算
except:
    print(traceback.format_exc())    # いつものTracebackが表示される
    traceback.print_exc()                 # これでも同じ

 
参考 : 29.9. traceback — Print or retrieve a stack traceback — Python 3.6.4 documentation


 

tracebacklimitの利用

Pythonでは、sys.tracebacklimitでトレースバック情報のレベル値を設定できる(0〜1000)。

そして0から1000と書いたが、この機能は既知のバグとしてPython 3.xで機能しない。
Issue 12276: 3.x ignores sys.tracebacklimit=0 - Python tracker

一応Noneにすることで、値とエラー内容だけ出せる。

import sys

sys.tracebacklimit=None
x = 1 / 0

# >>> ZeroDivisionError: division by zero

これなら str(Exception) で十分。
2系を使っていないならあまり恩恵が得られない。

また、traceback.format_exc(limit=1)等とした場合も同様に見える。


参考 : 29.1. sys — System-specific parameters and functions — Python 3.6.4 documentation

 

ColoredTracebackでシンタックスハイライト

Tracebackの表示にカラーリングするパッケージがある。

github.com

導入はpip

pip install colored-traceback
pip install colorama    # Windows環境下の場合

基本的には import colored_traceback.always としておけば良い

f:id:vaaaaaanquish:20171214053335p:plain


基本的にSyntaxはIPythonならサポートしてくれてるので、主にコンソールで作業する時用に。

 

Pygmentsでシンタックスハイライト

上記と同じ事がPygmentsでもできる(こちらの方が一般的か)。
Available lexers — Pygments

以下のようにsys.excepthookをOverrideするための関数を作ってやればよい。

import sys
import traceback
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import TerminalFormatter

def myexcepthook(type, value, tb):
    tbtext = ''.join(traceback.format_exception(type, value, tb))
    lexer = get_lexer_by_name("pytb", stripall=True)
    formatter = TerminalFormatter()
    sys.stderr.write(highlight(tbtext, lexer, formatter))

sys.excepthook = myexcepthook

エラーがカラーリングされて見やすくなる。


   

- パッケージの利用 -

StackTrace表示を見やすくするための関連パッケージを示す。
backtraceが短く表示するやつで、それ以降は基本的に詳細表示のパッケージにあたる。

 

backtrace

StackTraceを短くしてくれるパッケージ。
上記colored-tracebackを使ってカラーリングもされる。

github.com

導入はpipで。Winの場合は上記colored-tracebackのcoloramaを先にinstallしておくと吉。

pip install backtrace


「はじめに」に記載のコードを実行してみる。

import pandas as pd
import backtrace

backtrace.hook(
    reverse=True,         # 逆順
    strip_path=True    # ファイル名のみ
)

df = pd.DataFrame(dict(a=[1,2,3]))
df['b']

f:id:vaaaaaanquish:20171214060800p:plain

これくらい分かれば何とかなるなって気もしなくもない。
何より慣れたpandasの30行近いエラーがこれに収まるなら良い。
基本的にはこれで作業して、backtrace.unhook() するのが良さそう。


また、sys.exc_info()の返り値を渡す形にすればコンソール、IPython上でも利用できる

import pandas as pd
import backtrace
import sys

try:
    df = pd.DataFrame(dict(a=[1,2,3]))
    df['b']
except:
    tpe, v, tb = sys.exc_info()
    backtrace.hook(reverse=True, strip_path=True, tb=tb, tpe=tpe, value=v)

ただし、中身がcoloramaでカラーリングしてるので色は変わらない。
この辺colored-tracebackに修正していけばかなり使えそう。

 

better-exceptions

Exceptionを見やすくするパッケージ。

github.com

エラー発生時に変数に格納されている値やClassをちゃんと出してくれて見やすい(?)
https://github.com/Qix-/better-exceptions/raw/master/screenshot.png


導入は以下

pip install better_exceptions

export BETTER_EXCEPTIONS=1  # Linux / OSX
setx BETTER_EXCEPTIONS 1       # Windows


試しにこんな感じのをやってみる

import better_exceptions

def zero(x):
    y = x/0
    return y

t = zero(10)
print(t)

ちなみにスクリプト実行時のみなので、対話コンソール上やIPythonでは現状使えない。
f:id:vaaaaaanquish:20171214100150p:plain:w500
それっぽい

しかしここからpandasのExceptionの方に適応すると以下
f:id:vaaaaaanquish:20171214101256p:plain:w400
厳しい

複雑になるとちょっと厳しいものがある。

 

TBVaccine

上記better-exceptionsよりちょっと見やすいやつ

github.com

https://github.com/skorokithakis/tbvaccine/raw/master/misc/after-vars.png

導入はよしなにpipで入れてTBVACCINE変数を設定しておくか、明示的にimportする。

pip install tbvaccine
export TBVACCINE=1

 
上記Githubの画像では設定された変数の中身まで出してるけど、以下のようにshow_varsをFalseにしておけば出ない。

import tbvaccine
tbvaccine.add_hook(isolate=False, show_vars=False)
import pandas as pd

df = pd.DataFrame(dict(a=[1,2,3]))
df['b']

f:id:vaaaaaanquish:20171214111250p:plain
良い感じである。


上記したsysモジュールっぽくも使えるのも良いところ。

from tbvaccine import TBVaccine
try:
    x = 1 / 0
except:
    print(TBVaccine(isolate=False, show_vars=False).format_exc())

これならxonshでも動くし、良い感じかもしれないと思っている。
IPythonでも動くし良さ。
 
 

その他調べたやつ

tracebackturbo

雰囲気はTBVaccineっぽい。変数の中身まで見たい時と見たくない時があるよなあって思うけど消せなさそう。
github.com
3系はこっち :
GitHub - cxcv/python-tracebackturbo3: A drop-in replacement for the python3 traceback module that enables dumping of the local variable scope aside normal stack traces.

 

git-stacktrace

pinterestのgitリポジトリはじめて見た。
stacktraceと一緒に、問題が発生した箇所のGitの修正履歴をコミット単位で表示してくれる。
github.com
毎日Git使ってたら便利なのかも。
今回は趣向と外れすぎたのでノータッチ。

 

python-tblib

tracebackをPickleで固めてraiseしていくやつ。
multiprocessで処理をした時のtracebackが見やすくなる。
github.com
今回は趣向と外れすぎたのでノータッチ。いつか触る。

 

Skip-Traceback(jupyter)

jupyterの拡張でトレースバックを表示せずエラーの種類とメッセージのみ表示するやつ。
github.com
今回は趣向と外れすぎたのでノータッチ。

IPythonならまだmagic commandのdebugやpdbデバッグする方が普通に良い気がする。
JupyterまたはiPython Notebookでデバッグをする方法 - Qiita
IPython の豊富な機能を使いこなす (2) - Qiita

IPythonならultratb.ColorTBも使えるけどなあ…
Module: core.ultratb — IPython 3.2.1 documentation


 

- おわりに -

結論私の求めていた、短く良い感じにExceptionを表示する方法はbacktraceかTBVaccine辺りだろう。

個人的にbacktraceの表示形式が好きなので、ここを起点にxontribを作っていくぞという気持ち。

IPythonにも活用できたら、Jupyter notebookでミスって実行しちゃった時に出る長いエラーとおさらば出来る気がする…


 
//--- 以下参考 ---
29.9. traceback — Print or retrieve a stack traceback — Python 3.6.4 documentation
traceback — Exceptions and Stack Traces — PyMOTW 3
__exit__ must accept 3 arguments: type, value, traceback — Python Anti-Patterns documentation

PythonユーザのためのJupyter[実践]入門

PythonユーザのためのJupyter[実践]入門

blog.dscpl.com.au
d.hatena.ne.jp


 
追記:
xonshに導入する記事も書きました
vaaaaaanquish.hatenablog.com
なんとかしてJupyter notebookもbacktraceにできないかなあと思っています。

xonshにおけるxontribの紹介

- はじめに -

この記事は、Xonsh Advent Calendar 2017 - Qiita 14日目の記事です。

xonshにおけるいわゆる拡張機能であるところのxontribについて書いていきます。

「オススメXontrib!」と行きたい所ですが、そもそも2017年末時点で公開されているxontribは少ないのでほぼ全てです。

 

- xontribとは -

xontribはxonshの拡張群です。

例えばhogeパッケージをロードするには、以下Commandを入力します。

xontrib load hoge

~/.xonshrcに上記Commandを直接書くか、~/.config/xonsh/config.json内にあるxontribsという名前のlistにパッケージ名を書いておくと、xonshセッション起動時にロードされます。

手前味噌ですが、私も作っています。


現行のxontribパッケージについては、公式ドキュメントにまとまっています。
Xontribs — xonsh 0.9.11 documentation
この中にあるやつないやつで、少しだけ便利になるやつだけ紹介していきます。

[2019/06/29] 続編で作る側になろうという記事も書きました
vaaaaaanquish.hatenablog.com


 

- 補完 -

apt_tabcomplete

apt-getコマンドをtabで補完できるようにします

# 導入
pip install xonsh-apt-tabcomplete
xontrib load apt_tabcomplete

apt-getのinstall、remove、apt-cacheのsearchの補完がサポートされています。
github.com

Ubuntuなどaptを良く使う環境でxonshを使う時は入れておくと良いと思います。

 

docker-tabcomplete

dockerコマンド周りをtabで補完できるようにします

# 導入
pip install xonsh-docker-tabcomplete
xontrib load docker_tabcomplete

docker imageの補完などがxonsh上で上手くいかないのですが、こちらを導入することで解決します。
github.com

dockerで複数のコンテナを扱いながら開発している場合に便利です。

 

scrapy-tabcomplete

scrapyコマンド周りをtabで補完できるようにします

# 導入
pip install xonsh-scrapy-tabcomplete
xontrib load  scrapy_tabcomplete

scrap crawlコマンドやscrapy checkコマンドの結果のcacheから、次のコマンドを補完してくれます。

github.com

scrapyをコンソール上で使っている人(そんな人が居るかどうかは別として)には使えると思います。

 

fzf-widgets

sshコマンドの補完、historyの検索をサポートしてくれます。

公式のGIFが大体全てを説明してくれています。
https://raw.githubusercontent.com/shahinism/xontrib-fzf-widgets/master/docs/cast.gif

# 導入
pip install xontrib-fzf-widgets
xontrib load fzf-widgets

github.com

historyや補完はもともと強力なxonshですが、sshのconfigから補完は一応デフォでやってくれないので便利。

 

thefuck

コマンドを打ち間違えた時に「fuck」と入力すればコマンド候補を入力してくれるxontrib。
xonshrcの書き方の記事にもある$SUGGEST_COMMANDSをもうちょっと便利にする感じ。

# 導入
pip install xontrib-thefuck
xontrib load thefuck

xonsh記事ではないけど以下見ると大体の概要がわかります
thefuckのインストール方法 - Qiita

github.com


  

- UI -

powerline

おなじみpowerlineのサポートxontribです
見た目を色々できます

https://github.com/santagada/xontrib-powerline/raw/master/screenshot.png

# 導入
pip install xontrib-powerline
xontrib load powerline

プロンプトの右側を $PL_PROMPT、下側を $PL_TOOLBAR で設定します。
whoやbranchなどはセクションです。
pl_available_sectionsコマンドで全ての使えるセクションを表示できます。pl_build_promptコマンドで再ロードします。

$PL_PROMPT = '!'    # 使わない時
$PL_TOOLBAR = 'who>time'

xonshrcの書き方の記事でも紹介した $PROMPT も使えます。

github.com

iterm2ではpowerlineフォントが化けたり、表示が重なったりします。
その時は以下で対応できます。
iTerm2+powerline文字化け対策めも - Qiita

一回やってしまえばカッケーコンソールでxonshできます。

// 追記:2019/09/15
現在開発が止まっているようでした(開発者とも連絡がつかない状態)。私が別途xontrib-traceback2というのを作っています。
github.com

 

prompt-vi-mode

promptの表示に「vi-modeかどうか」を加えます。

# 導入
pip install xontrib-prompt-vi-mode
import xontrib.prompt_vi_mode

導入後は、xonshrcの書き方の記事でも出た、$PROMPTや$RIGHT_PROMPTに "{vi_mode}" もしくは "{vi_mode_not_input}"を突っ込むだけです。

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

github.com

仕方ないですがimportなところに注意です。
私はptkのショートカットを登録してINSERTとNORMALを切り替えています。
vim開けばよくね…」と言われればそれはそう。


 

- タスク、操作 -

z

ディレクトリ移動を潤滑にするxontrib。
/home/work に行ったことがあれば $ z work とだけ入力すれば高確率で移動できる。

# 導入
pip install xontrib-z
xontrib load z

基本的にはcacheされたとこに行きます

github.com

アドベントカレンダー内の以下でも紹介されていて、zshのz.shを上手く書き換えているので一読すると吉です。
qiita.com

 

autoxsh

.autoxsh」というスクリプトファイルをディレクトリに設置しておくと、そのディレクトリにcdした時に毎回そのスクリプトが実行されるようになります。

# 導入
pip install xonsh-autoxsh
xontrib load autoxsh

初回の実行時には以下autoxshを許容し実行するかのメッセージが出るのでYesで実行

Unauthorized ".autoxsh" file found in this directory. Authorize and invoke? (y/n/ignore): y

github.com

.autoxshは普通のxonshスクリプトが記述できるので、lsコマンドの自動実行や、注意喚起のprint、envの切り替えを記入しておくと便利です。

 

schedule

スケジューラです。
コマンドにtimeオブジェクトを投げておくと、指定時間に実行したり、delayをかけて実行したりできます。

# 導入
pip install xontrib-schedule
xontrib load schedule

以下scheduleパッケージwrapperです
schedule — schedule 0.4.0 documentation

import time

def func():
    print("20秒経ちました")

schedule.when(time.time()+20).do(func)

時間指定のwhenと、遅延実行のdelayが使えます。

github.com

これに似た方法を上手く使えば「xonshrcに書いておいて最速でxonshを起動しつつ、よく使うパッケージを遅れて読み込む」なんてこともできそうです。いつかやりたい。


 

- おわりに -

その他にも、xonsh用のenvである"vox"用のxontribxonsh上でcondaを扱うためのxontrib簡易エディタxoを提供するxontribコマンドを保存していくxontrib…などが使えそうです。

voxはまだ使った事無く、anacondaは宗教上無理。エディタはvimだしな…となって今の記事に収まっています。


Githubリポジトリを見てもらえば分かりますが、xontribは数行Pythonを書けばできてしまいます。
xonshでは実際色んなイベントをデコレータを付けることでキャッチできるので、自身でxontribを作っていくのもかなり簡易だと感じます。
 

xonsh アドベントカレンダーも後半戦に入り、空きが出てきたのでなんとか登録だけでもお願いします!
後半戦のどこかでxontrib作れたらいいなと思っています。

qiita.com

xonshrcを書く

- はじめに -

この記事は、Xonsh Advent Calendar 2017 - Qiitaの9日目の記事です。

1日目にXonshを勧める記事を書いて「アドベントカレンダーでxonshrcのオススメ設定が出揃う」と言ったものの、なかなかそれらしい記事が出てこないので書いておきます。


Python Prompt Toolkitが使えるなら、アドベントカレンダー内の以下の記事もオススメです。
qiita.com


追記2018/06/22:
さらにちゃんとまとめました
vaaaaaanquish.hatenablog.com
vaaaaaanquish.hatenablog.com


追記2019/08/28
さらに最新のxonsh 0.9.10のEnvを全て実装付きでまとめました
こちらのほうが参考になると思います。
vaaaaaanquish.hatenablog.com

 

- 私のxonshrc -

先に結論として設定している最もシンプルな部分のrcを出しておきます。
xonshrcを制御するxonsh/configファイルについては基本的にはdefaultのものを利用しています。

# vi風の操作
$VI_MODE = True
# Ctrl + D で終了しない
$IGNOREEOF = True
# tabではなく空白4つ
$INDENT = "    "
# 補完時に大小区別しない
$CASE_SENSITIVE_COMPLETIONS = False
# 補完選択Enterで即実行しない
$COMPLETIONS_CONFIRM = True
# 連続重複コマンドを保存しない
$HISTCONTROL = (ignoredups)
# 括弧を補完
$XONSH_AUTOPAIR = True
# ディレクトリ名を入力すればcdできる
$AUTO_CD = True
# エラー全て吐くように
$XONSH_SHOW_TRACEBACK = True
# プロンプトの設定タイムアウトのメッセージ抑制
$SUPPRESS_BRANCH_TIMEOUT_MESSAGE = True

# 見た目ウザかったら切か変更する
# 参考:http://xon.sh/tutorial.html#customizing-the-prompt
# プロンプト上には場所だけ出す
$PROMPT = "{INTENSE_YELLOW}[ {cwd} ] {GREEN}$ "
# 下部にuser, host情報を追加
$BOTTOM_TOOLBAR = "{BACKGROUND_WHITE}{BLACK}{user}@{hostname}"
# 右にbranch情報
$RIGHT_PROMPT = "{curr_branch}"
# 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"

#えいりあす
aliases["lt"] = "ls -ltr"
aliases["l"] = "ls -ltr"
aliases["la"] = "ls -la"
aliases["ll"] = "ls -l"
aliases["so"] = "source"
aliases["v"] = "vim"
aliases["vi"] = "vim"
aliases["vx"] = "vim ~/.xonshrc"
aliases["vv"] = "vim ~/.vimrc"
aliases["vs"] = "vim /etx/ssh/ssh_config"
aliases["cp"] = "cp -i"
aliases["rm"] = "rm -i"
aliases["ju"] = "jupyter notebook"

見た目はこんな感じ

f:id:vaaaaaanquish:20171209154013p:plain

これだけで大体良い感じですが、Pythonで関数を作ってそこにaliasを貼ることももちろんできるので、加えて幾つか記述していますがそれらについてはまた別途。


 

- xonshrcで使える環境変数まとめ -

以下ではxonshrcで使える環境変数をまとめておきます。
基本的には、以下Referenceに書いてある内容です。
Environment Variables — xonsh 0.9.11 documentation

  

見た目、表示系

    # カラーテーマ
    # 自分はdefaultでiTerm2側の設定を利用
    # xonfig styles コマンドで全てのスタイル表示
    # 良さげなのだと'native'
    $XONSH_COLOR_STYLE = ‘default’

    # 下部に表示するツールバー
    # prompt-toolkitが導入されているShellのみ
    $BOTTOM_TOOLBAR

    # Shell上のシンタックスハイライト強調表示
    $COLOR_INPUT = True

    # コマンド実行の戻り値にもシンタックスハイライト
    $COLOR_RESULTS = True

    # インデント文字列
    # 空白4つとかTabとか任意に
    $INDENT = "    "

    # 改行した時にでてくる左端の文字
    # 指定された長さ以下だった場合は繰り返し表示される
    $MULTILINE_PROMPT = "."

    # 右揃えで表示する文字列
    $RIGHT_PROMPT = ""

    # プロンプトのサブプロセス、書式設定などのタイムアウト
    $VC_BRANCH_TIMEOUT = 0.1

    # プロンプトの設定タイムアウトのメッセージ抑制
    $SUPPRESS_BRANCH_TIMEOUT_MESSAGE = False

    # MercurialのbranchをTerminalに表示するか
    $VC_HG_SHOW_BRANCH = True

    # datetime.strptime()で出せる時間の表示の仕方
    $XONSH_DATETIME_FORMAT = ‘%Y-%m-%d %H:%M’

    # xonshの見た目の色々、文字のシンボル
    # http://xon.sh/envvars.html#xonsh-gitstatus
    $XONSH_GITSTATUS_*

    # エラーが出たときトレースバックを表示するかどうか
    $XONSH_SHOW_TRACEBACK = False

    # defaultプロンプトの見た目設定
    # http://xon.sh/tutorial.html#customizing-the-prompt
    $PROMPT = xonsh.environ.DEFAULT_PROMPT

    # titleとpromptのカスタマイズ
    # http://xon.sh/tutorial.html#customizing-the-prompt
    $PROMPT_FIELDS = xonsh.prompt.PROMPT_FIELDS

    # PROMPTのエラー表示版
    $XONSH_STDERR_POSTFIX
    $XONSH_STDERR_PREFIX

 

操作

    # マウス操作で履歴戻ったりするかどうか
    # prompt_toolkitが導入されているShellのみ
    $MOUSE_SUPPORT = False

    # Ctrl + D で終了しない
    $IGNOREEOF = False

    # vi風の操作
    $VI_MODE

 

補完

    # キー入力ごとに評価する
    $UPDATE_COMPLETIONS_ON_KEYPRESS = False

    # プロンプト間でコマンドをcacheしない設定にするか
    # Trueにすると毎回キー入力待ち再評価が発生する
    $UPDATE_PROMPT_ON_KEYPRESS = False
    
    # 補完時に大文字小文字区別するか
    # LinuxだとdefaultでTrue
    # 個人的にはFalse派
    $CASE_SENSITIVE_COMPLETIONS

    # コマンド履歴の保存の仕方
    # ignoredupsは重複コマンドを保存しない
    # ignoreerrは失敗したコマンドを保存しない
    $HISTCONTROL = (ignoredups, ignoreerr)

    # 補完時に[]と()を考慮する
    $COMPLETIONS_BRACKETS = True

    # ユーザ確認する前に補完を表示する上限数
    $COMPLETION_QUERY_LIMIT = 100

    # Tab補完メニューで表示する行数
    # prompt-toolkitが導入されているShellのみ
    $COMPLETIONS_MENU_ROWS = 5

    # Tab補完メニューが表示されている時にEnterで即実行するか
    # Trueなら実行しない
    # prompt-toolkitが導入されているShellのみ
    $COMPLETIONS_CONFIRM = False

    # Tab補完のPathのsubsequence matching
    # ~/u/ro で ~/lou/carcolh をよしなに補完してくれるみたいなやつ
    $SUBSEQUENCE_PATH_COMPLETION = True

    # Pythonスクリプトの補完
    # 基本的にdefaultがサイコー
    # prompt-toolkitが導入されているShellのみ
    $COMPLETIONS_DISPLAY = multi

    # あいまい補完をするか
    $FUZZY_PATH_COMPLETION = True

    # 補完サジェストを出す
    # defaultでTrue
    $AUTO_SUGGEST_IN_COMPLETIONS = True

    # 補完サジェストを 右矢印キーで入力
    # $SHELL_TYPE=prompt_toolkitが指定されている必要あり
    # defaultがTrueなのでptkが入っていればデフォで動いてるはず
    $AUTO_SUGGEST = True

    # BASHの補完機能をそのまま利用するための
    # defaultでは以下、tupleを指定する
    $BASH_COMPLETIONS = ('/usr/share/bash-completion/bash_completion', )

    # パターンマッチなどglobした時に結果をソートするか
    $GLOB_SORTED = True

    # 実行可能なファイルの拡張子
    # 大文字で書く必要がある
    $PATHEXT = [".EXE"]

    # 変なコマンドを入力したら もしかして を出す
    $SUGGEST_COMMANDS = True

    # もしかして最大数
    $SUGGEST_MAX_NUM = 5

    # もしかしてサジェストの誤字数
    $SUGGEST_THRESHOLD = 3

    # 出力後に新しい空行を追加するか
    $XONSH_APPEND_NEWLINE = False

    # [], ()などの括弧を補完するか
    $XONSH_AUTOPAIR = False

    # 全てのコマンド実行をcacheするか
    $XONSH_CACHE_EVERYTHING = False

    # コードをcacheしておくか(T)、毎回コンパイルして実行するか(F)
    $XONSH_CACHE_SCRIPTS = True

 

ディレクトリ移動

    # ディレクトリ名を入力すればcdできる
    $AUTO_CD = False

    # ディレクトリ移動したらスタックにpush
    # dirstack実装 : https://pypkg.com/pypi/xonsh/f/xonsh/dirstack.py
    $AUTO_PUSHD = False

    # pushdした時にdirで現在のスタック内容を表示しない
    $PUSHD_SILENT = False

    # dirstack最大数
    $DIRSTACK_SIZE = 20

    # CD時にshとのroot関係が壊れないようにPATH指定するやつ
    # zshでいうこれ : https://robots.thoughtbot.com/cding-to-frequently-used-directories-in-zsh
    $CDPATH = ("/", )

    # cwdコマンドで指定したディレクトリに向けたショートカット
    $DYNAMIC_CWD_ELISION_CHAR = "/"

    # cwdプロンプト変数の文字数の指定
    $DYNAMIC_CWD_WIDTH = (inf, ‘c’)

 

変数、設定

    # PYTHON_PATH
    $PATH = (...)

    # 前回の作業ディレクトリ
    $OLDPWD

    # サブプロセスモードで文字列内の環境変数を展開するか
    $EXPAND_ENV_VARS = True

    # xonshの場所
    $XONSH_CONFIG_DIR = $XDG_CONFIG_HOME/xonsh

    # 外部のaliasを優先するかどうか
    # $XONSH_CONFIG_DIR/config.jsonに書くのでxonshrcに書いても意味はない
    $FOREIGN_ALIASES_OVERRIDE = False

    # $XONSH_CONFIG_DIR/config.jsonが読まれたかどうかがboolで入ってるので判定用
    $LOADED_CONFIG

    # 読見込まれたRCが動いているかのbool値のlist
    $LOADED_RC_FILES = (~/.xonshrc)

    # 連続でコマンド実行した時にスリープする秒数
    $XONSH_PROC_FREQUENCY = 0.0001

    # pretty printingの戻り値があるかどうか
    # import pprintしたりするなら
    $PRETTY_PRINT_RESULTS

    # configファイルへのPATH
    $XONSHCONFIG = $XONSH_CONFIG_DIR/config.json

    # rcファイルへのPATH
    $XONSHRC = ['/etc/xonshrc', '~/.xonshrc']

    # デスクトップ標準のディレクトリ
    $XDG_CONFIG_HOME = ~/.config

    # デスクトップ標準のデータディレクトリ
    $XDG_DATA_HOME = ~/.local/share

    # 実際のxonshデータが入っている場所
    $XONSH_DATA_DIR = $XDG_DATA_HOME/xonsh

    # ディレクトリプッシュがされているかどうかのフラグ
    $PUSHD_MINUS

    # xonsh環境が変化したかのフラグ
    $UPDATE_OS_ENVIRON

    # python実行環境へのPATH
    $VIRTUAL_ENV

    # インタラクティブ実行で動いてるかどうかのフラグ
    $XONSH_INTERACTIVE

    # xonshがログインシェルになっているかどうかのフラグ
    $XONSH_LOGIN

    # xonshスクリプトを実行している場合、そのスクリプトへの絶対Path
    $XONSH_SOURCE

 

Windows向け

    # Windowsのanxicon用
    $ANSICON = False

    # 補完した時に"/"を強制的に使用
    $FORCE_POSIX_PATHS = False

    # cmd.exe上で色を変える
    $INTENSIFY_COLORS_ON_WIN = True

    # Winでユニコード使う
    $WIN_UNICODE_CONSOLE

 

その他

    # readline, prompt_toolkit, random, best
    # 基本的にはptlが使われてそれ以外の状況ではよしなに選択してくれるbestでOK
    $SHELL_TYPE = best

    # プロンプトのタイトル
    # http://xon.sh/tutorial.html#customizing-the-prompt
    $TITLE = xonsh.environ.DEFAULT_TITLE

    # 文字コード
    $LANG = ‘C.UTF-8# サブプロセスが利用するエンコーディング
    $XONSH_ENCODING

    # エンコーディングエラーを処理するフラグ
    # https://docs.python.org/3/library/codecs.html#error-handlers
    $XONSH_ENCODING_ERRORS = surrogateescape

    # サブプロセスでエラーが出た時終了させるかどうか
    # xonshプロンプト上ではあまり良くないが、 xonshスクリプトを実行する時効果あり
    # subprocess.CalledProcessErrorがraiseされる
    $RAISE_SUBPROC_ERROR

    # historyでjson以外にsqliteが選べる
    $XONSH_HISTORY_BACKEND = ‘json’

    # historyファイルの保存先
    $XONSH_HISTORY_FILE = ~/.xonsh_history

    # コマンドやファイルのhistory保存数
    # http://xon.sh/envvars.html#xonsh-history-size
    $XONSH_HISTORY_SIZE = (8128, 'commands')

    # Standard I/OをHistoryに保存するか
    $XONSH_STORE_STDIN
    $XONSH_STORE_STDOUT

    # 端末エミュレータによるTerminalの設定色々。基本触る機会はない
    $TERM

    # エラーのトレースバックログを保存するPathの指定
    $XONSH_TRACEBACK_LOGFILE = "/"

    # デバッグ機能、数値に応じて以下が出てくる
    # 1, 重複importの抑制情報
    # 2, 入力変換、コマンド置換の情報
    # 3, PLYデバッガによる解析結果
    $XONSH_DEBUG = 0


 

- おわりに -

ひとまずまとめて終わりです。

またアドベントカレンダー内に空きがあれば自作のrcに書いている関数群を出していこうと思います。

誰か今からpip installして埋めてくれてもいいのよ!

qiita.com


 

xonshの過去のコマンド履歴を可視化する

- はじめに -

この記事は、Xonsh Advent Calendar 2017 - Qiitaの6日目の記事です。

せっかくxonshではMatplotlibが使えたりするので、Command履歴の分析等の補助をする関数を書いてメモしておきたいと思います。


 

- xonshのhistory -

xonshの過去の入力履歴は以下で呼び出す事ができます。

__xonsh_history__

実際には、xonshのコマンド履歴は以下のようにSession毎にjsonファイルで管理されています。

history file
# ~/.local/share/xonsh/xonsh-7305fb13-34ea-44fd-9845-197fe015359d.json

さっそくの余談ですが、このバックエンドにsqliteを指定する事も可能です(Tutorial: History — xonsh 0.6.0.dev151 documentation)。


__xonsh_history__で得られるxonsh.history.json.JsonHistoryオブジェクトは、Commandの履歴を取得するためのメソッドを2種類持っています。

__xonsh_history__.items()    # 今のSessionでのHistory
__xonsh_history__.all_items()    # 過去全てのSessionでのHistory

どちらもIteratorが返ってくるので、例えば現在過去全てのコマンド履歴を取得するには以下のように

for x in __xonsh_history__.all_items():
    print(x["inp"])

Reference : History Backend JSON – xonsh.history.json — xonsh 0.6.0.dev151 documentation


また、jsonを直接読みにいく事ももちろんでき、xonshのhistory.jsonを直接読みに行くスクリプトは、アドベントカレンダー内の以下の記事でも既に書かれています。

qiita.com


 

- 過去のコマンドを可視化 -

以下の記事でも利用したitermplotパッケージを利用して、iTerm2上にインラインに表示してみます。
導入や綺麗な表示の仕方は以下記事で。
(以下記事内にiTerm2以外の方法も一応記述しています)
vaaaaaanquish.hatenablog.com


空白で区切られた最初のコマンド部分が10文字以下のものをCounterに投げてMatplotlibに可視化します。

import itermplot
import matplotlib.pyplot as plt
import numpy
from collections import Counter

data = Counter([x["inp"].split(" ")[0] for x in __xonsh_history__.all_items() if len(x["inp"].split(" ")[0])<10])
labels, values = zip(*data.items())
indexes = np.arange(len(labels))

plt.figure(figsize=(20,10))
plt.bar(indexes, values, width)
plt.xticks(indexes, labels, rotation='vertical')
plt.show()

結果がxonshコンソール上で見えます
f:id:vaaaaaanquish:20171206225049p:plain


ちょっと業務で使ったものを削った結果を出していますが、これは恥ずかしいですね。

xonshなのでimportやforが多いのは当たり前ですが、ls しすぎだし、X, Yといった変数を使っていたり、果にはexitもかなり使っています。xonshが嫌いなのでしょうか。
これがまだ手元のMacなので良いですが、普段はリモートサーバで作業しているので、そちらではより酷いものが見れると思います(公開はしないですが)。


しかし、これでlsというコマンドにめちゃくちゃ時間を取られている事が分かりました。lsは基本的には自動的に発動するようにしていくのが吉という事ですね。

 
 
ついでなので最も多いimportから、何を多く使っているか見てみます。

data取得箇所を変えてやれば実現できそうです。

data = Counter([x["inp"].split(" ")[1] for x in __xonsh_history__.all_items() if x["inp"].split(" ")[0]=="import"])

 
import time, plt, mathが多そう。xonshrcに書いておいて事前importするようにすれば私の仕事も減りそうです。
f:id:vaaaaaanquish:20171206230255p:plain


 

- おわりに -

ギリギリ滑り込みセーフで記事を書きましたが何とかなりました。

もうちょっと分析らしいところまで行きたかったのですが、(見せてもOKそうな)Historyが以外に少なく、時間も微妙だったので断念しました。
まあでもギリギリでも可視化まで出来たのは、Pythonという言語がシェルで扱える事の利点でもあるなと思いました。

今まで使っていたzshのログのconvertとか前後のコマンドを確認とかまでやれれば良かった…が、アドベントカレンダーに空きを見つけて書きたいと思います。


それでも全然空きがありそうなので、xonsh試したい方でも是非ご参加下さい!!

qiita.com


 

Xonshでmatplotlibグラフをコンソールにインライン描画してメモリ状況を観察する

- はじめに -

この記事はXonsh Advent Calendar 2017 4日目の記事です。

Xonshの中にはxontribというメソッド群が存在します。
その中のmplhooksは、画像の描画をサポートしてくれます。

本記事では以下について記述します

  • xontrib.mplhooksを利用したxonshコマンドプロンプト上へのMatplotlibグラフのインライン描画
  • vm_statコマンドを利用したMac OS Xのメモリ状況の取得とPythonによるparse
  • Xonshでメモリ状況のリアルタイム可視化

以下アジェンダです


 

- Xonshのグラフインライン描画 -

Xonshでグラフは端末状にインライン描画する方法は、以下の2種類が容易されています。

  • xontrib.mplhooks.display_figure_with_iterm2(fig)
  • xontrib.mplhooks.show()

以下がソースコードです。
xontrib.mplhooks — xonsh 0.5.12.dev97 documentation
xonsh/mplhooks.py at master · xonsh/xonsh · GitHub
Pythonなのですぐ読めます。

display_figure_with_iterm2は、 iterm2_toolsを使ってiTerm2上にグラフを描画するようになっています。
showは、iterm2_toolsがimportできればdisplay_figure_with_iterm2で描画、iterm2_toolsが存在しない場合はplt.gcf()で現状プロット図を取得してきてRGB値をcolor stringにして描画する形になっています。

iTerm2でインライン描画

私はiTerm2利用者ですので以下でiterm2_toolsを導入しています。

pip install iterm2_tools

GitHub - asmeurer/iterm2-tools: iTerm2 tools for Python

以下エラーが出るのでXonshコンソールを再起動しておきます。

NameError: name 'display_image_bytes' is not defined

再起動後、Xonshコンソール上で以下を淡々と入力していきます。
もしくは適当にxshスクリプトにしてもOKです。

import matplotlib.pyplot as plt
import xontrib.mplhooks
import numpy as np
x = np.linspace(-3, 3, 20)
y = x ** 2
fig = plt.figure(figsize=(6, 4))
ax1 = fig.add_subplot(1, 1, 1)
ax1.plot(x, y)
xontrib.mplhooks.display_figure_with_iterm2(fig)

f:id:vaaaaaanquish:20171121140615p:plain:w350
xonsh上でもインライン描画できました

 

Terminal.app上のxonshでインライン描画

試しにMacにデフォルトで付随するTerminal.appでもXonsh上でインライン描画をしてみます。

上記iTerm2の時と同じGraphを作って以下。

xontrib.mplhooks.show()

f:id:vaaaaaanquish:20171121141037p:plain:w350
ちょっとアレですがまあまあ…

iTerm2の描画は良いですね

 

itermplotでインライン描画

上記2つに加えてiTerm2にはインライン描画パッケージとして、itermplotというのがあります。
GitHub - daleroberts/itermplot: An awesome iTerm2 backend for Matplotlib, so you can plot directly in your terminal.

Animation supportもされており、インラインでリアルタイム表示ができそうです。
(といっても事前にGIF画像にしてiTermの表示機能に送る形ですが…)

導入はpipで、xonshを起動する前にMPLBACKENDにitermplotを指定しておくとplt.show()がインライン描画になります。

pip install itermplot
export MPLBACKEND="module://itermplot"

また、後述しますがリアルタイム表示するにはImageMagickが必要なのでインストールしておきます。

brew install imagemagick

sampleスクリプトを動かしてみます。

import matplotlib.pyplot as plt
plt.plot([1,2,3])
plt.show()

f:id:vaaaaaanquish:20171202214602p:plain:w350
いちばんつよそう

背景が黒い場合は以下で良さげな表示に

export ITERMPLOT=rv


  

- vm_statコマンドでメモリ可視化 -

Macでメモリ状況を出力するコマンドではfreeやtop、vm_statといった選択肢があります。

今回は適当にvm_statを選択します。
Xonshであれば以下コマンドどちらも結果を得ることができます

vm_stat
$(vm_stat)    # python string

vm_statコマンドの結果をparseしてdictにする関数を作って実行してみます。

def vms():
    vm_dic = {}
    for i,x in enumerate($(vm_stat).split("\n")):
        row = x.split(":")
        if i>1 and len(row)==2:
            vm_dic[row[0]] = int(row[1].strip().replace(".",""))*4096/(1024.0 ** 3)
    return vm_dic

以下のようにGB単位でメモリ状況が出力されます。

$ vms()
{'"Translation faults"': 2588.0195083618164,
 'Anonymous pages': 7.266414642333984,
 'Compressions': 54.13356018066406,
 'Decompressions': 33.81982421875,
 'File-backed pages': 2.6624984741210938,
 'Pageins': 32.48748779296875,
 'Pageouts': 0.26355743408203125,
 'Pages active': 4.984306335449219,
 'Pages copy-on-write': 82.98124694824219,
 'Pages inactive': 4.944099426269531,
 'Pages occupied by compressor': 3.5574417114257812,
 'Pages purgeable': 0.14667129516601562,
 'Pages purged': 1.3369598388671875,
 'Pages reactivated': 22.909423828125,
 'Pages speculative': 0.000507354736328125,
 'Pages stored in compressor': 8.809425354003906,
 'Pages throttled': 0.0,
 'Pages wired down': 2.446277618408203,
 'Pages zero filled': 1392.7644996643066,
 'Swapins': 16.50379180908203,
 'Swapouts': 17.286388397216797}

 
せっかくですのでwatchコマンドのように、メモリ状況をリアルタイムで表示し続けてみます。

import json
while True:
    vm_dic = vms()
    $[clear]
    print(json.dumps(vm_dic, indent=4))

f:id:vaaaaaanquish:20171121144149g:plain:w400

watch vm_statでよくない!?みたいな何かが出来上がりました。

vms functionをprintするスクリプトにして、watchもできます。
watchコマンドはデフォルトでsh -cにコマンドを飛ばすだけなので、--exec, -xで指定できるよう環境設定するか、以下のようにxonshを定期的に作るという酷いやつで対応できます。

# mac -> $ brew install watch
watch "xonsh sample.xsh"

watch vm_statでよくない!?みたいな何かです。

  

- vm_statをMatplotlibでリアルタイム描画 -

上記2項目で作った関数を利用して、Matplotlibでvm_statの結果をリアルタイム描画してみます。

wired, active, inactive, speculative辺りがusedメモリに関連する項目ですのでそちらを可視化してみます。
(occupied by compressorも必要かな?)

スクリプトが適当です。

import time
import xontrib.mplhooks
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('ggplot') 

def init_data(fig, id, title):
    x = np.zeros(100)
    y = np.zeros(100)
    subplot = fig.add_subplot(2,2,id)
    subplot.set_title(title)
    subplot.set_ylabel('GB')
    subplot.set_xlabel('time')
    li, = subplot.plot(x, y)
    return x, y, subplot, li

def set_data(x,y,vm_dic,key):
    x = np.append(x, time.time())
    x = np.delete(x, 0)
    y = np.append(y, vm_dic[key])
    y = np.delete(y, 0)
    return x,y

def update_fig(x, y, subplot, li):
    li.set_xdata(x)
    li.set_ydata(y)
    subplot.set_ylim(min(y), max(y))
    subplot.set_xlim(min(x), max(x))

def watch_vms_windows():
    fig = plt.figure()
    x,y,subplot,li = init_data(fig, 1, "Pages active")
    x2,y2,subplot2,li2 = init_data(fig, 2, "Pages inactive")
    x3,y3,subplot3,li3 = init_data(fig, 3, "Pages speculative")
    x4,y4,subplot4,li4 = init_data(fig, 4, "Pages wired down")
    count = 0
    while True:
        vm_dic = vms()
        x,y = set_data(x, y, vm_dic, "Pages active")
        x2,y2 = set_data(x2, y2, vm_dic, "Pages inactive")
        x3,y3 = set_data(x3, y3, vm_dic, "Pages speculative")
        x4,y4 = set_data(x4, y4, vm_dic, "Pages wired down")
        if count > 100:
            update_fig(x, y, subplot, li)
            update_fig(x2, y2, subplot2, li2)
            update_fig(x3, y3, subplot3, li3)
            update_fig(x4, y4, subplot4, li4)
            plt.pause(.01)
        else:
            count += 1
            print(count, end="\r")

以下参考にしています。
Arduino で測定したデータを Matplotlib でリアルタイムプロット | org-技術

100ループ回してGraph描画に十分なデータを集めたらGraph描画を開始するスクリプトです。

f:id:vaaaaaanquish:20171121144909g:plain

QuickTime Playerを利用して画面の動画を撮っているのでactiveがガンガン上昇しinactiveが下降、メモリ利用と廃棄が頻繁に行われている事がわかります。


$[clear] して display_figure_with_iterm2(fig) でインライン描画するスクリプトにする事も可能で、試しましたがdisplay_figure_with_iterm2では実行時描画になるため、思ったより綺麗に表示されませんでした。


 

- MatplotlibのAnimationモジュールを使ってリアルタイム描画を実現する -

Matplotlibにはpltで作られた配列をアニメーションにして表示するモジュールが存在する。
全てのGraphを事前に配列に取っておいて描画するArtistAnimation、動的に生成していくFuncAnimationの2つである。
animation module — Matplotlib 2.1.0 documentation

Animationはgifやmp4を生成するだけなので、これらを前述したitermplotに流してiTerm2にインライン描画する。

itermplotのカッケー背景はAnimationには対応してないので以下だけ設定しておく。

export MPLBACKEND="module://itermplot"
export ITERMPLOT_FRAMES=100

適当に書いたコード

import time
import xontrib.mplhooks
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
plt.style.use('ggplot') 

class VMS:
    def __init__(self, fig):
        self.fig = fig
        self.x, self.y, self.subplot, self.li = self.init_data(1, "Pages active")
        self.x2, self.y2, self.subplot2, self.li2 = self.init_data(2, "Pages inactive")
        self.x3, self.y3, self.subplot3, self.li3 = self.init_data(3, "Pages speculative")
        self.x4, self.y4, self.subplot4, self.li4 = self.init_data(4, "Pages wired down")
        self.c = 0
        for i in range(100):
            self.plotsub(i)
        
    def init_data(self, id, title):
        x = np.zeros(100)
        y = np.zeros(100)
        subplot = self.fig.add_subplot(2,2,id)
        subplot.set_title(title)
        subplot.set_ylabel('GB')
        li, = subplot.plot(x, y)
        return x, y, subplot, li

    def vms(self):
        vm_dic = {}
        for i,x in enumerate($(vm_stat).split("\n")):
            row = x.split(":")
            if i>1 and len(row)==2:
                vm_dic[row[0]] = int(row[1].strip().replace(".",""))*4096/(1024.0 ** 3)
        return vm_dic

    def count(self):
        self.c += 1

    def plotsub(self, frame):
        vm_dic = self.vms()
        self.count()
        # subplot1
        self.x = np.append(self.x, self.c)
        self.x = np.delete(self.x, 0)
        self.y = np.append(self.y, vm_dic["Pages active"])
        self.y = np.delete(self.y, 0)
        self.li.set_xdata(self.x)
        self.li.set_ydata(self.y)
        self.subplot.set_ylim(min(self.y), max(self.y))
        self.subplot.set_xlim(min(self.x), max(self.x))
        self.subplot.set_xticks(np.arange(min(self.x), max(self.x), 20))
        # subplot2
        self.x2 = np.append(self.x2, self.c)
        self.x2 = np.delete(self.x2, 0)
        self.y2 = np.append(self.y2, vm_dic["Pages inactive"])
        self.y2 = np.delete(self.y2, 0)
        self.li2.set_xdata(self.x2)
        self.li2.set_ydata(self.y2)
        self.subplot2.set_ylim(min(self.y2), max(self.y2))
        self.subplot2.set_xlim(min(self.x2), max(self.x2))
        self.subplot2.set_xticks( np.arange(min(self.x2), max(self.x2), 20) )
        # subplot3
        self.x3 = np.append(self.x3, self.c)
        self.x3 = np.delete(self.x3, 0)
        self.y3 = np.append(self.y3, vm_dic["Pages speculative"])
        self.y3 = np.delete(self.y3, 0)
        self.li3.set_xdata(self.x3)
        self.li3.set_ydata(self.y3)
        self.subplot3.set_ylim(min(self.y3), max(self.y3))
        self.subplot3.set_xlim(min(self.x3), max(self.x3))
        self.subplot3.set_xticks( np.arange(min(self.x3), max(self.x3), 20) )
        # subplot4
        self.x4 = np.append(self.x4, self.c)
        self.x4 = np.delete(self.x4, 0)
        self.y4 = np.append(self.y4, vm_dic["Pages wired down"])
        self.y4 = np.delete(self.y4, 0)
        self.li4.set_xdata(self.x4)
        self.li4.set_ydata(self.y4)
        self.subplot4.set_ylim(min(self.y4), max(self.y4))
        self.subplot4.set_xlim(min(self.x4), max(self.x4))
        self.subplot4.set_xticks( np.arange(min(self.x4), max(self.x4), 20) )


fig = plt.figure(figsize=(8,8))
vms = VMS(fig)
animation.FuncAnimation(vms.fig, vms.plotsub, blit=True)
plt.show()

plt.show()がitermplotによって継承されてればインラインで見れる
f:id:vaaaaaanquish:20171203221702g:plain

これは動的に見れている訳ではなく、事前に100フレーム分をgif画像にしてbyteIOでiTerm2に送り込んでいるだけなのでリアルタイムの本質を見失っている状態である。

iTerm2に表示できるGIFのフレーム数を上手くコントロールできないとか課題は多いがまあできたという事で…

 

- おわりに -

「これ watch vm_stat で良くね!?」
「リアルタイムインライン描画とは」
Pythonスクリプトなら os.system や commands.getoutput でコマンド実行できるよね!?」
「これXonshじゃなくても良くない!?」
等の耳が痛い声が聞こえて来ます。

どんどんおかしい方向に進んだ気がしますが、xonshの繁栄のための知見として納めておきます。

おわり。

 

Webスクレイピングする際のルールとPythonによる規約の読み込み

- はじめに -

この記事は Webスクレイピング Advent Calendar 2017 - Adventar の1日目の記事です。

近年では、Pythonが様々な場面で使われるようになりました。
Webからデータを取ってくる際のスクリプトとして利用し、そのままデータを機械学習における学習データとするといった案件も多く見るようになっています。

ありがたい事に本年度書きました以下の記事は、はてなブログに投稿されたPython関連の記事の中で歴代はてブ数1位だそうです。

Webスクレイピングも日に日に情報が増え、様々なパッケージやフレームワークによって手軽になっています。

本記事は、スクレイピングやクローラを記述する際に抜けがちな、「規約」について記載するものです。

スクレイピングの間隔はどうすればいい?規約は?違法でないの?という人のために法律等もまとめています。

追記2019/01/07:
著作権法が改正され、機械学習モデリングなどに使えるスクレイピング関連のデータの取扱も変わりました。
以下の記事が詳しいので一読しておくと良いでしょう。
www.itmedia.co.jp


アジェンダ


 

- Webスクレイピングにおけるルール -

Webスクレイピングまとめ記事の後半で記載しているが、ネット上のデータを収集して取り扱う上での著作権法対象のWebサーバに適切にアクセスし身元の開示や負荷を考える上での動産不法侵入の2つを特に気にすべきである。

よりシンプルに以下にまとめてあるため、参考に。
https://vaaaaaanquish.hatenablog.com/entry/2017/06/25/202924#法律の話


 

著作権法

前者の著作権法は、スクレイピングしたデータでデータセットを作って公開したりしたらダメ、データ分析に使用する場合は他法律に触れなければOK等といった内容で、SNS等でもよく議論に上がる知的財産権に属する内容である。

日本におけるスクレイピングでの著作権の取扱は「つまるところ、データ分析や教育、引用等の認められた利用の範囲内であれば、スクレイピング行為自体は著作権法上認められた行為」となっている。これについては、以下記事が最も参考になる。
「日本は機械学習パラダイス」 その理由は著作権法にあり - ITmedia NEWS

公開されている情報の利用、機械学習におけるデータの取得等は、著作権法においては問題ない。
しかし得たデータの分析においては問題ないが、それらを公開したり、そのデータを直接的に利用して金銭を得る事は問題となる。
また当然、各サービスの規約や後述する動産不法侵入、robot.txtによって制限されている場合がある。

また、取得するスクリプトの公開等についても現状グレーとなっている。

ちなみに取得著作物が自由に使えるパターンについては文化庁のWebサイトにある以下を見ておくと良い。

 

動産不法侵入

後者の動産不法侵入は、スクレイピングするコードを書いたのだから意図性がある、相手のサーバへの負荷を考慮する、Webページが提示する条項を守るといった内容である。
著作権等で認められた場合でも高負荷なbotを作成した場合、例えツールを利用しただけの場合でも意図性が優先され裁判となる可能性がある。

相手のサーバの負荷、ネットワークの負荷、アクセス先の制約について「知らなかった」で済まされないようになっており、また実際の判例も存在する。

 
本記事で取り扱う規約保持のスクリプトは、主に後者の動産不法侵入の内容に含まれるWebページが提示する条項を守る「同意の欠如」から身を守るためのものである。

主に以下の内容をPythonスクリプトで実行していくための記事である。

  • aタグにrel="nofollow"が設定されていたら辿らない
  • robots metaタグの属性記載に従う
  • HTTP ヘッダーのX-Robots-Tagに従う
  • robots.txtに従う
  • User-agentなどを正しく設定する


   

- robot.txt -

metatagやHTTPヘッダーの確認も重要であり、後述するが、まず前提としてrobot.txt周りの話を記述する。

robot.txtとは

robot.txtとはクローラーに対する指示書である。

1994年に採用されたMK1994、および拡張のMK1996Google, Yahoo and Microsoftが2008年に出してきたGYM2008、2007年のACAP等が存在し、各検索サービス等が対応したりしていなかったりというのが現状である。

そもそもは検索エンジンのクローラに対する指示書。
これ以上の歴史的経緯は割愛するが、気になる場合は以下追っていくと良。
Nikita The Spider * Articles and News
Robots Exclusion Standard - WikipediaAutomated Content Access Protocol - Wikipedia

 
Webスクレイピング、クローリングをする上では、HTACCESS等より拘束力はないが、まずはじめにrobot.txtの準拠をしておくと良い。

robot.txtの拘束力については、「法として定義されたルールではない」「検索エンジンに向けたものでありスクレイピングにおいて準拠すべきか」などの議論が幾度となく繰り返されているが、そちらについてはここでは強く言及しないものとする。

拘束力についての議論もありながら、現実robot.txtの準拠については事件の判例にも関わっている。
検索エンジンと著作権 - 三浦基/小林憲一
フィールド対Google事件 - Wikipedia
Google、新聞記事掲載が著作権侵害とするベルギーの判決にコメント

また、以下日本における「1回URL叩いたら1秒Sleepしましょう」という言説の元となっている、国立国会図書館法に関連する資料にもrobot.txtに関する記載がある。
国立国会図書館法によるインターネット資料の収集について

この国立国会図書館の資料については、言及しておく。
岡崎市立中央図書館事件について調べ、容疑者のブログ等をよく読むと理解できるはずだが、「1アクセスごとに1秒の間隔」という基準に意味は何の意味もなく、これらの言説は無視すべきである(筆者はそういった基準を国立国会図書館が公開している事自体どうかと思う)。この岡崎市中央図書館の件は、スクレイピング時に間隔を開けていたにも関わらず、サーバ側の不具合によって高負荷と判断され、逮捕、起訴猶予となった。高木浩光氏のブログにもあるように、明らかな誤認逮捕であり、警察、検察ならびに岡崎市の技術理解が乏しい事が分かったというだけの事件ではあったものの、この事件のように「相手のサーバにとっての高負荷」が発生してしまうだけで裁判となる可能性があり、その事について我々は肝に命じておくべきである。

 
要は「節度」となるのだが、実際の判例を見るに準拠しておいて損はないだろう。


これらが気になる場合は、以下の書籍に歴史的経緯や判例の項目があるため、一読すべきである。

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

 

robot.txtフォーマット

  
robot.txtはドメイン直下に配置されるテキストファイルであり、Webスクレイピングやクローリングを行う際、以下のように読みに行く事になる。

https://hogehoge.com/robot.txt

以下にrobot.txtのsampleを示す。

User-agent: *
Disallow: *

User-agent: Googlebot
Allow: *
Disallow: /private

上記は「Googleクローラーは/private以外どこでもアクセスしていいけど、それ以外のbotは全て禁止する」という指示を記したものである。

基本的にMK1994のSyntaxに従っていれば、python内のurlib.robotparserでparseする事が出来る。
後述するように、自作パーサーを作る他、Robotexclusionrulesparser、reppyといったパーサーパッケージも存在する。

また、一部Webスクレイピングまとめ記事でも紹介したようなmechanizeパッケージ等、自動でrobot.txtを読み込んでキャッシュしておき、規約に応じたスクレイピングが可能なものもある。

# mechanize
# set_handle_robots(False)しなければ以下エラーになる
httperror_seek_wrapper: HTTP Error 403: request disallowed by robots.txt

http://wwwsearch.sourceforge.net/mechanize/

後述するようにスクレイピングのためのフレームワークとしても有名なScrapyにもそれらを適応するパラメータが存在する。


 

- Pythonスクリプトでの規約の読み込み -

上記の内容を踏まえ、Pythonで各規約情報を読み込むスクリプトを示す。
ここでは「aタグのrelチェック」「robot metaの検出」「HTTPヘッダーのチェック」に加えて、既存のPython謹製パーサーによるrobot.txtのパースについて記述する。

aタグのrelチェック

aタグにはrel属性でnofollowが指定されている場合がある。
rel="nofollow"はスパムコメントに対する防御策として提案され、コメント欄等にスパムURLが投稿される可能性のある箇所に自動的にnofollowを付ける事で、GoogleやYahooの検索エンジンにおけるリンク先考慮のPageRankを下げないように設置するものである。
nofollow - Wikipedia
RFC 3667 - IETF Rights in Contributions
RFC 3668 - Intellectual Property Rights in IETF Technology

近年ではSEO対策に加え、スクレイピング負荷対策等に使われる場合がある。

robot.txtに比べスクレイピング、クローリングに対する拘束範囲は小さいが、rel="nofollow"されたリンクはWebサイト作者の意図しないリンクである可能性が高いため、処理しておくと良い。


BeautifulSoupならfindAllの引数としてlambdaや正規表現が渡せるので、それらを利用する。

from bs4 import BeautifulSoup
import re

html = """
<html><body>
<a href="sample.com" />
<a rel="nofollow" href="badsample.com" />
<a rel="nofollow" href="badsample.com" />
<a href="sample.jp" />
<a href="sample.ne.jp" />
</body></html>
"""

soup = BeautifulSoup(html, "html5lib")
# hrefが設定されておりrelにnofollowが設定されてないaタグを探す
links = soup.findAll('a', href=True, rel=lambda x: "nofollow" not in str(x).lower())
for link in links:
    print(link['href'])


lxmlを使って解析している場合はrel属性のついたlinkを探せるfind_rel_linksがある。

import lxml.html

h = lxml.html.fromstring(html)
for x in h.find_rel_links("nofollow"):
    print(lxml.html.tostring(x))


 

robot metaの検出

クローリングされないため、Webサイトにmetatagとしてnofollow、noarchive、noindexを設置する方法がある(他にもnocacheや特定サービス拒否のcontent属性がある)。noarchiveであればそのページを保存しないし、nofollowがついていれば以降リンクを走査しないようにしておくと安全。
http://www.robotstxt.org/meta.html
HTML 4.01仕様における記載

BeautifulSupの解析器にLambdaを投げるのが吉。

from bs4 import BeautifulSoup
html = """
<html><body>
<meta content="nofollow" name="robots"/>
<meta content="noarchive" name="robots"/>
<meta content="noindex" name="robots"/>
<a href="sample.com" />
</body></html>
"""

soup = BeautifulSoup(html, "html5lib")
meta = soup.find_all('meta',
                     attrs={"name":"robots"},
                     content=lambda x: "nofollow" in str(x).lower() or "noarchive" in str(x).lower())
print(meta)
print(len(meta) > 0)


 

HTTPヘッダーにおけるX-Robots-Tagのチェック

Googleによると、HTTPヘッダーでも上記metatag設定と同義の事ができるらしい。
Robots meta tag and X-Robots-Tag HTTP header specifications  |  Search  |  Google Developers

一応チェックできる。例えばrequestsなら下記。

import requests
r = requests.get('https://hogehoge.com')
print( "nofollow" not in str(r.headers.get("X-Robots-Tag")) )
print( "noarchive" not in str(r.headers.get("X-Robots-Tag")) )

設定してるWebページがそこまで多い訳じゃないが、見ておくと良さげ。

 

urllib.robotparserを使ったrobot.txtのparse

21.10. urllib.robotparser — robots.txt のためのパーザ — Python 3.6.5 ドキュメント

Pythonデフォルトで使えるパーサ。

Googleのrobot.txtをparseしてみる。

[https://www.google.com/robots.txt]

以下のように読み込んでcan_fetchしていく

import urllib.robotparser
rp = urllib.robotparser.RobotFileParser()
rp.set_url("https://www.google.com/robots.txt")
rp.read()

# エージェントがURLを見れるか
if rp.can_fetch("*", "https://www.google.com/search"):
    print("OK")
else:
    print("NG")

# クローラの遅延時間指定パラメータの取得
# なければNone
print(rp.crawl_delay("*"))
print(rp.request_rate("*"))

参考:Parsing Robots.txt in python - Stack Overflow

 

Robotexclusionrulesparserを使ったrobot.txtのparse

Robotexclusionrulesparserはurllib.robotparserのBSD License代替スクリプト
Nikita The Spider * A Robot Exclusion Rules Parser for Python

前述したGYM2008とMK1994/96との構文の差で発生する問題(ワイルドカード*をどう読むか)等について触れられており、両者をparseできるようになっている。non-ASCIIやBOM等にも対応。
RobotExclusionRulesParser and RobotFileParserLookalikeの2つのクラスを提供していて前者がwrapperになっている。
BSDライセンスなのでGithubで検索すると導入しているところがちらほら。

以下URLのスクリプトを利用するか、pipで導入する。
http://nikitathespider.com/python/rerp/robotexclusionrulesparser-1.7.1.py

pip install robotexclusionrulesparser

使い方はほぼurllibと同じ。

import robotexclusionrulesparser

rerp = robotexclusionrulesparser.RobotExclusionRulesParser()
rerp.fetch('https://www.google.com/robots.txt')
if rerp.is_allowed('*', '/search'):
    print("OK")
else:
    print("NG")

get_crawl_delayやparseもあるので結構使える。
複数のWebサイトを取って回る、urllibじゃparseできない時に有用。

 

reppyを使ったrobot.txtのparse

robot.txtのparseパッケージ
クロール遅延やサイトマップの読み込み、robots.txtをキャッシュしておく機能が備わっている。
GitHub - seomoz/reppy: Modern robots.txt Parser for Python

導入はpipで

pip install reppy

使い方もほぼ上記と同じ。
sitemapが見れるだけでなくHeaderWithDefaultPolicyでこちらのPolicyに合わせてbool値が出せたりする。

from reppy.robots import Robots

# This utility uses `requests` to fetch the content
robots = Robots.fetch('https://www.google.com/robots.txt')
if robots.allowed('https://www.google.com/search', 'robot'):
    print("OK")
else:
    print("NG")
    
print(robots.agent('*').delay)
print(list(robots.sitemaps))

reppyの強みはcacheにある。

from reppy.cache import RobotsCache
robots = RobotsCache(capacity=100)
robots.allowed('https://www.google.com/search', 'robot')
print(robots.cache)

これによりrobotsを使いまわしながら、複数のサイトのrobot.txtを採用しながらスクレイピングを行うことができる。

 

scrapyではどうすればいいの?

scrapyでは、以上のrobot.txtパーサを利用しなくてもurllibを利用してくれるパラメータが存在する。

ご存知の通り、scrapyのデフォルト設定はスクレイピング先のサーバに負荷をかけまくる極悪設定になっているので、必ず以下くらいを設定しておくと良い。

DOWNLOAD_DELAY = 5
ROBOTSTXT_OBEY = True 

負荷を気にするなら、そもそもScrapyを使わないという選択肢も大いにあるが…


 

- おわりに -

robots.txt、Metatags辺りに対応する方法についてまとめておきました。

最近、辻さんがTwitterで仰っていましたが、robot.txtとnoindexを情報隠ぺいのように使う企業も存在します(下記はrobot.txtとタグの優先順序により失敗していましたが…)。


直近だと、Ads.txtなるものも出てきました。
Industry Aligns to Adopt ads.txt Specification – IAB Tech Lab

今後クローラ界隈がどうなっていくかは未知ですが、する側もされる側も良識を持って対応しなければいけないというのが現状で、こうしていれば間違いない、批判されないという方法はありません。

日本では解析のためのWebスクレイピングが認められている訳ですが、その中でも全員が節度を持って行動する事が全員のためになると私は思います。
「日本は機械学習パラダイス」 その理由は著作権法にあり - ITmedia NEWS


最後になりましたが、上記に加えてWebスクレイピングを行う際はUser-Agentを適切に設定しましょう。

明日はanoChickさん!期待です!
adventar.org


Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術