Vue + D3 + Leaflet でヒートマップを描画する時に詰まったところ
概要
バイトの関係で Vue, D3, Leaflet を使ったヒートマップを作成することになった.なんとかできたけど色々手こずったので,それらをメモしとく.
下の画像は今回作成したヒートマップ.
取り上げる項目
- D3とLeafletのバージョン
- jsonファイルの読み込み
- this変数の扱い
- 非同期処理の扱い
D3とLeafletのバージョン
D3とLeafletもバージョンでコードがかなり変わっており,
新しめなバージョンを使うと参考になるサイトがかなり少なくなる.
Leafletについては,普通にマップを描画するだけならあまり問題はないが,
今回のようにSVGを使う場合,ところどころコードが変わる.
具体的に,以下のバージョンでコードが変わる(最新=2018年8月時点の最新).
- D3: v3とv4のあたり(最新はv5)
- Leaflet: 0.7.0と0.8.0のあたり(最新は1.3.3)
新しいバージョンのものを使う場合,2014年あたりに書かれたサイトのコードはほとんど参考にならないと思っていい.
ちなみに,以下のサイトは,D3+Leafletで地理データを描画する時に参考にした.
- 古いバージョンを使う場合
https://shimz.me/blog/d3-js/3517 - 新しいバージョンを使う場合
D3 v4 + leaflet v1 - bl.ocks.org
また,Leafletの新しいバージョンではL.geoJSON().addTo()
でjsonを読み込み,描画できる.属性値(style)もゴリゴリいじれるので,D3で描画したようなヒートマップを描画できる.
D3を使うことにこだわりがないならLeaflet単体で実装した方が簡単.
コードもごちゃごちゃしない.
jsonファイルの読み込み
発生した問題
vue-cli
のコンパイル処理で jsonファイルに文句言われる
jsonファイルをassets
などのディレクトリに配置するとvue-cli
のコンパイル処理にエラーを吐かれる.コンパイルできない的なこと言われた気がする.d3.json()
でファイルを読めなかった
理由はよくわらん.
今回の対処方法
- jsonファイルを
static
ディレクトリに置く
ここにおいたらコンパイル処理されないっぽい. axios.get()
を使う
多分axios
じゃなくてもいいとは思う.ajax
みたいな適当なGET処理してくれるの使えばjsonファイルを読めると思う.
this変数の扱い
vueを使うとJSとHTML間で変数や関数を共有できて便利なのだが,その共有オブジェクトを使うために,this
変数を使わなければならない.
例えば,flag
という名前の変数を定義し,その変数を使おうと思ったら,以下の様にアクセスしなければならない.
this.flag = this.flag + 1;
(flag変数をインクリメントする処理)
しかし,JSでは色々な処理でthis
変数が上書きされてしまい,上記のような処理を実行できなくなる.
これの対処方法として,this
が上書きされる直前にthis
を退避させる方法がある.
例えとして,以下はaxios.get()
でGETしたgeojsonデータをthis.geojson
に格納する例である.
const vueObj = this; axios.get('../../../static/kurashiki.geojson') .then(function (json) { vueObj.geojson = json.data; });
これでもちゃんと動くが,axios
に対しては以下の様にbind()
を使うことで,axios
内でthis
にアクセスできる.
axios.get('../../../static/kurashiki.geojson') .then(function (json) { this.geojson = json.data; }.bind(this));
非同期処理の扱い
ヒートマップを描画する前にデータ取得などの処理をする必要があり, そういったデータ取得処理は非同期に実行されてしまう. このため,特定の前処理が終わるまで他の処理を実行させないようにする必要がある.
今回以下のコードを使った
- async
- await
- Promise.all()
詳しくは以下のサイトがわかりやすい.
ざっくり説明すると,関数を宣言する際,その関数名の前にasync
を付けるとその関数が非同期関数となる.返り値がPromise
になったり関数内でawait
を使えたりする.
await
を使うことで非同期処理の実行中,他の処理を停止させられる.
以下は実際に使ってみた例.
async setGeojson () { await axios.get('../../../static/kurashiki.geojson') .then(function (json) { this.geojson = json.data; }.bind(this)); }
上記例では,非同期処理のaxios.get()
の実行中,その他処理が止まる(たぶん).
上記のような非同期関数をまとめて実行し,
それら全ての完了を待ちたい時に便利なのがPromise.all()
.
Promise.all()
を使ったコードの例は以下の通り.
Promise.all([this.drawMap(), this.setGeojson(), this.setAreaValue()]) .then(() => { this.drawFeatures() })
上記例では,this.drawMap()
, this.setGeojson()
, this.setAreaValue()
の処理が全て完了した後,this.drawFeatures()
を実行するようになっている.
ちなみに,上記で呼び出している関数の処理内容は以下の様になっている.
this.drawMap()
: オープンストリートマップを描画する.this.setGeojson()
: geojson(地理データ)を読み込み,vueの変数に格納する.this.setAreaValue()
: 各エリアの値をvueの変数に格納する.この値を基に書くエリアの色の濃さを定義する.this.drawFeatures()
: ヒートマップを描画する.
今回のヒートマップ作成に対する感想
なんとかヒートマップを作成できた.非同期処理の扱いが前よりわかってきてなかなか勉強になった.
ただまだ理解しきれていないので,次に非同期処理問題の絡むコードを扱う時は適当なコードを書くのではなく,勉強してきれいなコードを書きたい.
vue-chartjs で二重Y軸を扱う
各単語について
Vue.js
Javascriptのフレームワークの一つ. HTML側とJS側で値の共有ができ(双方向データバインディング), また,HTML側でforやif文が書けるようになる便利なやつ.Chart.js
JavaScriptで洒落たグラフを簡単に書けるようにするライブラリ.vue-chartjs
Vue.js
上でChart.js
を使うためにChart.js
をラップしたもの.
今回の開発の概要
vue-chartjs
の二重Y軸の扱いに結構詰まったので,
今回の対処をメモする.
今回は以下の流れで開発した.
vue-chartjs
で同じ描画先に棒グラフを2本描画- Y軸を2本描画し,各グラフで異なるY軸を参照
- 各グラフが紐づくY軸を,切り替えるボタンを作成
Y軸を2本描画し,各グラフで異なるY軸を参照
"vue-chartjs
で同じ描画先に棒グラフを2本描画"はメモするまでもないので省略.
Y軸を複数描画する際は,グラフ描画関数の引数のoptions
要素を編集する.
具体的には,以下の様にyAxes
要素を配列にする.
options: { scales: { yAxes: [{ id: "y-axis-1", position: "left" }, { id: "y-axis-2", position: "right" }] ...
position
の値で描画先の上下左右どこに描画するか定義できる.
また,各グラフでどちらのY軸に紐付かせるかは,上記のid
を以下の様にyAxisID
要素に格納することで,定義できる.
{ type: 'bar', label: 'label1', backgroundColor: '#f87979', data: [11, 11, 19, 8, 8, 9, 30, 24, 25, 22, 11, 15], fill: false, tension: 0, yAxisID: 'y-axis-1' }, { type: 'bar', label: 'label2', backgroundColor: '#f87000', data: [5, 25, 17, 6, 29, 5, 5, 13, 16, 10, 14, 29], fill: false, tension: 0, yAxisID: 'y-axis-2' }
以下の用にY軸が左右に描画される. ピンクの棒グラフが左のY軸,オレンジの棒グラフが右のY軸に紐づいている.
各グラフが紐づくY軸を,切り替えるボタンを作成
ここで詰まった.結局,いいやり方にはなっていないと思うが, 今回は以下の方法で対処した.
- ボタンクリックにより,
yAxisID
の値を変更 - 修正したグラフのデータの
_meta
要素を削除し,グラフを再描画
ボタンクリックにより,yAxisID
の値を変更
以下のコードでyAxisID
の値を変更するラジオボタンを設置
<td v-for="col in filteredData.datasets.length"> <!-- id には (axis_select-列番号-軸番号) を格納 --> <input type="radio" :id="['axis_select-' + (col-1) + '-1']" value="y-axis-1" v-model="filteredData.datasets[col-1].yAxisID" @input="deleteMeta"> <label for="y-axis-1">軸1</label> <input type="radio" :id="['axis_select-' + (col-1) + '-2']" value="y-axis-2" v-model="filteredData.datasets[col-1].yAxisID" @input="deleteMeta"> <label for="y-axis-2">軸2</label> </td>
filteredData
変数にグラフ描画に使うデータが入っている.
上記コードで設置したボタンをクリックすることにより,deleteMeta()
関数を呼び出し,_meta
要素を削除する.
修正したグラフのデータの_meta
要素を削除し,グラフを再描画
上で設置したラジオボタンのクリックにより発火するdetaMeta()
関数は以下の通り.
// データの(_meta)要素を削除し,グラフを更新 deleteMeta (e) { // buttonId は axis_select-0-1 の形式 // ハイフン区切りで,(axis_select 列番号 軸番号) を意味する const buttonId = e.target.id; const index = Number(buttonId.split('-')[1]); delete this.localData.datasets[index]._meta; // グラフを更新 this.reloadGraph(); }
動作テスト
以下は今回開発したものを実際に動かしたものです. 二つ目のグラフ(オレンジの棒)のY軸を 左->右->左 と動かしています. オレンジのグラフの最大値はピンクのグラフの最大値よりも大きいため, ピンクの方のグラフも動いています.
感想
とりあえず,ちゃんと動くものはできたので,目的は達成しました. でも対処の仕方があまりきれいではないので,すっきりしない.
開発には関係ないことですが,今回初めて画面キャプチャで動画を作成し,GIFを作成しました. 楽しい.
PHPで簡単なAPIのモックを作る
PHPに触るきっかけ作り
Web関係のプログラムでNode(JavaScript)やPythonを触ったことはあるが,
未だにPHPのコードは一度も書いたことがないし,まともにコードを読んだことがない.
かつ,最近,アルバイト先でPHP需要が高まってる雰囲気がある.
そもそもWeb系といえばまずPHPっていう気もする.
なので,PHPをちょっとずつ勉強していきたい.
APIのモックの開発でPHPデビューする
Webページ(アプリ)のフロントエンドを開発する際,
開発してるページからまだ開発していないAPIを叩きたい時がある.
こんな時のため,とりあえず受け答えだけしてくれるような簡易版のサーバー(APIのモックサーバー)があると便利.
今回はPHPデビューのためにも,APIのモックをPHPで作成する.
実装内容
今回は今作ってるWebページで必要なGETとPOSTを受けてくれるサーバーを作る.
また,受け渡しするデータはJSONデータを想定している.
具体的な実装内容は,以下の通り.
今回のソースコード
今回書いたソースコードは最終的にこんな感じになりました.
<?php // Origin null is not allowed by Access-Control-Allow-Origin.とかのエラー回避の為、ヘッダー付与 header('Access-Control-Allow-Origin: *'); function api4leaflet () { // GET用のjson $spots = [ [ 'name' => 'スポット1', 'category' => 'A', 'lat' => '34.598804', 'lng' => '133.76884' ], [ 'name' => 'スポット2', 'category' => 'B', 'lat' => '34.606323', 'lng' => '133.793363' ], [ 'name' => 'スポット3', 'category' => 'A', 'lat' => '34.61858', 'lng' => '133.759183' ] ]; switch ($_SERVER['REQUEST_METHOD']) { case 'GET': echo json_encode($spots, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); break; case 'POST': echo json_encode(file_get_contents('php://input'), JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); break; default: echo 'unexpected request'; break; }; } switch ($_SERVER['REQUEST_URI']) { case '/leaflet': api4leaflet(); break; default: echo 'unexpected URI'; break; }
実行コマンド
以下のコマンドで実行しています.
$ php -S 127.0.0.1:5000 api_mock.php
この場合,ブラウザなどからhttp://127.0.0.1:5000
にアクセスすることで,
サーバーにアクセスできます.
各コードについて
Headerとかはよく調べてなくて,人のコードそのままパクっていますが, その他のコードの意味は以下の通り.
$_SERVER['REQUEST_URI']
サーバーのルート以下のパスが入っています.
例えば,http://127.0.0.1:5000/leaflet
にアクセスした場合, この変数には文字列/leaflet
が格納されます.$_SERVER['REQUEST_METHOD']
実行したAPIの種類が入っています.GET
やPOST
といった文字列がそのまま入っています.file_get_contents('php://input')
これでクライアントが送ってきたbody
が取り出せるっぽい.json_encode($spots, なんやかんや);
サーバーからクライアントにJSONなどのデータを送る際, そのデータを一旦文字列に変換する必要があります. これはその変換をするコード.
また,第二引数のなんやかんや
は省略しても送信できますが, 日本語のデータを送る際はこれを書かないとunicodeか何か謎の形式になってしまうので,今回はごちゃごちゃ書いてます.
この引数を書かない場合と書いた場合は以下の通り.
次に
ザンキゼロのシガバネをGoogleのToDoリストで管理する
ザンキゼロ
ザンキゼロとは,ダンガンロンパでお馴染みスパイク・チュンソフトの新作ゲームです.
このゲームにはバトル要素があり,キャラクターを強化するための以下の2つの育成要素があります.
- スキル
レベルアップ時に取得できるスキルポイントを消費し,取得する.
攻撃力強化やアイテムの効果を増強させるものなどがある. - シガバネ
キャラクターが特定の条件で死ぬと,復活する際に取得する.
例えば,毒状態で死ぬと,復活後に毒耐性を取得する(毒になりにくくなる).
シガバネはキャラクター毎(全8人)に独立しており,大半は内容が被っているが,中にはキャラクター固有のものもあります. また,一人あたりおよそ150個弱のシガバネがあり, このシガバネの取得が本作のやりこみ要素の1つになっています.
今回解決したい課題
↑で話した通り,シガバネの数は非常に多く,
ゲームをプレイしていると,今何のシガバネを取得していて何を取得していないかわからなくなってきます.
そこで,今回は,各キャラクターの未取得のシガバネを何らかのツールを使って管理しようと考えました.
解決方針
GoogleのToDoリストを使って未取得のシガバネを管理します.
- このToDoリストはGoogleのサービス(詳しく言うとGmailのサービス)なので,Googleのアカウントがあれば誰でも利用できます.
- また,スマホ用アプリもリリースしているので,パソコンとスマホの両方からこのサービスを手軽に利用できます.
ToDoリストでシガバネを管理するにあたって,以下の下準備を行います.
シガバネデータの取得とデータ整形
クロールしたサイトについて
今回は以下のサイトを利用させていただきました.
このサイトのrobots.txt
は以下の通りなので,クロールしてもいいサイトだと考えています.
User-agent: * Disallow: /*/?cmd=backup Disallow: /*/?cmd=edit Disallow: /*/?cmd=search Allow: /
クロール・データ整形のコード
以下のコードでデータを取得し,整形してcsvファイルに書き込みました.
今回は面倒くさがってコメントをほとんど書いていません.
# coding: UTF-8 import urllib.request from bs4 import BeautifulSoup from time import sleep import re import csv def encode_jpurl(japanese): return urllib.parse.quote_plus(japanese, encoding='utf-8') def make_url(character_name): query = "{0}%28{1}%29".format(encode_jpurl(character_name), encode_jpurl('シガバネ一覧')) return "https://h1g.jp/zanki_zero/?{0}".format(query) def get_html(url): f = urllib.request.urlopen(url) html = f.read().decode('utf-8') return html def extract_string_from_tr(tr_tag): td_array = tr_tag.find_all("td") data_array = [td.text for td in td_array] data_array.extend(["" for i in range(3-len(data_array))]) return data_array def extract_data_from_html(html): soup = BeautifulSoup(html, "html.parser") div_shigabane = soup.find("div", class_="mainbody") tbody_shigabane = div_shigabane.find("tbody") rows_shigabane = tbody_shigabane.find_all("tr") return [extract_string_from_tr(row_shigabane) for row_shigabane in rows_shigabane] def fill_the_empty(data): pre_data = ["", "", ""] for i in range(len(data)): if data[i][1] == "": data[i][1] = pre_data[1] if data[i][2] == "": data[i][2] = pre_data[2] pre_data = data[i] return data def write_data_to_csv(data, filename): f = open('csv/{0}.csv'.format(filename), 'w') writer = csv.writer(f, lineterminator='\n') writer.writerows(data) f.close() def shape4todolist(data, listname): shaped_data = [["tasklist_name", "title", "notes", "status", "due", "completed", "deleted", "hidden", "depth"]] shaped_data.append([listname, listname, "", "needsAction", "", "", "", "", 0]) # dataのヘッダを削除 data.pop(0) shaped_data.extend([[listname, d[0], d[1], "needsAction", "", "", "", "", 1] for d in data]) return shaped_data if __name__ == "__main__": character_names = ["ハルト", "リョウ", "ゼン", "マモル", "リンコ", "ユマ", "ミナモ", "サチカ"] for character_name in character_names: print(character_name) url = make_url(character_name) html = get_html(url) data = [["名称", "解説", "効果"]] data.extend(extract_data_from_html(html)) filled_data = fill_the_empty(data) write_data_to_csv(filled_data, character_name) data4todolist = shape4todolist(filled_data, character_name) write_data_to_csv(data4todolist, "{0}_todolist".format(character_name)) sleep(1)
整形後のデータ
以下のような9列のデータになっています.
tasklist_name
でラベルを指定し,depth
でそのタスクの階層を指定します.
画像では,タスクの階層を1に指定していますが,後々考えたら全部0にしたらよかったかもと思いました.
GoogleのToDoリストにインポート
データをインポートするために,以下のサイトを利用しました.
https://import-tasks.appspot.com/main
信用できるサイトかどうかは正直よくわかっていません.
ここにcsvファイルを送ると,タスクがGoogleのToDoリストに追加されます.
ToDoリストの見栄え
パソコンのブラウザ上では以下の様にタスクが表示されます.
csvファイルの時とタスクの順が一致していませんが,これは単純にブログ投稿前に操作(削除)してしまったからです.
インポート直後はcsvファイルと同じタスクの順になっています.
以下はiphoneのアプリで見た時のものです.
タスクをクリック(タップ)するとすぐに消せ,また,パソコンとスマホで連携できるので,操作感はいいと思います.
おわりに
今回はやりこみ要素(コンプリート系)のあるゲームとしてザンキゼロを取り上げましたが,
大概のゲームは何かしらのやりこみ要素があると思います.
例えば,ポケモンのまだ捕まえていないポケモン,モンハンの食材,メタルギアの武器,などなど.
今回の開発内容はザンキゼロに限らず,上記のようなやりこみ要素に対しても適用できます.
このため,今回の開発が今後自分がプレイするゲームに対しても攻略の助けになればと思います.
任意のディレクトリからDropboxのメモファイルを編集
やりたいこと
- Dropboxを利用してメモ(テキストファイル)を各デバイスで共有したい.
- ただし,メモを配置するディレクトリは
Dropbox/Memo
とする. Dropbox/Memo
ディレクトリの位置を意識せず,任意のディレクトリからメモの確認・編集を実行したい(memo
コマンドの開発).
また,今回の開発は以下の記事の続きです.
前回の開発により,memo
コマンドにDropbox/Memo
ディレクトリ下のファイル名が補完されるようになっています.
memo
コマンドの開発
開発手順
以下の処理を実現する.
- memoファイルの一覧を表示
- memoファイルの内容の確認(出力)
- memoファイルの追加
- memoファイルの削除
- memoファイルの編集
作成するコードは前回の開発のfunction memo(){}
内に記述する.
memoファイルの一覧を表示
以下のコマンドでmemoファイルの一覧を出力する.
$ memo
この処理はDropbox/Memo
ディレクトリをls
することで実現する.
コードは以下の通り.
local MEMO_DIR=$HOME"/Dropbox/Memo/" ls $MEMO_DIR
実行結果は以下の通り.ちゃんとファイル名を出力できてる.
memoファイルの内容の確認(出力)
以下のコマンドでDropbox/Memo/memo.txt
ファイルの内容を確認する.
$ memo memo.txt
この処理は,Dropbox/Memo/memo.txt
ファイルをless
することで実現する.
コードは以下の通り.
local MEMO_DIR=$HOME"/Dropbox/Memo/" function less_memo() { local MEMO_NAME=$1 # メモが既に存在する場合,less if [[ -f $MEMO_DIR$MEMO_NAME ]]; then less $MEMO_DIR$MEMO_NAME # メモが存在しない場合,echo で通知 else echo "「memo: "$MEMO_NAME"」は存在しません." fi } less_memo $1
実行結果は省略.
memoファイルの追加
以下のコマンドでDropbox/Memo/tmp2.txt
ファイルを作成する.
$ memo -a tmp2.txt
この処理は,Dropbox/Memo/tmp2.txt
ファイルをtouch
することで実現する.
コードは以下の通り.
function add_new_memo() { local MEMO_NAME=$1 # メモが既に存在する場合,echo で通知 if [[ -f $MEMO_DIR$MEMO_NAME ]]; then echo "「memo: "$MEMO_NAME"」は既に作成済みです." # メモを新規作成 else touch $MEMO_DIR$MEMO_NAME echo "「memo: "$MEMO_NAME"」を新規作成しました." fi } case $1 in # メモを新規作成 '-a' ) add_new_memo $2 ;; # 第1引数に関するエラー処理 * ) echo "error: オプションが適切ではありません." ;; esac
実行結果は以下の通り.
memoファイルの削除
以下のコマンドでDropbox/Memo/tmp2.txt
ファイルを削除する.
$ memo -r tmp2.txt
この処理は,Dropbox/Memo/tmp2.txt
ファイルをrm
することで実現する.
コードは以下の通り.
function remove_memo() { local MEMO_NAME=$1 # メモを削除 if [[ -f $MEMO_DIR$MEMO_NAME ]]; then rm $MEMO_DIR$MEMO_NAME echo "「memo: "$MEMO_NAME"」を削除しました." # メモが存在しない場合,echo で通知 else echo "「memo: "$MEMO_NAME"」は存在しません." fi } case $1 in # メモを新規作成 '-a' ) add_new_memo $2 ;; # メモを削除 '-r' ) remove_memo $2 ;; # 第1引数に関するエラー処理 * ) echo "error: オプションが適切ではありません." ;; esac
実行結果は以下の通り.
memoファイルの編集
以下のコマンドでDropbox/Memo/memo.txt
ファイルを編集する.
$ memo -e memo.txt
コードは以下の通り.
function edit_memo() { local MEMO_NAME=$1 # 規定のエディタを設定していれば,そのエディタを使用 # 規定のエディタを設定していなければ, vim を使用 if [[ -z "$EDITOR" ]]; then local DEF_EDITOR='vim' else local DEF_EDITOR=$EDITOR fi # メモを編集 if [[ -f $MEMO_DIR$MEMO_NAME ]]; then $DEF_EDITOR $MEMO_DIR$MEMO_NAME # メモが存在しない場合,echo で通知 else echo "「memo: "$MEMO_NAME"」は存在しません." fi } case $1 in # メモを新規作成 '-a' ) add_new_memo $2 ;; # メモを削除 '-r' ) remove_memo $2 ;; # メモを編集 '-e' ) edit_memo $2 ;; # 第1引数に関するエラー処理 * ) echo "error: オプションが適切ではありません." ;; esac
実行結果は省略.
今回着手していない部分
今回でとりあえずmemoファイルの操作が一通りできたと思うが,実際に使っていかないと,ちゃんとできているのかわからない.
ただ,補完周りで,以下の内容は課題として残る.
- memoファイルを
.txt
を意識せず,操作できるようにする. memo
コマンドを入力する際,入力している引数(番目)に応じて補完内容を変える.例えば,1つ目の引数を入力する際,「-a, -r, -e」が補完で出るようにする.(また,それぞれの内容を表示する.)
ソースコード全体
#!/usr/local/bin/zsh function memo() { local MEMO_DIR=$HOME"/Dropbox/Memo/" # 選択したメモを less する関数 function less_memo() { local MEMO_NAME=$1 # メモが既に存在する場合,less if [[ -f $MEMO_DIR$MEMO_NAME ]]; then less $MEMO_DIR$MEMO_NAME # メモが存在しない場合,echo で通知 else echo "「memo: "$MEMO_NAME"」は存在しません." fi } # メモを新規作成する関数 function add_new_memo() { local MEMO_NAME=$1 # メモが既に存在する場合,echo で通知 if [[ -f $MEMO_DIR$MEMO_NAME ]]; then echo "「memo: "$MEMO_NAME"」は既に作成済みです." # メモを新規作成 else touch $MEMO_DIR$MEMO_NAME echo "「memo: "$MEMO_NAME"」を新規作成しました." fi } # メモを削除する関数 function remove_memo() { local MEMO_NAME=$1 # メモを削除 if [[ -f $MEMO_DIR$MEMO_NAME ]]; then rm $MEMO_DIR$MEMO_NAME echo "「memo: "$MEMO_NAME"」を削除しました." # メモが存在しない場合,echo で通知 else echo "「memo: "$MEMO_NAME"」は存在しません." fi } # メモを編集する関数 function edit_memo() { local MEMO_NAME=$1 # 規定のエディタを設定していれば,そのエディタを使用 # 規定のエディタを設定していなければ,vim を使用 if [[ -z "$EDITOR" ]]; then local DEF_EDITOR='vim' else local DEF_EDITOR=$EDITOR fi # メモを編集 if [[ -f $MEMO_DIR$MEMO_NAME ]]; then $DEF_EDITOR $MEMO_DIR$MEMO_NAME # メモが存在しない場合,echo で通知 else echo "「memo: "$MEMO_NAME"」は存在しません." fi } # 引数がなければメモの一覧を表示 if [[ $# -eq 0 ]]; then ls $MEMO_DIR # 引数の数が1であれば,メモ(第一引数)の中身を less elif [[ $# -eq 1 ]]; then less_memo $1 # 引数の数が2であれば,オプションに応じた処理を実行 elif [[ $# -eq 2 ]]; then case $1 in # メモを新規作成 '-a' ) add_new_memo $2 ;; # メモを削除 '-r' ) remove_memo $2 ;; # メモを編集 '-e' ) edit_memo $2 ;; # 第1引数に関するエラー処理 * ) echo "error: オプションが適切ではありません." ;; esac # 引数の数が (0 ~ 2) でなければエラーを出力 else echo 'error: 引数の数が適切ではありません.' fi } _memo() { _values "description of completion" $(ls ~/Dropbox/Memo) } compdef _memo memo
Repl-AI を使って Line bot を作ってみた
やりたいこと
- 特定の言葉に反応する簡単な Line bot の作成
- 作成した機能が常時稼働するようにする
手順
Repl-AI で受け答えの処理(API)を作成
Repl-AI とは,プログラム知識なしで、AIチャットボットを作成できるWebアプリです.
具体的に,以下のようにビジュアルプログラミング言語を使ってメッセージの受け答えのルールを作成できます.
上記のルールでは,ユーザの「おはよう」「こんにちは」に対して,同じ言葉を返すことを定義しています.
Line bot を作成
まず,Line Developers に自分の LIne アカウントでログインする.
新規プロダイバーを作成し,新規チャンネルの作成から Message API を作成する.
- この時点でLineで友達登録でき,メッセージを送ることができる.
- 上の画像のチャンネルをクリックした先のページにQRコードがあるので,それから友達登録できる.
- 上の画像ではすでにアイコンが設定されているが,チャンネル作成直後は当然アイコンが設定されていない.
Repl-AI と Line bot を連携
Repl-AI と Line bot を連携させるために, Repl-AI の方に以下の項目を入力する必要がある.
- ID
- シークレット
- トークン
IDとシークレットについては,チャンネル(Line Dev側)作成時に定義されているが,トークンについては Line Dev 側で発行する必要がある. 発行後,指定時間内で有効期限が切れるので,トークン発行後はすぐに Repl-AI 側に入力しないといけない.(連携後にトークンの期限が切れることは問題ない)
また,上の画像のフォームについて,「2. シナリオ」を設定すると,「3. URL」にURLが生成されるので,これをコピーし,連携ボタンを押す.
コピーしたURLは Line Dev 側のチャンネルに入力する項目があるので,そこに入力する.
連携結果
以下の様に,「おはよう」と「こんにちは」に反応している.
最初に NOMATCH が出ているのは,連携前に発言したため.
Repl-AI で定義できていないユーザの発言については画像の通り,NOMATCH が出力される.
備考
自動応答メッセージ
Line Dev の方で「自動応答メッセージ」を利用しないに設定しておかないと,何を話しても以下のようなメッセージが出力される.
これは Line bot の仕様なので,Repl-AI 側では対処できない.
グループトーク
おそらく,Line bot は Slack bot と違い,複数人のチャット(トークルーム)に対応していない.(調査不足の可能性有)
bot との 個別トークで適切に応答する状態でも,他のアカウントを招待したトークルームでは応答してくれない.
展望
Repl-AI 側でユーザから入力してきたワードを覚えたり,複雑な応答ルールを作成したりできるみたいなので, そのあたり使ってみたい.
zsh の補完周りをちょっと触る
今回の内容
- 自作コマンド(
memo
コマンド)の補完を編集 - 任意のディレクトリから,
$ memo <TAB>
を入力すると,~/Dropbox/Memo
ディレクトリの中のテキストファイル名を補完する(補完リストに出す).
なぜこれをやりたいか
最終的に実現したいこと
- Dropboxを利用してメモ(テキストファイル)を各デバイスで共有したい.
- ただし,メモを配置するディレクトリは
Dropbox/Memo
とする. Dropbox/Memo
ディレクトリの位置を意識せず,任意のディレクトリからメモの確認・編集を実行したい(memo
コマンドを実装).
memo
コマンドを実装する上で
memo
コマンドの引数に確認・編集するメモの名前を与えることが考えられる.- 補完周りを弄らないとメモ名を補完なしで入力する仕様になってしまい,不便である.
- このため,
memo
の引数の補完にDropbox/Memo
ディレクトリ直下のファイル名を割当てたい. - 補完の設定方法は使ってるシェルによって違うらしいので,
zsh
用の方法で補完を設定する.
memo
コマンドの引数の補完にDropbox/Memo
ディレクトリ直下のファイル名を割当てる
以下のコードをzsh
実行時に読むファイル(.zshrc
等)に記述する.
_memo() { _values "description of completion" $(ls ~/Dropbox/Memo) } compdef _memo memo
この"description of completion"の文字列はzshの設定で補完候補の出力時に出力したりしなかったりできるが,現状どうでもいいので自分は今の所出力していない(デフォルト).
また,下記コードの実行が前提なので,.zshrc
等のファイルに下記コードが書かれているか要確認
autoload -U compinit
compinit
以下の通り,Dropbox/Memo
直下のファイル名を補完できた.
次に
- 補完の出力内容をもっといい感じにしたい.例えば,".txt"を消すとか,メモの概要を表示するとか.
memo
コマンドの中身を実装したい.(こっち優先)