Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8820 articles
Browse latest View live

Vue-CliとPythonの連携

$
0
0

1.目的

・Vue-CliとPythonの連携を図りたい。(Vue-CLiを使ったアプリ上でPythonを動作させたい)

2.方法

・フロントエンドはVue-Cliで構成する。Pythonコードはバックエンドに配置する。
・Vue-CliとPythonとのインターフェースはPython-shellを使う。
・フロントエンドとバックエンドはそれぞれ別のポートを割り当て、双方が通信できるようにする。

4.JPG

3.環境設定/準備

使用したパッケージやライブラリのインストール方法やversionを下記に記載しておきます。

項目versionインストール方法
Vue-Cli3.9.3LINK先を参照
python-shell1.0.8LINK先を参照
express4.17.1
bodyparser説明略
node.js12.14.0説明略
npm6.3.14説明略

4.作成したアプリ

Webページより、入力した値に+3を加算し、その演算結果を表示します。

5.JPG

★上記図中の(1)~(6)は、下記に説明する動作(1)~(6)に対応しています。

(1)「デモ画面」数値をテキストボックスに入力します。
(2) 取得ボタンをクリックします。
(3) (1)で入力した数値がindex.js(Python-shellを実行する)を介して、Pythonコード(sample.py)に送られます。
(4) sample.pyは、入力された数値に+3を加算します。
(5) sample.pyは、演算を完了した数値をindex.jsに返却します。
(6) index.jsは、sample.pyから受け取った数値を「デモ画面」に返します。
(7)「デモ画面」は、演算結果を表示します。

4-1 システム構成

Vue-Cliはすでにインストールされ、使用できる状態であるという前提です。

4-2 ディレクトリ構成

各ファイルのディレクトリ構造は下記のとおり配置されています。

6.JPG

4-3 コード

/src/component/main.js
// eslint-disable-next-line/* eslint-disable */// The Vue build version to load with the `import` command// (runtime-only or standalone) has been set in webpack.base.conf with an alias.importVuefrom'vue'importAppfrom'./App'importrouterfrom'./router'importaxiosfrom'axios'//axiosを使う場合、main.jsでimportする。Vue.config.productionTip=falseVue.prototype.$axios=axios//axiosを使う場合、main.jsでこの行の追記が必要/* eslint-disable no-new */newVue({el:'#app',router,template:'<App/>',components:{App}})
/src/components/Top.vue
<template><div><h1>デモ画面</h1><inputtype="button"value="移動"@click="goNewTask()"><br><inputtype="number"v-model="message"><inputtype="button"value="取得"@click="getdata()"><p><fontsize="2">入力データ :{{$data.message}}</font></p><p><fontsize="2">出力データ :{{$data.result}}</font></p><p><fontsize="2">状態 :{{$data.state}}</font></p></div></template><script>// eslint-disable-next-line/* eslint-disable */import*asd3from'd3'//有効にするexportdefault{name:'top',data:function(){return{message:'',//入力データを格納する変数。result:'',//演算結果を格納する変数。state:"wait"//現在の状況を格納する変数。}},methods:{//テキストボックスに入力されたデータをバックエンドに送り、バックエンドから演算結果を受け取り、その結果を表示するメソッドgetdata:function(){this.state="getting data"this.$axios.get('http://192.168.0.4:3000/api',{params:{dat:this.message}}).then(function(response){console.log(response.data.message)//バックエンドから返却された演算結果をconsole.logしている。this.result=response.data.messagethis.state="done"}.bind(this))//Promise処理を行う場合は.bind(this)が必要.catch(function(error){//バックエンドからエラーが返却された場合に行う処理についてthis.state="ERROR"}.bind(this)).finally(function(){}.bind(this))}}}</script>
/bkend/index.js
constexpress=require('express')constbodyParser=require('body-parser')constapp=express()app.use(bodyParser.json())//CORSポリシーを無効にしている。app.use(function(req,res,next){res.header("Access-Control-Allow-Origin","*");res.header("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type, Accept");next();});app.get('/api',function(req,res){var{PythonShell}=require('python-shell');varpyshell=newPythonShell('sample.py');console.log("req")console.log(req.query.dat)//フロントエンドから受け取ったデータをconsole.logしている。pyshell.send(req.query.dat);//本コードからpythonコードに'req.query.dat'を入力データとして提供する //pythonコード実施後にpythonから本コードにデータが引き渡される。pyshell.on('message',function(data){console.log("return data")res.send({message:data//pythonで実施した演算結果をフロントエンドに返している。})})})app.listen(3000)
/bkend/sample.py
importsysdata=sys.stdin.readline()#標準入力からデータを取得する
num=int(data)defsum(a):returna+3print(sum(num))#printの内容をpython-shellに返却する

4-4 実行方法

5.ポイント

5-1 Pyshon-shellを使用した理由

Vue-cliはwebpack上で動作していますが、python-shellは、webpack上では動作しないようです。よって、新たにvue-cliで動作させていないサーバーを立ち上げその上でpython-shellを動作させるようにしました。

5-2 CORS問題の回避方法

xxx.xxx.xx.xx:91 から xxx.xxx.xx.xx:3000にアクセスする場合、ドメインが異なるのでセキュリティー上通信がブロックされます[CORS問題]。CORS問題を避けるために、アクセスする側に対策を施す場合と、アクセスされる側に対策を施す場合があるが、本件ではアクセスされる側に対策を施しました。詳しくはこちらをご覧ください。

/bkend/index.jsの一部
/bkend///CORSポリシーを無効にしている。app.use(function(req,res,next){res.header("Access-Control-Allow-Origin","*");res.header("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type, Accept");next();});

【Node.js】楽天での消耗品の購入を自動化した

$
0
0

はじめに

私は普段、洗剤やトイレットペーパーなどの日常の消耗品の買い物は楽天で購入している。
適当なタイミングで家にある在庫を確認し、不足している物をまとめて注文するというスタイルだが、「家じゅうを見て回りつつ足りないものをメモする」→「PCに向かってメモを見ながら楽天で商品を検索して購入する」の作業が面倒くさかったので、一部を自動化した。

使うもの:

  • Node.js v10.16.0
    • puppeteer-core v2.0.0
    • googleapis v46.0.0
  • Google Sheets API v4
  • Visual Studio Code

前提条件:

  • 楽天市場に会員登録している

買い物リストを用意する

まずはじめに、Googleスプレッドシートに、以下のような買い物リストを作成する。

一般的に買い物リストと言えば、

  1. 洗剤が欲しくなったら「洗剤」とリストに書く
  2. 買ったら:white_check_mark:を付けて完了にする

という流れだが、このリストでは以下のような使い方をする:

  1. あらかじめ「洗剤」や「柔軟剤」といった買うものリストを作っておく
  2. 洗剤が欲しくなったら「洗剤」に:white_check_mark:を付ける
  3. :white_check_mark:が付いたものを購入する
  4. 買ったら:white_check_mark:を外す

なぜこのような流れにしたかというと、消耗品は定期的に買うものなので、洗剤が無くなる度に「洗剤」と書くのでは同じ作業の繰り返しに感じる。それに、消耗品として買うべきものの顔ぶれもあまり変化しない(よっぽど生活様式が変化しない限り、「洗濯洗剤」を買わなくなることはないと思う)。
そのため、あらかじめすべての買い物リストを作成しておいて、そのとき買いたいものだけを選択する形式にした。
ちなみに、買い物リストの時点では、まだ「洗濯洗剤」として何を買うかまでは書いていない。

参考情報として、Googleスプレッドシートでは、セルをチェックボックスにすることができる。目的のセルを選択した状態で、メニューの「挿入」→「チェックボックス」を選択すればOK。

こうするとセルにチェックボックスが表示され、チェックを入れたり外したりすると、値がTRUEまたはFALSEへと変化するようになる。

お気に入りリストを用意する

つぎに、楽天市場のお気に入りリストを作っておく。

例えば買い物リストに「柔軟剤」があるなら、実際に買いたい柔軟剤を探してお気に入りに登録しておく。そして、お気に入り商品にはメモが記入できるため、そこに「柔軟剤」と書いておく。この「柔軟剤」という文字が、買い物リストの「柔軟剤」とを結びつけるキーにする。

ついでに、メモ欄には一度に買う数量も指定できるようにしておいた。この場合はJSON形式でメモ欄に書いておく。

買い物リストを元に商品をカートに入れる(自動化したところ)

これで、「今何が欲しいのか」と「どの商品を買うのか」がそろった。この2つをインプットとして、ブラウザを操作してカートに入れる処理を自動化した。
Node.jsで、Sheets APIとpuppeteerを使用している。以下がメイン部分のスクリプトになる。
(細かい操作は別ファイルに記述したが、すべて載せると長いので割愛。ソースはGitHub参照。)

