Stimulator

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

『人工知能プログラミングのための数学がわかる本』が機械学習研究入門書としてとても良さそうだった

- はじめに -

本を読んで筆者に媚を売る記事シリーズです。

人工知能プログラミングのための数学がわかる本」という書籍を筆者の石川 聡彦(Aidemy)@akihiko_1022さんから譲り受けました。

人工知能プログラミングのための数学がわかる本

人工知能プログラミングのための数学がわかる本

明日2/24発売ですが、筆者である石川さんがCEOを務めるAidemyさんと人工知能機械学習のイベントにてご縁があり頂く形になりました。


そもそもAidemyは、Python及び機械学習のための知識と実装に関する学習を行えるWebサービスです。

aidemy.net

似たサービスではUdemy(https://www.udemy.com/jp/)というアメリカのサービスがかなりのシェアを誇っています。

Aidemyは後発ですが、丁寧な日本語解説と内容の質の高さから、機械学習界隈でも「Aidemyは良い」という声を聞く程優良なサービスです。
こういった初学者向けのサービスの僅かなミスを論う意地の悪い界隈でも評判が良いのがすごい。

私自身も最初のコースだけやりましたが、よく出来たWebエディタと正しい導き方を見て素晴らしいなと思いました。


筆者がそのAidemy CEOの石川さんという事で、丁寧な導きと潔い切り口で書かれた本でした。


 

- どんな人が読むと良さそうか -

個人的には以下のような人にオススメです
- 機械学習の研究室に入りたい、研究をはじめたい
- 機械学習の論文が読みたい
- 機械学習における数式を噛み砕いて理解したい
- 高校、大学数学の知識を呼び戻したい
- これからMLPシリーズや高レベルな書籍を読む

とにかく「機械学習の研究室に一冊あるとめっちゃ捗る」。
これだけは間違いなく声を大にして言えます。
研究入門として素晴らしい構成です。

以下書籍の雰囲気です。

f:id:vaaaaaanquish:20180223211609j:plainf:id:vaaaaaanquish:20180223211600j:plain
めっちゃ丁寧できれい

本当に高校数学から大学における確率、線形代数までこの優しさで解説されているのでGoodです。

 
対して「機械学習を業務でやる」「データサイエンスの知識が欲しい」人には直結して学習効果の高い本ではなさそうです。

直結して効果が薄い、というのは実際の業務や実装では「これくらい知ってて当然」という場合が多々あると思われるからです。私の知っている機械学習エンジニア各位なら、10分で読了して、内容に準じた小テストまでこなせるでしょう。
ただそういった人達でも、自分の復習と理解の落とし込みのためであればかなり良い書籍であるという事は間違いないと思いました。

加えて「数学、線形代数をより理解したい」という人は満足できないでしょう。高校、大学数学の基礎から、機械学習への導入の持っていき方が素晴らしい書籍であって、定義を明確に数式を展開していくものではないです。


 

- 書籍の良かったところ -

前述の通り、高校、大学数学の基礎から、機械学習への導入の持っていき方は非常に滑らかに感じました。

数学といっても本当に2次方程式平方根、指数関数といったレベルから説明が入ります。
三角関数、集合、行列、ベクトル、確率、…と進み総復習のような形です。

それらの説明に対し全ての節に「人工知能ではこう使われる!」という説明が入っています。
これがなかなか一般的な書籍にない所で、「この為に勉強してるんだ」という実感が持てる所が、入門書としての格を上げています。

f:id:vaaaaaanquish:20180223230129j:plain:w400
こう使われる!

また、数式内にも色や線を利用して丁寧に解説が挟まれていたり、イメージの図もより優しい表現が使われています。

 
機械学習の仕組みを読み解く上での基礎となるワードが配置されているかつ、実際の機械学習に使われる箇所まで説明される書籍というのはなかなか無く、研究をはじめる前に読んでおくことで他の少しレベルを上げた書籍が一気に読みやすくなるでしょう

そういった点からタイトルを「機械学習研究入門として良さそう」としています。

 
加えて、最後の章では実際に「Boston Housing Dataset」「青空文庫」「MNIST」といった入門向けデータセットを利用してデータ分析、自然言語処理、画像認識の実体験を進める事ができます。

これらもコードがGithubで見られるようになっており、実際に学んだ知識を使いながらスムーズに体験できる所が最も素晴らしい所だと思います。

 
あと以下の記事の時にも書きましたが、こういった書籍のコラムは本当に良いです。

vaaaaaanquish.hatenablog.com

機械学習界隈で使われるワード」というのは意外と外に出回るものではありません。
機械学習界隈の人間同士の会話の中で自然に出てくるワードの知見が得られるのも書籍の良いところだと改めて感じました。


 

- 書籍で足りないところ -

導入までが素晴らしい書籍ですが、「じゃあこの本で得た定義で機械学習の研究室や勉強会でドヤ顔できるか」と言われたらできないでしょう。

最終章でDeep Learningニューラルネットワーク誤差逆伝播法、勾配法をピックアップしてより詳しく説明していますが、それでもさらに話を進めて研究レベルに持っていくには一歩足りないイメージです。
ただ「連続とは何か」みたいなレベルから書いていたら辞書みたいなサイズになってしまうので、どこかで情報を切らないといけない訳ですが、そういう意味では潔い書籍であるとも言えます。
さらに深く学ぶにはより専門的な書籍を探しましょう。

(これはつまり「この後Aidemyをやれ」という事なのかも…)

 
個人的にはこれ以上批判すべき所がなく「面白くねえな…」「これ普通に高専で研究始める前とかに読みたかったわ…」となりました。


 

- おわりに -

「松尾豊氏推薦!」という強めのワードと、なかなか可愛い表紙が特徴的な一冊です。

あと姑息な宣伝ですが、そんなAidemyの石川さんと私が登壇するMANABIYAというイベントがあるらしいです。
石川のハンズオンは既に満員みたいですが是非私の与太話を聞きに来て下さい。
manabiya.tech


今回Aidemyのステッカーも貰ったので、さらに媚を売るためにPCに貼った写真で終わりにしたいと思います。

f:id:vaaaaaanquish:20180223205736j:plain:w400
かわいい表紙

普通に研究室や会社に一冊あると、ふとした時に復習できる良書籍だと私は思いました。

 

人工知能プログラミングのための数学がわかる本

人工知能プログラミングのための数学がわかる本

 

共同通信と朝日新聞の記事URLを含むツイートを削除するPythonスクリプト

- はじめに -

この記事の起点となったのは、2018年1月25日、共同通信が配信した「山中氏、科学誌創刊に深く関与か」というタイトルの記事が、同日午後8時頃「山中所長が給与全額寄付」というタイトルの記事に書き換えられていた件である。

下記ツイートの通り、追記や編集の知らせ無しにネットメディアが大幅に修正された場合、記事公開当初と意見の辻褄が合わず、自身の発言に責任が取れなくなる場合がある。


自身が良いと共有した記事が卑猥、卑劣な記事になっている場合を防ぐため、特定のメディアの記事に言及していたツイートを削除するスクリプトについてメモしておく。


事前に必要な要件は以下の通り


 

- スクリプト -

最初に全体のスクリプト

API周りのKeyは、http://phiary.me/twitter-api-key-no-japanese から電話番号をアカウントに紐付けた後、https://apps.twitter.com/ にて取得する。

全ツイート情報を含むCSVは、Twitter公式のSettingsから、全ツイート取得の申請を出すと登録しているメールアドレスに30分程でダウンロードリンクが送られてくる。

f:id:vaaaaaanquish:20180126231631p:plain:w300

import tweepy
import urllib
import csv
import urllib.request

CONSUMER_KEY = ''
CONSUMER_SECRET = ''
ACCESS_TOKEN = ''
ACCESS_SECRET = ''
CSV_PATH = 'tweets.csv'
DOMAIN_LIST = ["this.kiji.is", "www.asahi.com"]

auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET)
api = tweepy.API(auth)


