Quantcast
Channel: ほんじゃーねっと
Viewing all 199 articles
Browse latest View live

テンプレートエンジン(今回はECT)で結合したHTMLファイルを元の部品ファイルに分割する

$
0
0

f:id:piro_suke:20170529005556j:plain

Webアプリを開発する際、

最近の多くのテンプレートエンジンはHTMLをレイアウトや部品に分割して作成し、

それをextendしたりincludeしたりして1つのHTMLとして出力できるようになっている。

部品化することで、ヘッダーとかメニューとかフッターとか、

複数のページで共通して表示する要素を変更する際に、

1箇所だけ変更すれば済むようになるわけだ。

しかし部品化することで発生するデメリットもあって、

Webアプリとして実行してみないと完成版のHTMLが確認できない、

外部のデザイナーさんとデザインをやりとりしづらい、

といった問題が発生する。

JavaのSpring Frameworkなんかで利用されているThymeleafなんかは、

上記の問題を解決するために完成版のHTMLとしてWebアプリのテンプレートを

作成できるようになっているようなのだけど、

逆に部品化できないことで共通部分の変更が面倒になったりしないのだろうか。

Thymeleaf

結合されたファイルを元に戻す仕組み

この問題を解決する1つの方法として、

テンプレートの結合だけでなく、分割できるツールを用意すれば良いのではないか、

と考えて、試しに作ってみた。

テンプレートエンジンはECTを使用した。

ectjs.com

仕組みとしては、ファイルの結合を行う

extend機能(あらかじめ作成したレイアウトファイルにコンテンツを埋め込む機能)や

include機能(複数の画面で利用する部品をコンテンツに埋め込む機能)

を使用している位置にHTMLコメントで分割用の定義を書いておく。

例えば、includeされるファイルなら下記のような感じ。

pager.html.ect

<!-- tpl:include="parts/pager" --><div>ページャーのHTML...</div><!-- /tpl:include="parts/pager" -->

このように書いておくと、

結合されたHTMLにもHTMLコメントが残るので、

その要素がどのような部品から作成されたのかが分かる。

そして、

別途HTMLファイルを解析してこのコメントを元にファイルを分割するツールを作成する。

HTMLの解析はどの方法が最適か迷ったのだけど、

一番簡単そうな正規表現で置換する方法を採用した。

ツールとしては、

上記のコメント入りHTMLが入ったフォルダのパスと、

部品化されたテンプレートを出力するフォルダのパスを指定して

コマンドを実行すれば一括で部品に戻してくれるようにした。

github.com

ECTはNode.js用のテンプレートエンジンなので、

Javascriptで作成するのが最適なのだけど、

ついClojureで作成してしまった。

誰にもメリットのない組み合わせで作ってしまったかもしれない。

まあ処理内容はファイルの中身に正規表現置換をかけているだけなので、

Javascript版を作るのもそんなに難しくないと思う。

正規表現クックブック

正規表現クックブック


Excel無しでCSVファイルの内容を整形して表示するスクリプト

$
0
0

f:id:piro_suke:20170531231836j:plain

仕事でCSVファイルのやりとりをすることが多い。

CSVファイルというのは、Excelで開くと見やすいのだけど、

普通にテキストエディタやCUI環境で開くと結構見づらい。

そこで、

CUI環境でもExcelで開いた時のように縦の並びが整形された形で

閲覧できるスクリプトを書いてみた。

github.com

CSVファイルのパスを指定すると、

各列の最大文字数をチェックして、その長さでフォーマットして

縦の列が揃うように出力してくれる。

プログラミング初心者が練習で作りそうなレベルの

スクリプトだけど、地味に便利だ。

わずか5分で成果を上げる 実務直結のExcel術

わずか5分で成果を上げる 実務直結のExcel術

お絵かきおもちゃ「Etch-A-Sketch」のWeb版を作ってみた

$
0
0

f:id:piro_suke:20170603233743p:plain

小さい頃に気に入って遊んでたあのお絵かきおもちゃ、

なんだったかな、

と思って検索したら「Etch-A-Sketch」という製品だった。

Ohio Art Classic Etch A Sketch Magic Screen

Ohio Art Classic Etch A Sketch Magic Screen

懐かしい。

左右のダイアルをまわすと、上下左右にポインタが動いて線が描かれる。

くるくるダイアルを回してポインタを一生懸命動かして絵を描いていくわけだ。

両方のダイアルを同時に回すことで斜めの線を描いたりもできるのだけど、

それが結構難しくて、ふにゃふにゃした線になる。

絵を消したい時は、裏向けてシャカシャカ振ると消える。

最終的には直線を組み合わせた角ばった絵になっちゃうのだけど、

それがまた独特で楽しかった。

つくる

なんてことを思い出したら猛烈に遊びたくなったので、

記憶を頼りに似たようなものをWeb版で作ってみた。

keyboard-sketch

最大の特徴であるダイアルで操作する仕組みは

残念ながら実現するのが難しそうだったので、

キーボードの特定キーを押すとポインタを上下左右に動かせるようにした。

d : 左に移動

f : 右に移動

j : 上に移動

k : 下に移動

c : スケッチを削除

なかなかチープな仕上がりになってしまったけど、

描いてみると記憶にあるのとそこそこ似た操作感で、結構満足できた。

おわり

コードは ClojureScript + Quil で作ってみたのだけど、

キーイベントを定義して

ポインタの位置にドットを描くだけなので、案外簡単にできてよかった。

ソースはこちら。

github.com

効率的にやせるために栄養について理解するために栄養素データで遊ぶ

$
0
0

f:id:piro_suke:20170726011033j:plain

太り過ぎでそろそろ家族の冷たい視線と容赦ない言葉に耐えられなくなってきたので、

ダイエットに真剣に取り組みたい。

ダイエットといえば「食事のバランス」と「適度な運動」だけど、

おそらく日々の食事の方が体重への影響は大きいだろう、

ということでまずは「食」から取り組む。

取り組みを継続させるために重要なのは、

「このやり方が正しい」と信じ続けられるかどうかだと思われる。

食事制限や運動は、

その努力がどれくらい体重に反映されているのかが見えにくいので、

継続が難しく、なかなか成功しないのだろう。

そういえば食べたものがそれぞれ体にどんな影響を与えるのかも

全然知らないことに気づいたので、

まずは食品に含まれる栄養素とその効果について調べてみることにした。

栄養素データを入手する

探してみたら、国が食品の栄養素データを

「食品標準成分表」という形でまとめて公開してくれている。

日本食品標準成分表2015年版(七訂)について:文部科学省

食品毎の栄養素別含有量が記載されているので、

どの食品を食べたらどういう栄養素が摂取できるかが分かる。

栄養素についての説明は記載されていないので、

ネットやら書籍やらを参照しながら、栄養素の属性データをまとめてみた。

まだ整備が必要だが、

ひとまずこれで食品毎の栄養素とその効果は調べやすくなった。

うなぎの栄養素的特徴を調べてみる

試しに、土用の丑の日で話題の「うなぎの蒲焼き」について、

食べた時の効果を調べてみよう。

「疲労回復」「夏バテ防止」「スタミナ食」というイメージがあるが、

それはどの栄養素によるものなのか。

食品としての特徴が分かるように、

食品標準成分表に記載されている食品全体に対する

偏差値を算出して、偏差値50以上の栄養素を抽出してみた。

栄養素名効能リスト単位含有量平均値偏差値
イコセン酸[コレステロール調整, 血液サラサラ, 整腸作用]mg/100g1300.00144.6180.55
n-3 ドコサペンタエン酸[血液サラサラ]mg/100g420.0063.2677.51
n-3 イコサテトラエン酸[神経細胞強化]mg/100g160.0033.6971.85
ビタミン ビタミンB1[脂肪燃焼, 糖質代謝]mg/100g0.750.1470.81
n-3 ドコサヘキサエン酸(DHA)[血液サラサラ]mg/100g1300.00360.8067.10
パルミトレイン酸[脂肪代謝, 抗炎症作用, コレステロール調整]mg/100g1400.00282.2166.38
ビタミン ビタミンB2[脂肪代謝, 脂肪燃焼]mg/100g0.740.1866.28
20:5 n-3 イコサペンタエン酸(EPA)[血液サラサラ]mg/100g750.00245.4762.07
ビタミン (ビタミンA) レチノール[免疫アップ, 肌爪髪生成, 視力改善, 抗酸化作用]µg/100g1500.00196.2661.64
一般成分 たんぱく質g/100g23.0011.0060.45
ビタミン ビタミンD[骨成長, 免疫アップ, アレルギー予防]µg/100g19.006.1260.19
無機質 セレン[抗酸化作用, 免疫アップ, ガン予防]µg/100g42.0016.2659.02
ビタミン  (ビタミンE) α-トコフェロール[抗酸化作用, 肌爪髪生成]mg/100g4.901.7058.51
ビタミン パントテン酸[脂肪燃焼]mg/100g1.290.6758.27
テトラコセン酸mg/100g85.0036.2957.88
無機質 亜鉛[肌爪髪生成]mg/100g2.701.3757.81
コレステロール[]mg/100g230.00111.9557.39
無機質 リン[骨成長]mg/100g300.00155.3857.29
ミリスチン酸[免疫アップ, ガン予防]mg/100g850.00288.7556.41
n-3 オクタデカテトラエン酸[抗炎症作用, アレルギー予防]mg/100g170.0075.6155.96
ヘプタデセン酸[コレステロール調整]mg/100g110.0054.5955.37
パルミチン酸[抗酸化作用]mg/100g3600.001765.5855.23
ドコセン酸[コレステロール調整, 保湿]mg/100g500.00216.8754.18
ビタミン ナイアシン[脂肪燃焼, アルコール分解]mg/100g4.102.8652.82
無機質 カルシウム[骨成長]mg/100g150.0091.8152.07
ビタミン ビオチン[脂肪燃焼]µg/100g10.408.9350.68
n-6 アラキドン酸[血液凝集]mg/100g62.0058.7050.44
ステアリン酸[抗酸化作用, 保湿]mg/100g790.00725.1850.41
n-6 イコサトリエン酸[コレステロール調整]mg/100g16.0015.7750.09

「抗酸化作用」「整腸作用」「免疫アップ」の効果がある栄養素が

多く含まれているので、このあたりが「疲労回復」等の特徴につながっているのだろうか。

ビタミン類が豊富に含まれていると、

疲れをとったりスタミナの元になったりするらしい。

血液サラサラ効果のあるDHA、EPAも豊富に含まれているようだ。

印象として、血液がサラサラになると、

脂肪も流れていって痩せていくイメージを持っているのだけど、どうなのか。

栄養素のグループから食品を見てみる

上の表では食品に含まれる栄養素から特徴を洗い出したので、

次に特定の効果を持つ栄養素のグループが食品に

どの程度含まれているのかを調べて、

その食品の特徴を洗い出してみよう。

今回は、うなぎに豊富に含まれている「ビタミン」と

個人的に興味のある「血液サラサラ成分」でグラフ化してみた。

比較のため、「いわしのかば焼き」のグラフと並べてみよう。

ビタミンACE

まずは抗酸化作用があると言われるビタミンA,C,Eでまとめてみる。

うなぎのかば焼き

f:id:piro_suke:20170803235306p:plain

いわしのかば焼き

f:id:piro_suke:20170803235529p:plain

どちらもビタミンA、Eを含むが、

うなぎのみビタミンEのγトコフェロールを含有している。

これは体内の塩分排出を助ける栄養素らしい。

その他のビタミン

うなぎのかば焼き

f:id:piro_suke:20170803235319p:plain

いわしのかば焼き

f:id:piro_suke:20170803235542p:plain

どちらもビタミンB、Dを多く含んでいる。