index.js
constpuppeteer=require("puppeteer-core");constconfig=require("config");constrakuten=require("./modules/rakuten.js");constspreadsheet=require("./modules/spreadsheet.js");(async()=>{// キャッチされなかったPromiseのエラー詳細を出してくれるprocess.on("unhandledRejection",console.dir);letLAUNCH_OPTION={headless:false,executablePath:"C:/Program Files (x86)/Google/Chrome/Application/chrome.exe"};constbrowser=awaitpuppeteer.launch(LAUNCH_OPTION);try{// スプレッドシートから買い物リストを取得awaitspreadsheet.init();lettodos=awaitspreadsheet.getTodo();// ブラウザを起動してログイン&お気に入りリスト取得constpage=awaitbrowser.newPage();awaitrakuten.login(page);letbookmarks=awaitrakuten.getBookmarks(page);console.log("=== bookmarks ===")console.log(bookmarks);// 買い物リストのループfor(lettodoIndex=0;todoIndex<todos.values.length;todoIndex++){lettodo=todos.values[todoIndex];if(todo[0]!="TRUE"){continue;}// 商品リストを探すletfound=searchBookmark(todo[1],bookmarks);if(found!=null){letsucceed=awaitrakuten.addCart(page,found);if(succeed){// 完了にするawaitspreadsheet.updateTodo(todoIndex);}}}}finally{browser.close();}})();/**
 * お気に入りリストから、指定したキーを持つものを探す
 * @param {string} key 
 * @param {{key:string, units:number, url:string}} bookmarks 
 */functionsearchBookmark(key,bookmarks){for(letbookmarkofbookmarks){if(key==bookmark.key){returnbookmark;}}returnnull;}

puppeteerでChromeを操作する場合、普段使用するChromeとCookieは共有していない(と思う)。しかし、楽天市場に会員としてログインした状態でカートに追加したものは、会員の情報として保存されているようなので、スクリプトの動作が終わってもカートに追加した内容が保存されている。そのため、スクリプトでは最初にログインしカートに入れる作業だけを行った後、ブラウザを閉じている。

決済は手作業で

スクリプトを実行した後、手作業で楽天市場へログインしてカートの中身を確認する。買いたいものが正しくカートに追加されているかどうかの確認や、数量の調整、ほかに買いたいものの追加などをしてから、自分で決済する。決済まで自動化してしまうと色々問題が起きそうなので、していない。

おわりに

以上、消耗品購入の自動化した話はこれで終了。
巷で見かけるIoTや各種サービス等を使った自動化と比べるとだいぶショボいが、このくらいが自分の生活様式に合っていると感じる。

【Catalina】nodebrewからnode, yarnをインストール

$
0
0

はじめに

node.jsのバージョン管理とyarnのインストールについて
色々な所からインストールしていたら、brew doctorから怒られてしまいました。。。
Catalinaにアップデートしたら、読み込めないし、、、

$ node -v
zsh: node: command not found

PATHを通すと node -v, npm -v が使えるようですが

せっかくなので、一元管理するため
一度、全てアンインストールしてから再インストールしました。
nodebrewでの管理が上手くいったので、自分用メモとして残しておきます。

nodeの管理はnodebrewで行い、npmからyarnをインストールしました。
※公式では非推奨らしいですが、、、
https://yarnpkg.com/ja/docs/install#alternatives-stable

node.jsとyarnの削除

今まで入れていたnodeとyarnを全て削除します。

(例)Homebrewで入れた場合
$ brew uninstall yarn
$ brew uninstall node
$ rm -rf /usr/local/lib/node_modules
$ brew cleanup
念のため
$ brew doctor

アンインストール後にPATHが変更されているので .bash_profile の再読み込み。

$ source ~/.bash_profile 

nodebrew のインストール

Homebrew から nodebrew をインストール
$ brew install nodebrew
バージョンを確認
$ nodebrew -v

実行結果

nodebrew 1.0.1

Usage:
    nodebrew help                         Show this message
    nodebrew install <version>            Download and install <version> (from binary)
    nodebrew compile <version>            Download and install <version> (from source)
    nodebrew install-binary <version>     Alias of `install` (For backword compatibility)
    nodebrew uninstall <version>          Uninstall <version>
    nodebrew use <version>                Use <version>
    nodebrew list                         List installed versions
    nodebrew ls                           Alias for `list`
    nodebrew ls-remote                    List remote versions
    nodebrew ls-all                       List remote and installed versions
    nodebrew alias <key> <value>          Set alias
    nodebrew unalias <key>                Remove alias
    nodebrew clean <version> | all        Remove source file
    nodebrew selfupdate                   Update nodebrew
    nodebrew migrate-package <version>    Install global NPM packages contained in <version> to current version
    nodebrew exec <version> -- <command>  Execute <command> using specified <version>

Example:
    # install
    nodebrew install v8.9.4

    # use a specific version number
    nodebrew use v8.9.4

nodebrew の PATH を通す

.bash_profile から .zprofile へお引っ越し
$ cat .bash_profile >> .zprofile

.zprofile にPATHを追加
$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile

.zprofile を再読み込み。
$ source ~/.zprofile

安定版の Node.js をインストール

$ nodebrew install-binary stable

ディレクトリが見つからない場合は、作成

$ mkdir -p ~/.nodebrew/src
$ nodebrew install-binary stable

Node.jsを設定します。

利用可能バージョンを確認
$ nodebrew list

current: noneなので、バージョンを指定
$ nodebrew use v12.14.0

Node.js のバージョンを確認
$ node -v

yarnをインストール

$ npm install -g yarn
$ yarn -v
バージョンが表示されれば OK

nodeを終了する方法(コマンド)4選

$
0
0

nodeを終了する方法4選

① .exit

.exit と打つことで終了出来ます。

[name]MacBook-Pro:[filename] [name]$ node
Welcome to Node.js v13.5.0.
Type ".help" for more information.
> .exit
[name]MacBook-Pro:[filename] [name]$ 

② Ctrl + d

Ctrlを押しながらDを入力することで終了出来ます。
こちらが一番簡単ですが、2回Ctrl + Dを入力してしまうとターミナル自身が終了してしまうので注意が必要です。

[name]MacBook-Pro:[filename] [name]$ node
Welcome to Node.js v13.5.0.
Type ".help" for more information.
>  (ここで Ctrl + dを入力)
[name]MacBook-Pro:[filename] [name]$ 

③ Ctrl + c を2回入力

[name]MacBook-Pro:[filename] [name]$ node
Welcome to Node.js v13.5.0.
Type ".help" for more information.
>  (ここで Ctrl + cを入力)
(To exit, press ^C again or ^D or type .exit)
>   (ここで Ctrl + cを入力)
[name]MacBook-Pro:[filename] [name]$ 

④ Ctrl + c の次に Ctrl + d

表示は、③と同じ表示になります。
②と同様に、二回Ctrl + dを入力してしまうとターミナル自身が終了してしまうので注意が必要です。

[name]MacBook-Pro:[filename] [name]$ node
Welcome to Node.js v13.5.0.
Type ".help" for more information.
>  (ここで Ctrl + cを入力)
(To exit, press ^C again or ^D or type .exit)
>  (ここで Ctrl + dを入力)
[name]MacBook-Pro:[filename] [name]$ 

最後に

ターミナル終了の可能性がない、①か②をおすすめします。

async/await完全に理解した(JavaScript)

$
0
0

きっかけ

最近Node.jsを触る機会があるが、言語を体系的に勉強してきたわけではないので、必要な機能から調べながら使っている。
あるとき一定時間待ってから次の処理を行うシーケンス的なことをしたいなと思って調べたところ、やりたいことは「非同期処理」と言われるようだ。次のようなサンプルコードが出てきた。

コールバックを使う方法
constt0=Date.now();// 開始時刻を取得console.log('開始: '+(Date.now()-t0));// 経過時刻をあわせて出力setTimeout(function(){console.log('1秒後に実行されるはず: '+(Date.now()-t0));// 経過時刻をあわせて出力},1000);
出力
開始: 0
1秒後に実行される: 1004

あーそーゆーことね完全に理解した(画像は脳内補完願います)
よし、ちゃんと1秒後に実行された。
じゃあ処理を2段階にしてみよう。

コールバックを使う方法2
constt0=Date.now();console.log('開始: '+(Date.now()-t0));setTimeout(function(){console.log('1秒後に実行されるはず: '+(Date.now()-t0));setTimeout(function(){console.log('2秒後に実行されるはず: '+(Date.now()-t0));},1000);},1000);
出力
開始: 0
1秒後に実行されるはず: 1003
2秒後に実行されるはず: 2004

あーそーゆーことね完全に理解した(画像は脳内補完願います)
じゃあ10個に増やしてみよう。

コールバックを使う方法4
constt0=Date.now();console.log('開始: '+(Date.now()-t0));setTimeout(function(){console.log('1秒後に実行されるはず: '+(Date.now()-t0));setTimeout(function(){console.log('2秒後に実行されるはず: '+(Date.now()-t0));setTimeout(function(){console.log('3秒後に実行されるはず: '+(Date.now()-t0));setTimeout(function(){console.log('4秒後に実行されるはず: '+(Date.now()-t0));setTimeout(function(){console.log('5秒後に実行されるはず: '+(Date.now()-t0));setTimeout(function(){console.log('6秒後に実行されるはず: '+(Date.now()-t0));setTimeout(function(){console.log('7秒後に実行されるはず: '+(Date.now()-t0));setTimeout(function(){console.log('8秒後に実行されるはず: '+(Date.now()-t0));setTimeout(function(){console.log('9秒後に実行されるはず: '+(Date.now()-t0));setTimeout(function(){console.log('10秒後に実行されるはず: '+(Date.now()-t0));},1000);},1000);},1000);},1000);},1000);},1000);},1000);},1000);},1000);},1000);

いやよく見たらクソむかつく(画像は脳内補完願います)

なんだこのネストの嵐は。
調べてみると、これはJavaScript界隈で言われているコールバック地獄というやつらしい。
更に調べてみると、イマドキのJavaScriptにはコールバック地獄を解消する方法が用意されているということがわかった。ですよねー。

Promise と async/await

コールバック地獄を解消する方法は2種類ほどあるらしい。
1. Promiseを使う方法
2. async/awaitを使う方法
後者のasync/awaitの方が新しくてイマドキらしい。ということで前者のPromiseは飛ばしても大丈夫かな。
というわけで、async/awaitの概要を調べてみた。

引用:async/awaitを使ったモダンな非同期処理 - Qiita

リピートミー。「async/awaitはPromiseで作られている」。

!!??

async関数はPromiseを返します。
この関数を呼び出すときにawaitを付けると、このコードはPromiseがresolvedかrejectedを返すまで停止します。

async/awaitを完全に理解するには、結局Promiseを理解しないといけないらしい

async/awaitの完全理解

この辺をじっくり読んでいろいろコードを書いて試した。

コードと解説

先程の10回待つ処理をasync/awaitを使って書くとこうなる。

async/awaitを使ったコード
constt0=Date.now();console.log('開始: '+(Date.now()-t0));asyncCall();functionwait1sec(){returnnewPromise(resolve=>{setTimeout(()=>{resolve();},1000);});}asyncfunctionasyncCall(){awaitwait1sec();console.log('1秒後に実行されるはず: '+(Date.now()-t0));awaitwait1sec();console.log('2秒後に実行されるはず: '+(Date.now()-t0));awaitwait1sec();console.log('3秒後に実行されるはず: '+(Date.now()-t0));awaitwait1sec();console.log('4秒後に実行されるはず: '+(Date.now()-t0));awaitwait1sec();console.log('5秒後に実行されるはず: '+(Date.now()-t0));awaitwait1sec();console.log('6秒後に実行されるはず: '+(Date.now()-t0));awaitwait1sec();console.log('7秒後に実行されるはず: '+(Date.now()-t0));awaitwait1sec();console.log('8秒後に実行されるはず: '+(Date.now()-t0));awaitwait1sec();console.log('9秒後に実行されるはず: '+(Date.now()-t0));awaitwait1sec();console.log('10秒後に実行されるはず: '+(Date.now()-t0));}
出力
1秒後に実行されるはず: 1009
2秒後に実行されるはず: 2013
3秒後に実行されるはず: 3015
4秒後に実行されるはず: 4019
5秒後に実行されるはず: 5022
6秒後に実行されるはず: 6028
7秒後に実行されるはず: 7029
8秒後に実行されるはず: 8036
9秒後に実行されるはず: 9042
10秒後に実行されるはず: 10046

おまけ

asyncは「エイシンク」と読むらしい。「アシンク」だと思ってた。
awaitは「アウェイト」で良いらしい。

やらかしJS先生がみた関数パラメータの闇({name, value = 0}) => console.log({name, value})

$
0
0

Overview

過去にバグになってしまったものを忘れないよう書き留めておくシリーズです。
今回の題材は関数パラメータのデフォルト引数です。

constprint=({name,value=1})=>console.log({name,value});print({name:"value is null",value:null});print({name:"value is undefined"});

Target reader

  • この結果がわからない方

Prerequisite

  • JavaScriptを一通り理解している

Body

答え合わせ

正解はこうなる。

>Object{name:"value is null",value:null}>Object{name:"value is undefined",value:1}

どうだろう?value: nullにnullが来ることを予想できただろうか?
私はできなかった:joy:
valueにnullは存在しない前提だったので、nullレコード大量発生に泣いた:cry:

ちなみにオブジェクトじゃなくて、分割したらどうだろう?

constprint2=(name,value=1)=>console.log({name,value});print2("value is null",null);print2("value is undefined");

答えはこうだ。

>Object{name:"value is null",value:null}>Object{name:"value is undefined",value:1}

オブジェクトかどうかは関係なく、単純にデフォルト値はundefinedに作用してnullには作用しないということだ。
MDNにこの定義はあるのか見たら、undefinedに作用するとはあった。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Default_parameters

関数のデフォルト引数 は、関数に値が渡されない場合や undefined が渡される場合に、デフォルト値で初期化される形式上の引数を指定できます。

違和感を解消する

MDN見る限り、デフォルト値はundefinedにしか作用しない。理解した。
だけど、よく考えると今まで下記のように使い慣れたもののはず。

constprint=(v)=>console.log(v);print(null||undefined||true);// trueprint(!null);// trueprint(!undefined);// true

この原因を究明するためMDNを漁った。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Logical_Operators

true に変換できる値は、いわゆる truthy です。false に変換できる値は、いわゆる falsy です。

false と見ることができる式の例は、null、0、空文字列 ("")、あるいは、undefined と評価されるものです。

…まとめると、論理演算子は非常に感覚に沿ったものだが、デフォルトパラメータはそれとは別物ということだ:wink:

Conclusion

言語の理解が浅いとこんなことをやってしまう例として紹介しました。
本来であればテストコードで防御したかったのですが、開発優先だとなかなかテストコードを追加する時間を作れず…:persevere:

デフォルトパラメータが仕事するのはundefinedの時だけ(パラメータなしを含む)

Appendices

undefinedの使いどころ

nullとundefinedの使い分けは明確なものはないようですね。
TypeScriptではnullは使わないといっていますが、これはContributorのため物で一般的な規約ではないといっています。

https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines#null-and-undefined

Use undefined. Do not use null.

他の言語を習っているとnullを使うかと思いますが、undefinedを使うと効果的なユースケースが一つあるので紹介しておきます。
そのユースケースとはJSONにシリアライズするときです。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

変換の際に undefined、 関数 (Function)、シンボル (Symbol) は有効な JSON 値ではありません。変換中にそのような値に遭遇した場合は、 (オブジェクトの中で発見された場合は) 省略されたり、 (配列の中で見つかった場合は) null に変換されたりします。

WebAPIでリクエストを返す際、値が無効なフィールド名を除外したい場合があります。
その無効な値にundefinedを設定することで、条件分岐や三項演算子等なしに除外することが可能です。
上記の引用にある通り、配列でのundefinedは除外されませんので、その場合はfilter(v => v)等で除去しましょう。
(filter(v => v)はfalsyとして定義された値 (false, 0, "", null, undefined, NaN)が該当するため、除外してはいけないものがないことに注意すること)

References

分割代入
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

デフォルト引数
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Default_parameters

undefined
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/undefined

null
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/null

等価性の比較と同一性
https://developer.mozilla.org/ja/docs/Web/JavaScript/Equality_comparisons_and_sameness

論理演算子
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Logical_Operators

JSON.stringify()
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

Auth0でログインユーザーのロール(Roles)を元にハンドリングするRulesを書いてみた

$
0
0

Auth0でログインしたユーザーによって処理を変えたいときに、Rulesというログイン時に任意スクリプトを実行してハンドリングさせる機能があります。

参考: 「Auth0」のRulesが便利機能だった!! / 『「Auth0」で作る!認証付きシングルページアプリケーション』でAuth0をさわってみた

今回は、 ユーザーのロールを元にハンドリングをする処理を書いてみます

このRulesはNode.jsで記述するFaaS的なものになっているので、公式サンプルには無いものも比較的カスタマイズしやすそうです。

例: whitelist - 特定のメールアドレスの人を通す

公式サンプルです。こんな雰囲気。

function(user,context,callback){// Access should only be granted to verified users.if(!user.email||!user.email_verified){returncallback(newUnauthorizedError('Access denied.'));}constwhitelist=['user1@example.com','user2@example.com'];//authorized usersconstuserHasAccess=whitelist.some(function(email){returnemail===user.email;});if(!userHasAccess){returncallback(newUnauthorizedError('Access denied.'));}callback(null,user,context);}

ユーザーのロールを元に判定

ここで、ユーザーのロールを見て、特定のロールの人は通して、それ以外は除外するといった処理を追加してみます。

HTTPモジュールのRequestが利用できるのでRequestの使い方を調べつつ書いてみました。

参考: Node.jsのrequestモジュールを使ってHTTPSでPOSTリクエストを行う

Enpty Rulesを選んで以下をコピペで使えると思います。

また、settingsの箇所でtokenなどの値を設定することができます。
RulesのNode.js側からはconfiguration.キー名って形で取得できます。

キャプチャだとtokenというキー名にしたのでconfiguration.tokenでNode.js側で呼び出せます。

function(user,context,callback){console.log(context,user,user.identities[0].access_token);// Access should only be granted to verified users.if(!user.email||!user.email_verified){returncallback(newUnauthorizedError('Access denied.'));}consttoken=configuration.token;//Auth0のAPIのアクセストークン (Settingsで指定したもの)constoptions={url:`https://xxxxxx.auth0.com/api/v2/users/${user.user_id}/roles`,method:'GET',headers:{'Authorization':`Bearer ${token}`},json:true,};request(options,(err,response,body)=>{//許可するロール名    constwhiteList=['role1','role2'];constuserRoles=body.map(item=>item.name);constuserHasAccess=userRoles.some(value=>whiteList.includes(value));if(!userHasAccess){returncallback(newUnauthorizedError('Access denied.'));}callback(null,user,context);});}

ホワイトリストの配列のチェックをさせてますが、.some()便利ですね。

所感

Node.jsなので割と色々と任意の処理をさせることができそうですね。

Rules内でnpm上の外部モジュールは使えるのかとか色々気になります。

個人的にはaxiosが使えると便利なんだけどなぁと思いつつまた次回にしたいと思います。

まだまだAuth0の使えてない機能がいっぱいあるので試していきたいです。

[メモ] GitHub Actions を使って Electron の multi-platform-build をやる

$
0
0

やりたかったこと

最近個人的に Electron で小さなアプリを作る機会があったが、公開するにあたってビルド -> Release のフローを自動化したかった。当初 TravisCI を使っていたが、この機会に GitHub Actions を使ってみたのでその時のメモ。

ちなみに今回想定しているフローは、

  1. 新しい Tag を Push (e.g., v0.0.2)
  2. Tag Push をトリガーに GitHub Actions の build job がスタート
  3. 成功すると、Tag 名の Release (draft) を作成し、ビルドした成果物 (e.g., .exe.dmg) ファイルを作成した Release の Assets としてアップロード。
  4. Release を行う (手動で draft -> public release として更新する)

勝手に public release まで公開してほしくはなかったので、今回のゴールは draft 状態の Release までです。

イメージ
Screen Shot 2020-01-08 at 15.43.18.png

electron-builder

Electron アプリをビルドするには、

  • electron-forge
  • electron-builder
  • electron-packager

といったツールがありますが、おそらく electron-builder が1番使われていると思います。今回は electron-builderを使います。

electron-builder 本体の GitHub インテグレーション

今回は GitHub Actions の中でビルドしたパッケージを Release に紐付ける、という事をやるのでこの機能は使いませんが、electron-builder 自体がもっている GitHub インテグレーションを使うことでも、同様に Release の作成 -> ビルドしたパッケージを紐付け、が可能です (-pオプション)。当初 TravisCI ではこの機能を使っていました。

.travis.ymlの設定例も公開されています。
https://www.electron.build/multi-platform-build#sample-travisyml-to-build-electron-app-for-macos-linux-and-windows

build options

実際にビルドする時には下記のようなコマンドを使います。

# macOS 用にビルド$ electron-builder --mac# Windows 用にビルド$ electron-builder --win--x64

* 使っているネイティブモジュールがビルド対象となる OS 用の prebuilt バイナリを提供していない場合は、実際にその OS 上でしかビルドができない。(i.e., Windows 用のパッケージは Windows 上でしかビルドできない)

この時、色々なオプションが指定でき、通常は package.json内に buildオプションを記載していきます。
https://github.com/electron-userland/electron-builder#quick-setup-guide

package.json内に書かず、個別に YAML ファイルとして保存し、-cで指定することもできます。

GitHub Actions 設定

ワークフローの YAML は下記のようになりました。

name:CIon:push:tags:# "v" で始まる Tag 名の push だけをトリガーの対象にします (e.g., "v0.0.2")-'v*'jobs:# macOS 用の job 定義build_on_mac:runs-on:macos-lateststeps:-uses:actions/checkout@v2-name:Use Node.js 12.xuses:actions/setup-node@v1with:node-version:12.x-name:npm installrun:npm install# ここでアプリをビルド-name:build applicationrun:npm run build:mac# Release の作成とビルドしたパッケージのアップロード-name:create releaseuses:softprops/action-gh-release@v1if:startsWith(github.ref, 'refs/tags/')with:# 作成される Release は draft に留めておきたいdraft:true# アップロード対象となるファイル名files:|dist/*.dmgenv:GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}# Windows 用の job 定義build_on_win:runs-on:windows-lateststeps:-uses:actions/checkout@v2-name:Use Node.js 12.xuses:actions/setup-node@v1with:node-version:12.x-name:npm installrun:npm install-name:build applicationrun:npm run build:win-name:create releaseuses:softprops/action-gh-release@v1if:startsWith(github.ref, 'refs/tags/')with:draft:truefiles:|dist/*.exeenv:GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}

今回はアプリに対して署名を行わず、あくまでもオレオレアプリとしての公開だったので内容はとてもシンプルです。使っているパッケージも少なく、いずれも prebuilt バイナリが提供されていたので、実際には Windows 環境は使わなくてもビルドできたのですが、試しに綺麗に分けてみました。

TravisCI から切り替えて良かった点

  • 体感ではビルド時間が早くなった (個人的な感覚)。
  • Token をいちいち発行して env に設定、みたいな手間がなくなった。
  • なんかいい

スクレーピング用のクローラー作成

$
0
0

概要

機械学習用にコンテンツデータを集めないと行けなくて、毎回クローラー書くの面倒だったので、汎用的なクローラーを開発

構成

クローラー.png

仕組み

LinkCrawlerで起点のURLからリンクを辿り対象のURL収集しDBに保存、
ContentsCrawlerで収集したURLを取得しコンテンツの取得を行ってDBに保存する

AWS

aws.png

アプリケーション

・puppeteer
・node.js

DataBase

table
CREATETABLE`site`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`title`varchar(255)NOTNULL,`url`varchar(300)NOTNULL,`created_at`datetimeNOTNULL,`updated_at`datetimeNOTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;CREATETABLE`site_links`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`site_worker_id`int(11)NOTNULL,`url`varchar(300)NOTNULL,`crawl_status`int(11)NOTNULLDEFAULT'0',`crawl_date`datetimeDEFAULTNULL,`created_at`datetimeNOTNULL,`updated_at`datetimeNOTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;CREATETABLE`site_structure_data`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`site_links_id`int(11)NOTNULL,`structure_data`textNOTNULL,`created_at`datetimeNOTNULL,`updated_at`datetimeNOTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;CREATETABLE`site_worker`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`start_url`varchar(300)NOTNULL,`allow_domains`varchar(300)NOTNULL,`depth_limit`tinyint(4)NOTNULLDEFAULT'0',`allow_url_regex`varchar(300)DEFAULTNULL,`deny_url_regex`varchar(300)DEFAULTNULL,`site_type`varchar(10)NOTNULL,`json_column`varchar(10)DEFAULTNULL,`is_deleted`tinyint(4)NOTNULLDEFAULT'0',`created_at`datetimeNOTNULL,`updated_at`datetimeNOTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;CREATETABLE`site_worker_structure`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`site_worker_id`int(11)NOTNULL,`name`varchar(100)NOTNULL,`is_deleted`tinyint(4)NOTNULLDEFAULT'0',`created_at`datetimeNOTNULL,`updated_at`datetimeNOTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;CREATETABLE`site_worker_structure_selector`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`site_worker_id`int(11)NOTNULL,`site_worker_structure_id`int(11)NOTNULL,`selector`varchar(1000)NOTNULL,`attribute`varchar(100)DEFAULTNULL,`created_at`datetimeNOTNULL,`updated_at`datetimeNOTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;

クローラー

link_crawler.js
require('dotenv').config();constpuppeteer=require('puppeteer');constconfig=require('config');constmysql=require('mysql2/promise');constrequest=require('request-promise');const{URL}=require('url');constviewportWidth=1024;constviewportHeight=600;constuserDataDir=`${config.root_path}/tmp/crawler`;letconnection;constuserAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36';console.log('NODE_ENV=%s',process.env.NODE_ENV);constin_array=(arr,str)=>{letret=false;for(constiinarr){if(arr[i].url===str){ret=true;break;}}returnret;};asyncfunctionlinkCrawler(browser,args){constlinks=[];awaitgetContents(browser,args,links);for(constiinlinks){constdata=[links[i].site_worker_id,links[i].url];const[rows,fields]=awaitconnection.execute('SELECT id FROM site_links WHERE site_worker_id = ? AND url = ?',data);if(rows.length>0){continue;}awaitconnection.execute('INSERT INTO site_links(site_worker_id, url, created_at, updated_at) VALUES(?, ?, now(), now())',data);}console.log(links);}asyncfunctiongetJson(args){constsite_worker_id=args.id;conststart_url=args.start_url;constallow_domains=args.allow_domains;constjson_column=args.json_column;console.log('start_url',start_url);constoptions={url:start_url,json:true,headers:{'User-Agent':userAgent}};constlist=awaitrequest(options);for(constiinlist.list){constdetail=list.list[i];constdata=[site_worker_id,`https://${allow_domains}${detail[json_column]}`];console.log(data);const[rows,fields]=awaitconnection.execute('SELECT id FROM site_links WHERE site_worker_id = ? AND url = ?',data);if(rows.length>0){continue;}awaitconnection.execute('INSERT INTO site_links(site_worker_id, url, created_at, updated_at) VALUES(?, ?, now(), now())',data);}}asyncfunctiongetContents(browser,args,links){constpage=awaitnewPage(browser);constsite_worker_id=args.id;conststart_url=args.start_url;letdepth=1;constallow_domains=args.allow_domains;constallow_url_regex=args.allow_url_regex;constdeny_url_regex=args.deny_url_regex;constdepth_limit=args.depth_limit;if(args.depth){depth=args.depth+1;}console.log('start_url',start_url);awaitpage.goto(start_url);awaitpage.waitFor(1000);constitems=awaitpage.$$('a');for(leti=0;i<items.length;i++){letpattern;conststr_href=await(awaititems[i].getProperty('href')).jsonValue();if(str_href===''){continue;}consturl=newURL(str_href);// remove hashurl.hash='';conststr_url=url.href;if(in_array(links,str_url)===true){continue;}// domainif(allow_domains!==undefined){pattern=newRegExp(`//${allow_domains.replace(',','|//')}`);if(str_url.match(pattern)===null){continue;}}// url patternif(allow_url_regex!==null){pattern=newRegExp(allow_url_regex);if(str_url.match(pattern)===null){continue;}}if(deny_url_regex!==null){pattern=newRegExp(deny_url_regex);if(str_url.match(pattern)!==null){continue;}}console.log(depth,str_url);links.push({url:str_url,depth:depth,site_worker_id:site_worker_id});if(depth_limit>depth){constparams={};Object.assign(params,args);params.start_url=str_url;params.depth=depth;awaitgetContents(browser,params,links);}}awaitpage.close();}asyncfunctionnewPage(browser){constpage=awaitbrowser.newPage();awaitpage.setExtraHTTPHeaders({'Accept-Language':'ja,en-US;q=0.9,en;q=0.8'});constoptions={viewport:{width:viewportWidth,height:viewportHeight,},userAgent,};awaitpage.emulate(options);returnpage;}(async()=>{try{connection=awaitmysql.createConnection({host:'localhost',user:'root',password:'',database:'crawler'});const[rows,fields]=awaitconnection.execute('SELECT * FROM site_worker WHERE is_deleted = 0');if(rows===undefined||rows.length<=0){console.log('no data');connection.end();return;}constparams=rows[0];if(params.site_type==='contents'){constbrowser=awaitpuppeteer.launch({headless:false,devtools:false,executablePath:config.chrome,userDataDir:userDataDir,args:['--no-sandbox','--disable-setuid-sandbox'],});awaitlinkCrawler(browser,params);browser.close();}if(params.site_type==='json'){awaitgetJson(params);}connection.end();}catch(e){console.error(e);}})();
contents_crawler.js
require('dotenv').config();constpuppeteer=require('puppeteer');constmoment=require('moment');constconfig=require('config');constmysql=require('mysql2/promise');const{URL}=require('url');constuuidv4=require('uuid/v4');constviewportWidth=1024;constviewportHeight=600;constuserDataDir=`${config.root_path}/tmp/crawler`;letconnection;console.log('NODE_ENV=%s',process.env.NODE_ENV);asyncfunctioncontentsCrawler(browser,links,structure){for(constiinlinks){awaitgetContents(browser,links[i],structure);}}asyncfunctiongetContents(browser,args,structure){constpage=awaitnewPage(browser);constid=args.id;constsite_worker_id=args.site_worker_id;consturl=args.url;console.log('crawl_url',url);constresponse=awaitpage.goto(url);awaitpage.waitFor(10000);conststatus=response.status();constdata={};for(constiinstructure){constname=structure[i].name;constselector_dic=structure[i].selector;for(constjinselector_dic){constselector=selector_dic[j].selector;constattribute=selector_dic[j].attribute;constitem=awaitpage.$(selector);if(item===null){data[name]=null;continue;}if(attribute===null){data[name]=await(awaititem.getProperty('textContent')).jsonValue();break;}if(attribute==='src'){constfile_name=uuidv4();constpath=`tmp/images/${file_name}.jpg`;constimage=awaitpage.$(selector);awaitimage.screenshot({path:path,omitBackground:true,});data[name]=path;break;}data[name]=await(awaititem.getProperty(attribute)).jsonValue();if(data[name]){break;}}}letparams=[JSON.stringify(data),id];letsql='INSERT INTO site_structure_data(structure_data, site_links_id, created_at, updated_at) VALUES(?, ?, now(), now())';const[rows,fields]=awaitconnection.execute('SELECT id FROM site_structure_data WHERE site_links_id = ?',[id]);if(rows.length>0){sql='UPDATE site_structure_data set structure_data = ?, updated_at = now() WHERE site_links_id= ?';}awaitconnection.execute(sql,params);params=[status,id,site_worker_id];awaitconnection.execute('UPDATE site_links SET crawl_status = ?, crawl_date = now() WHERE id = ? AND site_worker_id = ?',params);awaitpage.close();returndata;}asyncfunctionnewPage(browser){constpage=awaitbrowser.newPage();awaitpage.setExtraHTTPHeaders({'Accept-Language':'ja,en-US;q=0.9,en;q=0.8'});constoptions={viewport:{width:viewportWidth,height:viewportHeight,},userAgent:'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36',};awaitpage.emulate(options);returnpage;}(async()=>{try{connection=awaitmysql.createConnection({host:'localhost',user:'root',password:'',database:'crawler',charset:'utf8mb4'});constsite_worker_id=4;const[structure]=awaitconnection.execute('SELECT id, site_worker_id, name FROM site_worker_structure WHERE site_worker_id = ? order by id',[site_worker_id]);if(structure===undefined||structure.length<=0){console.log('no data');connection.end();return;}const[structure_selector]=awaitconnection.execute('SELECT id, site_worker_id, site_worker_structure_id, selector, attribute FROM site_worker_structure_selector WHERE site_worker_id = ?',[site_worker_id]);if(structure_selector===undefined||structure_selector.length<=0){console.log('no data');connection.end();return;}for(constiinstructure){for(constjinstructure_selector){if(structure[i].id!==structure_selector[j].site_worker_structure_id){continue;}if(structure[i].selector===undefined){structure[i].selector=[];}constselector={selector:structure_selector[j].selector,attribute:structure_selector[j].attribute};structure[i].selector.push(selector);}}const[links]=awaitconnection.execute('SELECT id, site_worker_id, url FROM site_links WHERE site_worker_id = ? AND crawl_status = 0',[site_worker_id]);if(links===undefined||links.length<=0){console.log('no data');connection.end();return;}constbrowser=awaitpuppeteer.launch({headless:false,devtools:false,executablePath:config.chrome,userDataDir:userDataDir,args:['--no-sandbox','--disable-setuid-sandbox'],});awaitcontentsCrawler(browser,links,structure);browser.close();connection.end();}catch(e){console.error(e);}})();

最後に

うまく動かない場合はごめんなさい
ツールを作ってweb上から設定できるようにしていますが、長いので割愛

Auth0のManagement APIのアクセストークンを取得するメモ

$
0
0

Auth0のManagement APIの利用時の話です。

前回の記事だとアクセストークンを直指定だったので、時間が立つとアクセストークンが切れてしまいます。

API経由で動的に生成する方法を調べたのでメモです。

アクセストークンのテスト

APIトークンを試す際には、公式のGet Access Tokens for Testing
にあるように、管理画面からAPIトークンを取得して直接指定すると良いです。

Auth0でログインユーザーのロール(Roles)を元にハンドリングするRulesを書いてみたの記事で書いてたやり方です。

アクセストークンを動的に生成する - プロダクション

公式のGet Access Tokens for Productionに載っているAPI(https://YOUR_DOMAIN/oauth/token)にリクエストしてAPIトークンを生成して利用できます。

公式サンプル

varoptions={method:'POST',url:'https://YOUR_DOMAIN/oauth/token',headers:{'content-type':'application/x-www-form-urlencoded'},form:{grant_type:'client_credentials',client_id:'YOUR_CLIENT_ID',client_secret:'YOUR_CLIENT_SECRET',audience:'https://YOUR_DOMAIN/api/v2/'}};request(options,function(error,response,body){if(error)thrownewError(error);console.log(body);});

これをRules内(Node.js)で書いていきます。

Rulesに適用する

Auth0でログインユーザーのロール(Roles)を元にハンドリングするRulesを書いてみたのスニペットと合わせるとこんな感じです。

util.promisifyでrequestをpromise化して、async/awaitで利用する形にしました。

asyncfunction(user,context,callback){constutil=require('util');constrequestPromise=util.promisify(request);console.log(user);//1. メール確認if(!user.email||!user.email_verified){returncallback(newUnauthorizedError('Access denied.'));}//2-1. トークン生成consttokenApiOptions={method:'POST',url:`https://${configuration.domain}/oauth/token`,headers:{'content-type':'application/x-www-form-urlencoded'},form:{grant_type:'client_credentials',client_id:configuration.client_id,client_secret:configuration.client_secret,audience:`https://${configuration.domain}/api/v2/`}};consttokenApiRes=awaitrequestPromise(tokenApiOptions);consttoken=JSON.parse(tokenApiRes.body).access_token;//API経由で取得したトークン//2-2. ユーザーが所属するロールを取得constoptions={url:`https://${configuration.domain}/api/v2/users/${user.user_id}/roles`,method:'GET',headers:{'Authorization':`Bearer ${token}`},json:true,};constroleApiRes=awaitrequestPromise(options);//2-3. ロール判定constwhiteList=['role1','role2'];//許可するロール名constuserRoles=roleApiRes.body.map(item=>item.name);constuserHasAccess=userRoles.some(value=>whiteList.includes(value));if(!userHasAccess){returncallback(newUnauthorizedError('Access denied.'));}callback(null,user,context);}

Settingsでconfigrationの値を設定すればOKです。

エラーなど

ClientIDとSecretについて

ちなみに、client_idclient_secretはテストでトークン作成した際のMACHINE TO MACHINEのものを利用したらうまく行きました。

通常のログインアプリケーションのclient idを利用すると以下のようなエラーが出てうまくいかなかったです。

"error":"unauthorized_client","error_description":"Grant type 'client_credentials' not allowed for the client."

Generate token in WebApi. Error: “Grant type ‘client_credentials’ not allowed for the client.”

function外だとUnexpected token const

function外には基本的に記述はしない方が良さそうです。

constutil=require('util');constrequestPromise=util.promisify(request);asyncfunction(user,context,callback){省略}
{
"code": 400,
"message": "Compilation failed: Unexpected token const",
"error": "Unexpected token const",
・
・
・
省略

これだとエラーはでなかったです。

asyncfunction(user,context,callback){constutil=require('util');constrequestPromise=util.promisify(request);省略}

npm install で har-validator-5.1.2.tgz が Not Found と言われた

$
0
0

ちょっと前に表題の現象が発生してたので、手元のメモを元に記事にしておく。

以下の記事の方が解説されている方法が正攻法だと思われるが、なんらか事情でうまくいかなかった記憶。(うろ覚え)

yarnコマンドで 404 Not Found に遭遇したときの対処法 - Qiita

そこで、Github の Issue のコメント欄を参考に package-lock.jsonhar-validatorの登録内容を自分で 5.1.3のものに書き換えて npm installする方法で対応した。

package-lock.json
"har-validator":{"version":"5.1.3","resolved":"https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz","integrity":"sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==","requires":{"ajv":"^6.5.5","har-schema":"^2.0.0"}}

スマートコンセント(HS105)を使って電気毛布を自動ON/OFF制御する #SleepTech

$
0
0

はじめに

こんにちは。電気毛布を自動制御しようと頑張っているエンジニアです。

先日、「最近寒いので電気毛布のリモート制御にチャレンジする #SleepTech」で、サーボモータを使って電気毛布のつまみを自動で上下させようとしたのですが、
電気毛布コントローラーの固定が甘かったせいでうまくいきませんでした。

そこで、今回は「tp-link製のスマートコンセントHS105」を使って、電気毛布のON/OFF制御をしていこうと思います。

前提

本記事は、「快適な睡眠を目指して寝床内気象を測定する(温度編) #sleeptech」で作った仕組みの拡張です。
先にお読みいただくと、より理解しやすいかと思います。

用意したもの

全体イメージとシステムイメージ

シンプルに、1分毎に布団の温度を測り、電気毛布のON/OFFを制御します。
ON/OFFする部分にHS105を使用していきます。

システムイメージは以下のようになります。

HS105

初期設定をする

公式サイトのFAQ「アプリ「Kasa」でミニ スマートWi-Fiプラグの初期設定をするには」がめちゃくちゃわかりやすいので、そちらをご覧ください。
以降は、HS105がWifiにつながっている前提の手順となります。

Node.jsで操作する

HS105をNode.jsで操作できるライブラリ(tplink-smarthome-api)が公開されているので、ありがたくこちらを使用していきます。

IPアドレスを調べる

HS105を操作するために、割り当てられているローカルIPアドレスが必要です。
tplink-smarthome-apiのCLIを使って簡単に調べられます。

まずはCLIをインストール。

$ npm install-g tplink-smarthome-api

searchコマンドを実行する。

$ tplink-smarthome-api search

結果の中にHS105に割り当てられているIPアドレスが表示されるので、コピーしておきます。
私の場合は、192.168.10.3でした。
以降のソースでは、このIPアドレスを使用して記述しますので、実行する場合は自身のIPアドレスに書き換えて実行してください。

$ tplink-smarthome-api search

Searching...
startDiscovery({
  discoveryInterval: 2000,
  discoveryTimeout: 10000,
  breakoutChildren: true})
HS105(JP) plug IOT.SMARTPLUGSWITCH 192.168.10.3 9999 XXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
                                   ^^^^^^^^^^^^

実際にON/OFFしてみる

https://github.com/plasticrake/tplink-smarthome-api/blob/master/examples/plug-turn-on-off.jsのコードを実行してみます。

const{Client}=require('tplink-smarthome-api');constclient=newClient();constplug=client.getPlug({host:'192.168.10.3'});// 各自のIPアドレスに書き換えplug.setPowerState(true);plug.setPowerState(false);

実行するとこんな感じで動く。

布団の温度からON/OFFを制御する

obniz + 温度センサーから取得した温度を元に、HS105を制御します。
obnizとHS105の使い方さえわかれば、非常にシンプルなプログラムで動かすことができます。

constObniz=require('obniz');constobniz=newObniz(process.env.OBNIZ_ID);const{Client}=require('tplink-smarthome-api');constMAX=100;constcallback=async()=>{obniz.connect();// obnizに接続awaitobniz.connectWait();consttempsens=obniz.wired('LM35DZ',{gnd:0,output:1,vcc:2});constclient=newClient();// HS105に接続constplug=client.getPlug({host:'192.168.10.3'});// 各自のIPアドレスに書き換えlettemperature=0;// obnizの温度センサーから温度を取得(バラつきがあるので100回平均している)for(leti=0;i<MAX;i++){temperature+=awaittempsens.getWait();awaitobniz.wait(10);}temperature/=MAX;if(temperature<32){// 温度に応じてHS105をON/OFFplug.setPowerState(true);}else{plug.setPowerState(false);}obniz.close();// 繰り返し実行するので1回毎に切断すろplug.closeConnection();};setInterval(callback,60000);// 1分毎に実行する

実際に使ってみる

大晦日に作成してから使い続けていますが、今までと比べるとかなり快適になった気がします。

Ambientというサービスを使って、布団の温度や部屋の温湿度をグラフにしているのですが、昨日のグラフは以下のようになりました。
(左上:布団温度、右上:HS105のON/OFF、左下:寝室気温、右下;寝室湿度)

  • 布団温度を見てみると、およそ30℃~37℃の範囲で推移しているようです。平均は33℃前後でしょうか。
    • プログラムを少し改良して、午後10時~翌朝8時までの間で電気毛布を制御するようにしました。
    • 電気毛布がOFFの時間になると20℃前後で推移するようです。
    • それにしても測定誤差が大きいですね。10%程度はありそう。センサーや測定方法を検討する必要がありそうです。
  • HS105のON/OFFは頻繁に切り替わっており、1時間に数回~十数回は切り替わっています。
    • 切り替わるたびにカチッカチッとリレーが切り替わるような音がします。少し気になりますが、もう慣れました。
  • 寝室気温は、18℃前後からほとんど変わらないようです。
  • 寝室湿度は、加湿器を強めにつけているので、朝まではずっと湿度95%でした。(センサーの上限が95%なので実際は100%かもしれません)

まとめ

簡単に作成した割には、満足のいく出来となりました。
早いところ誰かに使ってもらってフィードバックをもらいたいところです。

HS105のライブラリを使うと、同一LANにいる端末からしか操作ができないようでした。
そのためノートPCから制御をかけているのですが、後々はAWS Lambdaから操作できるようにしたいので調査が必要そうです。

GoogleドライブのファイルをGoogle Cloud Storageへコピーする

$
0
0

Google Cloud APIの中には、gs://[bucket]/[path]の形式になっているCloud StorageのURLを与えることができるものがある。今回はGoogleドライブに入っているファイルをCloud Storageにコピーしてgs://のURLとして使えるようにしたかった。

streamをうまく使うことで、一時ファイルを作らずにコピーすることができた。

準備

以下のパッケージをインストールしておく。

$ npm i googleapis @google-cloud/storage

ドライブ用にGoogle APIのOAuthクライアントIDを作成する。APIとサービスの認証情報から[OAuth クライアント ID の作成]で作成できる。コマンドラインツールの場合は[その他]を選んで作成する。JSONをダウンロードしておく。

Cloud Storage用にサービスアカウントキーを発行して、これもJSONをダウンロードしておく。

Googleドライブの認証

ドライブ公式のクイックスタートをまとめ直したクラスを作った。

https://gist.github.com/iseebi/4d21bf68c79bc020ea3515693676a35f

コピー関数

こんな感じの関数を書いた。

import{Bucket,Storage}from"@google-cloud/storage";import{Readable}from"stream";importDrive=drive_v3.Drive;asyncfunctioncopyDriveToStorage(drive:Drive,bucket:Bucket,driveFileId:string,uploadFileName:string){constmeta=awaitdrive.files.get({fileId:driveFileId,supportsTeamDrives:true,fields:'mimeType'});constmedia=awaitdrive.files.get({fileId:driveFileId,supportsTeamDrives:true,alt:'media'},{responseType:'stream'});constuploadFile=bucket.file(uploadFileName);awaitnewPromise<void>((resolve,reject)=>{constdownloadStream=media.dataasReadable;constuploadStream=uploadFile.createWriteStream({metadata:{cacheControl:'no-cache',contentType:meta.data.mimeType,}});downloadStream.pipe(uploadStream);downloadStream.on('error',reject);uploadStream.on('error',reject);uploadStream.on('finish',resolve);});}

ポイントはこのあたり。

  • drive.file.getのparamsにalt: 'media'、optionsにresponseType: 'stream'を指定
  • その戻り値のdataをReadableにキャストする → 読み込み用のストリーム
  • bucket.filecreateWriteStream→ 書き込み用のストリーム
  • 読み込み用のストリームから、書き込み用のストリームにpipeする
  • 書き込み用のストリームがfinishしたら完了、どちらかのストリームがエラーを返したらreject

今回実際に使ったものでは、mimeTypeを事前に絞り込んでいたので、metaの取得はせずにハードコーディングした。

呼び出し

こんな感じで使う。

import{Bucket,Storage}from"@google-cloud/storage";import{Authenticator}from"./authenticator";import{google}from"googleapis";constauth=awaitauthenticator.authenticateAsync();constdrive=google.drive({version:'v3',auth});conststorage=newStorage({projectID:'00000000000',keyFilename:'service_account.json'});constbucket=storage.bucket('my-bucket-name');constfileID='......';constdestination='filename';awaitcopyDriveToStorage(drive,bucket,fileID,destination);

開発環境の1つであるVSCodeの開発環境についてチラ見してみる

$
0
0

リーマンサットについて

趣味で宇宙開発を行う団体「リーマンサット・プロジェクト」がお送りする新春アドベントカレンダーです。インデックスはこちら

リーマンサット・プロジェクトは「普通の人が集まって宇宙開発しよう」を合言葉に活動をしている民間団体です。
他では経験できない「宇宙開発プロジェクト」に誰もが携わることができます。
興味を持たれた方は https://www.rymansat.com/joinからお気軽にどうぞ。

概要

機械学習周りやWEBサービスを担当してますAkira Sugawaraです。

開発環境、、、エンジニアなら誰しも気になりますよね?
リーマンサットでは趣味開発なので、特に大きな制限を設けずに自由に開発しています。
開発エディタはVSCodeやIntelliJ、ソースコード・タスク管理はBacklog、といった感じです。

VSCodeの開発環境

ふと、VSCodeってどう作られてるんだろうか、どんな開発環境なんだろうか、と考えてしまいました。
思いを馳せると夜も眠れなくなってしまいます。

開発環境の一部であるエディタを開発している開発環境は、さぞこだわりがあるんだろう…。

ということで、本記事ではVSCodeの開発環境を調べてみます。

VSCodeのツールチェーン

2019/12/30時点でのmasterブランチをcloneして確認しています。。

typedetails
Languagetypescript,javascript
Application FrameworkElectron
LinterESLint, TSLint
Formatterprettier
TaskRunnerNPM, gulp
TestFrameworkmocha
CI/CDAzure Pipeline

CI/CDに関しては、./build/azure-pipelinesのフォルダの下にOS毎のビルド定義ファイルが格納されています。
さすがにMicrosoftのOSSということなので、Azureになってるんですね。

使用してるVSCodeExtension

もちろんVSCodeの開発もVSCodeですよね!
ワークスペースのRecommendリストは下記です。

  1. "ms-vscode.vscode-typescript-tslint-plugin"

    eslintがtypescriptサポートを強化するという発表もありましたが、フロントエンドによくあるESLint/TSLint併用構成のようです

  2. "dbaeumer.vscode-eslint"

    言わずもしれたESLintのvscodeプラグインです。

  3. "EditorConfig.EditorConfig"

    恥ずかしながら使用経験がありませんでした。
    複数人で作業するときのEditorのConfigurationToolで、言語非依存・エディター非依存で規約の準拠が可能になります。
    Prettierと役割重なってるんじゃない?と思いましたが、この記事が違いがわかりやすかったです。

  4. "msjsdiag.debugger-for-chrome"

    Chromeでjavascriptをデバックするための連携ツールです。

まとめ

さらーっと、VSCodeの開発環境を見ていきました。
何かしらのプロジェクトで新しく開発を始める場合、著名なOSSの環境を見てみるのは参考になるかもしれませんね。

次の記事は、@KingBritainの「航空機の流体力学(2次元翼の流体解析)」になります。

参考

GitHub
How to build

Oculus Quest で日本語を打つ

$
0
0

VR 空間でも日本語入力

rdgqs-1spna.gif
文字小さくてごめんなさい💦

製作物

日本語メモ (Glitch プロジェクト)
Oculus Quest なら Alt + Space で半角 ⇔ 日本語入力の切り替え。
URL を開くと ID が URL に付与されますので、再度編集したい場合は ID が付けられた URL をブックマークしてください。
テキストが変更がされると自動でサーバーに保存されます。

解説

Oculus Quest + Firefox Reality + Glitch + A-Frame で VR 内 VR 開発を推進している gaegae です。

前回の記事でデメリットに上げませんでしたが、
2020/01/10現在、Oculus Quest では Bluetooth キーボードから日本語入力できません。
Firefox Reality ではソフトキーボードで日本語入力できますが、 Bluetooth は非対応です。
デフォルトブラウザはソフトキーボードも対応していません。

コーディング時に必須ではないので特に問題ないだろうと思っておりましたが、
作業しているときにひらめきを即座にメモできないのがきつく感じてきました💦

英語で書くのはしんどい、メモのためにヘッドセット外したら本末転倒。
Firefox Reality のイシューに日本語入力をお願いしていますが、無理難題を Mozilla さんに押し付けるのも失礼。

なのでプログラマなのだから自分でどうにかしたいと思います。
VR 開発の記事をどんどん上げていきたいところですが、その前に足固めです。

方針

Oculus Quest の IME をどうにかするのは正直無理。やる前からあきらめてました。
最低限、日本語のメモが取れればよかったので、
Javascript で日本語入力を実装した Web ページを作成することにしました。

詳細

フロントエンドとサーバーサイド、2つの Glitch プロジェクトを作りました。

フロントエンド:https://glitch.com/~oculusquest-jpn-ime
テキストエリアが一つだけの Web ページです。
日本語入力には IgoIMEを使用させていただいております。
変更があればサーバーに送信し、再度画面が表示されたときに読み込みます。

サーバーサイド:https://glitch.com/~dot-spruce
認証不要で使えるシンプルな Key Value Store です。
node.js 上でキーをファイル名、値を内容にしてファイルとして保存しています。
他のプロジェクトでも使い回せるように分けました。

おわりに

これでまたヘッドセット着脱の手間が減り、VRへの没入感がさらに上がりました。

今回は Oculus Quest よりも Glitch の話がメインでした。
やはりボタン一発で瞬時に Linux・node.js・IDEと Web 開発に必要なもの一通りを提供してくれるのは素晴らしいですね。
Google や AWS 、Github ページなどを利用するよりも気軽に開発・配信ができると思われますので
Web コンテンツ作りたい方、ぜひどうぞ!

bxeaf-a7wmf.gif
VR開発もやってます~。楽しー!


スマートコンセント(HS105)を使って電気毛布を自動ON/OFF制御する #SleepTech

$
0
0

はじめに

こんにちは。電気毛布を自動制御しようと頑張っているエンジニアです。

先日、「最近寒いので電気毛布のリモート制御にチャレンジする #SleepTech」で、サーボモータを使って電気毛布のつまみを自動で上下させようとしたのですが、
電気毛布コントローラーの固定が甘かったせいでうまくいきませんでした。

そこで、今回は「tp-link製のスマートコンセントHS105」を使って、電気毛布のON/OFF制御をしていこうと思います。

前提

本記事は、「快適な睡眠を目指して寝床内気象を測定する(温度編) #sleeptech」で作った仕組みの拡張です。
先にお読みいただくと、より理解しやすいかと思います。

用意したもの

全体イメージとシステムイメージ

シンプルに、1分毎に布団の温度を測り、電気毛布のON/OFFを制御します。
ON/OFFする部分にHS105を使用していきます。

システムイメージは以下のようになります。

HS105

初期設定をする

公式サイトのFAQ「アプリ「Kasa」でミニ スマートWi-Fiプラグの初期設定をするには」がめちゃくちゃわかりやすいので、そちらをご覧ください。
以降は、HS105がWifiにつながっている前提の手順となります。

Node.jsで操作する

HS105をNode.jsで操作できるライブラリ(tplink-smarthome-api)が公開されているので、ありがたくこちらを使用していきます。

IPアドレスを調べる

HS105を操作するために、割り当てられているローカルIPアドレスが必要です。
tplink-smarthome-apiのCLIを使って簡単に調べられます。

まずはCLIをインストール。

$ npm install-g tplink-smarthome-api

searchコマンドを実行する。

$ tplink-smarthome-api search

結果の中にHS105に割り当てられているIPアドレスが表示されるので、コピーしておきます。
私の場合は、192.168.10.3でした。
以降のソースでは、このIPアドレスを使用して記述しますので、実行する場合は自身のIPアドレスに書き換えて実行してください。

$ tplink-smarthome-api search

Searching...
startDiscovery({
  discoveryInterval: 2000,
  discoveryTimeout: 10000,
  breakoutChildren: true})
HS105(JP) plug IOT.SMARTPLUGSWITCH 192.168.10.3 9999 XXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
                                   ^^^^^^^^^^^^

実際にON/OFFしてみる

https://github.com/plasticrake/tplink-smarthome-api/blob/master/examples/plug-turn-on-off.jsのコードを実行してみます。

const{Client}=require('tplink-smarthome-api');constclient=newClient();constplug=client.getPlug({host:'192.168.10.3'});// 各自のIPアドレスに書き換えplug.setPowerState(true);plug.setPowerState(false);

実行するとこんな感じで動く。

布団の温度からON/OFFを制御する

obniz + 温度センサーから取得した温度を元に、HS105を制御します。
obnizとHS105の使い方さえわかれば、非常にシンプルなプログラムで動かすことができます。

constObniz=require('obniz');constobniz=newObniz(process.env.OBNIZ_ID);const{Client}=require('tplink-smarthome-api');constMAX=100;constcallback=async()=>{obniz.connect();// obnizに接続awaitobniz.connectWait();consttempsens=obniz.wired('LM35DZ',{gnd:0,output:1,vcc:2});constclient=newClient();// HS105に接続constplug=client.getPlug({host:'192.168.10.3'});// 各自のIPアドレスに書き換えlettemperature=0;// obnizの温度センサーから温度を取得(バラつきがあるので100回平均している)for(leti=0;i<MAX;i++){temperature+=awaittempsens.getWait();awaitobniz.wait(10);}temperature/=MAX;if(temperature<32){// 温度に応じてHS105をON/OFFplug.setPowerState(true);}else{plug.setPowerState(false);}obniz.close();// 繰り返し実行するので1回毎に切断すろplug.closeConnection();};setInterval(callback,60000);// 1分毎に実行する

実際に使ってみる

大晦日に作成してから使い続けていますが、今までと比べるとかなり快適になった気がします。

Ambientというサービスを使って、布団の温度や部屋の温湿度をグラフにしているのですが、昨日のグラフは以下のようになりました。
(左上:布団温度、右上:HS105のON/OFF、左下:寝室気温、右下;寝室湿度)

  • 布団温度を見てみると、およそ30℃~37℃の範囲で推移しているようです。平均は33℃前後でしょうか。
    • プログラムを少し改良して、午後10時~翌朝8時までの間で電気毛布を制御するようにしました。
    • 電気毛布がOFFの時間になると20℃前後で推移するようです。
    • それにしても測定誤差が大きいですね。10%程度はありそう。センサーや測定方法を検討する必要がありそうです。
  • HS105のON/OFFは頻繁に切り替わっており、1時間に数回~十数回は切り替わっています。
    • 切り替わるたびにカチッカチッとリレーが切り替わるような音がします。少し気になりますが、もう慣れました。
  • 寝室気温は、18℃前後からほとんど変わらないようです。
  • 寝室湿度は、加湿器を強めにつけているので、朝まではずっと湿度95%でした。(センサーの上限が95%なので実際は100%かもしれません)

まとめ

簡単に作成した割には、満足のいく出来となりました。
早いところ誰かに使ってもらってフィードバックをもらいたいところです。

HS105のライブラリを使うと、同一LANにいる端末からしか操作ができないようでした。
そのためノートPCから制御をかけているのですが、後々はAWS Lambdaから操作できるようにしたいので調査が必要そうです。

[Electron] IPC には新しい ipcRenderer.invoke() メソッドを使ったほうが便利 (v7+)

$
0
0

TL;DR;

Electron v7 から、ipcRenderer.invoke()ipcMain.handle()が新たに追加されました。これは、従来まで利用されてきた ipcRenderer.send()ipcRenderer.sendSync()の上位互換のようなものです。今後は積極的にこちらを使ったほうがよさそう。

従来の Renderer <-> Main プロセス間通信 (IPC)

同期: ipcRenderer.sendSync()

文字通り、同期 (Sync) 的にプロセス間通信を行います。
この際に重要なのは、sendSync によって Main プロセスが呼ばれるとその間は Renderer プロセス上の処理は完全にブロックされます。Main プロセスからの応答があるまでは、renderer プロセス側の操作画面はいわゆるフリーズしたような状態になります。描画処理も止まるので、ローディング画面のような CSS アニメーションも容赦なく固まります。

ドキュメントにも下記のように、どうしても使わざるを得ない状況下においてのみ最終手段として使う、そうでない場合は利用を避けることが推奨されています。

⚠️ WARNING: Sending a synchronous message will block the whole renderer process until the reply is received, so use this method only as a last resort. It's much better to use the asynchronous version, invoke().

renderer
// renderer から Main プロセスを呼び出すconstdata=ipcRenderer.sendSync('sync-test','ping')console.log(data)
main
ipcMain.on('sync-test',(event,message)=>{// message には 呼び出し元からのデータ ('ping') が入っているconsole.log(message)event.returnValue='pong'return})
console
> pong
> ping

Main プロセス側での処理が終わってから renderer 側で console.log が走るため、結果は上記のようになります。メリットとして、event.returnValueを通じて呼び出し元にデータを返すことができます。

非同期: ipcRenderer.send()

非同期実行のため、Main プロセスの処理中であっても renderer 側ではフリーズすることなく操作が可能です。

ただし Main プロセス側の処理がいつ終わったのか renderer 側では分からないので、これをトリガーに何らかの処理を行いたい場合、renderer 側で ipcRenderer.on()を定義しておくことで、Main -> rendere 方向の戻りの通信が可能になります。

renderer
// main からの呼び出しを待ち受けるipcRenderer.on('async-test-complete',(event,message)=>{console.log(message))}// renderer から Main プロセスを呼び出すipcRenderer.sendSync('async-test','ping')console.log('started')
main
ipcMain.on('async-test',(event,message)=>{// message には 呼び出し元からのデータ ('ping') が入っているconsole.log(message)// main から renderer プロセスを呼び出すevent.sender.send('async-test-complete','pong')return})
console
> started
> ping
> pong

新しい Renderer <-> Main プロセス間通信 (IPC)

非同期: ipcRenderer.invoke()

ようやく本題です。Electron version 7+ から利用可能になりました。
こちらも非同期ですが、Main プロセスからは Promise が返ってきます。ですので、awaitを使って Main プロセスからのデータの受取を下記のようにシンプルに書くことができます。

renderer
// renderer から Main プロセスを呼び出すconst=data=awaitipcRenderer.invoke('invoke-test','ping')console.log(data)
main
ipcMain.handle('invoke-test',(event,message)=>{// message には 呼び出し元からのデータ ('ping') が入っているconsole.log(message)// renderer プロセスにデータを返すreturn'pong'})
console
> ping
> pong

Main 側では、ipcMain.handle()とメソッド名が変わっています。

ServerlessFramework+Slackで運行遅延情報をお知らせする

$
0
0

はじめに

AWSLambdaで運行遅延情報をslackに通知するbotを作りました。
運行遅延APIを利用して、遅延が発生して入れば運行会社のWEBから遅延内容をスクレイピングしてSlackにお知らせします。
何番煎じか分かりませんが、lambdaとnodejsで書かれた記事が見当たらなかったので紹介します。

作ったもの

スクリーンショット 2020-01-09 16.40.55.png

GitHub
https://github.com/t-yasukawa/incoming-webhook

環境

  • macOS Catalina 10.15.2
  • VSCode 1.41.1
  • AWS(Lambda, CloudFormation)
  • ServerlessFramework 1.60
  • Node.js 12.14.1
  • puppeteer 2.0.0
  • chrome-aws-lambda 2.0.0

事前準備

1. ServerlessFrameworkの導入

まずはバージョン確認

$ npm ls --depth=0 -g
/Users/t-yasukawa/.nodebrew/node/v12.14.1/lib
└── npm@6.13.4

ServerlessFrameworkを入れます。
npm i serverless -gでも良いのですが、他のプロジェクトでも使っているので今回はプロジェクト直下におきます。
npm initで生成される package.jsonの初期値は適当に埋めてください。

$ mkdir incoming-webhook
$ cd incoming-webhook
$ npm init
package name: (incoming-webhook) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 

$ npm i serverless
$ npm ls --depth=0
incoming-webhook@1.0.0 /Users/t-yasukawa/git/incoming-webhook
└── serverless@1.60.5

無事インストールできました。
しかしこのままでは ./node_modules/.bin/slsと毎回打たないといけないので面倒です。
方法は色々ありますが、安直にパスを追加します。

$ echo 'export PATH=node_modules/.bin:$PATH' >> ~/.bash_profile
$ source ~/.bash_profile
$ sls -v
Framework Core: 1.60.5
Plugin: 3.2.7
SDK: 2.2.1
Components Core: 1.1.2
Components CLI: 1.4.0

これでOKです。
早速プロジェクトファイルをテンプレートから作成します。

$ sls create --template aws-nodejs --path ./
Serverless: Generating boilerplate...

  Serverless Error ---------------------------------------

  The directory "/Users/t-yasukawa/git/incoming-webhook/" already exists, and serverless will not overwrite it. Rename or move the directory and try again if you want serverless to create it"

はい、怒られました。
プロジェクトディレクトリはこのタイミングで作らないといけないようです。
serverlessをグローバルにしなかったせいですね。仕方ないので一旦パスを変えます。

$ sls create --template aws-nodejs --path ./src/models/lambda
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/t-yasukawa/git/test/src/models/lambda"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.60.5
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"

生成されたファイルを変更します。

$ mv src/models/lambda/serverless.yml ./
$ mv src/models/lambda/.gitignore ./
$ tree -a -L 1
.
├── .gitignore
├── node_modules
├── package-lock.json
├── package.json
├── serverless.yml
└── src

2.puppeteer導入

スクレイピング処理にpuppeteerを利用しますが、このままローカルで利用することができたのですが、
いざLambda上にデプロイしようとした時にLambdaの上限250MBを超えてしまう問題が発生しました。
An error occurred: SessionLambdaFunction - Unzipped size must be smaller than 262144000 bytes.

そこで、軽量版の puppeteer-coreとAWS上でchromiumが動く chrome-aws-lambdaを入れることでこれを回避します。
(ついでにAPI取得用のaxiosも入れます。)
※versionを揃えないと実行時にエラーとなるので注意
Error: Chromium revision is not downloaded. Run "npm install" or "yarn install"

$ npm i chrome-aws-lambda puppeteer-core axios
$ npm ls--depth=0
incoming-webhook@1.0.0 incoming-webhook
├── axios@0.19.1
├── chrome-aws-lambda@2.0.0
├── puppeteer-core@2.0.0
└── serverless@1.60.5

3.実装

  • 遅延情報APIで遅延情報を取得
  • お知らせしたい路線を検出
  • 検出できたらWebに飛んでスクレイピング
  • Slackに送信
handler.js
'use strict'constaxios=require("axios")constchromium=require("chrome-aws-lambda");// 取得したい路線情報constCHECK_LIST=[{'name':'常磐線','company':'JR東日本','website':'https://traininfo.jreast.co.jp/train_info/tohoku.aspx','selector':async(page)=>awaitselectorForJrEast(page,'常磐線')},{'name':'東北本線','company':'JR東日本','website':'https://traininfo.jreast.co.jp/train_info/tohoku.aspx','selector':async(page)=>awaitselectorForJrEast(page,'東北本線')},{'name':'仙台市営地下鉄','company':'仙台市交通局','website':'https://www.kotsu.city.sendai.jp/unkou/','selector':async(page)=>awaitselectorForSendaiSubway(page)},]module.exports.sendToSlack=async()=>{// 鉄道運行遅延の情報を取得constnotifyDelays=awaitgetNotifyDelays()if(notifyDelays.length==0){console.log('遅延情報はありませんでした。')return;}console.log('遅延情報が見つかりました。')// 遅延内容を取得constmessages=awaitgetDelayMessage(notifyDelays)console.log(messages.join('\n'))// Sclackに送信awaitpostSlack(messages.join('\n'))}/**
 * 遅延情報を取得
 */asyncfunctiongetNotifyDelays(){constdelay_url=process.env['TRAIN_DELAY_JSON_URL']constnotifyDelays=[]try{// 運行遅延情報を取得constres=awaitaxios.get(delay_url)// res = [{//     "name":"東北本線",//     "company":"JR東日本",//     "lastupdate_gmt":1578638905,//     "source":"鉄道com RSS"// }]// 通知する路線のみ抽出res.data.forEach(delayItem=>{CHECK_LIST.forEach(checkItem=>{if(delayItem.name==checkItem.name&&delayItem.company==checkItem.company){notifyDelays.push(checkItem)}})})}catch(error){console.error(error)}returnnotifyDelays}/**
 * 遅延メッセージを取得
 * 
 * @param {Array} delays 
 */asyncfunctiongetDelayMessage(delays){constmessages=[];letbrowser=nulltry{browser=awaitchromium.puppeteer.launch({args:chromium.args,defaultViewport:chromium.defaultViewport,executablePath:awaitchromium.executablePath,headless:chromium.headless})constpage=awaitbrowser.newPage()for(constiofdelays){// websiteから遅延情報をスクレイピングawaitpage.goto(i.website)constdetail=awaiti.selector(page)constmessage=`*・${i.company} \<${i.name}\>* (<${i.website}|jump>)\n ${detail}\n`messages.push(message)}}catch(e){console.warn(e)}finally{if(browser!==null){awaitbrowser.close()}}returnmessages;}/**
 * JR東日本(東北エリア)の遅延内容をスクレイピング
 * 
 * @param {Page} page Page
 * @param {string} target 路線名
 */asyncfunctionselectorForJrEast(page,target){constselector='#wrapper > div.main_con02 > div.table_access > table > tbody > tr'constmessages=[]try{for(constitemofawaitpage.$$(selector)){constlineName=awaitgetTextContext(item,'.line_name')if(lineName==target){constmessage=awaitgetTextContext(item,'.status_text')messages.push(message)}}}catch(error){console.error(error)return`:warning: ノードの取得に失敗しました。DOMが変更されている可能性があります。\n  \`${selector}\` `}returnmessages.join('\n')}/**
 * 仙台市地下鉄(南北・東西)の遅延内容をスクレイピング
 * 
 * @param {Page} page Page
 */asyncfunctionselectorForSendaiSubway(page){constselector='#unkou_detail'try{constitem=awaitpage.$(selector)consttext=awaitgetTextContext(item)if(text==null){return`:warning: ノードの取得に失敗しました。DOMが変更されている可能性があります。\n  \`${selector}'\` `}}catch(error){console.error(error)return`:warning: ノードの取得に失敗しました。DOMが変更されている可能性があります。\n  \`${selector}'\` `}returntext}/**
 * textContent取得
 * 
 * @param {ElementHandle} elementHandle 
 * @param {string} target 
 */asyncfunctiongetTextContext(elementHandle,target){consttag=awaitelementHandle.$(target)constprop=awaittag.getProperty('textContent')consttext=awaitprop.jsonValue()returntext}/**
 * Slackへ送信
 * 
 * @param {string} message 
 */asyncfunctionpostSlack(message){constslack_url=process.env['SLACK_WEBHOOK_URL']constpayload={'username':'運行遅延お知らせbot','icon_emoji':':train:','attachments':[{'fallback':message,'color':'#36a64f','pretext':'<!channel> 電車の遅延があります。','text':message,"mrkdwn_in":["text"],'channel':'#列車運行情報'}]}constres=awaitaxios.post(slack_url,payload)console.log(res)}
serverless.yml
service:incoming-webhookprovider:name:awsruntime:nodejs12.xtimeout:300profile:${self:custom.profiles.${self:provider.stage}}region:${opt:region, self:custom.defaultRegion}custom:defaultRegion:ap-northeast-1profiles:dev:defaultpackage:exclude:-node_modules/serverless/**-node_modules/chrome-aws-lambda/**-chrome-aws-lambda/**functions:sendTrainDelayToSlack:handler:src/models/lambda/handler.sendToSlackevents:-schedule:rate:cron(15 9,22,23 ? * MON-FRI *)## 7:15,8:15,18:15 月~金 layers:-{Ref:ChromeLambdaLayer}environment:TRAIN_DELAY_JSON_URL:'https://tetsudo.rti-giken.jp/free/delay.json'SLACK_WEBHOOK_URL:'https://hooks.slack.com/services/****************'layers:chrome:package:artifact:./chrome-aws-lambda/chrome_aws_lambda.zip

4. 動作確認(問題発生)

動作確認のためにローカルでLambdaを実行するとchromiumが起動できないとエラーになりました。 :weary:

$ sls invoke local--function sendTrainDelayToSlack
遅延情報が見つかりました。
Error: Failed to launch chrome!
/var/folders/v8/ydzbmvkj6_zbm8msr6x730nr0000gn/T/chromium: /var/folders/v8/ydzbmvkj6_zbm8msr6x730nr0000gn/T/chromium: cannot execute binary file

TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md

Mac環境でデバッグしようとするとPC内のChromeアプリのバイナリを利用しようとするのですが、
バイナリファイルのchromiumを実行することができませんでした。

Chromeとモジュールのバージョンがリビジョン単位で違うけど、そのせい?よくわからず。。
Chromeバージョン: 79.0.3945.117 (2020/01/10時点)

puppeteer Versionchrome-aws-lambda VersionChromium Revision
2.0.*npm i chrome-aws-lambda@~2.0.2705776 (79.0.3945.0)

参照: https://github.com/alixaxel/chrome-aws-lambda

解決策

ローカルでは容量が大きいけど puppeteerでchroniumのバイナリファイルをDLして利用し、
AWS上では puppeteer-corechrome-aws-lambdaを使うことにしました。

$ npm i --save-prod chrome-aws-lambda puppeteer-core

$ npm i --save-dev puppeteer
Downloading Chromium r706915 - 111.8 Mb [====================] 100% 0.0s 
Chromium downloaded to /Users/t-yasukawa/git/incoming-webhook/node_modules/puppeteer/.local-chromium/mac-706915

ただ、これだけではローカルで動かないのでデバッグ中だけ executablePathを変える必要があります。

browser = await chromium.puppeteer.launch({
    args: chromium.args,
    defaultViewport: chromium.defaultViewport,
-    executablePath: await chromium.executablePath,
+    executablePath: null,
    headless: chromium.headless
    })

もしくは上記でDLしたchromiumのパスでも行けると思います。

executablePath:`${process.cwd()}/node_modules/puppeteer/.local-chromium/mac-706915/chrome-mac/Chromium.app/Contents/MacOS/Chromium`

5.動作確認(解決)

$ sls invoke local--function sendTrainDelayToSlack
遅延情報が見つかりました。
*・JR東日本 <東北本線>*(<https://traininfo.jreast.co.jp/train_info/tohoku.aspx|jump>)東北本線は、釜石線内でのシカと衝突の影響で、盛岡~花巻駅間の上下線で一部列車が運休となっています。

無事スクレイピングできました。・・・シカさん :scream_cat:

6.解説

API関連は axiosを使いました。とてもシンプルで使いやすい!

constres=awaitaxios.get(delay_url)constres=awaitaxios.post(slack_url,payload)

遅延情報はJR東日本と仙台市地下鉄の2サイトからスクレイピングしました。
地下鉄はノード指定ですんなり取得できましたが、JRの方はノード取得に癖があったので力技でした。

スクレイピングのやり方ですが簡単に取得できます。(chromeの場合)
デベロッパーツールを開く(検証モード) → 指定ノードの箇所で右クリック → Copy → Copy selector

スクリーンショット 2020-01-10 11.57.13.png

コンソール上で以下を入力することでも確認できます。
document.querySelector('{copyしたノード}').textContent

jsなのでそのままコードで使えますが、今回は puppeteerの用意したものを使います。

constbrowser=awaitchromium.puppeteer.launch()constpage=awaitbrowser.newPage()awaitpage.goto({webUrl})consttag=awaitpage.$('#wrapper > div.main_con02 > div.table_access > table > tbody > tr:nth-child(21) > td > p.status_text')constprop=awaittag.getProperty('textContent')consttext=awaitprop.jsonValue()console.log(text)// 水郡線は、台風の影響で、西金~常陸大子駅間の上下線で当面の間運転を見合わせます。同区間でバスによる代行輸送を実施します。 

それにしても長い。。。

7.AWSへデプロイ

さぁ、最後はAWSへデプロイしてCloudWatchを使って定時実行させれば完成です。
Lambdaの容量をなるべく節約して使うため不要なモジュールたちを削除します。
ローカルで使っていた puppeteerを除いた状態で再インストールします。
serverlessもLambdaには必要ないのですがデプロイコマンドで必要なのでproductionに含めます。

$ rm-rf node_modules
$ npm i --production

chrome-aws-lambdaもそこそこの容量なのでそのまま入れずzipに固めてLambda Layerに格納させます。
他のLambdaでスクレイピングしたい時はこのLayerが汎用的に使えて便利です。
READMEにしたがってzipに圧縮します。
パーミッションも変えないとデプロイできなかったので適宜変えてください。

$ git clone --depth=1 https://github.com/alixaxel/chrome-aws-lambda.git
$ cd chrome-aws-lambda
$ make chrome_aws_lambda.zip
$ chmod 777 chrome_aws_lambda.zip

Lambdaアプリをzipで固める前にさらに不要なファイルを除外します。

serverless.yml
package:exclude:-node_modules/serverless/**-node_modules/chrome-aws-lambda/**-chrome-aws-lambda/**layers:chrome:package:artifact:./chrome-aws-lambda/chrome_aws_lambda.zip

最後にデプロイして終了!

ちょっとしたアプリですがモジュールを入れるとそこそこのサイズになりますね。
incoming-webhook.zip file to S3 (25.49 MB)
chrome_aws_lambda.zip file to S3 (41.63 MB)

$ sls deploy --verbose--profile dev
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service incoming-webhook.zip file to S3 (25.49 MB)...
Serverless: Uploading service chrome_aws_lambda.zip file to S3 (41.63 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
CloudFormation - UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - incoming-webhook-dev
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::LayerVersion - ChromeLambdaLayer
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::LayerVersion - ChromeLambdaLayer
CloudFormation - UPDATE_COMPLETE - AWS::Lambda::LayerVersion - ChromeLambdaLayer
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Function - SendTrainDelayToSlackLambdaFunction
CloudFormation - UPDATE_COMPLETE - AWS::Lambda::Function - SendTrainDelayToSlackLambdaFunction
CloudFormation - UPDATE_COMPLETE_CLEANUP_IN_PROGRESS - AWS::CloudFormation::Stack - incoming-webhook-dev
CloudFormation - DELETE_IN_PROGRESS - AWS::Lambda::LayerVersion - ChromeLambdaLayer
CloudFormation - DELETE_COMPLETE - AWS::Lambda::LayerVersion - ChromeLambdaLayer
CloudFormation - UPDATE_COMPLETE - AWS::CloudFormation::Stack - incoming-webhook-dev
Serverless: Stack update finished...
Service Information
service: incoming-webhook
stage: dev
region: ap-northeast-1
stack: incoming-webhook-dev
resources: 7
api keys:
  None
endpoints:
  None
functions:
  sendTrainDelayToSlack: incoming-webhook-dev-sendTrainDelayToSlack
layers:
  chrome: arn:aws:lambda:ap-northeast-1:*:layer:chrome:9

Stack Outputs
SendTrainDelayToSlackLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:*:function:incoming-webhook-dev-sendTrainDelayToSlack:17
ChromeLambdaLayerQualifiedArn: arn:aws:lambda:ap-northeast-1:*:layer:chrome:9
ServerlessDeploymentBucketName: incoming-webhook-dev-serverlessdeploymentbucket-*

Serverless: Removing old service artifacts from S3...
Serverless: Run the "serverless"command to setup monitoring, troubleshooting and testing.

参考

HomebrewでMySQL8を入れたが、node.jsから接続できない(認証方式変更)

$
0
0

エラー内容

Client does not support authentication protocol requested by server; consider upgrading MySQL client

原因

MySQL8から認証方式が変わった(caching_sha2_password)ようで、
Homebrewからインストールできるmysqlはその認証方式に対応していないのが原因っぽい。

やったこと

パスワードのポリシーを確認

SHOW VARIABLES LIKE 'validate_password%';

認証方式の変更

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';

反映

flush privileges;

確認

SELECT user, host, plugin FROM mysql.user;

 2020-01-10 16.39.54.jpg

参考

https://stackoverflow.com/questions/50093144/mysql-8-0-client-does-not-support-authentication-protocol-requested-by-server
https://its-office.jp/blog/web/2019/02/13/mysql8.html
https://qiita.com/ucan-lab/items/3ae911b7e13287a5b917

Windows10で快適な開発環境を得るための道のり~Node.js編~

$
0
0

はじめに

環境

  • WIndows 10 Home
  • WSL2
  • Ubuntu 18.04

導入手順

node.js, npmをインストールする.

$ sudo apt install -y nodejs npm

再ログイン後,バージョンの確認.(なぜかnodejsでコマンドで登録される.)

$ nodejs --version
v8.10.0

古い..

安定版の最新バージョンを使用したいのでn packageを導入する.

$ sudo npm install n --global
/usr/local/bin/n -> /usr/local/lib/node_modules/n/bin/n
/usr/local/lib
└── n@6.1.3

※n packageについては,こちらで詳しく紹介されています. → Node.jsのバージョンを管理するライブラリ「n」

n packageを用いて,安定版の最新バージョンをインストールする.

$ sudo n stable

  installing : node-v12.14.1
       mkdir : /usr/local/n/versions/node/12.14.1
       fetch : https://nodejs.org/dist/v12.14.1/node-v12.14.1-linux-x64.tar.gz
   installed : v12.14.1 (with npm 6.13.4)

Note: the node command changed location and the old location may be remembered in your current shell.
         old : /usr/bin/node
         new : /usr/local/bin/node
To reset the command location hash either start a new shell, or execute PATH="$PATH"

最初に入れた古いnode.js, npmは削除する.

$ sudo apu purge -y nodejs npm
$ exec $SHELL -l

バージョンの確認

$ node --version
v12.14.1

→ OK!

参考

Viewing all 8820 articles
Browse latest View live