def expand(url):
    req = urllib.request.Request(url, method='HEAD')
    resp = urllib.request.urlopen(req)
    return resp.url


def expand_url(url):
    eurl = expand(url)
    while eurl != url:
        url = eurl
        eurl = expand(url)
    return eurl


def main():
    with open(CSV_PATH, 'r') as f:
        reader = csv.reader(f)
        header = next(reader)
        for i, row in enumerate(reader):
            flag = False
            for x in list(set(row[9].split(","))):
                if x != "":
                    try:
                        y = expand_url(x)
                        for domain in DOMAIN_LIST:
                            if urllib.parse.urlparse(y).netloc == domain:
                                flag = True
                    except KeyboardInterrupt:
                        raise
                    except:
                        pass
            if flag:
                api.destroy_status(row[0])

if __name__ == '__main__':
    main()

print等は適宜。

ツイートが削除されるスクリプトなので確かめながら使う。


 

- 適当な色々 -

以下は駄文である。

このスクリプトを書くにあたっての実験的な色々とか。

短縮URLの展開がurllibだけでできるようになってた

短縮URL 展開 Python」みたいに適当にググると、Python2系のhttplib.HTTPConnectionを使ってHEADメソッド投げるスクリプトが沢山でてくるのは知ってたけど、3系からurllib.request.RequestでHEADできるの知らなかった。

Python3系で短縮URLを展開するのは以下みたく

import urllib.request
root_u = "http://hogehoge"

def expand(url):
    """curl --head url"""
    req = urllib.request.Request(url, method='HEAD')
    resp = urllib.request.urlopen(req)
    return resp.url

def expand_url(url):
    """短縮URLをできるだけ展開する"""
    eurl = expand(url)
    while eurl != url:
        url = eurl
        eurl = expand(url)
    return eurl

print(root_u, expand_url(root_u))

個人的には使い所は今のところないがハッピーな気がする

 

CSVの読み込み

CSVは大体30分くらいでメールが来て、ツイート数226Kで40Mくらいのzipになってた。
前回ダウンロードした時より大分大きくなってる気がした。

試しに1000件くらいやってみたけど、割りと接続できないURLがあったので、そちらも削除した方が良いような気がした。

import urllib
import csv
with open('tweets.csv', 'r') as f:
    reader = csv.reader(f)
    header = next(reader)
    for i, row in enumerate(reader):
        # 複数URLはカンマ区切り
        for x in list(set(row[9].split(","))):
            if x != "":
                try:
                    y = expand_url(x)
                    print("base: {}\nexpanded: {}\ndomain: {}".format(x, y, urllib.parse.urlparse(y).netloc))
                except KeyboardInterrupt:
                    raise
                except:
                     print("Not Found : ", x)
        if i > 1000:
            break

headerはtweet_idとexpanded_urlsしか使ってないけど、textとか時間も考慮した方が良い気がした。

 

記事の書き換えについて

全体を通して嫌な気分になる話だったが、解決方法が見当たらず難しい問題だと思う。

日本の今の社会形態からして、記者が常に誠実かつ知識を多く習得し続けるというのは難しいだろうし、それらを補正するには専門家の意見を割く事になる。
記者側としても「スピード感持って数多くの読まれる記事を出したい」という気持ちは強いだろうし時間の制約は大きい。

その点ネットでは公開直後から意見が集まる訳なので、今回あくまで修正方法が下衆だったという話にして、今後「指摘があったので修正しました」「間違っていたので差し止めます」が気軽に言える社会になっていけば良いなと思う。

法律の範囲内であれば、間違う事自体は決して悪い事ではない。


 

- おわりに -

花金の飲み会おわりの勢いで書いた。

間違ってたら修正すればええねん。


 
追記 2018/01/27 0:14 :
ミスを指摘されたのでサイレントで修正しました。


 

自動運転シミュレータのCARLAを動かす

- はじめに -

Python APIを備えた自動車運転シミュレータである「CARLA: An Open Urban Driving Simulator」を動かすまでの記事です。

CARLAはConference on Robot Learning 2017でも発表された、Unreal Engine 4を使ったシミュレータパッケージです。
自動運転技術開発のための様々なカメラ、センサー情報をPythonで取得し、車操作に反映する事ができます。

とやかく言っても仕方ないので以下YouTubeの動画を一回参照して下さい。

www.youtube.com

本記事はCARLAの導入と触りまでを記述するものです。

特に物を作ってどうこうしてるアレではありません。

 

- CARLAについて -

CARLAは、CoRL2017発表論文のAbstにもあるように、自動運転技術の発展のためのOSSです。

作者

作者はAlexey Dosovitskiy、German Ros、Felipe Codevilla、Antonio Lopez、Vladlen Koltunらで、IntelTOYOTAの研究所、Computer Vision Centerのメンバーです。
(Toyota Research Instituteなのでシリコンバレーの方だと思います)

 

競合

競合パッケージではMicrosoftのAirSimが一番有名だと思います

CARLAと同じくUE4を利用しており、C++, Python, C# and Javaのクライアントがあります。
また、CARLA同様、カメラ画像情報だけでなく深度やSegmentationのセンサー情報を利用できます。
車だけでなくドローンも対応しており、Windowsバイナリもあるのでパパっと始められる点で優位です。

強いてCARLAを使った理由は環境(天気とか)が変えられる事くらいです。
正直buildしんどかったのでAirSimの方が良いのかなと思ったりしています。

あと他にもUdacityの教材リポジトリもありました。
GitHub - udacity/self-driving-car-sim: A self-driving car simulator built with Unity
こちらはUnityを直接触る内容のようです。
こういうのがちらほらあります。

 