うなぎのみビオチンを含んでいるが、これは皮膚や髪を作る栄養素。

表の上部を占めるビタミンB群は疲労回復に効果があるようで、

特にビタミンB1は必要量を摂取できていないことが多いらしい。

ビタミンB1の含有量が多いうなぎが「疲労回復」に良いとされるのは

これが理由かな?

血液サラサラ成分

うなぎのかば焼き

f:id:piro_suke:20170803235254p:plain

いわしのかば焼き

f:id:piro_suke:20170803235552p:plain

これまたどちらも血液サラサラ成分を多く含むようだ。

うなぎはEPAをより多く含み、いわしはイグノセリン酸を多く含む。

この2つの栄養素としてのちがいはよく分からなかった。

おわり

うなぎのかば焼きといわしのかば焼きの比較結果としては、

ビタミンや血液サラサラ成分に関しては

それほど大きな違いがないということは分かった。

調べるのはなかなか楽しかったので、

勉強を進めつつ、興味を持った食材について調べてみたい。

栄養素について理解することで、自然と良い選択ができるようになり、

痩せていけるといいのだけど。

世界一やさしい! 栄養素図鑑

世界一やさしい! 栄養素図鑑

決定版 栄養学の基本がまるごとわかる事典

決定版 栄養学の基本がまるごとわかる事典

子どもの塗り絵にするために写真を線画化してみたら、むしろ大人向けな味のあるものができた

$
0
0

f:id:piro_suke:20170827020934j:plain

小学校にあがった娘が、

「もうお子様向けのぬりえでは満足できません」

というので、

写真を線画化して細かくてリアルな塗り絵を作ってみたら、

超上手なデッサンみたいな画像ができた。

小学生に程よい塗り絵は案外見つからない

まずは本屋で探してみよう、ということで何軒かまわってみたのだが、

小さい子向けのキャラクターものの塗り絵か、

大人向けのセラピー要素の強いもののどちらかしか見つからず、

うちのおませな小学生にちょうど良さそうなものがない。

普通のこどもは小さい子向けの塗り絵に飽きたら、

もう塗り絵をしなくなるのか。

用意された絵に色を塗るだけの段階を終え、

自分で絵を描く段階に進むのが

正しい成長のしかたなのか。

ないなら作ってみよう

とはいえ、ここであきらめて娘の期待を裏切る訳にはいかない。

ないなら作ってみよう、ということで

色々ググってみると、

画像処理のテクニックとして、

画像から色を抜く「線画化」というものがあるらしいのを発見。

これを使えばイラストから写真まで

何でも思いのままに塗り絵にできそうだ。

作ってみた

画像を食わせると塗り絵化してくれる、

「画像を塗り絵に変換するプログラム」を書いて

手持ちの写真を変換してみた。

こんなあじさい写真が…

f:id:piro_suke:20170828005736j:plain

こうなる。

f:id:piro_suke:20170827012848j:plain

…なにこれちょっとした手書き感もあってすごく良い。

夢中になって色々変換してみた。

フィリピン出張で行ったどこかの寺院。

f:id:piro_suke:20170827013123j:plain

近くの公園。

f:id:piro_suke:20170827013217j:plain

うーむ、人の感じがまた良い。

こんな絵が手描きできたら最高なのだけど。

箕面温泉。

f:id:piro_suke:20170827013330j:plain

梅田の風景。都会の風景はくっきり変換できるようだ。

f:id:piro_suke:20170827013501j:plain

モールのショップ。

f:id:piro_suke:20170827021514j:plain

ちょっと林明子さんの絵本ぽくない?

そういえばこういう絵が好きだったことを思い出す。

橋から川を眺める我が子。

f:id:piro_suke:20170827013918j:plain

上着の水玉がえらくくっきり出てる。

おわり

早速いくつか印刷して娘に渡してみたところ、

大変喜んでくれたので良かった。

かなり細いけど、果たして塗りきれるのだろうか。

それはさておき、

生成される絵が個人的にすごく好みなので、

他にも色々写真を変換してみたい。

写真撮影の楽しみが1つ増えた。

はじめてのおつかい(こどものとも傑作集)

はじめてのおつかい(こどものとも傑作集)

あさえとちいさいいもうと (こどものとも傑作集)

あさえとちいさいいもうと (こどものとも傑作集)

Java/Pythonプログラマの自分が今からC#を学ぶ価値があるかどうかを検討する

$
0
0

f:id:piro_suke:20180416234346j:plain

WindowsのGUIアプリとして作りたいものがいくつかあって、

PythonのGUIライブラリを

いくつか(PyQtとかwxPythonとかKivyとか)試してみたけどしっくりこず、

やっぱりWindows用アプリを作るなら.NETじゃない?

ということでC#での開発について調べている。

今まで「Windows限定でVisualStudioがないと開発できないJavaみたいな言語」という、

汎用的じゃないイメージがあって避けていたのだけど、

調べてみると.NET CoreとかLinuxでも動作するCUIアプリを開発する仕組みもあって、

興味が湧いてきた。

新しい言語を学ぶならメイン言語にする価値があるものが良いので、

自分がプログラミング言語に求めることが一通りできるかどうか、調べてみた。

プログラミング言語に期待することは

個人的にプログラムを書く際によく利用している技術やライブラリを思い出しつつ、

簡単に実現できるようになっててほしいことを洗い出してみた:

  • テキストエディタとシェル(コマンドプロンプト)で開発できること
  • 実行環境の起動が遅くないこと
  • サードパーティライブラリ管理機能があること
  • サクッと作って実行可能なスクリプトが書けること
  • リスト内包表記的なリスト処理ができること
  • PostgreSQLに接続できること
  • Excel操作できること
  • SSH接続とトンネリングができること
  • Webスクレイピングができること
  • Web APIに接続できること
  • Apacheで動作するWebアプリを開発できること
  • データ分析・機械学習ができること
  • iOS用アプリを開発できること

テキストエディタとシェル(コマンドプロンプト)で開発できること

ちょっとしたデータ変換がしたい時にIDEを起動せずにプログラムを書いて実行したい。

VisualStudio無しでビルドや実行ができるかな、と調べてみたら

「.NET Core SDK」で実現できるようになっていた。

これはJavaでいうJDK的なもので、VisualStudioに含まれているものを使うか、

下記のサイトからダウンロードして単体で使用することもできる。

www.microsoft.com

インストールすると、下記のような感じでビルド・実行できる

コンソールアプリ用プロジェクトの作成

dotnet new console -o TestConsoleApp

プロジェクトをビルドする

dotnet build

プロジェクトを実行する

dotnet run

なんとLinuxやMac用のSDKもあり、クロスプラットフォームなアプリが開発できる。

サクッと作って実行可能なスクリプトが書けること

いちいちプロジェクトを作らず、

Pythonみたいにスクリプトを1つ書いて実行して終わり、

みたいな感じで使いたい。

ありました。C#スクリプト(.csx)

www.buildinsider.net

csiコマンドで実行できる。

実行環境の起動が遅くないこと

これはもう少し試してみないと分からない。

今のところ、そんなに速くないけど、そんなに遅くない感じ。

サードパーティライブラリ管理機能があること

PythonのpipやNode.jsのnpmみたいな、

サクッと必要なライブラリを導入して使える環境が欲しい。

あった。そりゃあるか。NuGet。何の略かは分からない。

NuGet Gallery | Home

VisualStudio経由でも、.NET Coreのdotnetコマンド経由でも使える。

dotnetコマンドは下記のように使用する。

dotnet add package パッケージ名

リスト内包表記的なリスト処理ができること

期待してなかったのだけど、できるっぽい。

var evens = from n in numList where n % 2 == 0 select n;

これはLINQのクエリ構文というもので、コレクションをSQL風に処理できるらしい。

まだ触ってないけど、これだけでC#好きになれそうな気がする。

PostgreSQLに接続できること

PostgreSQL好きなので、SQLServer以外にも接続できることを一応確認しておく。

www.buildinsider.net

できる。

Excel操作できること

マイクロソフト製なのだからネイティブな感じでできるだろう、

と思ったら、こちらのサードパーティ製のライブラリが使いやすいらしい。

github.com

SSH接続とトンネリングができること

DBを扱うような開発ツールを作る時用に、

SSHトンネリングができるかどうかを確認しておく。

あった。

github.com

Web APIに接続できること

HTTPクライアントライブラリはチェックしておかないと。

色々あるけど、HttpClientクラスを使うのがよさげ。

HttpClient クラス (System.Net.Http)

Webスクレイピングができること

この辺は、どの言語もいくつか選択肢があるイメージ。

最初に見つけたやつ。

qiita.com

Apacheで動作するWebアプリを開発できること

これはIISがあるので無理だろうと思ってたのだけど、

.NET Coreに ASP.NET Core というのが含まれていたので、調べてみたらできそう。

docs.microsoft.com

素晴らしい。

データ分析・機械学習ができること

検索してみると、Accord.NETというライブラリがよく利用されてるっぽい。

Introduction - Accord.NET Machine Learning in C#

iOS用アプリを開発できること

これも期待してなかったのだけど、

Xamarin.iOSで提供されてるSDKで開発できるみたい。

docs.microsoft.com

何でもできるな。

おわり

C#は思ってたより柔軟に書けるようになってるし、

クロスプラットフォームで開発できるし、

ということでかなり興味が湧いた。

起動速度とかは試してみないと分からないけど、

これでGUIアプリがほかの言語より簡単に作れるなら、

学ぶ価値ありそう。

More Effective C# 6.0/7.0

More Effective C# 6.0/7.0

数当てゲームを作る(Vue.js + Vuex + TypeScriptに挑戦)

$
0
0

料理人の子がパパにおいしい料理を作ってもらえるように、

うちの子も何がしかプログラマの子としてのメリットを受けて然るべきではないか、

と急に思い立ち、子供用のゲームを色々作ってあげよう!

と燃えてきたので、すぐ作れそうな「数当てゲーム」を作ってみた。

せっかくなので、興味があったけど試してなかったVue.jsも試してみた。

数当てゲームのルール

ゲーム開始時にランダムな数字を1つ答えとして設定しておいて、

ユーザーはその答えを予想して画面上に並んだ数字をクリックする。

クリックした数字がはずれなら、はずれメッセージとともに

答えと比較した数字の大小をヒントとして表示する。

正解なら、正解メッセージを表示して、もう一度遊ぶためのリンクを表示する。

完成イメージ

f:id:piro_suke:20180704231243p:plain

作り方

せっかくだからモダンな環境で開発してやろう、ということで

Vue.js + Vuex + TypeScript で開発した。

jp.vuejs.org

vuex.vuejs.org

www.typescriptlang.org

ソースはこちら:

github.com

完成版で遊ぶにはこちら:

number-guess

Vue.jsめっちゃ作りやすい。バインディングバリバリですぐできた。

ドキュメントが分かりやすくてよかった。

TypeScriptは苦労した割に活用できてない気がする。

おわり

我ながら最初のゲームとして実に程よい題材を選んだ気がする。

Vue.js楽しいので、次はもう少し複雑なゲームに挑戦してみたい。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

Vue.js + SVG でマインスイーパーもどきをつくる

$
0
0

前回作った数当てゲームがこどもに好評だったので、

blog.honjala.net

次はもう少し複雑なマインスイーパー作成に挑んでみた。

数当てゲームはシンプルすぎて2秒で飽きられたけど、

マインスイーパーならしばらく遊んでくれるにちがいない。

今回は前回使った Vue + Vuex + TypeScript に加えて、

今後より高度なゲームを作るにあたって役に立つであろう、

SVGを使って画面を作成してみた。

マインスイーパーのルール