CARLAパッケージ情報

Github
github.com

Document:http://carla.readthedocs.io/en/latest/

CoRL2017発表論文:http://vladlen.info/papers/carla.pdf

 

動作環境

Documentには、WindowsLinuxの項目がありますが、現在Windows版の説明部分はComing Soonとなっています。
How to build on Windows - CARLA Simulator


Linuxでは、Ubuntu 16.04かそれ以降のバージョンを推奨しています。
How to build on Linux - CARLA Simulator

他に環境構築をしていて気になった点は以下です

  • pyenvやvirtualenv環境下のPythonではPATH周りで死ぬ
    • 素直にaptでpython3-dev入れるのが現状良い
  • ストレージがそれなりに必要
    • UnrealEngine本体のbuildと素材のダウンロード
    • 50GBでやったら足りませんでした
  • sudoersアカウント欲しい
  • Unreal Engine 4がOpenGL 3.x or 4.xを使う
  • 別途Windowsが必要
    • Winからしかダウンロードできないパッケージを使う
    • .NETFrameworkも必要
    • UE4の権利関係の問題


正直注意点が多いのでクリーンでストレージも多いUbuntu 16.04が入ったハード1つ用意出来るならそれがベストです。
その中でVMWindows動かすとかが良いでしょう。


 

- CARLAインストール -

buildしていきます。
一応ドキュメントに従っていきますが、まあまあ大変です。
How to build on Linux - CARLA Simulator

 

私の環境

一応私の環境を書いておきます

MacbookPro上のVMware Fusion
Ubuntu 16.04
python3-dev
ストレージ 100GB
コア数 4
メモリ 8GB
(UE4のEditor開くだけでもmmapで1.5GBくらい確保してたのでそれなりに必要)

別途適当なWindows10端末

 

依存パッケージのインストール

CARLA依存パッケージは以下コマンドで導入
途中でrequestsが必要になったのでここでインストールしています。

sudo apt-get install clang-3.9 cmake ninja-build python-dev tzdata sed curl wget unzip autoconf libtool python3-dev libxml2-dev libxslt-dev git build-essential python3-pip python3-requests
pip3 install protobuf requests


以下を参考にUnrealEngineのbuildの依存パッケージを導入
Building On Linux - Epic Wiki

# Ubuntu 16.04
sudo apt-get install mono-mcs mono-devel mono-xbuild mono-dmcs mono-reference-assemblies-4.0 libmono-system-data-datasetextensions4.0-cil libmono-system-web-extensions4.0-cil libmono-system-management4.0-cil libmono-system-xml-linq4.0-cil cmake dos2unix clang-3.5 libfreetype6-dev libgtk-3-dev libmono-microsoft-build-tasks-v4.0-4.0-cil xdg-user-dirs


各Setupスクリプトでchmodコマンドが幾つか使われているので、自身のアカウントをsudoersに入れておく。
Ubuntuでsudoersに自身を入れるには以下

sudo gpasswd -a {アカウント名} sudo


Unreal Engineとの依存関係と互換性問題を解決するには、コンパイラC++ runtime libraryを全て統一する必要がある。
今回は素のUbuntuが用意できたのでドキュメントの通り、clang3.9とLLVMのlibc++を使用した。

sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/lib/llvm-3.9/bin/clang++ 100
sudo update-alternatives --install /usr/bin/clang clang /usr/lib/llvm-3.9/bin/clang 100

update-alternativesによってclang提供のメッセージが出ればOK

 

Unreal Engine 4のインストール

UnrealEngineをcloneしてくるが、EpicGamesのGithubリポジトリはcloseしているため、開発者として登録する必要がある。

以下URLの右上からサインアップしてGithubアカウントをProfileに記載する必要がある。
https://unrealengine.com

ここ
f:id:vaaaaaanquish:20180105214109p:plain:w400

サインアップしたら登録したメールアドレスの認証も行っておく(認証してないと後述の自動車素材がダウンロードできない)


サインアップ後、右上に人型アイコンができるのでそこから開発者用Profileページへ。
「接続済みアカウント」に行って下にスクロールしていくと、Githubアカウントを記載するフォームがあるので書く。
f:id:vaaaaaanquish:20180105214406p:plain:w400


https://github.com/EpicGamesとか適当にgithubページに行くと招待状が届いている。
invitationを表示してjoin memberする
f:id:vaaaaaanquish:20180105214423p:plain:w400
(背景が黒いのは自分が拡張しているから)


これでやっと以下スクリプトが通るようになるので、clone時にGithubアカウント認証して設定スクリプトを走らせてmakeする。

makeでは[x/N]みたいな表示が何回か出るし、素材ダウンロードもあるので適当なマシンでやると4時間くらいかかる。
最初適当に50GBくらいVMで容量設定したが、make時足りなくなって途中で止まったのでGPartedを久々に使う事になった。
ストレージは多めに見積もる必要がある。

git clone --depth=1 -b 4.17 https://github.com/EpicGames/UnrealEngine.git ~/UnrealEngine_4.17
cd ~/UnrealEngine_4.17
./Setup.sh
./GenerateProjectFiles.sh
make

 
Setup.shでは、「Success」となっていてもその下にエラーメッセージが表示されている場合がある(大抵権限周りで)。
~/.configを触る権限がないとかなのでsudoersでやり直し。

Setup.shの真のSUCCESSは以下画像参照
f:id:vaaaaaanquish:20180105215246p:plain:w400


makeする時もsudoersに入ってないアカウントで作業して、sudo等の扱い間違えると権限の関係で以下辺りで死ぬ。
chmodやchownコマンドで権限を読み書きできるようにして無理やり通しても、後続のCARLAビルドする時に死ぬので諦めてやり直すのが吉。

Refusing to run with the root privileges. (rootじゃmakeできないよー)
Access to the path "~~~~~" is denied (Access権限足りんよー)
CrushReport-linux-Shippingのレシピで失敗しました

  
権限周りを丁寧にやっていてもUE4Editorのレシピだけ失敗する場合がある。

Makefile:188: commands for object 'UE4Editor' failed
 make: *** [UE4Editor] Error 137

依存パッケージのビルド順の問題だったので以下のように1つ1つ愛のある手作業でビルドしていくと通った。

make ShaderCompileWorker
make UnrealLightmass
make UnrealPak
make UE4Editor
make CrashReportClient-Linux-Shipping RequiredTools UnrealFrontend

UnrealFrontend-Linux-Shippingじゃないの…と思ったけどUnrealFrontendっぽい。
参考:Compilation error: make: *** [UE4Editor] Error 137 - UE4 AnswerHub


もしUEのbuildをやり直したい時はmake cleanがないので以下のようにARGSを設定する
(Building On Linux - Epic WikiのEnhancing the Makefile)

make ARGS=-clean

 

CALRAのインストール

CARLAをcloneしてきてbuildする。
~/CARLA/Unreal/CarlaUE4/Contentへの追加や色々ダウンロード等が入るので、これもまた1時間ほどかかる。

(後で知ったけどCompiled versionもダウンロードできるっぽい…)
Release CARLA 0.7.0 · carla-simulator/carla · GitHub


Windows端末が必要な作業が後半にあるので、Setup中に出来る所までやっておくと吉。

ここでpyenv使ってる人はfaildやskippedが出たり、protobufやpyconfig.hが見つからないよと出る。
pyenv global systemして、UE4の関連パッケージインストールからやり直せば大丈夫。

git clone https://github.com/carla-simulator/carla ~/CARLA
cd ~/CARLA
./Setup.sh

SUCCESSとなったら設定おわり
f:id:vaaaaaanquish:20180105215841p:plain:w400

 
この後まさかのWindows端末が必要になる。
これも時間がかかるのでSetup中に出来る所まで以下やっておくと吉。

~/CARLA/Unreal/CarlaUE4/Content/配下に、追加で手動で自動車素材となるAutomotiveMaterialsを入れる作業である。
http://carla.readthedocs.io/en/latest/how_to_add_automotive_materials/

WindowsにAutomotive Materialsを追加したダミーのプロジェクトを作り、プロジェクト内にあるAutomotiveMaterialsディレクトリをLinux端末にコピーしてくる(現状ライセンスの関係で自分で入れる形になっているらしくこの方法の改善に取り組んでいるらしいが…)

 
以下URLからパッケージを0$で購入してダウンロードする訳だが、そのダウンロードがWindowsのLauncherアプリからしかダウンロードできない。
Automotive Materials by Epic Games in Epic Showcase,Materials - UE4 Marketplace

Windows端末で上記のパッケージのURLにアクセスして、右上の「ダウンロード」からEpic Games Launcher自体のインストールためのInstallerを落としてくる。

そしてInstaller起動してよしなにWindowsにインストール。
インストールできたらLauncherを起動して「次へ」をおしながら、.NETFrameworkが必要だったりするので、ウィンドウの表示に従ってポチポチしていけば良い。

起動したらLauncher内でマーケットプレイスに行けるので移動して、Automotiveで検索。
バージョンを4.17に合わせてプロジェクトを作成する(最新が4.18になってたので注意)。
f:id:vaaaaaanquish:20180105220528p:plain:w400
プロジェクト作成したらダウンロードがはじまる。

上記作業が終わったら、Windowsの「プロジェクト作成」で作ったプロジェクト配下にある{プロジェクト名}/Content/AutomotiveMaterialsディレクトリをコピーして、scp等でどうにかLinuxマシンに移動する。


Linuxに送信したら、Linux端末の~/CARLA/Unreal/CarlaUE4/Content/配下に送信してきたAutomotiveMaterialsディレクトリを丸々コピーする。
(ContentディレクトリはSetup.shによって生成される)

以下のようにPathを設定しておく

echo 'export UE4_ROOT=~/UnrealEngine_4.17' >> ~/.profile
source ~/.profile


Rebuildスクリプトを走らせて、素材ファイルのリンクと周辺のビルドをする。
OpenGL 3.x環境の場合はUE4Editorの起動時の引数に-opengl3が必要になるので、Rebuild.shの編集が必要。

Rebuild.sh 最後の方(65行目くらい)にある以下を変更
- ${UE4_ROOT}/Engine/Binaries/Linux/UE4Editor "${PWD}/CarlaUE4.uproject"
+ ${UE4_ROOT}/Engine/Binaries/Linux/UE4Editor -opengl3 "${PWD}/CarlaUE4.uproject" 
UE4_ROOT=~/UnrealEngine_4.17 ./Rebuild.sh


リンク作業のためにUE4Editorが起動する(ここでOpenGLが3以上でないと詰む)
初回起動時は初期化が入るのでそれなりの時間を要する。

f:id:vaaaaaanquish:20180105221116p:plain:w400
死んでいる様子

リンク作業は以下参照
http://carla.readthedocs.io/en/latest/how_to_add_automotive_materials/

コンテンツブラウザでContent/Static/Vehicles/GeneralMaterials/MaterialRedirectorsに行って「RedirectorInstance」を開き検索フォームで「Parent」を検索。DummyCarをM_Carpaintに名前を変えて上書き保存して終わり。
https://carla.readthedocs.io/en/latest/img/materials_screenshot_00.png
https://carla.readthedocs.io/en/latest/img/materials_screenshot_01.png

多分この作業で失敗すると人や車が表示されないので何度かやってみる。

 
以下のようにxbuild周りでエラーが出た場合は、UEのmakeをやり直すか権限設定を見直す。

ERROR: UBT ERROR: Failed to produce item
not found xbuild


 

- CARLA Serverの実行と操作 -

CARLAの操作はServerを建ててPython Clientから操作という形になる

具体的にはC lang APIを叩いており、ドキュメントにある図で言うとこんな感じ
https://carla.readthedocs.io/en/latest/img/carlaserver.svg

http://carla.readthedocs.io/en/latest/how_to_run/

ドキュメントの通りbuildに成功していれば~/CARLAディレクトリ直下に以下スクリプトができているので実行すると、UE4が起動しCarla Serverが動き始める。

./CarlaUE4.sh /Game/Maps/Town01 -carla-server -benchmark -fps=15

ない場合はbuild失敗してるか、compile済みのlatestから取ってくるか。

操作用のクライアントsampleを動かのに必要なパッケージを入れる

sudo pip3 install numpy Pillow pygame

別コンソールを開いて~/CALRA/PythonClient/内にあるsampleを動かせばひとまず操作できるようになる。

python3 ~/CALRA/PythonClient/manual_control.py

f:id:vaaaaaanquish:20180109215835p:plain:w400

manual_controlの操作は以下の通り

    W  : 進む
    S  : ブレーキ
    A  : 左
    D  : 右
    Q  : バッグ
    Space : ハンドブレーキ
    R   : restart

とりあえず楽しいです。


 

- おわりに -

ひとまず動作させるまでで終わりです。

本当は年末年始でAirSimも試し、クライアント作りまでやって設定色々書こうくらいまで思っていましたが、UE4のbuildに何回も失敗したり、Ubuntuが死んだり、CARLAが起動しても人Moduleが表示されなかったりとまあまあ辛かったのでここまでになりました。


CARLAが動けばAirSimも動くはずなのでチャレンジします。


次は自身で自動運転操作するまでの工程とか、CARLAのパラメータsettingについて書いていきたいと思います。