昔Windowsに付属してたマインスイーパーの簡易版のイメージ。

  • 9 x 9 のマスを用意する
  • そこにランダムに10個のマイン(子供に分かるよう、「落とし穴」にした)を埋め込む
  • マスをクリックしてマインを引いたらゲームオーバー
  • マイン以外のマスをクリックすると、そのマスのまわりにいくつマインがあるか数字で表示される
  • まわりにマインがないマスをクリックするとまわりのマスも開き、そこにマインがないマスがあったらそれも開く
  • マインが入っているマス以外をすべて開いたら勝ち
  • 「やりなおし」ボタンをクリックするとリセットされる

完成イメージ

f:id:piro_suke:20180714012239p:plain

SVGを使っていることを微塵も感じさせないデザインになってしまったのが残念。

こちらで遊べます。

minesweeper

ソースはこちら

github.com

Vue.jsを使用した開発は、

画面とのやりとりはVueがバインドしてくれるので

Vuex側でのデータ処理とSVGでのUI調整に集中できるのがとても良い。

おわり

ゲーム作りは自分でちまちま遊びながら進められるのが楽しい。

SVGで色々作って、もう少したのしげなUIを作れるようになりたい。

Webで使える!SVGファーストガイド

Webで使える!SVGファーストガイド


Vue.js + SVG でヌメロン風数当てゲームをつくる

$
0
0

Vue.jsでこども用ゲーム作成シリーズ第3弾。

前回作ったマインスイーパーは、ゲームルール自体の中毒性のおかげか、

たまにプレイしてくれてるようで作った甲斐があった。

blog.honjala.net

飽きないうちに次のゲームを作成しておこう、ということで

今回は昔テレビで観たヌメロンというゲームを参考に、

数当てゲームを作ってみた。

Numer0n - Wikipedia

ルール

本家はいくつかアイテムが使えるのだけど、まずは基本ルールのみ実装。

あらかじめ3桁の重複しない数字がランダムに選ばれて、

プレイヤーはそれを予想して数字を入力する。

選ばれた数字と入力した数字が一致したらプレイヤーの勝ち。

数字の入力は何回でもできて、外れてもヒントが表示される。

入力した数字の位置と値が合っている個数が「あたり」として表示され、

位置だけが合っている個数が「おしい」として表示される。

プレイヤーはそのヒントをみて予想の数字を調整し、

なるべく少ない回数での正解を目指す。

画面イメージ

f:id:piro_suke:20180806005853p:plain

こちらで遊べます。

numeron

ソースはこちら。

github.com

おわり

数当てゲームは、答え生成処理、判定処理、UIの組み換えで

いろんなパターンのものが作れてゲーム開発入門に良い。

自分で遊んでみて気づいたのだけど、

今回のルールは、これまでつくった2つのような

「適当に入力してたら終わる」というタイプのものではないので、

こども向けとしては難しい方かもしれない。

入力を数字じゃなくて文字とかイラストに変更しても遊べそうなので、

挫折するようならもう少し選択肢を減らしたイラストバージョンを

作って、レベルアップしていけるような仕組みを検討したい。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

node.jsがやたら非同期化しようとするのをasync/awaitでどうにか同期化する

$
0
0

node.jsのライブラリって

非同期的に実行できるようにしてくれてるものがとても多い。

きっとそれは大変ありがたいことなのだけど、

とりあえず動いてくれればそれで良いような

意識の低いコマンドラインツールを作るような時は

思ったような順番で処理が実行されなかったり、

コールバック地獄になったりで

「ただ同期的に動かしたいだけなのに!」と困ることがよくある。

何ならnode.jsを使う時の悩みの大半は

この非同期処理の部分なんじゃないだろうかと思うぐらい。

このままではnode.jsが好きになれん、

ということであれこれ試してみた結果、

v7.6から使えるようになったasync/awaitを

使う形でようやく自分なりのパターンが見えてきたので、

ここでまとめておきたい。

async/awaitの説明

ここからの説明は

「多分こうなんじゃない?」レベルのものなので、

正確な情報についてはまた別途調べてみていただきたい。

さて、async/awaitはどういうものかというと、

「非同期な関数」に「await」をつけると非同期な関数の処理が終わるまで

待ってから次の処理を行ってくれる、というものだ。

例えば「asyncFunc()」という非同期な関数がある場合に、

let result = await asyncFunc();

という形で書くことで、

「asyncFunc()」の実行が終わるのを待ってから

resultに結果を入れて次の処理に進んでくれるようになる。

awaitをつけるだけで、

asyncFuncにコールバック関数を仕込んだりする必要がなく、

普通の非同期でない関数と同じように結果を受け取ることができる。

「await」は「async」な関数の中でのみ使用できる、というルールがある。

なので、実際に使う時は下記のような形になる。

async function main() {let result = await asyncFunc();
}

main();

awaitをつけるだけでどんな非同期関数でも同期化できたら簡単なのだけど、

awaitを使って同期化できる関数には条件がある。

それは、「Promise」オブジェクトを返すということ。

Promiseオブジェクトは

「そのうち実行することを約束された処理を持ったオブジェクト」

という感じのもので、Promiseオブジェクトを返す非同期関数は下記のようなもの。

function asyncFunc2() {returnnew Promise((resolve) => {// ...何かしらの時間がかかる処理...let result = '返したい値';

        resolve(result);
    });
}

非同期関数が返すPromiseオブジェクトのthenメソッドに

Promiseオブジェクト内の処理後に実行したい処理を指定しておくと、

実行してくれる。

asyncFunc2().then((result) => {// Promiseのresolve関数実行後に実行したい処理
    console.log(result);
});

これがasync/awaitを使用せずにPromiseを使うパターン。

コールバック関数を重ねる形ではないが、やはりコールバック関数が必要となる。

一方、async/awaitを使うと、thenメソッドを定義する代わりに

関数の戻り値として直接resolve関数の結果を受け取ることができる。

function asyncFunc2() {returnnew Promise((resolve) => {// ...何かしらの時間がかかる処理...let result = '返したい値';

        resolve(result);
    });
}

async function main() {const result = await asyncFunc2();
    console.log(result);
}

main();

処理内容は同じだけど、async/awaitを使った方が

コールバック感がなくなり、コードが同期的で分かりやすい感じになる。

例えば一定時間処理を停止するsleep関数を作って使いたい時とかは

下記のような関数を作成しておき、

function sleep(waitMillSeconds: number) {returnnew Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, waitMillSeconds);
    });
}

awaitで呼び出せばシンプルにsleep処理を実行できる。

async function main() {
    console.log('開始');

    // 1秒待機
    await sleep(1000);

    console.log('終了');
}

main();

最近のライブラリはだいたいPromise形式の返り値に対応しているので、

そういったライブラリはthenを使う代わりにasync/awaitを使えばすっきり書ける。

理解しきれてない「async」定義

ところで上に書いたとおり、awaitはasyncな関数内でしか使えないという制限がある。

「async」な関数とは何かというと、「返り値が必ずPromiseになる関数」らしい。

例えばPromiseオブジェクトを明示的に返さない関数でも

function func3() {return'hello';
}

async function func4() {return'hello';
}

asyncをつけるとPromiseを返すようになる。

// helloconst res3 = func3();

// Promiseconst res4 = func4();

// helloconst res4_2 = await func4();

なので、「await」を1回でも使おうと思ったらそれをラップする関数は

全部「async」な関数にする必要がある。

async function asyncFunc5() {return await asyncFunc();
}

async function asyncFunc6() {return await asyncFunc5();
}

async main() {const result = await asyncFunc6();
}

main();

もはや「async」ってわざわざ書く必要ある?みんなこう書いてるの?

ってなるけど、調べきれてないので、

おまじないとして一旦受け入れた。

Promise非対応の非同期関数への対応

async/awaitはPromiseオブジェクトを返す非同期関数を前提としたものなので、

Promiseを返さない非同期関数には使えない。

使いたい時は、自分でラッパー関数を作ることになる。

例えばファイルを行ごとにリストにして読み込みたい時なんかは、

下記のような関数を定義するとasync/awaitで呼べるようになる。

import fs from 'fs';
import readline from 'readline';

function readFileLines(srcFilePath, encoding) {let rs = fs.createReadStream(srcFilePath, {encoding: encoding});
    let rl = readline.createInterface({input: rs});

    returnnew Promise((resolve, reject) => {const lineList = [];
        rl.on('line', (line) => {
            lineList.push(line);
        })
        .on('close', () => {
            resolve(lineList)
        });
    });
}

おわり

まだ理解しきれていない部分はあるものの、

Promiseとasync/awaitの概念をこれぐらい押さえておけば

同期的に処理を行うツールをつくる時に困ることがだいぶ減るはず。

実践Node.jsプログラミング Programmer's SELECTION

実践Node.jsプログラミング Programmer's SELECTION

node.jsでリモートのLinux環境やデータベースの操作を自動化する

$
0
0

ぼくが仕事でLinuxサーバ環境に接続して行う操作というのはだいたい決まっていて、

  • コマンドでサーバの状態を確認
  • ログをファイル出力してダウンロード
  • サービスの設定変更と再起動
  • SSHトンネル経由でDBアクセス

のうちどれかを行うことが多い。

基本は手作業だったり、シェルスクリプトを作って

実行したりする形で対応できるものなのだけど、

結構めんどくさいので、

今回はnode.jsでどこまで自動化できるかを試してみたい。

事前準備

ローカルのWindowsマシンでnode.jsプログラムを実行して、

プログラム経由でLinux環境に接続して色々操作することを

想定しているので、ローカルにnode.js環境をインストールしておく。

この記事に載せてるサンプルは

Typescriptで書いているけど、だいたいJavascriptとしても動くと思う。

SSHでLinux環境に接続する

まずはSSHで接続してコマンドを実行できる状態にしてみよう。

SSHライブラリは「node-ssh」を使う。

npm install --save node-ssh

github.com

Promise形式で実行できるのでasync/awaitですっきり書ける。

下記はSSH接続して、lsコマンド実行して、切断するサンプル。

const Ssh  = require('node-ssh');

async function main() {const ssh = new Ssh();

    const sshPassword = 'パスワード';
    
    // 接続
    await ssh.connect({
        host: 'SSHサーバアドレス',
        port: ポート番号,
        username: 'ユーザー名',
        password: sshPassword
    });

    // コマンド実行
    res = await ssh.execCommand('ls -al', {options: {pty: true}});

    // 切断
    ssh.dispose();
}

main();

optionsで指定してるオプションはなくても良いのだけど、

サービスの再起動なんかを行った時に切断後もサービスが起動したままに

したい時に指定が必要になるので、おまじない的に全部のコマンドにつけてる。

コマンドの実行結果は戻り値として受け取ることができるので、

ローカルでファイルに出力したりできる。リモートスクリプトの良いところ。

sudoで管理用コマンドを実行したい時は、

下記のようにexecCommandメソッドを呼び出す。

const res2 = await ssh.execCommand('sudo ls /var/log/httpd', {stdin: sshPassword + '\n', options: {pty: true}});

sudoコマンドがパスワードを求めてきたらstdinで指定したパスワードが入力される。

node-sshについてはここまでのことを押さえておけば

大抵のコマンドを実行してその結果を受け取ることができる。

鍵認証でも接続もできるし、node-sshの機能では足りない場合は、

node-sshが参照しているssh2ライブラリを使えばもっと色々できる。

github.com

試してないけどSFTPでのファイル転送もできるみたい。

SSHトンネル経由でLinux上のDBにアクセス

続いて、リモートサーバ上で動作しているデータベースに接続してみよう。

SSHトンネルを使用する。

WindowならPuttyとかでSSHトンネル設定をしておいてから

pgadminとかでトンネルのローカルポートに接続して操作する、

みたいなことはよくやると思うが、これをnode.jsで実現するイメージ。