がんばるぞ。

 

Xonshのconfigを書く

- はじめに -

//------------------------
追記 2018/06/22:

以下の通り、config.jsonはサポートされなくなり、xonshrcになりました。

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

xonshrcは以下にまとめました
vaaaaaanquish.hatenablog.com

//------------------------


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

xonshには.xonshrcに加えて、staticなjson形式のconfigファイルで設定するパラメータがある。

本記事では、config.jsonの書き方について記載する。


xonshrcについては以下
vaaaaaanquish.hatenablog.com


 

- config.jsonとは -

config.jsonはデフォルトでは ~/.config/xonsh/config.json に配置する。

より正確なPathは $XONSH_CONFIG_DIR/config.json である。

このファイルは、xonshrcより前に読み込まれ、外部シェルからのデータロードや、xonshrc内でも採用されるような重要な環境変数の設定、xontribの記載などを行うものである。

以下参考
http://xon.sh/xonshconfig.html


 

- configを書く -

現状keyとして読み込まれるのは、envxontribsforeign_shellsの3つ。

ここに書いておくと良い点も含めて下記に示す。

 

env

xonshrcファイルでも設定できるけど環境変数

静的なjsonファイルというだけなので、現状xonshとしてenv設定出来てメリットがあるのは以下くらいか。

{
    "env": {
        "XONSH_DEBUG": 1,
        "FOREIGN_ALIASES_OVERRIDE": True
     }
}

XONSH_DEBUGでデバッグ設定しておけば、xonshrc等のデバッグができる。
FOREIGN_ALIASES_OVERRIDEは、外部のShellのエイリアスを優先するかどうか。

Examples等では$EDITORとか設定してるけど、xonshrcの方が管理しやすい良い気がする。
Windowsなら$ALLUSERSPROFILEにrcファイルへのPathを書いておいてもよい。
Python系ならjupyterやpyenvへのPathなんかはここで書いても良い(気がする)。

 

xontribs

xontribsは、xonshの拡張であるxontribをlistで書いておくところ。

xonshrcでは以下のようにloadしないといけない所、パッケージ名をlistで書くだけになるのでちょっと楽。

xontrib load hoge

Sampleで自分のやつ。
名前を書いておくだけでよい。

{
    "xontribs": ["z","docker_tabcomplete","fzf-widgets"]
}

xontribは以下で書いたので参考に
vaaaaaanquish.hatenablog.com

 

foreign_shells

foreign_shellsでは、subprocessで呼び出す外部シェルの情報が記述できる。

より正確には、以下で呼び出されるforeign_shell_data()内で、subprocessとしてshellが一度起動され、該当shellのenvとaliasを読み込んでくる。
xonsh/foreign_shells.py at adcd20f72fcbe6962533cb3ad78a4a9ec396e150 · xonsh/xonsh · GitHub

 
list形式でdictを設定していく。dict中のkeyとしては以下が設定できる。
http://xon.sh/xonshconfig.html#foreign-shells

{
    "foreign_shells":[
        {
            # 起動したいshell
            "shell": "/bin/zsh",
            # 対話起動するか(しなくてもenv読み込めるshellなら不要)
            "interactive": True,
            # ユーザとしてlogin必要か
            "login": False,
            # 起動shellにおけるenvコマンド
            "envcmd": "env",
            # 起動shellにおけるaliasコマンド
            "aliascmd": "alias",
            # 起動するshellの引数(str)
            "extra_args": [],
            # 手動で起動shellにenv設定するためにdict
            "currenv": {},
            # 例外を安全に処理するか(Falseにするとraiseされる)
            "safe": True,
            # 色々読み込む前後にshellで実行させるコマンド
            "prevcmd": "",
            "postcmd": "",
            # 呼び出すshellのrcみたいなもん(以下参考に)
            # https://github.com/xonsh/xonsh/blob/adcd20f72fcbe6962533cb3ad78a4a9ec396e150/xonsh/foreign_shells.py#L33:title
            "funcscmd": "",
            # 呼び出したshellでsourceするファイル
            "sourcer": "",
            # 該当shellでスクリプトを実行する時のコマンド
            "runcmd": "",
            # 該当shellでexit-on-errorを設定するためのコマンド
            # "set -e"や"if errorlevel 1 exit 1"
            "seterrprevcmd": "",
            "seterrpostcmd": ""
        }
    ]
}

foreign_shellsで既に設定しているshell(xonshに乗り換える前に使っていた色々)をそのまま流用できたりする。


 

おわりに

あんまり書く機会がないかも知れないが、configについて記述した。

env、xontribs、foreign_shellsしか今の所書く事はないがxonshrcがちょっと簡略化される所がメリットか。


ひとまずアドベントカレンダー埋めた感じです。

qiita.com


 

Pythonモジュールの遅延import

- はじめに -

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

Pythonのmoduleのloadを実際に利用する前に遅延してやろうというTipsです。

加えて、xonshのxonshrcに記載する事で、xonshの起動も早くしようという話を書いています。


アジェンダ

 

- 遅延importを実現する -

遅延させて、使いたい時にpythonスクリプトをロードしたりするには大抵importlibを使います。

そこでimportlibでxonshの起動爆速化を狙おうとしていた所、同僚に「lazyasdっていうのがあるよ」と言われて調べたら大体それで出来たので、これで良いやという感じでした。

以下には一応どちらも記述しています。

 

importlibで動的ロード -

Pythonでモジュールをスクリプト上で動的にロードするにはimportlibを使います。

31.5. importlib — The implementation of import — Python 3.6.6rc1 documentation

import importlib

os = importlib.import_module("os")
print(os.listdir("."))

machineryを使えばimportのタイミングをhookできます。
https://docs.python.org/3.6/library/importlib.html#module-importlib.machinery

遅延してロードさせる場合は、Python3.5にて追加されたimportlib.util.LazyLoaderを使うと良いです。
31.5. importlib — The implementation of import — Python 3.6.6rc1 documentation


参考:Python: モジュールを動的にロードする - CUBE SUGAR CONTAINER


 

lazyasdを使った遅延import

同僚が「xonsh使ってるならxonsh.lazyasdの中にLazyObjectやBackgroundModuleLoaderがあるのでそれ使うと良いよ」と教えてくれました。

Lazy & Self-destructive Objects (xonsh.lazyasd) — xonsh 0.6.7 documentation


その後、調べてみるとlazyasdだけパッケージとして切り離されているようです。

GitHub - xonsh/lazyasd: Lazy & self-destructive tools for speeding up module imports

pipでinstallできるのでやります

pip install lazyasd


lazyasdで一番簡易にモジュール使う時ロードを実現できるのはデコレータを付けることです。

import importlib
from lazyasd import lazyobject

@lazyobject
def os():
    return importlib.import_module('os')

# importが発生するのはココ
print(os.listdir("."))

実際にモジュールを利用する時にimportする事が割りと簡単にできました。

 
compile済みの正規表現も遅延させて読み込めます。

import re
from lazyasd import lazyobject

@lazyobject
def hoge_re():
    return re.compile('hoge')

print(hoge_re.search("hoge piyo str") is not None)

 
より良い方法として、別スレッドで読み込むload_module_in_backgroundもあります。

from lazyasd import load_module_in_background

os = load_module_in_background('os')
print(os.listdir("."))

引数のreplacementsも便利です。

 
関数を引数に取るためlambda使ったりとちょっと面倒ですが、LazyObjectLazyDictといったClassも用意されています。

from lazyasd import LazyObject
from lazyasd import LazyDict
import re

# 最初にosを使う時にglobalにosをimportする
os = LazyObject(lambda: importlib.import_module('os'), globals(), 'os')
print(os.listdir("."))

# 一回目に使う時に正規表現をcompileする
RES = LazyDict({
        'dot': lambda: re.compile('.'),
        'all': lambda: re.compile('.*'),
        'two': lambda: re.compile('..'),
        }, globals(), 'RES')
print(RES["dot"].search("hogehoge.text") is not None)

「使うか分からないので、もし使うなら最初にロードしたいな」という時に使えます。


多分以下を見るかソースコードを見ると良いです。
Lazy & Self-destructive Objects (xonsh.lazyasd) — xonsh 0.6.7 documentation


 

- xonshrcに書いていく -

ここからはxonshrcに書いておいてxonsh起動までを爆速にしてやろうという話です。

xonshであればpip installする必要もなく、xonsh.lazyasdを利用できます。

使うか分からないがimportを忘れがちなやつを全部こうしてやります

from xonsh.lazyasd import lazyobject

@lazyobject
def os():
    return importlib.import_module('os')

 
全部listに突っ込んでおけば、execしてくれるSampleです。
import asのようにしたい場合は、dictか何かにしてformat第一引数xをよしなにやれば良いです

from xonsh.lazyasd import lazyobject
import importlib

# list版
lazy_module_list = ["requests", "numpy", "pandas", "matplotlib",...]
for x in lazy_module_list:
    t = "@lazyobject\ndef {}():\n    return importlib.import_module('{}')".format(x, x)
    exec(t)