Puttyで作成したSSHトンネルを使ってnode.jsからDB接続しても良いし、

node.jsのSSHトンネリング用ライブラリを使って

node.jsだけでSSHトンネルを作ってDBに接続する、ということもできる。

せっかくなので今回はnode.jsで完結させてみよう。

SSHトンネル作成には、「tunnel-ssh」という、

上で紹介したssh2ライブラリをトンネリング用に拡張したライブラリを使う。

npm install --save tunnel-ssh

DB接続はknex + pgを使ってPostgreSQLデータベースに接続する。

npm install --save knex @types/knex pg @types/pg

SSHトンネルを作るテンプレは下記のような感じ

const tunnel = require('tunnel-ssh');

async function main() {const sshUserName = 'SSHユーザー名';
        const sshPassword = 'SSHパスワード';
    
        let sshConfig = {
            host: 'SSHサーバアドレス',
            port: SSHサーバポート番号,
            username: sshUserName,
            password: sshPassword,
            keepaliveInterval: 60000,
            keepAlive: true,
            dstHost: 'トンネリング先サーバアドレス',
            dstPort: トンネリング先サーバポート番号,
            localHost: 'localhost',
            localPort: ローカルポート番号
        };
        
        const tnl = tunnel(sshConfig, async (err: any, server: any) => {if (err) {throw err;
            }// ここにトンネリング中に実行したい処理を書く
    
            tnl.close();
        });
}

main();

上記のスクリプトは最後にトンネルを終了(tnl.close())してるので

処理完了後にプログラム終了する。

トンネルを終了しなければずっとトンネルを開いたまま待機状態になるので、

node.jsスクリプトでトンネルだけ開いておいて、pgadmin等他のアプリで

トンネルを利用する、ということもできるかも。

node.jsでトンネル経由でDBにアクセスする場合は下記のような

形で処理を追加する。

const tunnel = require('tunnel-ssh');
import knexLib = require('knex');

async function main() {const sshUserName = 'SSHユーザー名';
        const sshPassword = 'SSHパスワード';
    
        let sshConfig = {
            host: 'SSHサーバアドレス',
            port: SSHサーバポート番号,
            username: sshUserName,
            password: sshPassword,
            keepaliveInterval: 60000,
            keepAlive: true,
            dstHost: 'トンネリング先サーバアドレス',
            dstPort: トンネリング先ポート番号,
            localHost: 'localhost',
            localPort: 45432
        };
        
        const tnl = tunnel(sshConfig, async (err: any, server: any) => {if (err) {throw err;
            }let knex = knexLib({
                client: 'postgresql',
                connection: {
                    database: dbSetting.database,
                    port:     45432,
                    user:     dbSetting.user,
                    password: dbSetting.password
                }});
        
            const rows = await knex.select().from(tableName);
            console.log(rows);
       
            await knex.destroy();
    
            tnl.close();
        });
}

main();

トンネルのローカルポートとDB接続時のポート番号を合わせるだけ。

この方法を使えば、DBの実行結果をローカルに保存したり、

ローカルのExcelから読み込んだ内容をDBに登録したりできる。

おわり

今回はSSH経由でLinuxサーバを操作する方法を2つ紹介した。

応用すれば複数のLinux環境やDBに対して

同じ処理を実行したりすることもできるので、

自動化の幅を広げる役に立つと思う。

JavaScriptの絵本 第2版 Webプログラミングを始める新しい9つの扉

JavaScriptの絵本 第2版 Webプログラミングを始める新しい9つの扉

node.jsでWebスクレイピングして取得データを保存する

$
0
0

node.jsでデータ収集のためのWebスクレイピングを行う。

Webスクレイピングの流れというのはだいたい決まっていて、

  1. WebページにアクセスしてHTMLを取得する
  2. 取得したHTMLの中から必要なデータを抽出する
  3. 抽出したデータを保存する

の3段階となる。

通常、Webスクレイピングが必要となるのは

  • データ取得用のAPIが提供されていない
  • 必要なデータが1ページに収まらず多くのページにまたがっており、手作業でコピペしていくのが難しい

場合で、そのうちWebスクレイピングが可能なのは

  • 対象の各ページが同じフォーマットになっており、パターン化された処理で必要なデータを取得できる

場合となる。

例えば「ページングされた検索結果画面」とか

「同じフォーマットでHTMLが書かれたたくさんの商品詳細画面」

なんかがスクレイピングでデータ取得しやすい。

前提として、

Webスクレイピングは手作業 でWebページにアクセスしてコピペする、

という作業をプログラムが自動で行うようなもので、手動か自動かに関わらず

対象のデータを取得したり保存したりする権利なり許可なりが必要となる。

また、プログラムを使用する場合は

手動とちがってWebページに一度に大量のリクエストを行なうことが可能なので、

サーバに負荷をかけないように注意する必要がある。

node.jsでWebスクレイピングする場合、いくつか使えるライブラリがあるけど

「cheerio-httpcli」がつまづきにくくて良いと思う。

www.npmjs.com

puppeteerの方が人気ありそうだけど、自分のWindows環境ではうまく動かなかった。

取得したデータをデータベースに保存する場合、自分は「knex」を使ってる。

Knex.js - A SQL Query Builder for Javascript

あとは日付処理に便利な「moment」と

Moment.js | Home

リストやマップの処理に便利な「lodash」を必要に応じてインポートしておく。

lodash.com

だいたい下記のような形でスクリプトを始める:

// cheerio-httpcliでhttpsアクセスするための設定
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

// 必要なライブラリをインポートconst httpClient = require('cheerio-httpcli');
const knexLib = require('knex');
const _ = require('lodash');
const moment = require('moment');

// knexで生成するDB情報保存用ファイルのインポートconst knexfile = require('./knexfile')['development'];

// リクエストごとに一定時間空けるためのsleep関数
async function sleep(waitMillSeconds) {returnnew Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, waitMillSeconds);
    });
}

メイン処理は下記のような感じ。 例としてYahoo!天気の地震情報を数ページ分取得してみる。

typhoon.yahoo.co.jp

async function main() {// ベースURL。リクエストごとに変わらない部分const baseUrl = 'https://typhoon.yahoo.co.jp/weather/jp/earthquake/list/';

    // DBアクセス用のknexオブジェクトを生成const knex = knexLib({
        client: knexfile.client,
        connection: knexfile.connection
    });
    
    // リクエストごとに3秒待つための設定const waitMillSeconds = 3000;

    // 取得件数が500件を超えたら終了する設定const maxB = 500;

    // 1ページあたりの件数設定。const intervalB = 100;

    // 1ページ目から開始let currentB = 1;

    while (currentB < maxB) {
        console.log('b = ', currentB);

        // HTMLデータを取得const result = await httpClient.fetch(baseUrl, {sort: 1, key: 1, b: currentB});

        const $ = result.$;
        const logList = [];

        // HTML内の表の行データを取得して変換してリストに入れる
        $('tr', '#eqhist').filter(function () {return $(this).attr('bgcolor') === '#ffffff'; }).each(function () {const row = $(this);
            const cellList = $('td', row).map(function () {return $(this).text(); }).get();
            const rowId = $('a', $('td', row).first()).attr('href');
            const cellMap = _.zipObject(['happened', 'announced', 'place', 'magnitude', 'intensity'], cellList);
            cellMap.happened = moment(cellMap.happened, 'YYYY年M月D日 H時m分ごろ').toDate();
            cellMap.id = rowId;
            if (cellMap.magnitude !== '---') {
                logList.push(cellMap);
            }});

        // リストに入れたデータをデータベースに登録するfor (const log of logList) {
            await knex('test_logs').insert(log);
        }

        currentB += intervalB;

        // 指定ミリ秒数待機
        await sleep(waitMillSeconds);
    }

    await knex.destroy();
}

main();

このスクリプトを流すと、取得したデータがデータベースに入るので、

あとはSQLで集計してみたりグラフ表示に使ってみたりすることができる。

上記スクリプトはinsertにしか対応してないので、2回実行すると落ちる。

ちゃんと作るなら既存データは無視する処理を入れたりして、

新しいデータが増えても新しいデータの分だけ取得するようにする。

JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック

JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック

Vue.js + SVG でブロック崩しゲーム(自動版)を作る

$
0
0

数当てゲーム、マインスイーパーに続き、Vue.jsでこども向けゲームを作る。

もう少し動きのあるゲームが作ってみたいので、

今回はボールが跳ね回るブロック崩しゲームに挑戦する。

これまでに作ったゲームは初期処理で何か答えを生成しておいて、

あとはユーザーのイベントに応じて処理を実行するだけだったのだけど、

ブロック崩しの場合はゲームがスタートしたら終始ボールが

動いている必要があるので、この仕組みから考えてみよう。

完成イメージ

f:id:piro_suke:20180826231844p:plain

本当はユーザーが操作するバーを作って、

ちゃんとしたブロック崩しにしたかったのだけど、

ブロックを消せるようになった時点でちょっと飽きてしまったので、

ボールがブロックを消してくれるのをただ待つだけの

「自動」ブロック崩しになってしまった。

ちゃんとしたやつはまた気持ちが盛り上がってから作る。

ソースはこちら:

github.com

こちらで遊べます...というか見れます:

breakout

ボールが動き続ける仕組みを作る

上に書いたとおり、ボールを動かし続ける仕組みが必要なので、

processing.js や Quil にある draw/update メソッドのような

定期的に呼び出される描画メソッドを作る方法を試してみた。

具体的には、あらかじめvuexのactionにボールその他の動くものの

位置を更新する関数(update)を用意しておき、

それをsetIntervalで定期的に呼び出すようにする。

store.ts

import Vue from 'vue';
import Vuex from 'vuex';

import * as _ from 'lodash';

Vue.use(Vuex);

exportdefaultnew Vuex.Store({
  state: {},
  getters: {},
  mutations: {
    updateBallPositions(state, payload) {},
  },
  actions: {
    ...
    update(context) {// ここに動くものの位置を更新する処理を書く
      context.commit('updateBallPositions');
    },
  },
});

Game.ts(updateアクションを呼び出し続けるクラス)

exportdefaultclass Game {private fps: number;
    private interval: any;
    private updateFunc: any;

    // オブジェクト生成時に定期的に呼び出す関数を受け取る。
    constructor(updateFunc: any) {this.fps = 60;
        this.interval = null;
        this.updateFunc = updateFunc;
    }// このメソッドを呼び出すと定期的な関数呼び出しが開始される。public start() {this.interval = setInterval(() => {this.update();
        }, 1000 / this.fps);
    }public end() {
        clearInterval(this.interval);
    }public update() {this.updateFunc();
    }}

App.vue

import{ Component, Vue } from 'vue-property-decorator';
import Game from './Game';

@Component({
  components: {},
})
exportdefaultclass App extends Vue {public created() {const game = new Game(() => this.$store.dispatch('update'));
    game.start();
  }}

これで、ボールに限らず、動くものはstore.tsのupdateアクションに

位置更新処理を書いておけば動いてくれる。

試しにボールを3つ配置して動かす処理を追加すると、

下記のような感じになる。

store.ts

import Vue from 'vue';
import Vuex from 'vuex';

import * as _ from 'lodash';

Vue.use(Vuex);