# dictならこんな感じか
lazy_module_dict = {
    'requests': 'requests',
    'sys': 'sys',
    'random': 'random',
    'shutil': 'shutil',
    'pd': 'pandas',
    'np': 'numpy',
    '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)

これをxonshrcに書いて優勝です


いやいや、絶対コンソール使ってたら使うでしょという物はbackgroundでimportしてやりましょう。

from lazyasd import load_module_in_background

background_module_list = ["os", "sys", "random", "shutil", "linecache",...]
for x in background_module_list:
    exec("{}=load_module_in_background('{}')".format(x, x))


参考:Lazy & Self-destructive Objects (xonsh.lazyasd) — xonsh 0.6.7 documentation


 

- おわりに -

lazyasd便利なので、環境に合わせてロードするやつとかも拡張として書いていきたい所。

importlibのLoader周りはあんまりExamplesもなくて厳しいですが、まあなんとか。


Xonshアドベントカレンダーの方もよろしくお願いします。

Xonsh Advent Calendar 2017 - Qiita


 

PythonでHatenaブックマークのホットエントリを取得して表示する

- はじめに -

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

本記事では、PythonスクリプトでHatenaブックマークのホットエントリのリストを取得、xonshへ表示する内容を記載します。

f:id:vaaaaaanquish:20171225175808p:plain

追記:2018/11/23
ptk 2.xでは、本記事のコードが動作しないため、移行のための記事とrepositoryを公開しています。
vaaaaaanquish.hatenablog.com

 

- Hatenaホットエントリの取得 -

はてなにはブログの投稿や取得、はてブ数の取得等のAPIが用意されています。
はてなブックマークドキュメント一覧 - Hatena Developer Center

上記を見る限り、Hatenaホットエントリを取得することは出来ないので、requestsでスクレイピングしてくる必要がありそうです。

import requests
import bs4

# ホットエントリページの取得、解析
res = requests.get("http://b.hatena.ne.jp/hotentry")
bs_res = bs4.BeautifulSoup(res.text, "lxml")

# はてブ数とタイトルの取得
hotentry = []
for x in bs_res.findAll("li", attrs={"class":"entry-unit"}):
    a_tag = x.find("a", attrs={"class":"entry-link"})
    if a_tag is not None:
        hotentry.append((x.find("span").text, a_tag.attrs["title"], a_tag.attrs["href"]))
# はてブ数でソート
hotentry = sorted(hotentry, key=lambda x:int(x[0]), reverse=True)

# 表示
for x in hotentry:
    print('{} || {} \n {}'.format(x[0], x[1], x[2]))

上記コードでクローリングしてきた結果が以下のように出ます

1635 || Pythonの学び方と,読むべき本を体系化しました2018〜初心者から上級者まで - Lean Baseball 
 http://shinyorke.hatenablog.com/entry/python2018
627 || コンピューターで全漢字使用可に 6万字コード化 | NHKニュース 
 https://www3.nhk.or.jp/news/html/20171224/k10011270111000.html
567 || 日本テレビのみなさまへ、生活保護についての悪意のある番組放送はやめてください(大西連) - 個人 - Yahoo!ニュース 
 https://news.yahoo.co.jp/byline/ohnishiren/20171224-00079667/
534 || アニメ犯罪を追う海外ドラマ「ANIME CRIMES DIVISION」がカオスすぎて面白い「お前は地下遊戯王の危険さをわかっていない」 - Togetter 
 https://togetter.com/li/1183065
534 || みずほ銀行、人事評価と結びついた金融商品の押し売りの実態がNHKより流出 : 市況かぶ全力2階建 
 http://kabumatome.doorblog.jp/archives/65905156.html
...

常々良さそうです。
この結果を結果を使っていきたい。


 

- xonshのセレクタで選択したらBrowserで開く -

以下の記事で、python prompt toolkit (ptk)を利用した、シェル上で対話的選択する方法を記載しました。

vaaaaaanquish.hatenablog.com


これを利用して、xonshではてなホットエントリを取得してセレクトし、Browserで開くスクリプトとしてxonsh向けに書いてみます。

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.document import Document
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.interface import CommandLineInterface
from prompt_toolkit.key_binding.manager import KeyBindingManager
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.layout.margins import ScrollbarMargin
from prompt_toolkit.shortcuts import create_eventloop
from prompt_toolkit.filters import IsDone
from prompt_toolkit.layout.controls import TokenListControl
from prompt_toolkit.layout.containers import ConditionalContainer, ScrollOffsets, VSplit, HSplit
from prompt_toolkit.layout.screen import Char
from prompt_toolkit.layout.dimension import LayoutDimension as D
from prompt_toolkit.mouse_events import MouseEventTypes
from prompt_toolkit.token import Token
from prompt_toolkit.styles import style_from_dict
import webbrowser
import requests
import bs4

def _get_hotentry():
    res = requests.get("http://b.hatena.ne.jp/hotentry")
    bs_res = bs4.BeautifulSoup(res.text, "lxml")

    hotentry = []
    for x in bs_res.findAll("li", attrs={"class":"entry-unit"}):
        a_tag = x.find("a", attrs={"class":"entry-link"})
        if a_tag is not None:
            hotentry.append((x.find("span").text, a_tag.attrs["title"], a_tag.attrs["href"]))
    hotentry = sorted(hotentry, key=lambda x:int(x[0]), reverse=True)
    hotentry = [('{} || {}'.format(x[0], x[1]), x[2]) for x in hotentry]
    return hotentry

def _open_url(url):
    webbrowser.open(url)

def _if_mousedown(handler):
    def handle_if_mouse_down(cli, mouse_event):
        if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN:
            return handler(cli, mouse_event)
        else:
            return NotImplemented
    return handle_if_mouse_down


class InquirerControl(TokenListControl):
    selected_option_index = 0
    answered = False
    choices = TokenListControl

    def __init__(self, hotentrys, **kwargs):
        self.choices = [x[0] for x in hotentrys]
        self.urls = [x[1] for x in hotentrys]
        super(InquirerControl, self).__init__(self._get_choice_tokens, **kwargs)

    @property
    def choice_count(self):
        return len(self.choices) 

    def _get_choice_tokens(self, cli):
        tokens = []
        T = Token

        def append(index, label):
            selected = (index == self.selected_option_index)

            @_if_mousedown
            def select_item(cli, mouse_event):
                self.selected_option_index = index
                self.answered = True
                cli.set_return_value(None)

            token = T.Selected if selected else T
            tokens.append((T.Selected if selected else T, ' > ' if selected else '   '))
            if selected:
                tokens.append((Token.SetCursorPosition, ''))
            tokens.append((T.Selected if selected else T, '%-24s' % label, select_item))
            tokens.append((T, '\n'))

        for i, choice in enumerate(self.choices):
            append(i, choice)
        tokens.pop()  # Remove last newline.
        return tokens

    def get_selection(self):
        return self.choices[self.selected_option_index], self.urls[self.selected_option_index]


def _hotentry():
    hotentry = _get_hotentry()
    ic = InquirerControl(hotentry)

    def __get_prompt_tokens(cli):
        tokens = []
        T = Token
        tokens.append((Token.QuestionMark, '?'))
        tokens.append((Token.Question, ' hotentrys '))
        if ic.answered:
            tokens.append((Token.Answer, ' ' + ic.get_selection()[0]))
            _open_url(ic.get_selection()[1])
        else:
            tokens.append((Token.Instruction, ' (Use arrow keys)'))
        return tokens

    layout = HSplit([
        Window(height=D.exact(1), content=TokenListControl(__get_prompt_tokens, align_center=False)),
        ConditionalContainer(
            Window( ic, width=D.exact(43), height=D(min=3), scroll_offsets=ScrollOffsets(top=1, bottom=1)),
            filter=~IsDone())])
    
    manager = KeyBindingManager.for_prompt()
    @manager.registry.add_binding(Keys.ControlQ, eager=True)
    @manager.registry.add_binding(Keys.ControlC, eager=True)
    def _(event):
        event.cli.set_return_value(None)
    @manager.registry.add_binding(Keys.Down, eager=True)
    def move_cursor_down(event):
        ic.selected_option_index = (
            (ic.selected_option_index + 1) % ic.choice_count)
    @manager.registry.add_binding(Keys.Up, eager=True)
    def move_cursor_up(event):
        ic.selected_option_index = (
            (ic.selected_option_index - 1) % ic.choice_count)
    @manager.registry.add_binding(Keys.Enter, eager=True)
    def set_answer(event):
        ic.answered = True
        event.cli.set_return_value(None)

    inquirer_style = style_from_dict({
        Token.QuestionMark: '#5F819D',
        Token.Selected: '#FF9D00',  # AWS orange
        Token.Instruction: '',  # default
        Token.Answer: '#FF9D00 bold',  # AWS orange
        Token.Question: 'bold',
    })

    _app = Application(
        layout=layout,
        #buffers=buffers,
        key_bindings_registry=manager.registry,
        mouse_support=True,
        #use_alternate_screen=True
        style=inquirer_style
    )

    _eventloop = create_eventloop()
    try:
        cli = CommandLineInterface(application=_app, eventloop=_eventloop)
        cli.run(reset_current_buffer=False)
    finally:
        _eventloop.close()

aliases["hotentry"] = _hotentry


これでxonsh上でのhotentryコマンドが作れました。
いざ実行してみます。

 
f:id:vaaaaaanquish:20171225175447g:plain


クールっぽい。
Browserで開くならあんまり意味ない気もしなくもないので、textだけでよしなに取ってくるマンを作って組み合わせたい所ですね。


- おわりに -

 
よさそうなセレクタの使い方ができました。

今年はWebスクレイピング記事で技術記事歴代一位のはてブ数を獲得した年でもあるので、良い記事で締めくくれていると自負しています。

vaaaaaanquish.hatenablog.com


アドベントカレンダーも盛り上がって、xonsh関連の日本語情報はかなり充実したと思います。
Xonsh Advent Calendar 2017 - Qiita

皆さんxonshでよい年末を。


 

Python Prompt Toolkitで対話的な選択コマンドを作る

- はじめに -

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

遅れ気味ですが、Python Prompt Toolkit (以下、ptk)を利用して、対話型のセレクタ(上下矢印で回答を選択できるやつ)を作りたいなと思います。

名前が分からないんですが、selectコマンドみたいなやつです。

f:id:vaaaaaanquish:20171225155520g:plain:w400:h170


ptkが扱えれば、xonshにも安易に応用できるため、xonshを扱う上で覚えておきたいアイデアです。


追記:2018/11/23
ptk 2.xでは、本記事のコードが動作しないため、移行のための記事とrepositoryを公開しています。
vaaaaaanquish.hatenablog.com


 

- 選択コマンド -

Pythonで擬似的なselectコマンドを作るには、printとreadlineを組み合わせて作る方法や、ptk等のコンソール生成moduleを利用する方法があります。

python-prompt-toolkitリポジトリの中にも以下のように、選択できるメニューを生成するにはというissueが立っています、
How do I create a menu? · Issue #281 · prompt-toolkit/python-prompt-toolkit · GitHub

上記issueに対して、以下でsampleコードが示されています。
sample for custom control based on TokenListControl by markfink · Pull Request #427 · prompt-toolkit/python-prompt-toolkit · GitHub
sample for custom control based on TokenListControl by markfink · Pull Request #427 · prompt-toolkit/python-prompt-toolkit · GitHub


具体的には、ptkのfull-screen-layout APIを利用して、選択用の画面を作ってあげて、その中でkeyバインドで操作できるようにするというものです。
https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/examples/full-screen-layout.py

full_screen_appのdocsは以下
python-prompt-toolkit/full_screen_apps.rst at master · prompt-toolkit/python-prompt-toolkit · GitHub


 
以下に、上記を参考にしながら対話selectorによってコマンドを選択、コピー、実行するスクリプトを示します。
prompt_toolkitに加えて、クリップボードを利用するためにpyperclipモジュールを使っているので、必要に応じてpipすると良いです。

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.document import Document
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.interface import CommandLineInterface # ptk 1.x
from prompt_toolkit.key_binding.manager import KeyBindingManager # ptk 1.x
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.layout.margins import ScrollbarMargin
from prompt_toolkit.shortcuts import create_eventloop
from prompt_toolkit.filters import IsDone
from prompt_toolkit.layout.controls import TokenListControl
from prompt_toolkit.layout.containers import ConditionalContainer, ScrollOffsets, VSplit, HSplit
from prompt_toolkit.layout.screen import Char
from prompt_toolkit.layout.dimension import LayoutDimension as D
from prompt_toolkit.mouse_events import MouseEventTypes
from prompt_toolkit.token import Token
from prompt_toolkit.styles import style_from_dict

# セレクトアイテム
choices = ['ls', 'ifconfig', 'pwd', 'who']
# 質問文
string_query = ' Command Select '
# 操作説明
inst = ' (Use arrow keys)'

# 選択した際に実行する関数
def selected_item(text):
    # クリップボードにコピー
    # 必要 : pip install pyperclip
    import pyperclip
    pyperclip.copy(text)
    # Command実行
    import subprocess
    res = subprocess.call(text)
    print(res)


# 以下セレクタ実装

# マウス操作が入った時に落とすためのデコレータ
def if_mousedown(handler):
    def handle_if_mouse_down(cli, mouse_event):
        if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN:
            return handler(cli, mouse_event)
        else:
            return NotImplemented
    return handle_if_mouse_down

# セレクトアイテムを受け取って選択させるための制御Class
# controlsのTokenListControlを継承する
class InquirerControl(TokenListControl):
    selected_option_index = 0
    answered = False
    choices = []

    def __init__(self, choices, **kwargs):
        self.choices = choices
        super(InquirerControl, self).__init__(self._get_choice_tokens, **kwargs)

    @property
    def choice_count(self):
        return len(self.choices)

    def _get_choice_tokens(self, cli):
        tokens = []
        T = Token

        def append(index, label):
            selected = (index == self.selected_option_index)

            @if_mousedown
            def select_item(cli, mouse_event):
                # bind option with this index to mouse event
                self.selected_option_index = index
                self.answered = True
                cli.set_return_value(None)

            token = T.Selected if selected else T
            tokens.append((T.Selected if selected else T, ' > ' if selected else '   '))
            if selected:
                tokens.append((Token.SetCursorPosition, ''))
            tokens.append((T.Selected if selected else T, '%-24s' % label, select_item))
            tokens.append((T, '\n'))

        for i, choice in enumerate(self.choices):
            append(i, choice)
        tokens.pop()  # Remove last newline.
        return tokens

    def get_selection(self):
        return self.choices[self.selected_option_index]

# インスタンス生成
ic = InquirerControl(choices)

# prompt情報の取得
def get_prompt_tokens(cli):
    tokens = []
    T = Token
    tokens.append((Token.QuestionMark, '?'))
    tokens.append((Token.Question, string_query))
    if ic.answered:
        # 選択した値を取得
        tokens.append((Token.Answer, ' ' + ic.get_selection()))
        # 任意関数実行
        selected_item(ic.get_selection())
    else:
        tokens.append((Token.Instruction, inst))
    return tokens


# 疑似レイアウトをトークンリストから設定
layout = HSplit([
    Window(height=D.exact(1),
           content=TokenListControl(get_prompt_tokens, align_center=False)),
    ConditionalContainer(
        Window(
            ic,
            width=D.exact(43),
            height=D(min=3),
            scroll_offsets=ScrollOffsets(top=1, bottom=1)
        ),
        filter=~IsDone())])

# keyバインディングの設定
manager = KeyBindingManager.for_prompt()
@manager.registry.add_binding(Keys.ControlQ, eager=True)
@manager.registry.add_binding(Keys.ControlC, eager=True)
def _(event):
    event.cli.set_return_value(None)
@manager.registry.add_binding(Keys.Down, eager=True)
def move_cursor_down(event):
    ic.selected_option_index = (
        (ic.selected_option_index + 1) % ic.choice_count)
@manager.registry.add_binding(Keys.Up, eager=True)
def move_cursor_up(event):
    ic.selected_option_index = (
        (ic.selected_option_index - 1) % ic.choice_count)
@manager.registry.add_binding(Keys.Enter, eager=True)
def set_answer(event):
    ic.answered = True
    event.cli.set_return_value(None)

# Color Font Style
inquirer_style = style_from_dict({
    Token.QuestionMark: '#5F819D',
    Token.Selected: '#FF9D00',
    Token.Instruction: '',
    Token.Answer: '#FF9D00 bold',
    Token.Question: 'bold',
})

# layoutを選択モデルにしたAppを設定
app = Application(
    layout=layout,
    key_bindings_registry=manager.registry,
    mouse_support=True,
    style=inquirer_style
)

# eventloopで実行、終了時close
eventloop = create_eventloop()
try:
    cli = CommandLineInterface(application=app, eventloop=eventloop)
    cli.run(reset_current_buffer=False)
finally:
    eventloop.close()

 
実行してみます。
f:id:vaaaaaanquish:20171225155917p:plain

概ね良さそうです。
様々な関数と組み合わせられるので、疑似pecoみたいな感じの物も実装できそうです。


 

- おわりに -

Python Prompt Toolkitを利用したセレクタの実装でした。

もちろん、ptkを利用しているxonsh上でも動くので、xonshで自作スニペットセレクタを作ったり、ssh先を選んだりする際に表示させるようなものが作れそうだなと思っています。

次の記事では、これを利用したxonsh関数を作って書きたいと思います。

 
Xonsh Advent Calendar 2017 - Qiita


追記:
この記事を利用してxonshでselectコマンドを作る記事を書きました
vaaaaaanquish.hatenablog.com