// ボールの次の位置を算出するfunction calcNextBallStates(ballStates: any[]) {return ballStates.map((ballState) => {return{
      name: ballState.name,
      minX: ballState.x + ballState.vx,
      maxX: ballState.x + ballState.vx + ballState.r,
      minY: ballState.y + ballState.vy,
      maxY: ballState.y + ballState.vy + ballState.r,
      movingRight: ballState.vx > 0,
      movingDown: ballState.vy > 0,
    };
  });
}// ボールが壁に当たるか判定して、当たる場合は跳ね返った時の進行方向を返すfunction calcNextBallDirectionsByWallHit(nextBallStates: any[], gameAreaWidth: number, gameAreaHeight: number) {const nextBallDirections: any = {};
  for (const ballState of nextBallStates) {const newBallState: any = {
      movingRight: ballState.movingRight,
      movingDown: ballState.movingDown,
    };
    if (ballState.maxX > gameAreaWidth) {
      newBallState.movingRight = false;
    }elseif (ballState.minX < 0) {
      newBallState.movingRight = true;
    }if (ballState.maxY > gameAreaHeight) {
      newBallState.movingDown = false;
    }elseif (ballState.minY < 0) {
      newBallState.movingDown = true;
    }
    nextBallDirections[ballState.name] = newBallState;
  }return nextBallDirections;
}// 2つの進行方向が同じかどうかを判定するfunction isDirectionEqual(ballState1: any, ballState2: any) {return (ballState1.movingRight === ballState2.movingRight)
    && (ballState1.movingDown === ballState2.movingDown);
}exportdefaultnew Vuex.Store({
  state: {
    gameArea: {
      width: 500,
      height: 600,
      blockListMarginTop: 50,
      blockListMarginLeft: 50,
    },
    balls: [{
        name: 'ball-1',
        x: 400,
        y: 400,
        vx: 10,
        vy: 10,
        r: 5,
        minSpeed: 5,
        maxSpeed: 10,
        fill: '#000',
      },
      {
        name: 'ball-2',
        x: 300,
        y: 300,
        vx: -5,
        vy: 5,
        r: 10,
        minSpeed: 5,
        maxSpeed: 10,
        fill: '#c00',
      },
      {
        name: 'ball-3',
        x: 300,
        y: 300,
        vx: 5,
        vy: 5,
        r: 5,
        minSpeed: 3,
        maxSpeed: 5,
        fill: '#00c',
      },
    ],
  },
  getters: {
    getGameArea: (state, getters) => () => {return state.gameArea;
    },
    getBalls: (state, getters) => () => {return state.balls;
    },
  },
  mutations: {
    updateBallPositions(state, payload) {// ボールの次の位置を算出するconst nextBallStates = calcNextBallStates(state.balls);

      // 壁に当たるかどうかを算出してボールの次の進行方向を返すconst nextBallDirectionsByWallHit = calcNextBallDirectionsByWallHit(nextBallStates,
        state.gameArea.width, state.gameArea.height);

      // ボールの次の進行方向と位置を算出してセットするconst newBallStateMap: any = {};
      for (const nextBallState of nextBallStates) {const newBallState = {
          movingRight: nextBallState.movingRight,
          movingDown: nextBallState.movingDown,
        };
        const wallHitState = nextBallDirectionsByWallHit[nextBallState.name];
        if (!isDirectionEqual(nextBallState, wallHitState)) {
          newBallState.movingRight = wallHitState.movingRight;
          newBallState.movingDown = wallHitState.movingDown;
        }

        newBallStateMap[nextBallState.name] = newBallState;
      }for (const ballState of state.balls) {if (ballState.vx < 0 && newBallStateMap[ballState.name].movingRight) {
          ballState.vx = _.random(ballState.minSpeed, ballState.maxSpeed);
        }elseif (ballState.vx > 0 && !newBallStateMap[ballState.name].movingRight) {
          ballState.vx = 0 - _.random(ballState.minSpeed, ballState.maxSpeed);
        }
        ballState.x = ballState.x + ballState.vx;

        if (ballState.vy < 0 && newBallStateMap[ballState.name].movingDown) {
          ballState.vy = _.random(ballState.minSpeed, ballState.maxSpeed);
        }elseif (ballState.vy > 0 && !newBallStateMap[ballState.name].movingDown) {
          ballState.vy = 0 - _.random(ballState.minSpeed, ballState.maxSpeed);
        }
        ballState.y = ballState.y + ballState.vy;
      }},
  },
  actions: {
    update(context) {
      context.commit('updateBallPositions');
    },
  },
});

このあとのブロック崩し処理追加を想定して

必要以上に複雑な構成になっているけど、

内容としてはボールごとに現在位置(x, y)と

x方向、y方向への進行速度(vx, vy)を持っていて、

updateが呼ばれる度に現在位置に進行速度が追加されるようにしている。

壁にぶつかったら跳ね返るように座標チェックを入れていて、

跳ね返る時に少しランダムな角度と速度で跳ね返るようにしている。

Ball.vue

<template>
    <g transform="translate(1, 1)">
        <circle v-bind:r="ballR" v-bind:fill="ballFill" stroke="#fff" v-bind:cx="ballX" v-bind:cy="ballY" />
    </g>
</template>

<script lang="ts">
import{ Component, Prop, Vue } from 'vue-property-decorator';

import * as _ from 'lodash';

@Component
exportdefaultclass Ball extends Vue {

    @Prop({type: String})
    public ballId!: string;

    @Prop({type: String})
    public ballFill!: string;

    @Prop({type: Number})
    public ballR!: number;

    @Prop({type: Number})
    public ballX!: number;

    @Prop({type: Number})
    public ballY!: number;

}</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

これはただのボールの表示定義。

基本的に属性情報は親のApp.vueから受け取る。

App.vue

<template>
  <svg id="app" v-bind:width="gameArea.width + 'px'" v-bind:height="gameArea.height + 'px'" style="border: 1px solid #000;">
    <g transform="translate(1, 1)">
      <Ball v-for="ball in balls" v-bind:key="ball.ballId" v-bind:ball-id="ball.ballId" v-bind:ball-r="ball.r" v-bind:ball-x="ball.x" v-bind:ball-y="ball.y" v-bind:ball-fill="ball.fill" />
    </g>
  </svg>
</template>

<script lang="ts">
import{ Component, Vue } from 'vue-property-decorator';
import Ball from './components/Ball.vue';
import Game from './Game';

@Component({
  components: {
    Ball,
  },
})
exportdefaultclass App extends Vue {public created() {const game = new Game(() => this.$store.dispatch('update'));
    game.start();
  }

  get balls() {returnthis.$store.getters.getBalls();
  }

  get gameArea() {returnthis.$store.getters.getGameArea();
  }}</script>

<style>
</style>

store.tsでボールの位置を更新したら

あとはvuexが勝手に画面に反映してくれるので、

App.vueは現在の各ボールの位置を描画する処理を書いておくだけ。

この辺が vue + vuex の便利なところ。

ここまで書いて実行したら、ボールがずーっと跳ね回る画面ができるはず。

これを応用することで、ブロック崩し以外にもエアーホッケーとか

シューティングゲームとか色々作れるんじゃないかと思ってる。

ブロックを表示して当たり判定をつける

ブロック崩しなので、ボールが当たったら消えるブロックを配置する。

ボールはブロックに当たると跳ね返る。

この当たり判定と跳ね返る方向を実装するのがゲーム開発初心者には

結構大変だった。

最終的なソースは下記のような形になった。

store.ts

import Vue from 'vue';
import Vuex from 'vuex';

import * as _ from 'lodash';

Vue.use(Vuex);

// ボールの次の位置を算出function calcNextBallStates(ballStates: any[]) {return ballStates.map((ballState) => {return{
      name: ballState.name,
      minX: ballState.x + ballState.vx,
      maxX: ballState.x + ballState.vx + ballState.r,
      minY: ballState.y + ballState.vy,
      maxY: ballState.y + ballState.vy + ballState.r,
      movingRight: ballState.vx > 0,
      movingDown: ballState.vy > 0,
    };
  });
}// ボールが壁に当たったか判定し、当たった場合は新しい進行方向を返すfunction calcNextBallDirectionsByWallHit(nextBallStates: any[], gameAreaWidth: number, gameAreaHeight: number) {const nextBallDirections: any = {};
  for (const ballState of nextBallStates) {const newBallState: any = {
      movingRight: ballState.movingRight,
      movingDown: ballState.movingDown,
    };
    if (ballState.maxX > gameAreaWidth) {
      newBallState.movingRight = false;
    }elseif (ballState.minX < 0) {
      newBallState.movingRight = true;
    }if (ballState.maxY > gameAreaHeight) {
      newBallState.movingDown = false;
    }elseif (ballState.minY < 0) {
      newBallState.movingDown = true;
    }
    nextBallDirections[ballState.name] = newBallState;
  }return nextBallDirections;
}// ボールの移動前後の位置とブロックの位置によってどの面にぶつかったか判定するfunction calcHitDirection(prevX: number, prevY: number, ballR: number, nextX: number, nextY: number,
                          blockX: number, blockY: number, blockWidth: number, blockHeight: number) {const movingRight = prevX < nextX;
  const movingDown = prevY < nextY;

  if (movingRight && movingDown) {if ((nextX + ballR) - blockX >= (nextY + ballR) - blockY) {return'above';
    }else{return'left';
    }}if (movingRight && !movingDown) {if ((nextX + ballR) - blockX >= (blockY + blockHeight) - nextY) {return'below';
    }else{return'left';
    }}if (!movingRight && movingDown) {if ((blockX + blockWidth) - nextX >= (nextY + ballR) - blockY) {return'above';
    }else{return'right';
    }}if (!movingRight && !movingDown) {if ((blockX + blockWidth) - nextX >= (blockY + blockHeight) - nextY) {return'below';
    }else{return'right';
    }}}// 2つの進行方向が同じか判定するfunction isDirectionEqual(ballState1: any, ballState2: any) {return (ballState1.movingRight === ballState2.movingRight)
    && (ballState1.movingDown === ballState2.movingDown);
}// ボールがブロックに当たったか判定し、当たった場合は新しい進行方向を返すfunction calcNextBallDirectionsByBlockHit(ballStates: any[], nextBallStates: any[], blocks: any[],
  blockListMarginLeft: number, blockListMarginHeight: number) {const nextBallDirections: any = {};
  const hitBlocks: string[] = [];
  for (const ballState of nextBallStates) {const newBallState: any = {
      movingRight: ballState.movingRight,
      movingDown: ballState.movingDown,
    };
    for (const block of blocks) {const blockInfo = {
        minX: block.x + blockListMarginLeft,
        maxX: block.x + blockListMarginLeft + block.width,
        minY: block.y + blockListMarginHeight,
        maxY: block.y + blockListMarginHeight + block.height,
      };
      const isBallXInBlock =
        (blockInfo.minX <= ballState.minX && ballState.minX <= blockInfo.maxX)
        || (blockInfo.minX <= ballState.maxX && ballState.maxX <= blockInfo.maxX);
      const isBallYInBlock =
      (blockInfo.minY <= ballState.minY && ballState.minY <= blockInfo.maxY)
      || (blockInfo.minY <= ballState.maxY && ballState.maxY <= blockInfo.maxY);
      if (!block.isHit && isBallXInBlock && isBallYInBlock) {
        hitBlocks.push(block.blockId);
        const prevBallState = _.find(ballStates, {name: ballState.name});
        const hitDirection = calcHitDirection(prevBallState.x, prevBallState.y, prevBallState.r,
          ballState.minX, ballState.minY,
          blockInfo.minX, blockInfo.minY, block.width, block.height);
        switch (hitDirection) {case'above':
          newBallState.movingDown = false;
          break;
          case'below':
          newBallState.movingDown = true;
          break;
          case'left':
          newBallState.movingRight = false;
          break;
          case'right':
          newBallState.movingRight = true;
          break;
        }break;
      }}
    nextBallDirections[ballState.name] = newBallState;
  }return[nextBallDirections, hitBlocks];
}exportdefaultnew Vuex.Store({
  state: {
    gameArea: {
      width: 500,
      height: 600,
      blockListMarginTop: 50,
      blockListMarginLeft: 50,
    },
    balls: [{
        name: 'ball-1',
        x: 400,
        y: 400,
        vx: 10,
        vy: 10,
        r: 5,
        minSpeed: 5,
        maxSpeed: 10,
        fill: '#000',
      },
      {
        name: 'ball-2',
        x: 300,
        y: 300,
        vx: -5,
        vy: 5,
        r: 10,
        minSpeed: 5,
        maxSpeed: 10,
        fill: '#c00',
      },
      {
        name: 'ball-3',
        x: 300,
        y: 300,
        vx: 5,
        vy: 5,
        r: 5,
        minSpeed: 3,
        maxSpeed: 5,
        fill: '#00c',
      },
    ],
    blocks: [] as any[],
  },
  getters: {
    getGameArea: (state, getters) => () => {return state.gameArea;
    },
    getBallState: (state, getters) => (name: string) => {return _.find(state.balls, {name});
    },
    getBalls: (state, getters) => () => {return state.balls;
    },
    getBlocks: (state, getters) => () => {return state.blocks;
    },
  },
  mutations: {// ボールの位置を更新する
    updateBallPositions(state, payload) {// ボールの移動後の位置を算出するconst nextBallStates = calcNextBallStates(state.balls);
      // 移動後に壁にぶつかるか判定して進行方向を返すconst nextBallDirectionsByWallHit = calcNextBallDirectionsByWallHit(nextBallStates,
        state.gameArea.width, state.gameArea.height);

      // 移動後にブロックにぶつかるかを判定して進行方向を返す。一緒にぶつかったブロックも返す。const[nextBallDirectionsByBlockHit, hitBlocks] = calcNextBallDirectionsByBlockHit(state.balls,
        nextBallStates, state.blocks, state.gameArea.blockListMarginLeft, state.gameArea.blockListMarginTop);

      // ボールがぶつかったブロックを非表示にする。
      state.blocks = state.blocks.map((block) => {if (_.includes(hitBlocks, block.blockId)) {
          block.isHit = true;
        }return block;
      });

      // ボールの位置と進行方向を更新する。const newBallStateMap: any = {};
      for (const nextBallState of nextBallStates) {const newBallState = {
          movingRight: nextBallState.movingRight,
          movingDown: nextBallState.movingDown,
        };
        const wallHitState = nextBallDirectionsByWallHit[nextBallState.name];
        const blockHitState = nextBallDirectionsByBlockHit[nextBallState.name];
        if (!isDirectionEqual(nextBallState, wallHitState)) {
          newBallState.movingRight = wallHitState.movingRight;
          newBallState.movingDown = wallHitState.movingDown;
        }elseif (!isDirectionEqual(nextBallState, blockHitState)) {
          newBallState.movingRight = blockHitState.movingRight;
          newBallState.movingDown = blockHitState.movingDown;
        }

        newBallStateMap[nextBallState.name] = newBallState;
      }for (const ballState of state.balls) {if (ballState.vx < 0 && newBallStateMap[ballState.name].movingRight) {
          ballState.vx = _.random(ballState.minSpeed, ballState.maxSpeed);
        }elseif (ballState.vx > 0 && !newBallStateMap[ballState.name].movingRight) {
          ballState.vx = 0 - _.random(ballState.minSpeed, ballState.maxSpeed);
        }
        ballState.x = ballState.x + ballState.vx;

        if (ballState.vy < 0 && newBallStateMap[ballState.name].movingDown) {
          ballState.vy = _.random(ballState.minSpeed, ballState.maxSpeed);
        }elseif (ballState.vy > 0 && !newBallStateMap[ballState.name].movingDown) {
          ballState.vy = 0 - _.random(ballState.minSpeed, ballState.maxSpeed);
        }
        ballState.y = ballState.y + ballState.vy;
      }},

    // ブロックを生成する
    generateBlocks(state, payload) {
      state.blocks = [];
      for (const y of _.range(10)) {for (const x of _.range(10)) {
          state.blocks.push({
            x: x * 40,
            y: y * 20,
            width: 40,
            height: 20,
            blockId: x + ':' + y,
            isHit: false,
          });
        }}},
  },
  actions: {
    resetGame(context) {
      context.commit('generateBlocks');
    },
    update(context) {
      context.commit('updateBallPositions');
    },
  },
});

これがベストのやり方かどうかは分からん。

BlockList.vue

<template>
    <g v-bind:transform="'translate(' + gameArea.blockListMarginTop + ',' + gameArea.blockListMarginLeft + ')'">
        <g v-for="item in blockList" v-bind:key="item.blockId">
            <rect v-bind:width="item.width" v-bind:height="item.height" v-bind:stroke="(item.isHit ? '#fff' : '#ccc')" v-bind:fill="(item.isHit ? '#fff' : '#00c')" stroke-width="1" v-bind:x="item.x" v-bind:y="item.y" />
        </g>
    </g>
</template>

<script lang="ts">
import{ Component, Prop, Vue } from 'vue-property-decorator';

@Component
exportdefaultclass CellList extends Vue {
    get blockList() {const blocks = this.$store.getters.getBlocks();
        const blockViewList: any[] = [];
        for (const block of blocks) {
            blockViewList.push(Object.assign(block, {}));
        }return blockViewList;
    }

    get gameArea() {returnthis.$store.getters.getGameArea();
    }}</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

ブロックリストを表示する。

App.vue

<template>
  <svg id="app" v-bind:width="gameArea.width + 'px'" v-bind:height="gameArea.height + 'px'" style="border: 1px solid #000;">
    <g transform="translate(1, 1)">
      <BlockList />
      <Ball v-for="ball in balls" v-bind:key="ball.ballId" v-bind:ball-id="ball.ballId" v-bind:ball-r="ball.r" v-bind:ball-x="ball.x" v-bind:ball-y="ball.y" v-bind:ball-fill="ball.fill" />
    </g>
  </svg>
</template>

<script lang="ts">
import{ Component, Vue } from 'vue-property-decorator';
import Ball from './components/Ball.vue';
import BlockList from './components/BlockList.vue';
import Game from './Game';

@Component({
  components: {
    Ball,
    BlockList,
  },
})
exportdefaultclass App extends Vue {public created() {this.$store.dispatch('resetGame');
    const game = new Game(() => this.$store.dispatch('update'));
    game.start();
  }

  get balls() {returnthis.$store.getters.getBalls();
  }

  get gameArea() {returnthis.$store.getters.getGameArea();
  }}</script>

<style>
</style>

ブロックリスト表示処理を追加する。

これでひとまず完成。

おわり

とにかくブロックとボールの当たり判定とその後の跳ね返る方向を

算出する処理を作成するところで時間がかかった。

当たり判定がこんなに大変なものだとは...。

でも動きがあるゲームはやっぱり見てて楽しいので、

また他にも挑戦してみたい。

Game Programming Patterns ソフトウェア開発の問題解決メニュー (impress top gear)

Game Programming Patterns ソフトウェア開発の問題解決メニュー (impress top gear)

Windowsでタスクスケジューラを使用せずにnode.jsスクリプトを定期実行する

$
0
0

会社の自分のPC(Windows)で何かを定期的に実行したいときは

だいたいPythonなりnode.jsなりでスクリプトを書いて

タスクスケジューラに登録しておく。

のだけど、

タスクスケジューラは未ログイン時に実行しようとすると

ログインアカウントを登録しておく必要があったり、

パスワードを変更した時に更新を忘れて実行に失敗しまくってたり、

そのログイン失敗のせいで社内ネットワークから閉め出されちゃったり、

色々と困ることがある。

Linuxのcrondと同じような仕組みができないか、と探してみたら

「pm2」と「node-cron」の組み合わせで実現できそうだったので、試してみた。

参考情報

こちらの参考ページを読んだらこのあとの内容は読まなくても良いくらいなのだけど、

node.jsで定期的にスクリプトを実行するにはpm2が便利 |

「node-cron」はその名の通りnode.jsでcron機能を実現するためのライブラリで、

www.npmjs.com

cron風の書式でスケジュールと関数を登録しておくと

指定したタイミングでその関数を実行してくれる。

これだけでもまあ十分なのだけど、PCの起動時に自動起動したいので、

「pm2」でプロセスを管理する仕組みにしておく。

pm2.keymetrics.io

サンプル

例えばRedmineの未完了チケットの情報を

定期的にDBに保存してあとで集計したいとして、

「import_tickets.js」というファイルに

asyncなexportされたmain関数でその処理を書いたとする。

こいつを定期実行する場合、下記のような手順となる。

まず、前準備として、pm2とnode-cronをnpm installしておく。

npm install --save node-cron
npm install -g pm2

あと、Windowsで自動起動させるためにpm2のプラグイン的なものを追加する。

npm install -g pm2-windows-startup

次にnode-cron用のJSファイルを作成する。

毎時0分0秒に実行したいとすると、下記のようなコードになる。

import_tickets_cron.js

const cron = require('node-cron');

const importTickets = require('./import_tickets');

cron.schedule('0 0 * * * *', async () => {
    await importTickets.main();
    console.log('job completed');
});

node-cronだけで動かす場合は下記のように起動すれば

ちゃんとプロセスが立ち上がって毎時実行してくれる。

node import_tickets_cron.js

次にこのスクリプトをpm2で管理できるようにする。

pm2 start import_tickets_cron.js

再起動しても設定が残るよう、プロセスリストを保存する。

pm2 save

最後に、PC起動時に自動起動するように登録する。

pm2-startup install

これでOK。

Windowsのログインアカウトを求められることもなく、

バックグラウンドで毎時間処理してくれる。

おわり

予想よりも簡単に定期処理が実現できた。

SSHトンネルを立ち上げておくとか、社内ポータルサイトをチェックするとか、

色々な自動化に応用できそう。

退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング

退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング

Node.jsでテレビの映画放映情報をWebスクレイピングしてSlackに通知する

$
0
0

最近素晴らしいことに家族内でSlackを使ってやりとりするようになった。

せっかくなので何かボット的なものを作ろう、ということで

昔Clojureで作った、

「映画情報をスクレイピングして通知する」スクリプトのNode.js版を作って、

定期的にSlackに通知するようにしてみたい。

blog.honjala.net

仕様

下記のページで今後1週間のテレビ映画放映予定を掲載してくれているので、

今週TV放映予定の映画

この内容を週2回程度スクレイピングして家族用Slackのgeneralチャンネルに通知する。

あらかじめ家族用Slackに「Bots」Appを追加して、

APIトークンを取得しておき、

generalチャンネルに作成したボットをAppとして追加しておく。

このスクリプトを実行すると、下記のような投稿が追加されるイメージ。

f:id:piro_suke:20180913002614p:plain

つくる

構成としては、

「メイン部分」「映画情報スクレイピング部分」「Slack投稿部分」に分けて作成する。

まずは映画情報スクレイピング部分。

「cheerio-httpcli」ライブラリを使う。

movie_scrape.js

const httpClient = require('cheerio-httpcli');

async function fetch() {const baseUrl = 'http://cinema.pia.co.jp/title/tvlist/';
    const scheduleList = [];

    const result = await httpClient.fetch(baseUrl, 'sjis');
    const $ = result.$;
    $('tr', '#mainTvSchedule').each(function () {const row = $(this);
        const captions = $('.commonCaption', row).map(function () {return $(this).text().replace(/([\s\t\n]|&nbsp;)+/g, ''); }).get();
        scheduleList.push({
            title: $('h3 a', row).text().trim(),
            date: $('.date', row).text().trim().replace(/([\s\t\n]|&nbsp;)+/g, ''),
            start: $('.time .start', row).text().trim(),
            end: $('.time .end', row).text().trim(),
            ch: $('.star strong', row).text().trim(),
            caps: captions
        });
    });

    return scheduleList;
}function stringifyList(scheduleList) {return scheduleList.map(schedule => stringify(schedule)).join('\n\n');
}function stringify(schedule) {let text = schedule.date + " " + schedule.start + "-" + schedule.end + "(" + schedule.ch + ")\n";
    text += schedule.title + "\n";
    text += schedule.caps.join('\n');
    return text;
}

module.exports = {
    fetch,
    stringifyList,
    stringify
}

fetch関数でスクレイピングして、stringify系関数でテキストとして整形してる。

スクレイピング結果を取得するところでthisを使っているのだけど、

アロー関数がthisを束縛しないことを忘れてて全然取得できず、しばらくハマった。

thisを使う時はちゃんと「function () {}」を使おう。

続いて、Slack投稿部分。

slack.js

const request = require('request');

module.exports = {
    postMessage: async (token, channel, text) => {const options = {
            headers: {'Authorization': 'Bearer ' + token
            },
            form: {
                channel: channel,
                text: text,
                as_user: true},
            json: true};
        returnnew Promise((resolve, reject) => {
            request.post('https://slack.com/api/chat.postMessage', options, (error, res, body) => {if (error) {
                    reject(error);
                }

                resolve({
                    response: res, 
                    body: body
                });
            });
        });
    }};

postMessage関数にトークンとチャンネルと投稿内容を渡したら投稿できる。

最後にメイン部分:

index.js

const slack = require('./slack');
const movieScrape = require('./movie_scrape');

async function main() {const scheduleList = await movieScrape.fetch();
    const scheduleListText = movieScrape.stringifyList(scheduleList);
    const slackMessageText = "<映画情報>\n\n" + scheduleListText;
    
    const token = "<APIトークン>";
    await slack.postMessage(token, "#general", slackMessageText);
}

main();

スクレイピング用モジュールとSlack投稿モジュールを呼び出し。

割とシンプルにできた。

おわり

これで子どもたちがジブリ映画や細田守監督映画を見逃すこともなくなるだろう。

Slack通知系は今回作ったSlack投稿モジュールで使いまわせそう。

時をかける少女

時をかける少女


「 Javascript / Node.js で仕事自動化」記事まとめ

作成した Node.js スクリプトをサーバーレスな AWS Lambda で手軽に定期実行する

$
0
0

以前作成した映画情報取得用スクリプトのように

blog.honjala.net

定期的に実行したいスクリプトを作成した場合、

そのスクリプトをどこで動かすか、というのが悩みどころになる。

いくつか方法はあるのだけど、

ローカルのPCでタスクスケジューラなりcronなりで動かす方法は

無料だし手軽にできるけど、実行したい時間にPCを起動しておく必要がある。

さくらインターネット等のVPSを借りてcronで動かす方法は

費用はそんなに高くないけど、ファイアウォールの設定等

安心して使える環境を作るまでのハードルが結構高い。

一昔前はそこで頑張ってVPSを立てるか、諦めるかしかなかったのだけど、

今はそんな時のためのサーバーレス(=サーバ管理のいらない)なサービスが使える。

今回は AWS の AWS Lambda を使って、

作成したスクリプトを定期実行する環境を作ってみたい。

AWS Lambda とは

aws.amazon.com

AWS Lambda は上記の説明に記載があるとおり、

なにがしかのスクリプトを作って、

それを実行する条件となるトリガーを設定しておくと、

そのトリガーが発動した時にスクリプトを実行してくれるサービス。

トリガーは例えば、

「指定したテーブルのデータが更新された」「指定した日時になった」

みたいなものが多数用意されているので、そこから選択する。

HTTPリクエストをトリガーとしてJSONやHTMLを返すような

Webアプリ的な使い方もできるけど、

Webアプリのように常時立ち上がっているものではなく、

呼び出されて初めて起動し、実行される。

イベントが発生しなければ費用も発生しない。

そして、 AWS Lambda の場合は100万回/月の実行まで無料で利用可能。

つまり、月に数回から数十回定期的に実行するようなスクリプトなら

無料枠内で試せるので、自動化を始めるには非常に適したサービスになっている。

ハードルがあるとしたら、

AWSにLambda以外にもサービスがたくさんあったり、色々細かく設定できすぎて、

慣れるまで何をどうしたらよいか分からない、というところくらい。

まずは読み進める前にAWSのアカウントを取得して、

操作用のIAMアカウントを作成し、

AWSのサービス管理画面にログインするところまで頑張ろう。

AWS Lambda 関数を作成する

AWSのサービス管理画面にログインできたら、

下記の手順で関数を実行できる環境を作る。

  1. スクリプトを作成する
  2. AWS Lambda の関数作成画面でスクリプトを登録する
  3. テストする
  4. トリガーを追加する

スクリプトを作成する

AWS Lambda 用のスクリプトは、

関数作成画面で直接編集する方法と、

ローカルで作成して必要なファイルをzipでまとめてアップロードする方法がある。

標準以外のライブラリを使用する場合は後者のzipアップロードの方法を

使う必要があるので、こちらで進める。

AWS Lambda はイベントハンドラとなる関数を定義しておくことで、

トリガーが発火した時にその関数を実行してくれる仕組みになっている。

上述の「映画情報取得用スクリプト」の記事で作成した

メイン実行用ファイルは下記のように作成していたが、

const slack = require('./slack');
const movieScrape = require('./movie_scrape');

async function main() {const scheduleList = await movieScrape.fetch();
    const scheduleListText = movieScrape.stringifyList(scheduleList);
    const slackMessageText = "<映画情報>\n\n" + scheduleListText;
    
    const token = "<APIトークン>";
    await slack.postMessage(token, "#general", slackMessageText);
}

main();

AWS Lambda で実行する場合は下記のようにメイン関数をexportする形の

メイン実行用ファイルを作成する。

const slack = require('./slack');
const movieScrape = require('./movie_scrape');

exports.handler = async (event) => {const scheduleList = await movieScrape.fetch();
    const scheduleListText = movieScrape.stringifyList(scheduleList);
    const slackMessageText = "<映画情報>\n\n" + scheduleListText;
    
    const token = "<APIトークン>";
    await slack.postMessage(token, "#general", slackMessageText);

    return{
        statusCode: 200,
        body: ''};
}

このファイルを「lambda.js」として作成したとしよう。

作成したら、これを必要なファイルとともにzipファイルにまとめる。

必要なのは、自作のjsファイルとnode_modulesフォルダ。

Macなら下記のようなコマンドで「fetch_tv_movies.zip」というファイルを作成できる。

zip -r fetch_tv_movies.zip lambda.js slack.js movie_scrape.js node_modules

うまく実行できなかったら何度もzip化することになるので、

シェルスクリプトにでもまとめておく。

zipファイル名は適当で良いけどlambda関数名と合わせておくのが良いかも。

AWS Lambda の関数作成画面でスクリプトを登録する

AWS Lambda 関数を登録する準備が整ったので、

AWS マネジメントコンソールにIAMアカウントでログインして登録処理を行う。

ログインして Lambda メニューを選択し、

f:id:piro_suke:20180924223058p:plain

Lambda の管理画面に遷移して「関数の作成」ボタンをクリックすると

関数の作成画面に遷移する。

そこで下記のように好きな関数名をつけて、ランタイムに「Node.js 8.10」を選択して、

f:id:piro_suke:20180924223941p:plain

実行ロールについて今回は特にログ保存以外のAWSサービスを使用しないので

「テンプレートから新しいロールを作成」を選択し、

「lambda_basic」みたいな名前をつけて

関数を作成する。

f:id:piro_suke:20180924224144p:plain

関数の作成に成功すると、トリガーやスクリプトの登録ができる設定画面に遷移する。

とりあえず必要なのが関数コードの登録。

f:id:piro_suke:20180924224352p:plain

ここでzipファイルをアップロードする。

その際、ハンドラには先程作成した

メイン実行用ファイル名(lambda.jsのlambda)と

ハンドラ名(exports.handlerのhandler)を組み合わせて

「lambda.handler」を設定しておく。

今回のスクリプトはWebスクレイピングとAPI接続を伴うので、

「基本設定」でタイムアウトを「30秒」に延ばしておく。

f:id:piro_suke:20180924224821p:plain

テストする

トリガーを追加する前にアップロードしたスクリプトがちゃんと実行できるか、

テストしておこう。

テストは「テストイベント」設定でスクリプトに渡すパラメータを指定しておき、

それを実行する形で行う。

画面右上の「テスト」メニューでテストイベントの設定ができる。

今回のスクリプトはパラメータを受け取らないので、

「NoParamTest」みたいな名前で「{}」を保存しておけばOK。

作成したテストイベントはプルダウン選択できるようになるので、

「NoParamTest」を選択して「テスト」ボタンを実行すると関数が実行される。

それで成功メッセージが表示されてSlackに投稿が行われたら問題なし。

f:id:piro_suke:20180924225443p:plain

トリガーを追加する

最後に、この関数を実行するためのトリガーを登録する。

ひとまず、「指定時実行」と「URLアクセス時の実行」の

2つの方法を押さえておけば大抵事足りると思うので、この2つの方法について書いておく。

トリガーの登録は編集画面上部の「トリガーの追加」ブロックで行う。

「指定時実行」する場合は「CloudWatch Events」を選択する。

選択すると下に編集フォームが表示されるので、ここで細かい設定を行う。

例えば「毎週月曜0時0分」に実行したい場合、下記のように設定する。

ルール名は分かりやすい名前をつけておく。

f:id:piro_suke:20180924230409p:plain

時間はUTCなので実際に必要な時間の9時間前にセットしておく。

月曜0時なら日曜15時かな。

f:id:piro_suke:20180924230508p:plain

スケジュール式の設定方法は下記のヘルプページに記載されている。

ルールのスケジュール式 - Amazon CloudWatch Events

これで追加しておけばOK。

次に「URLアクセス時に実行」する場合は「API Gateway」を使用する。

選択すると下記のような編集フォームが表示される。

f:id:piro_suke:20180924230835p:plain

API項目で「新規APIの作成」を選択するとセキュリティ項目が表示され、

APIへのアクセスにどのような認証を必要とするかを選択できる。

「AWSIAM」はIAM認証だと思うけど試してない。

「オープン」は認証なし。APIのURLにアクセスするだけで実行できる。

「APIキー使用でのオープン」は正しいAPIキー付きでアクセスした場合だけ実行できる。

今回は「APIキー使用でのオープン」を選択するとする。

アクセス用のURLとAPIキーは追加時に生成される。

APIキー付きのアクセスはリクエスト時のヘッダーに

「x-api-key」という名称でAPIキーをつけてアクセスする。

curlなら下記のような形

curl https://<APIのURL> --header 'x-api-key:<APIキー>'

これで実行してSlackに投稿されたら登録に成功してる。

これでトリガー登録完了で、Lambda関数の登録完了。

おわり

説明は長くなったけど、1つ作ってしまえば他のスクリプトの登録も

そんなに難しくないと思う。

データベースにデータを保存したり、ファイルを入出力する場合は

追加の設定が必要だけど、ちょっとしたデータ収集、変換と通知なら対応できるはず。

AWS Lambda実践ガイド

AWS Lambda実践ガイド

Node.jsでSQLServerにWindows認証接続する

$
0
0

苦労したのでメモ。

Node.jsでのデータベース操作にはいつもknexを使用していて、

Knex.js - A SQL Query Builder for Javascript

knexはSQLServerにも対応しているのだけど、

そこで使用されているmssqlパッケージがWindows認証には対応していないのか、

うまく接続できなかった。

www.npmjs.com

このmssqlパッケージで使用されるドライバには

デフォルトで使用される「Tedious」と

Node.jsの0.12以上で使用可能な「msnodesqlv8」の

2種類があるようで、「Tedious」だとうまく接続できないっぽい。

knexでどうドライバを切り替えるのかが分からなかったのだけど、

単体で「msnodesqlv8」を使用するとうまく接続できた。

www.npmjs.com

使用方法

下記のWikiにドキュメントが記載されている。

github.com

Promise使ってデータ取得するなら下記のような感じ。

const msnodesql = require("msnodesqlv8");

async function fetchDataList(conn, query) {returnnew Promise((resolve, reject) => {
        conn.query(query, (err, rows) => {if (err !== null) {
                reject(err);
                return;
            }

            resolve(rows);
        });
    });
}

async function main() {const connectionString = "server=<接続先サーバ>\\<インスタンス名>;Database=<DB名>;Trusted_Connection=Yes;Driver={SQL Server Native Client 11.0}";
    msnodesql.open(connectionString, async (err, conn) => {const resultList = await fetchDataList(conn, 'select * from dbo.テーブル名');
    }}

main();

更新処理とかは試してないけど、上記Wikiに記載されてるSQLを参考に

作成すれば実行できるはず。

現場で通用する力を身につける Node.jsの教科書

現場で通用する力を身につける Node.jsの教科書

マインクラフトBE (Win10版)をNode.jsで自動操作する

$
0
0

子どもたちがマインクラフト(iOS版)にはまっており、一緒に遊んだりしている。

クリエイティブモードで一緒にわーわー言いながら街や建物を作るのはなかなか楽しい。

楽しいのだけど、

ちまちまブロックを置いていくのはそろそろ飽きてきたので、

ビャーっとブロックを配置したり、

建物を自動生成したり、

自動化してもっと面白いことができないものかと調べてみた。

調べたところ、

自動化できる仕組みがいくつか用意されていた。

functionコマンド

最初に見つけたのが、「function」コマンド。

これはファイルに書いたコマンドのリストを実行するもので、

まさに求めていたものだったのだけど、

残念ながらJava版のみでWindows版は非対応だった。

napoan.com

iOS版の Pocket Edition (PE) と一緒に遊ぶには、

Windows版でないといけないらしい。

MakeCode for Minecraft

次に見つけたのが「MakeCode for Minecraft」。

minecraft.makecode.com

Microsoft製で、

Scratchのように処理を組み合わせてコマンドを作ったり、

Javascriptでコマンドを作ったりできる。

子供にプログラミングに触れてもらうこともできて最高なツール。

MakeCodeを起動して、マイクラ側からWebSocketでconnectして、

MakeCode側でイベントや発言に対して実行する処理を定義したら、

そのイベントをマイクラ側で実行するだけで処理を実行できる。

Windows10版でも使えるし、大抵の環境はこれで自動化できるはず...

なのだけど

うちのPCが非力すぎるのか、Mac上の仮想Windows環境だからなのか、

やたら負荷がかかって、コマンドを実行するとすぐ落ちる。

残念。これは使いたかった。PCを買い替えたら再挑戦したい。

自前WebSocketサーバーを立てる

それで諦めかけていたのだけど、

MakeCodeとの接続に使用しているconnectコマンドは

WebSocketサーバに接続するためのコマンドのようなので、

MakeCodeの代わりに自前でWebSocketサーバーを立ててマイクラとやりとりしたら

コマンドも流せるんじゃない?ということで試してみた。

WebSocketは基本的にサーバ側とクライアント側でイベント名を決めておき、

そのイベント名を使ってデータのやりとりをしているはずなので、

まずはMakeCodeにWebSocketクライアントとして接続してみたり、

WebSocketサーバにマイクラから接続してみたり、

同じようにマイクラとWebSocket通信してる事例がないかググってみたりした。

どうやらMakeCodeはJSONデータをマイクラとやりとりしており、

あらかじめ受け取りたいイベントを登録(subscribe)するリクエストを

マインクラフトに投げることでマイクラがそのイベントが発生した時に

メッセージイベントを投げてくれるらしい。

また、マイクラに対してコマンドリクエスト(commandRequst)を投げることで

マイクラ側でコマンドを実行してくれるらしい。

で、マイクラ側からはJSONデータでコマンドの成否や結果情報がレスポンスとして返される。

下記のページが参考になった。

https://www.reddit.com/r/MCPE/comments/5ta719/mcpewin10_global_chat_using_websockets/

EventSubscribe.js · GitHub

MCPE/Win10 WebSocket JSON Messages · GitHub

これで、

  1. イベント発生時のサーバへの送信設定(subscribe)
  2. コマンドの実行(commandRequest)
  3. レスポンスの取得

がJSON形式でやり取りできることが分かった。

受け取れるイベントの種類は上記の参考ページに記載されている

「MCPE & W10 Event Names」で確認できる。

コマンドは、マイクラで実行できるコマンドをそのまま書けばいいみたい。

手順としては、WebSocketサーバを立てておき、

マインクラフト側で「/connect」コマンドで対象サーバに接続する。

接続後、受け取りたいイベントをsubscribeしておき、

イベント処理としてイベント発生時に実行したいコマンドを定義しておく。

試しに、Node.jsで

「build」と発言したら自分のいる位置にブロックを配置する

WebSocketサーバを書いてみた。

const WebSocket = require('ws');
const app = require('express')();
const server = require('http').Server(app);
const uuid = require('uuid/v4');

// setblockコマンドリクエスト用JSON文字列を生成する関数function setBlockCommand(x, y, z, blockType) {return JSON.stringify({"body": {"origin": {"type": "player"},
            "commandLine": util.format("setblock %s %s %s %s", x, y, z, blockType),
            "version": 1
        },
        "header": {"requestId": uuid(),
            "messagePurpose": "commandRequest",
            "version": 1,
            "messageType": "commandRequest"}});
}// ユーザー発言時のイベント登録用JSON文字列を生成する関数function subscribePlayerChatEventCommand() {return JSON.stringify({"body": {"eventName": "PlayerMessage"//"eventName": "PlayerChat"},
        "header": {"requestId": uuid(), // UUID"messagePurpose": "subscribe",
            "version": 1,
            "messageType": "commandRequest"}});
}const wss = new WebSocket.Server({server});

// マイクラ側からの接続時に呼び出される関数
wss.on('connection', socket => {
    console.log('user connected');

    // ユーザー発言時のイベントをsubscribe
    socket.send(subscribePlayerChatEventCommand());

    // 各種イベント発生時に呼ばれる関数
    socket.on('message', packet => {
        console.log('Message Event');
        console.log(packet);
        const res = JSON.parse(packet);

        // ユーザーが「build」と発言した場合だけ実行if (res.header.messagePurpose === 'event'&& res.body.properties.Sender !== '外部') {if (res.body.eventName === 'PlayerMessage'&& res.body.properties.Message.startsWith('build')) {
                console.log('start build');

                // 石ブロックを配置するリクエストを送信
                socket.send(setBlockCommand('~0', '~0', '~0', 'stonebrick'));
            }}});

});

server.listen(3000, () => {
    console.log('listening on *:3000');
});

マイクラ側で

/connect <WebSocketサーバが動いているPCのIPアドレス>:3000

と実行するとサーバ側で「user connected」と表示されるはず。

その後、マイクラ側で「build」と発言したらその位置にブロックが配置されるはず。

ここまでできたら、

様々なイベントをsubscribeして移動した後ろにブロックを配置していったり、

ブロック生成処理をループ処理して建物を作る、みたいなこともできる。

おわり

これまでWebSocketを使う機会がなかったので、良い勉強になった。

次は元々の目的だった、建物自動生成に挑戦したい。

Minecraft (PC/Mac 版)

Minecraft (PC/Mac 版)

Minecraft (マインクラフト) - Switch

Minecraft (マインクラフト) - Switch

さらなる自動化のために、作成したツールをGUI化する

$
0
0

作業を自動化するためのツールをGUI化するメリットについて考えてみた。

後半でNode.jsでのGUI化の方法についても紹介する。

GUI化するとメインの処理とは別で画面の作成や入力処理を実装する必要があり、

単純に開発時の作業が増えるのだけど、CLIと比較してそれだけのメリットがあるのか?

まず思いつくことで、一番重要なのは

GUI化することで作成したツールを

プログラマ以外の人が抵抗なく使えるようになるので、

対象ユーザーがグンと増える、ということ。

自動化ツールを必要としているのはむしろプログラマ以外の人たちであると考えると、

CLIスクリプトをソースと一緒に提供して頑張って勉強してもらうよりも、

GUIアプリを提供してそれを使ってもらう方が当然使ってもらいやすい。

長くプログラマをやってると忘れちゃうけど、

コマンドプロンプトを開いてコマンドを実行するのは、

プログラマ以外にとってはとてもハードルが高い。

もう1つ、CLIツールよりもGUIの方が良いな、と思うのは

「入力パラメータの保存と選択がしやすくなる」、ということ。

例えばリモートのLinux環境やDBを操作するような自動化ツールの場合、

色々な接続先に対して使うので、

接続先アドレスやユーザー名を保存しておき、一覧から選べると使いやすい。

GUI化するとメニュー等でメイン機能以外のサブ機能を持たせやすくなるので、

こういった入力の簡略化がしやすい。

あと出力についても、テキストだけでなく

グラフ等テキスト以外の形式で見せることができるので、

データの内容に適した分かりやすい出力ができる。

Node.jsで作成したスクリプトのGUI化方法

今まで書いてきた記事を見返すと、

どの自動化ツールもGUI化すればより多くの人にメリットを提供できそうなので、

今後は自動化ツールを提供する時は

できる限りGUI化したものを提供するように心がけたい。

GUI化の形としては

  • Webアプリ
  • デスクトップアプリ
  • スマホアプリ

があるけど、自動化ツールを作るならまずはデスクトップアプリだろう。

Javascript/Node.jsでデスクトップアプリを作る方法としては、

「Electron」で作るのがおそらく実績として一番多いんじゃないかと思うのだけど、

ちょうど数日前にGoogleが「carlo」という、

Node.jsアプリをより手軽にGUI化できるライブラリをリリースしたようなので、

今後はこちらも選択肢になりそう。

github.com

まだ試してないので、なにをどこまでできるのか近いうちに検証してみたい。

UIフレームワークを選択する

どういう形でGUI化を行うのであれ、

GUI化する場合はレイアウトとかコンポーネントのデザインを考慮する必要がある。

Node.jsベースのアプリをGUI化する場合、

WebアプリだろうがElectronだろうがcarloだろうが、

UIはHTML、CSS、Javascriptで作成するので、

ハイブリッドに使えるUIフレームワークを1つ習得しておくと使い回しが効いて良い。

大抵はAngular/React/Vueのいずれかのフレームワークと、

それに対応したUIフレームワークを組み合わせて使う感じだと思う。

私は今のところ「Vue.js + VuetifyJS」がお気に入り。

vuetifyjs.com

VuetifyJSはアプリ風レイアウトができること、

画面サイズによってレスポンシブにレイアウトを変更できること、

コンポーネントが十分に揃っていることが気に入ったポイント。

スマホ用WebアプリとElectronアプリを作ってみたけど、

どちらもアプリの形式に関わらずサクッと書けた。

他にもBulmaとかOnsenUIとか、色々フレームワークがあるので、

試してしっくりくるものを選ぶのが良いと思う。

bulma.io

onsen.io

おわり

GUI化に慣れていないうちは、

考えることが多くて「めんどくさ...」となることが多いけど、

何個か作ってるうちに慣れるはずなので頑張ろう。

Electronではじめるアプリ開発 ~JavaScript/HTML/CSSでデスクトップアプリを作ろう

Electronではじめるアプリ開発 ~JavaScript/HTML/CSSでデスクトップアプリを作ろう

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

Viewing all 199 articles
Browse latest View live