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

QualityForward APIを使ってテストスイートを作成する際の注意点

$
0
0

QualityForwardはクラウドベースのテスト管理サービスです。APIを公開しており、テスト管理に関するデータのCRUD操作ができるようになっています。テストケースであったり、それらをまとめたテストスイートなどを作成できます。

今回、APIを使うためのSDKを開発していてハマったポイントについて、メモしておきます。

テストスイートが表示されない

テストスイートは次のAPI操作で作成できます。

POST /api/v2/test_suites.json

そして、作成はできるのですが、なぜか一覧には出ません。Node.js SDKで書くと、次のようなコードです。

constcount=testSuites.length;consttestSuite:TestSuite=client.TestSuite();testSuite.name='APIから作成したテストスイート';testSuite.project_id=748;testSuite.label_category1='機能カテゴリ';testSuite.use_category1=true;testSuite.label_content1='環境';testSuite.use_content1=true;testSuite.coverage_panel_column=null;awaittestSuite.save();

Web上での操作を真似る

そこでWeb上の管理画面での操作を見てみたところ、テストスイートを作成すると同時にテストスイートバージョンが作成されていることが分かりました。テストスイートバージョンはテストスイートをバージョン管理し、同じテストケースを繰り返しテストするための仕組みです。

つまりテストスイートを作成しただけでは十分ではなく、同時にテストスイートバージョンも作成する必要がありました。コードとしては、下記を追加します。

// テストスイートバージョンの作成consttsv=testSuite.version();tsv.name='バージョン1';// テストスイートにセットtestSuite.setVersion(tsv);// そしてテストスイートを保存awaittestSuite.save();

こうしてあげることで、テストスイート一覧にAPIから作成したテストスイートが表示されるようになりました。

Screenshot_ 2020-01-22 16.09.08.png

まとめ

APIドキュメントには、まだアンドキュメントな部分が多く、手探りが必要な部分が多々あります。SDKは現在、Node.js / Python / Ruby / Google Apps Script版を開発しています。SDKから利用することで、なるべくアンドキュメントなところをエラーで返すようにしていきますので、ぜひご利用ください。

QualityForward


[Visual Studio Code] [MacOS] .nvmrcで指定したバージョンに自動で切り替えてプロジェクトをスタートする

$
0
0

複数の Node.js プロジェクトに参加していると、利用すべき Node のバージョンがプロジェクト毎に異なる場合があって、毎回手動で切り替えるのは大変面倒なので自動化します。

動作確認環境

  • MacOS
  • Visual Studio Code

にて動作を確認しております。Mac かつ VSCode で開発をしていて、ビルドなども VSCode 上のターミナルで行なっている方は参考になると思います。

ロードマップ

  1. nvmのインストール
  2. Visual Studio Code のターミナルをzshに設定する
  3. .zshrc を作成、または編集する
  4. 自動バージョン切り替えを実行したいプロジェクトで .nvmrc を作成する

1.nvmのインストール

日本語の参考記事がたくさんありますのでそちらを参照してください。

nvm use <バージョン>

コマンドでいくつかの node バージョンを切り替えられるようになればOKです。

2.Visual Studio Code のターミナルをzshに設定する

まずは現在の VSCode のターミナルのシェルが何か、確認します。
Image from Gyazo

青枠部分がzshでない場合は設定で変更します。Mac 自体が2019年に、デフォルトの処理を bash から zsh に切り替えているので、基本的には VSCode もそれに沿う形で問題ないはずです。既に bash でいろいろ設定やってるよという方は zsh に切り替えることの影響を考慮する必要があります。
Image from Gyazo

⌘ + ,で設定パネルを開き、
Terminal › Integrated › Automation Shell: Osx
の項目で Edit in settings.jsをクリックします。

Image from Gyazo

"terminal.integrated.shell.osx": "/bin/zsh"
terminal.integrated.shell.osx の値を上記のようにします。基本的にこちらの値で問題ないと思いますが、環境によって、zshがインストールされていない、場所が違うなどあるかもしれませんのでご確認ください。

編集したら改めて VSCode 上のターミナルを確認し、zsh となっていればOKです。

.zshrc を作成、または編集する

基本これで良いはず。

$vim .zshrc
source ~/.nvm/nvm.sh
# place this after nvm initialization!
autoload -U add-zsh-hook
load-nvmrc(){if[[-f .nvmrc &&-r .nvmrc ]];then
    nvm use
  elif[[$(nvm version)!=$(nvm version default)]];then
    echo"Reverting to nvm default version"
    nvm use default
  fi}
add-zsh-hook chpwd load-nvmrc
load-nvmrc

既に .zshrc ファイルが存在している場合は既存の設定内容に影響がないか注意が必要です。また、1行目の nvm のパスも環境によっては違うかもしれません。

4.自動バージョン切り替えを実行したいプロジェクトで .nvmrc を作成する

ここまでくればただただバージョン番号が書かれた.nvmrcファイルがプロジェクトディレクトリの直下に存在していれば、プロジェクトを開いた際にnvmによるバージョン切り替えを行なってくれます。バージョン番号指定ではなくlts/*のように安定バージョンの最新というような書き方もできます。(が、該当バージョンをnvmでインストール済みでないと動かないと思います。私は常にバージョン番号で指定しているので未確認です。)

.nvmrc はコマンドラインでサクッと作成してしまいましょう。

$echo"8.13.0"> .nvmrc #番号指定$echo"lts/*"> .nvmrc #最新バージョン

以上で設定完了です。VSCode で該当プロジェクトを開き、VSCode 上のターミナルを立ち上げれば .nvmrc で指定したバージョンに切り替えたことを示すメッセージがターミナルで確認できるはずです。

また、複数でプロジェクトを進める際に node のバージョンをバシッと統一するときもこの形がスマートだと思います。

参考リンク

https://qiita.com/ysd_marrrr/items/e58df8dfd509b25ff9c9
https://medium.com/fbdevclagos/updating-visual-studio-code-default-terminal-shell-from-bash-to-zsh-711c40d6f8dc
https://medium.com/@kinduff/automatic-version-switch-for-nvm-ff9e00ae67f3

完全にクラウドで完結する無料の Web 開発環境 2020 年春

$
0
0

目的

2020 年春時点での、ぼくのかんがえたさいきょうのうぇぶかいはつかんきょうを作ります。完全にクラウドで完結する Web 開発環境を無料で構築します。ここで言う「完全にクラウドで完結する」とは、環境をセットアップしてコードを書き、テストをして本番環境にデプロイするまでの全てをブラウザだけで完結することを指します。ローカルのコマンドラインツールやローカルで動く IDE などは一切使わないというのがポイントです。

つまり、Windows、Linux、Mac など OS の依存がないだけでなく、ブラウザが動く環境さえあれば良いので、iPad や Android タブレット、果てはネカフェの PC でもそのままに適用することが出来ます。また全ての環境がクラウド側にあるという事は、作業中の状態なども全てクラウド側にあるという事です。これはスタバでドヤるどころではない究極のノマド環境です。ネットがあってブラウザがあれば、いつでもどこでも開発が出来ます

本当に無料?
開発環境までは無料ですが、本番環境で一部課金が必要になる場合があります。2020 年春時点での限界です。

環境の概要と選択理由

この開発環境は 3 つの PaaS に依存しています。GitHub、Visual Studio Online (以下 VS Online)、Heroku で、それぞれの役割分担は下記のようになっています。

  • GitHub
    • git リポジトリの保持・管理
    • 本記事では触れませんが必要に応じてイシュー管理など
  • VS Online
    • IDE
    • 従来のローカル環境に相当する動作確認テスト
  • Heroku
    • 本番環境
    • 独自ドメインのホスト
    • 本記事では触れませんが必要に応じて CI/CD

実は Heroku の部分に関しては、Google App Engine や Azure Web Apps でも代替可能なのですが、今回は Node.js をターゲットにしており、また独自ドメインを使いたかったので、Heroku という選択肢になりました。

2020 年春時点で、Google App Engine には Node.js を使用する無料のオプションがなく、Azure Web Apps には独自ドメインを使う無料のオプションがありません。これらは将来的に変わる可能性があるので、現時点での最適解と受け止めてもらえればと思います。例えば Python を使うのであれば Google App Engine が良いかもしれませんし、独自ドメインが不要であれば Azure Web Apps が良いかもしれません。個人的には Azure Web Apps に一番期待をしています。同じ Microsoft の傘の下で、GitHub、VS Online、Azure Web Apps の連携が将来的にもっと良くなる可能性があるからです。

環境構築

以下、環境構築の手順を解説していきます。スクリーンショットが概ね英語になってますが、これは VS Online がそもそもまだ英語しかない、Heroku のアカウント作成にバグがあり (?) 日本語だと失敗した、などの理由によるものです。日本語環境で作業される方も UI は変わらないでしょうから、適宜読み替えて下さい。

新規レポジトリの作成

まず、お手持ちの GitHub アカウントで GitHub に新規レポジトリを作成します。そんなに注意点はありませんが、VS Online 環境の構築が上手くいかないので、リポジトリは公開設定にしておいて下さい。後述する VS Online 上での GitHub の認証を済ませた後ならプライベートに設定しなおしても大丈夫なのですが、初期段階では公開設定にしておいて下さい。

今回は Node.js アプリを作るので、.gitignoreのデフォルトを Node にし、ライセンスはいつもの MIT にしています。
01_create-a-new-repository.png

VS Online 環境の構築

VS Online 環境を構築するにあたって、持っていない場合はあらかじめ MS アカウントを作っておいて下さい。MS アカウントを有効にしてログインしたら、VS Online のログインページにアクセスします。
02_sign-in-to-vsonline.png

ここで VS Online にサインイン "Sign in" すると規約への同意を求められ、その後、新しい環境を作成 "Create environment" 出来るようになります。適当な名前を付けて、"Git Repository" に先ほど作ったリポジトリの "https" の URL を指定して下さい。SSH だと動作しないので注意が必要です。また、前述のようにリポジトリは公開設定である必要があります。
03a_create-environment.png

それ以外の項目はデフォルトのままで良いでしょう。現状 VS Online はパブリックプレビューの段階でこれまで課金は求められていないですが、将来的に課金が求められる可能性を鑑みデフォルトの設定、つまり最低パフォーマンスにとどめておく方が無難であろうと思います。

VS Online 環境の作成には数十秒の時間がかかり、その後、作成された環境に接続 "Connect" すると、ブラウザ上で VS Code が立ち上がりコードが書ける状態になります。なかなかのインパクトですね。
03c_create-environment.png

VS Online からコードをプッシュする

コードを書いたら当然 GitHub にプッシュしなければ始まらないのですが、なにか特別な設定が必要でしょか?否、この時点ですでに必要な設定は完了しているのです。

まずメニューからターミナルを立ち上げます。
04a_new-terminal.png

すると見慣れたプロンプトが立ち上がりますが、この時点で、VS Online 上のインスタンスに SSH した状態になっています。

そうしたらいつものように、addして commitして pushするだけです。
04b_git-commands.png

なお、初回やトークンの期限が切れた時は GitHub の OAuth 認証を求められるので、VS Online からのアクセスを許可して下さい。またここで OAuth 認証をした後であれば、GitHub 側でリポジトリをプライベートに設定しても問題ありません。

Web アプリを作る

いよいよ VS Online 上で Web アプリを作っていきます。今回は検証用なのでごくごく簡単な Web アプリです。VS Online 上のターミナルを使って、いつものように作成してきます。

最初にやる事は Node.js のバージョンの固定です。現在のデフォルトバージョンは v12 で、そのままでも現状なら問題はないのですが、将来的にデフォルトのバージョンが上がった際、後述の Heroku でのデフォルトバージョンとの間に差異があると面倒なことになりかねません。ですので、VS Online でも Heroku でも v12 を明示的に指定して進める事にします。

といっても何も面倒なことはありません。VS Online 環境には nvmもインストール済みなので、いつものようにするだけです。

$ nvm install 12
$ nvm use 12

続いて Web アプリの初期設定、必要なパッケージのインストールをしていきましょう。これまたいつも通りですね。

$ npm init
$ npm install express --save

プロジェクトのルートディレクトリに app.jsを作りごくごく簡単な Web アプリを作ります。

app.js
constexpress=require('express');constapp=express();constport=3000;app.get('/',(req,res)=>res.send('The app is running!'));app.listen(port,()=>console.log(`Example app listening on port ${port}!`));

package.jsonにサーバを立ち上げるコマンドを追加しましょう。

package.json
"scripts":{"start":"node app"}

あとは走らせるだけ。簡単ですね。

$ npm start

VS Online 環境上の Web アプリを開く

さて前項で立ち上げた Web アプリは VS Online のインスタンス上で 3000 ポートを開いて走っています。ローカル環境であればここで http://localhost:3000を開いて動作確認するところですが、同じようには行かないのでポートフォワーディングの設定をします。

下記の設定を手動でしなくても自動で同様の設定がされていることがあるようです。条件はよく分かっていないのですが、ポートを開いただけで設定済みの状態になってたらラッキーくらいに思っておけば良いと思います。

左側のメニューの中に、VS Online の設定をするアイコンがあるので開いて、"ENVIRONMENT DETALIS" > "Forwarded Ports"の右にあるコンセントのアイコンをクリックすると、フォワードするポートが入力できるようになるので、3000 と入力して Enter します。
07a_tcp-port-forward-2.png

するとポートフォワーディングの設定が完了しブラウザから先ほど作った Web アプリが開けるようになります。ここでは設定の名前をデフォルトのまま "localhost:3000" としているので、そのエントリがメニューに追加されます。ここで "localhost:3000" はそのままブラウザで開くリンク、その右のアイコンはクリップボードへの URL のコピー、一番右のアイコンは設定の削除です。
07b_tcp-port-forward.png

このポートフォワーディングされた先の URL を開き、"The app is running!" と表示されればバッチリです。あとはライブラリを追加するなり、ファイルを増やすなりして下さい。ローカルの VS Code を使って Node.js アプリを作っている時と同じ事が全部クラウド上で実現します。

Web アプリを Heroku 上で動くように修正する

VS Online 上で開発し動作確認も出来た Web アプリを本番環境である Heroku 上で動かすためには、いくらかの変更を加える必要があります。

まず、Heroku 上で走らせる Web アプリに正しく接続できるようにするためには、Heroku 側から指定されているポートでサーバを立ち上げないといけません。環境変数 PORTにその値が格納されているので、下記のように app.jsで指定します。

app.js
constport=process.env.PORT||3000;

次に、VS Online で Node.js のバージョンを固定したのと同様、Heroku でもバージョンを固定するために、package.jsに記述を追加します。

package.json
"engines":{"node":"12.x","npm":"6.x"}

これだけで基本的には動くはずですが、package.jsonの "scripts" でより細かな制御を行うことも出来ます。

package.json
"scripts":{"start":"node app","build":"webpack","heroku-postbuild":"node heroku-special-command && webpack"}

"start" に関しては変わりません。サーバを立ち上げるためのコマンドです。"build" を指定すると "start" の前に呼ばれます。webpackを呼び出したりするのが典型的な使い道になります。"heroku-postbuild" は Heroku 専用の "build" スクリプトです。これが定義されている場合は、"build" の代わりに呼ばれます。VS Online 上でのビルドと、Heroku 上でのビルドで異なる動作が必要な場合に使えますね。

通常これだけで十分かと思いますが、より細かな制御についてはオフィシャルドキュメントを参照して下さい。

Web アプリを Heroku にデプロイする

さあ、いよいよ本番環境へのデプロイです。Heroku のアカウントはあらかじめ作成しておいて下さい。Heroku のダッシュボードにログインしたら、新規アプリの作成 "Create a new app" をします。
09b_create-a-new-app.png

アプリの名前は、Heroku が自動的に付与するサブドメイン名でも使用されるので、アルファベット、数字、ハイフン、しか使えません。リージョンは現在のところアメリカかヨーロッパしかないので、日本のユーザが大半ならアメリカにしておくのが良いでしょう。パイプラインは、CI/CD をやるのであれば設定が必要ですが、本記事では触れません。

アプリを作成したら "Deploy" タブの "Deployment method" で GitHub を選んで、GitHub に接続 "Connect to GitHub" しましょう。
09c_connect-to-github.png

例によって OAuth 認証が求められるので GitHub へのアクセスを Heroku に許可すると、GitHub 上のリポジトリが一覧から選べるようになるので、最初に作ったリポジトリを選びます。
09e_connect-to-github.png

あとは同じページの一番下、"Manual deploy" で対象のブランチ (デフォルトは master) を選んでブランチをデプロイ "Deploy branch" すると、手動によるデプロイが始まります。
09i_manual-deploy.png

上部のボタンからアプリを開く "Open app" と、https://[指定した名前].herokuapp.com/が開くので、そこで "The app is running!" と表示されていればデプロイ完了です。

自動デプロイを設定する

手動でのデプロイが確認出来たら次にこれを自動化します。といっても、"Manual deploy" のすぐ上 "Automatic deploys" で対象のブランチを選択し自動デプロイを有効化 "Enable Automatic Deploys" するだけです。すると下記のように、緑のチェックマークが表示されます。
10a_automatic-deploys.png

この状態で VS Online を開き例えば app.jsの一部を編集して変更を git にプッシュすると、程なくして Heroku 上で自動的にデプロイが走ります。その様子は "Activity" タブで確認出来るので、デプロイが終わったら先ほどの URL で変更後の状態が確認出来るはずです。
10b_activity.png

独自ドメインを設定する

URL は https://[指定した名前].herokuapp.com/のままでいいや。どうせ趣味だし。くらいの場合は、これ以降の設定は必要ありません。herouapp.comだとちょっと格好付かないなぁと思った場合は、お手持ちのドメインを Heroku に設定して運用することが出来ます。

独自ドメインの設定自体は簡単にできるのですが、初回のみ Heroku アカウントの認証を行う必要があります。認証というのはクレジットカードの登録で、ユーザメニューの "Account Setting" を開き、"Billing" タブの "Billing Information" から行います。
11a_verify-account.png

クラジットカードの登録が終わるとこのように表示されて、アカウントの認証が済んだことになります。クレジットカードを登録しただけで課金されることはないので安心して良いですが、独自ドメイン自体に課金はせずクレジットカードの登録だけ求めるというのは、いずれ課金に誘導したいサービスとしては上手いところを突くなと思いました。
11c_verify-account.png

アカウントの認証が済んだら、"Setting" タブの中にある "Domains" セクションからドメインを追加 "Add domain" します。"Domain name" には使用する予定のドメイン名を入力してください。
11f_add-custom-domain.png

ここから設定を進めるとそのドメイン専用の "DNS target" が発行されるので、これをメモしておきます。
11g_add-custom-domain.png

そしたら、次にお手持ちの DNS サーバの管理画面で "CNAME" レコードに先ほどの "DNS target" を指定してください。ここでは手元で確認出来る、Google と提携している GoDaddy 並びにさくらインターネットの管理画面の例を載せておきますが、どこのサービスもそんなに大きく違わないのではないかと思います。発行された "DNS target" の末尾に .を足す必要があるかどうかは DNS 管理サービスによって異なるので注意してください。
11h-dns-setting.png
11i-dns-setting.png

以上で設定完了です。簡単ですね!一つ注意点としては、Heroku のサーバは pingを返さない (ICMP に応答しない) ので、疎通確認を pingだけでやっていると見誤ります。nslookupが返ってくるのを待って HTTP で疎通確認するのが良いでしょう。

SSL を設定する

これで最後の設定になります。独自ドメインのサイトに対して、Let's Encrypt の SSL 証明書を設定し HTTPS を有効にします。令和の時代になっても HTTP なんて寝ぼけたことは言ってられないですからね。

ただ…ごめんなさい、かく言う筆者も途中まで気づいていなかったのですが、タイトルに「無料」と書きながらここだけは課金が必要になります。Google App Engine は無料で独自ドメインの SSL を提供しているので、どうしても無料で完了させたい場合は Node.js を使わずに Google App Engine を使う方が良いかもしれません(GitHub 連携のやりやすさは試したことがないので分かりません)。

Heroku で独自ドメインの SSL が使えるのは、最低課金の Hobby プランからで $7/月になります。Hobby プランのもう一つの大きなメリットは、Free プランと違ってアイドル時間が長くなってもインスタンスがサスペンドされる事がない点で、アクセスが少ないサイトであってもスピンアップの時間を気にする必要が無くなります。あと、Google App Engine で Node.js を運用するより、少なくとも手元の計算では安いです。

まず "Resources" タブを開き "Change Dyno Type" ボタンから "Hobby" プランを選択し保存 "Save" します。
12b_change-dyno-type.png

これだけで SSL が設定可能になるので、"Settings" タブの "SSL Certificates" セクションで SSL を設定 "Configure SSL" します。そこで ACM を選べば設定は完了です。
12d_configure-ssl.png

数十秒も待てば自動設定が終わるはずなので "Settings" タブをリロードし、"ACM Status" の欄が Ok になっていれば完了です。https://[設定済み独自ドメイン]にアクセスしてみましょう。Let's Encrypt で暗号化されたサイトが表示されるはずです。
12c_configure-ssl.png

あとがき

以上で、2020 年春時点での、ぼくのかんがえたさいきょうのうぇぶかいはつかんきょうの設定は完了です。あとはもう好きなだけ、いつでもどこでもコードが書けます!

なお前述のように Azure Web Apps には大きな期待をしています。Azure Web Apps が独自ドメイン+SSL を無料でサポートしたあかつきには、最強の座は Azure に渡ることになりますからね。

雑感

こと Web の開発に関しては、GitHub を買って、npm までも (間接的に) 買って、VS Code 作ってオンライン化して、TypeScript を業界標準に押し上げ、そして Azure を擁する Microsoft 最強じゃないですか? Microsoft がこんなに Web に強い会社になるとは 10 年前には考えられなかったです。

付録

作業時にもっと詳細まで取得していたスクリーンショットを GitHub上で公開しています。どこまで役立つか分かりませんが参考までに。

Puppeteerなら、Google App Engine Node.jsでスクレイピングしてスプレッドシート(GAS経由)にデータを蓄積するのが無料だよ!

$
0
0

以下の記事の概要です。

  • 日経225オプションのデータを入手したい。(理由は、分析して儲けるため)
  • 私が契約している証券会社は、Javascriptでデータを取得するタイプのサイトなので、Headless Chromeの利用が必須。
  • これまでの経験で、Google App Engine(python)は使えるが、それ以外はハードル高い。
  • Cloud Runに手を出そうとしたが、ちょっとハードルが高かった。
  • Google App Engineだと、Javascriptでデータ取得するタイプのサイトにスクレイピングするのは、無理。
  • と思っていたら、Puppeteerなるツールがあって、それを使えば、Google App Engineで運用できると最近知った。
  • てことで、Puppeteerでアプリを作って、Google App Engine Node.js(GAE)で運用しようということになった。
  • GAEなら、cronで定期実行可能なので、定期的にデータを取得できる。
  • 定期的にデータが取得できるなら、Googleスプレッドシートへ書き込んでおけば、後から分析可能だよね。
  • 分析する時は、BigQueryが有るから、楽勝じゃん。
  • という思いつきで、だいたい1ヶ月(2月半ばから3月半ばまで)くらいかかって、作ったのが以下のシステムです。(1ヶ月もかかってこれかよ?ってツッコミは無しで。。。。褒めて育つタイプなので。)
  • 本業でフロントエンドをjavascriptで書いてる人なんて、こんなツールで、自動テストとかして、爆速で自動化してるんだろうなーーー(遠い目)。
  • 2020/1月後半から、コロナウイルス大流行で、日経平均爆下げ状態。(プロはこんな時こそボロ儲けのようです。:私じゃない。涙)
  • もう少し、安定してる時が良かったけど、仕方がない。
  • データをためて、頑張って分析しよう!!
  • もうちょっとこうした方が、ええ感じやで等、ゆるめのツッコミ歓迎です。
  • ちなみに、私はエンジニアではなく、ただのサンデープログラマかつ、コピペプログラマです。自分に必要な事しか知らないです。

前提事項
  • puppeteerをスクレイピング用アプリとして利用します。(言語はNode.js(javascript)です。)

    • Google App Engine(GAE) Node.jsでスクレイピングする為にpuppeteerが必要です。
    • GAEでは、特殊なメソッドなどを利用することが多いですが、ほぼpuppeteerだけで動きます。
    • puppeteerが、ブラウザを動かし、ブラウザ経由でWEBサイトにアクセスするので、GAEの特殊メソッドが不要です。
  • Google App Engine Node.jsをスクレイピングの基盤として利用します。

    • ここチェックしてくださいね。無料で利用できる枠の説明です。Google Cloud Platform の無料枠
    • Google App Engine Node.jsは、Cloud Buildが必要で、Cloud Buildを利用するには、課金が必要です。
    • Cloud Buildは、コンテナをビルドするためのツール的なものだと思われます。(分かってません。)
    • 詳しくは、こちら超入門!GCP のCIツール、Cloud Build でリリースサイクル高速化!!
    • Cloud Buildに課金は必要ですが、無料で利用可能です。(ただし、操作が必要です。操作方法は後ほど出てきます。)
    • Cloud Buildを利用するってことは、自分ではpuppeteerアプリを書いてるけど、AppEngine上ではコンテナで動いてるはず。
    • コンテナの知識不要だから、Cloud Run よりもハードルが低いです。しかも、軽い処理ならだいたい無料で動きます。
  • Google Apps Scriptを利用します。

    • puppeteerでスクレイピングしたデータを受け取る処理をします。
    • 受け取ったデータをGoogleスプレッドシートへ書き込みます。
  • Googleスプレッドシートを利用します。

    • puppeteerでスクレイピングしたデータをGoogle Apps Scriptを通じてGoogleスプレッドシートへ蓄積していきます。
    • 最終的に、BigQueryで分析して、投資で儲けます。(ホンマか?とつっこむところ!)
    • ちなみに、BigQueryは、SQLが分かれば使えます。クレジットカードの登録は必要ですが、趣味程度で金払うことは多分無いです。(自己責任でよろしく)
    • Googleスプレッドシートにデータを入れておけば、BigQueryを利用する際のデータの投入が簡単です。(ここでは触れません。)
  • 私のローカル環境は、ChromeBook(HP CromeBook x360)です。

    • Visual Studio Code 1.41.1
    • linuxは、Debian 9.11
    • Node.js v12.14.0
    • npm 6.14.3
    • これを書いた日:2020/3/24

puppeteer(Node.js)の準備
  • とりあえずは、ローカル環境でやってみる。
  • うまくいったら、Cloud Shellで実行してみる。
  • ホームディレクトリで、手動で以下を実施する。
  • (もしかして、以下の手動コマンドが無理なら、更に下のinstall.shを先に実行したらいいかも。)
mkdir ppt
cd ppt
npm init -y
  • 上記コマンドで、package.jsonができる。
  • 以下のように、"scripts": { の中に、"start": "node app.js",を追加してpackage.jsonを保存する。
package.json
{"name":"ppt","version":"1.0.0","description":"","main":"index.js","scripts":{"start":"node app.js","test":"echo \"Error: no test specified\"&& exit 1"},"keywords":[],"author":"","license":"ISC"}
  • 現時点で、lsすると、package.jsonだけがあります。

  • 次に、install.shを現在のディレクトリに作成します。

  • touch install.sh とかで空ファイル作ってもいいし、エディタで作ってもいいですね。

  • install.shに以下コードを貼り付ける。 echoうるさめです。

install.sh
#!/bin/bashecho'Cloud Shell では、毎回以下のエラーが出る'echo'(node:791) UnhandledPromiseRejectionWarning: Error: Failed to launch the browser process!'echo'/home/あなたのID/ppt/node_modules/puppeteer/.local-chromium/linux-722234/chrome-linux/chrome: error while loading shared libraries: libXss.so.1: cannot open shared object file: No such file or directory'echo'回避するために、以下のsudo apt-get install libxss1 を実行する'echo'面倒なので、いろいろまとめてこのシェルを実行する'echo-----------------echo-----------------echo-----------------echo npm install request
npm install request

echo-----------------echo-----------------echo-----------------echo npm install date-fns-timezone
npm install date-fns-timezone

echo-----------------echo-----------------echo-----------------echo npm install-g npm
npm install-g npm

echo-----------------echo-----------------echo-----------------echo npm install express puppeteer --save
npm install express puppeteer --saveecho-----------------echo-----------------echo-----------------echo sudo apt-get update 
sudo apt-get update

echo-----------------echo-----------------echo-----------------echo sudo apt-get install libxss1
sudo apt-get install libxss1

echo-----------------echo-----------------echo-----------------echo cd /home/あなたのID/ppt/node_modules/puppeteer/.local-chromium/linux-722234/chrome-linux
cd /home/あなたのID/ppt/node_modules/puppeteer/.local-chromium/linux-722234/chrome-linux

echo-----------------echo-----------------echo-----------------echo'ldd chrome | grep not'echo'libXss.so.1 => not found  <=
echo   '↑↑これがでないように sudo apt-get install libxss1 した'
echo   'だから、下の命令の結果は、何も表示されない'
ldd chrome | grep not

  • 実行は、 ./install.sh 
  • 実行できない時は、以下のコマンドを試すべし(実行権限追加)
  • sudo chmod +x install.sh
  • Croud Shellでは起動のたびに実行しないとエラーが発生したので、めんどくさくて作成した。
  • シェル実行後にlsすると、install.sh node_modules package.json package-lock.json がある。
  • 最後に、app.jsを作成し、以下の「puppeteerのソースコード」を貼り付ける。
  • 「puppeteerのソースコード」の、あなたのID、あなたのパスワード、証券会社のURL、GASで発行したURLを自分の物に書き換えれば、動くはず。
  • GASで発行したURLは、後ほど出てきます。
  • 証券会社名は、ヒントが有るのですぐにわかりますよね。

puppeteerのソースコード
app.js
//Node.js//var、let、constの使い方が適当なので、ええ感じに書き換えてね!asyncfunctionrun(){constuser_id='';//s??証券のあなたのIDconstuser_pass='';//s??証券のあなたのパスワードconstshouken_url='';//証券会社のURL(s??証券)constspreadsheet_url='';//GASで発行したURLconstpptr=require('puppeteer');varnow1=newDate();constouttime=80000;constwaittime=8000;// ブラウザを起動するconstbrowser=awaitpptr.launch({// headless: false,//GAEにデプロイする時、CloudShellではコメントにして、ブラウザ非表示にする。ローカルテストの時はコメント外して、ブラウザの動きを確認してデバッグする。// 目視確認用に操作遅延(ms)これを入れておくと、間違いが少ない"slowMo":10,args:[// デフォルトでは言語設定が英語なので日本語に変更'--lang=ja,en-US,en',// Chromeウィンドウのサイズ'--window-size=1200,800',// Chromeウィンドウのポジション'--window-position=10,10','--no-sandbox',]})try{// ページつくるconstpage=awaitbrowser.newPage()page.setDefaultNavigationTimeout('120000')awaitpage.setViewport({width:1200,height:800})letnavigationPromise=page.waitForNavigation()awaitpage.goto(shouken_url,{timeout:outtime})console.log('証券会社 WEBサイトサイトへアクセス')awaitpage.waitForSelector('.sb-box-sub-02-content ',{visible:true,timeout:outtime})awaitpage.waitForSelector('.sb-box-sub-02-content > dl > dd > #user_input > input',{visible:true})awaitpage.click('.sb-box-sub-02-content > dl > dd > #user_input > input')awaitpage.type('.sb-box-sub-02-content > dl > dd > #user_input > input',user_id)awaitpage.waitForSelector('.sb-box-sub-02-content > dl > dd > #password_input > input')awaitpage.click('.sb-box-sub-02-content > dl > dd > #password_input > input')awaitpage.type('.sb-box-sub-02-content > dl > dd > #password_input > input',user_pass)awaitpage.waitForSelector('.sb-box-sub-02-inner > .sb-box-sub-02-content > .sb-position-c > a > .ov')awaitpage.click('.sb-box-sub-02-inner > .sb-box-sub-02-content > .sb-position-c > a > .ov',{timeout:outtime})console.log('クリック:ログイン')awaitnavigationPromisenavigationPromise=page.waitForNavigation()awaitpage.waitForSelector('#navi01P',{timeout:outtime})awaitpage.click('#navi01P > ul > li:nth-child(8) > a > img',{timeout:outtime})console.log('クリック:先物・オプション')awaitnavigationPromise//参考記事//Puppeteerで次ページへの遷移を待つ (別タブや別ウインドウの遷移を待つ)//https://qiita.com/hnw/items/a07e6b88d95d1656e02fconstnewPagePromise=newPromise(resolve=>browser.once('targetcreated',target=>resolve(target.page())));console.log('waitFor  : '+waittime)awaitpage.waitFor(waittime)awaitpage.waitForSelector('.md-t-box-btn-01-inner > .floatR > p > a > img')page.click('.md-t-box-btn-01-inner > .floatR > p > a > img',{timeout:outtime})console.log('クリック:先物・オプション取引サイト')constnewPage=awaitnewPagePromise;awaitnewPage.setViewport({width:1200,height:800})//newPage.waitFor(waittime)しないと、frameだけしかない状態で次の処理をしてエラーになるのを防ぐため。//もっとカッコいい方法があるはず!!console.log('waitFor  : '+waittime)awaitnewPage.waitFor(waittime)awaitnewPage.waitFor('frameset > frame',{timeout:outtime});//先物・オプション取引サイトは、frameset > frame > frameset > frameの構造//Frameの位置を正しく認識させる//画面上メニューの「取引」をクリックしたい//トップframeset下の0番目のframeを選し、その下のframeset下の1番目のframeを選択する//具体的にはココ!letframe_menu=awaitnewPage.frames()[0].childFrames()[0].childFrames()[1]//frameが選択できたので、idの#menu20をクリック//idのみでクリックできる場合は、これで良いframe_menu.click('#menu20')console.log('クリック:取引')awaitnewPage.waitFor('frameset > frame',{timeout:outtime});console.log('waitFor  : '+waittime)awaitnewPage.waitFor(waittime)//取引−オプション新規注文 をクリックawaitclick_submenu(newPage,outtime,waittime);//取引−オプション新規注文画面にいる状態//取引−オプション新規注文画面のframeに移動した状態letframe_main=awaitnewPage.frames()[0].childFrames()[2].childFrames()[1]//「20000より上」「17000より下」のボタンが有るか確認するletpath_down='/html/body/div[1]/table/tbody/tr/td[3]/span/span/form/table[2]/tbody/tr[2]/td/table/tbody/tr/td/table[1]/tbody/tr[4]/td[1]/input'letpath_up='/html/body/div[1]/table/tbody/tr/td[3]/span/span/form/table[2]/tbody/tr[2]/td/table/tbody/tr/td/table[1]/tbody/tr[2]/td[2]/input'letlink_down=awaitframe_main.$x(path_down)letlink_up=awaitframe_main.$x(path_up)varymd=awaityymmdd();if(link_up.length!=0){//「20000より上」があれば、上ページのデータを取得awaitclick_next(newPage,path_up,outtime,waittime);//画面に表示されるデータを取得するlettextdata=awaitgetTable(newPage,ymd);//取得したデータをスプレッドシートへ書きこむawaitpostSpreadsheet(textdata,spreadsheet_url);}//取引−オプション新規注文 をクリック//最初に開く画面へ移動awaitclick_submenu(newPage,outtime,waittime);//画面に表示されるデータを取得するlettextdata=awaitgetTable(newPage,ymd);//取得したデータをスプレッドシートへ書きこむawaitpostSpreadsheet(textdata,spreadsheet_url);if(link_down.length!=0){awaitclick_next(newPage,path_down,outtime,waittime);lettextdata=awaitgetTable(newPage,ymd);awaitpostSpreadsheet(textdata,spreadsheet_url);}varnow2=newDate();varnow11=now1.getMinutes()*60+now1.getSeconds()varnow21=now2.getMinutes()*60+now2.getSeconds()console.log('time : '+(now21-now11));awaitbrowser.close()console.log('Exit')console.log('Exit')}catch(e){//失敗時、すぐに止めたい。GAEのインスタンスを無駄に使わないawaitbrowser.close()console.log('ERR')console.log('ERR')console.log(e)logError(e)}//try {}//async function run(){asyncfunctionclick_submenu(newPage,outtime,waittime){letframe_menu2=awaitnewPage.frames()[0].childFrames()[0].childFrames()[1]letframe_submenu=awaitframe_menu2.$x('/html/body/form/div/table/tbody/tr/td/table[2]/tbody/tr/td[2]/span/a[2]')//                  ↑await これ忘れがち!! 忘れるとエラーになる。何度も失敗した!!awaitframe_submenu[0].click();console.log('クリック:取引 > オプション新規注文')awaitnewPage.waitFor('frameset > frame',{timeout:outtime});console.log('waitFor  : '+waittime)awaitnewPage.waitFor(waittime)}asyncfunctionclick_next(newPage,path,outtime,waittime){console.log('mainコンテンツのframe取得_click_next')letframe_main2=awaitnewPage.frames()[0].childFrames()[2].childFrames()[1]//上へor下へのボタンへのパスframe_submenu=awaitframe_main2.$x(path)console.log('クリック:次ページ')awaitframe_submenu[0].click();awaitnewPage.waitFor('frameset > frame',{timeout:outtime});console.log('waitFor  : '+waittime)awaitnewPage.waitFor(waittime)}asyncfunctiongetTable(newPage,ymd){//参考//puppeteerでの要素の取得方法//page.$のところ//https://qiita.com/go_sagawa/items/85f97deab7ccfdce53ea//日経平均、日経225先物期近 取得//日経平均、日経225先物期近、などが並ぶframeへ移動console.log('frame取得    toolBarコンテンツ')letframe_toolBar=awaitnewPage.frames()[0].childFrames()[1].childFrames()[1]//xpathのフルパスでテーブルタグのtrを指定する(IDで上手く取れない場合は、これが簡単)console.log('テーブルタグ取得 日経平均、日経225先物期近');lettbla=awaitframe_toolBar.$x('/html/body/table/tbody/tr/td/table/tbody/tr[2]')console.log('データ取得    日経平均、日経225先物期近');vartxtNikkei='';for(leti=0;i<tbla.length;i++){lettr=await(awaittbla[i].getProperty('innerHTML')).jsonValue();lettr1=tr.split('<')letdatas=[];//console.log(tr1)    for(letiintr1){//i=2は、日経平均 i=12は、日経225先物期近if(i==2||i==12){letinner=tr1[i].split('>')[1]if(inner!=''&&inner!=null&&inner!='\n'&&inner!='\n'){//上のconsole.log(tr1)で、発見ーーーーー↑↑↑↑↑↑↑↑↑↑↑↑↑↑            ↑↑↑↑↑↑↑↑↑↑↑↑ if(inner.toString().split(',').length>0){letsplitdata=inner.toString().split(',')if(splitdata[1]!=''&&splitdata[1]!=null){datas.push(splitdata[0]+splitdata[1]);//console.log('i = ' + i + ' ' + splitdata[0] + splitdata[1]);}else{datas.push(splitdata[0]);//console.log('i = ' + i + ' ' + splitdata[0]);}}}else{datas.push('-1');console.log('i = '+i+'日経平均のデータが空');}}}txtNikkei+=datas.toString();}//SQ日 取得console.log('frame取得    mainコンテンツ')letframe_main=awaitnewPage.frames()[0].childFrames()[2].childFrames()[1]console.log('テーブルタグ取得 SQ日');letsq=awaitframe_main.$x('/html/body/div[1]/table/tbody/tr/td[3]/span/span/form/table[2]/tbody/tr[2]/td/table/tbody/tr/td/table[2]/tbody/tr/td/table/tbody/tr[2]/td[2]/table/tbody/tr/td[2]')console.log('データ取得    SQ日');varSQ=await(awaitsq[0].getProperty('innerHTML')).jsonValue();//日経225オプションのデータ 取得 console.log('テーブルタグ取得 日経225オプション');lettbl1=awaitframe_main.$x('/html/body/div[1]/table/tbody/tr/td[3]/span/span/form/table[2]/tbody/tr[2]/td/table/tbody/tr/td/table[1]/tbody/tr[3]/td/table/tbody/tr[2]/td[2]/table/tbody/tr')console.log('データ取得    日経225オプション');vartextdata='';for(leti=0;i<tbl1.length;i++){vartr=await(awaittbl1[i].getProperty('innerHTML')).jsonValue();vartr1=tr.split('<')vardatas=[];for(letiintr1){letinner=tr1[i].split('>')[1]if(inner!=''&&inner!=null&&inner!='新規買'&&inner!='新規売'){datas.push(inner);}}textdata+=ymd+','+datas.toString()+','+txtNikkei+','+SQ+',\n';}console.log(textdata)returntextdata;}asyncfunctionpostSpreadsheet(textdata,spreadsheet_url){//参考 curlコマンドをPythonやnode.jsのコードに変換する方法//https://qiita.com/tottu22/items/9112d30588f0339faf9bvarrequest=require('request');varheaders={'Content-Type':'text/csv'};vardataString=textdata;varoptions={url:spreadsheet_url,method:'POST',headers:headers,body:dataString};functioncallback(error,response,body){if(!error){console.log(' OK post Google Spreadsheet');console.log(' OK statusCode :  '+response.statusCode);console.log(' OK result     :  '+body);}else{console.log(' NG post Google Spreadsheet');console.log(' NG statusCode : '+response.statusCode);console.log(' NG err        : '+error);}}request(options,callback);/*
    デプロイするまでは、curlでやっていたが、GAEでファイル書込は不可だったので、上記にした。
    GASのテストをする分には、curlが簡単で良かったかも。
    const exec = require('child_process').exec;
    exec('curl -v -H "Content-Type: text/csv" -X POST -d @./textdata.csv ' + spreadsheet_url
        , (err, stdout, stderr) => {
        if (err) { console.log(err); };
        console.log(stdout);
    });

    */};asyncfunctionyymmdd(param){//AppEngineは、UTCなのでAsia/Tokyoへタイムゾーンを変更//node.jsでタイムゾーンの変換処理にdate-fns-timezoneを利用する//https://qiita.com/kazuhiro1982/items/b1235a893ee874d8ff65const{startOfDay,addDays}=require('date-fns');const{convertToTimeZone}=require('date-fns-timezone');// タイムゾーン定義consttimeZone="Asia/Tokyo";// 現在時刻(UTC)を取得consttargetDate=newDate();// TimeZone付きDateに変換constnow=convertToTimeZone(targetDate,{timeZone:timeZone});varyear=now.getYear();// 年varmonth=now.getMonth()+1;// 月varday=now.getDate();// 日varhour=now.getHours();// 時varmin=now.getMinutes();// 分varsec=now.getSeconds();// 秒vardayOfWeek=now.getDay();//曜日 [ "日", "月", "火", "水", "木", "金", "土" ]if(year<2000){year+=1900;}if(month<10){month='0'+month}if(day<10){day='0'+day}if(hour<10){hour='0'+hour}if(min<10){min='0'+min}if(sec<10){sec='0'+sec}varymd=year+'/'+month+'/'+day+''+hour+':'+min+':'+sec;if(param=='week'){returndayOfWeek;}elseif(param=='hour'){returnhour;}else{returnymd;}};asyncfunctionrun2(){varweek=awaityymmdd('week');varhour=awaityymmdd('hour');if(week>=2&&week<=5){//火〜金[ "日", "月", "火", "水", "木", "金", "土" ]console.log('Start Job week:'+week+' hour:'+hour);run();}elseif(week==1&&hour>=7){//月曜かつ7時以上console.log('Start Job week:'+week+' hour:'+hour);run();}elseif(week==6&&hour<=7){//土曜かつ7時以下console.log('Start Job week:'+week+' hour:'+hour);run();}else{console.log('No Start  week:'+week+' hour:'+hour);}}run2();

puppeteerのソースコードの説明
  • 説明1
  • 以下の4ヵ所を書き換える
  • const user_id = '';//S??証券のあなたのID
  • const user_pass = '';//S??証券のあなたのパスワード
  • const shouken_url = '';//証券会社のURL(S??証券)
  • const spreadsheet_url = '';//GASで発行したURL(Google Apps Scriptの公開方法で、説明が出てきます。)

  • 説明2

    • // headless: false,//GAEにデプロイする時、CloudShellではコメントにして、ブラウザ非表示にする。
    • headless: false を有効にすると、ブラウザが表示されます。ローカルの開発環境で作業中の際は、こちらの方がデバッグしやすいです。
  • 説明3(frameのたどり方が分からなくて一番苦労したところ!)

    • //先物・オプション取引サイトは、frameset > frame > frameset > frameの構造
    • //Frameの位置を正しく認識させる
    • 証券会社_Frame構造01.JPG
  • 説明4(XPathが分かりにくくて苦労したところ!)

    • XPathを活用すると、ID等が簡単に指定できますが、そのままでは動かない場合や、うまくいかないことが多かった。
    • そのため、Chromeの「デベロッパーツール」で、ソースのたどりたいタグを右クリックして、Copy → Copy full XPath で full XPathを使っています。
  • 説明5

    • この時点で、app.jsが動くので、以下のコマンドで実行してみる
    • npm start  または、 node app.js
  • その他 
     - 参考にさせてもらったサイトは、ソースのコメントや、最後に記載してます。

Google Apps Scriptのソースコードとコメント
doPost(e)
functiondoPost(e){varcsvText=e.postData.getDataAsString();//テキストファイルにする(CSVになってる)Logger.log("csv : "+csvText);varspreadsheet=SpreadsheetApp.openById('');//書込先のスプレッドシートのIDvarsheet=spreadsheet.getSheetByName('シート1');//スプレッドシートのシート名vararr=csvText.split(',');//CSVをカンマでsplitする。vararrData=sheet.getDataRange().getValues();//現在スプレッドシートに入ってるデータを全部取得して、2次元配列にする。varlen_arrData=arrData.length;//現在スプレッドシートに入ってるデータが、何行あるか確認してるvarj=0;varsheet_cols=19;//GAEで取得したデータの1行当たりの列数が19なので、19としてる。/*
  (arr.length -1) の意味:GAE上で改行コードを入れたが、改行コードをうまく認識できないので、全部で
          配列がいくつ有るか数えて、最後に1引く(1引くのは、GAE上最後のデータの後にカンマを入れたから)
  (arr.length -1)は、19の倍数になってるので、19(sheet_cols)で割って、何行あるかを取得する。
  */varcols_arr=(arr.length-1)/sheet_cols;for(vari=0;i<cols_arr;i++){arrData.push([arr[j],arr[j+1],arr[j+2],arr[j+3],arr[j+4],arr[j+5],arr[j+6],arr[j+7],arr[j+8],arr[j+9],arr[j+10],arr[j+11],arr[j+12],arr[j+13],arr[j+14],arr[j+15],arr[j+16],arr[j+17],arr[j+18]]);j=j+sheet_cols;};/*
  もともと入っていた配列データを消して、上のpushで追加した行だけのデータにする。
  */for(vari=0;i<len_arrData;i++){arrData.shift();//配列の上からデータを削除}varrows=arrData.length;//行数の確認varcols=arrData[0].length;//列数の確認/*
  (len_arrData + 1) :今取得したデータを書き込む行番号を決める。元々スプレッドシートに入ってるデータの行数+1で最終行の次の行に書き込む。
  1                 :1列目(A列)に書く
  rows              :今回追加する行数
  cols              :今回追加する列数(19)
  arrData           :今回追加するデータの配列
  */sheet.getRange((len_arrData+1),1,rows,cols).setValues(arrData);// 結果を返す  varoutput=ContentService.createTextOutput();output.setMimeType(ContentService.MimeType.JSON);output.setContent(JSON.stringify({message:"success!"}));returnoutput;}

Google Apps Scriptの公開方法
  • メニューバー => 公開 => ウェブアプリケーションとして導入
  • Current web app URL: このURLを、GAE側で利用する。 => const spreadsheet_url = '';//GASで発行したURL
  • Project version: 更新の都度、上げていくと、何回デプロイしたか分かって楽しい
  • Execute the app as: Meにする
  • Who has access to the app: Anyone,even anonymousにする
  • 「更新」ボタンをクリック

スプレッドシートの状態

GAE用 app.yaml
  • app.yamlは、最初に作成したディレクトリ内に作成します。
app.yaml
runtime:nodejs10instance_class:F4_1G#puppeteer利用のためhandlers:-url :/script :autosecure :always#このあたりに、adminと書けば、自分しか使えないサイトになったと思うが、エラーになる。

GAE用 cron.yaml
  • cron.yamlは、最初に作成したディレクトリ内に作成します。
cron.yaml
cron:-description:"PJ1"url:/schedule:every  5 minutes from 08:50 to 12:00timezone:Asia/Tokyo

GAE用 デプロイ方法
  • 最初に作成したディレクトリ内で、実行します。
  • あなたのプロジェクトIDが、PJ1の場合。
  • --quiet を入れない場合、確認があります。
  • シェルで実行する場合は、--quietが便利
  • デプロイが失敗しまくる時は、以下2つのファイルを消しに行くべし
  • 1. AppEngineのバージョン画面の最新情報以外を全部消す
  • 2. Strageの「staging.〜」「us.artifacts.〜」をクリックして出てきたファイルを全部消す。 CloudBuildのファイルが溜まってるから?
gcloud app deploy --quiet--project PJ1
gcloud app deploy cron.yaml --quiet--project PJ1

GAEを無料で使うテクニック(ってほどでもないか?)
  • Google App Engine は、もともと無料枠がけっこう大きい

    • 課金しない状態で、以下のように運用可能です。
    • 28CPU時間を毎日利用しても、無料です。
    • 28CPU時間を使い切ると、システムが勝手に止まります。
    • システムがリセットされて再び動き出すのは、16時。
  • Google App Engine は、課金してなくても動くが、デプロイができない。

    • デプロイには、Cloud Buildが必要で、Cloud Buildに課金が必要
    • だから、デプロイするときだけ、Cloud Buildを課金状態にする。 デプロイ終わったら忘れず課金を消す。
    • この操作をすれば、無料で運用可能。
    • 課金状態で、1日に28CPU時間以上動かしてしまうと、課金されます。
  • もっとGAEを使いたい場合

    • app.yamlの設定で、instance_class: F4_1G と設定したので、CPU時間をかなり消費する。
    • 5分に1回の実行を、3時間回すと、12x3=36回程度で、28CPU時間を消費します。
    • 9時から12時で終わります。 足りませんよね。
    • そこで、プロジェクトIDを複数作成します。例PJ1 PJ2等。そして以下のように設定します。
    • cron.yamlのPJ1用には、schedule: every 5 minutes from 08:50 to 12:00
    • cron.yamlのPJ2用には、schedule: every 5 minutes from 12:05 to 15:20
    • 注意点:cron.yamlをデプロイするだけの場合は、Cloud Buildの課金が不要なので、課金作業無しで、デプロイ可能です。
    • しかし、PJ1 PJ2 を課金対象にした上でデプロイした後、課金をやめないと、誤って課金される場合があるので注意要です。

参考にさせて頂いた資料

三井住友銀行口座の残高を自動取得する(Puppeteerで)

$
0
0

やりたいこと・背景

  • 三井住友銀行のインターネットバンキング SMBCダイレクトで口座にログインし、現在の口座残高を取得
  • 他行では個人向けのAPIが公開されていたりする(個人で動作を試せる銀行系オープンAPIまとめ等参照)が、三井住友銀行では現在法人向けしかないようす
  • MoneyForwardやZaim等のFinTechサービスのAPIを使って取得することはできるかもしれませんが、それらのサービスを日常的には利用していないため、調べていません
  • 三井住友銀行のLINE公式アカウントに友だち登録してID連携すると、チャット形式で残高が取得できます。この記事ではよりプログラマブルに取得したい場合を想定しています。たとえば「オレオレ家計簿APIみたいなものをつくりたい」や「残高が一定金額以上(以下)になったら通知させたい」といった応用ができます

使用するもの

  • Node(v12.16.1)
  • npm (v6.13.4)
  • Puppeteer (v2.1.1):ヘッドレスブラウザを立ち上げてスクレイピングなどに使えるNodeライブラリ

手順

$ mkdir puppeteer-smbc
$ cd puppeteer-smbc
$ npm init -y$ npm install puppeteer dotenv
$ touch server.js .env

.env000...を自分のものに書き換える)

BRANCH_CODE=000 #支店名3桁
ACCOUNT_NUMBER=0000000 #口座番号7桁
PASSWORD=0000 #第一認証4桁

server.js(そのまま)

constpuppeteer=require('puppeteer')constdotenv=require('dotenv')dotenv.config()constBRANCH_CODE=process.env.BRANCH_CODEconstACCOUNT_NUMBER=process.env.ACCOUNT_NUMBERconstPASSWORD=process.env.PASSWORD;(async()=>{// validate .envif(!BRANCH_CODE||!ACCOUNT_NUMBER||!PASSWORD){console.log('Invalid Try. Make sure to create a file ".env" and write your BRANCH_CODE / ACCOUNT_NUMBER / PASSWORD')return}// launch browserconsole.log('launching browser...')constbrowser=awaitpuppeteer.launch({headless:false,args:['--no-sandbox']})// go to pageconstpage=awaitbrowser.newPage()awaitpage.setViewport({width:1440,height:2000})awaitpage.setExtraHTTPHeaders({'Accept-Language':'ja'})constLOGIN_URL='https://direct.smbc.co.jp/aib/aibgsjsw5001.jsp'awaitpage.goto(LOGIN_URL,{waitUntil:'domcontentloaded'})// set input-data then submitawaitpage.type('input[name=S_BRANCH_CD]',BRANCH_CODE)awaitpage.type('input[name=S_ACCNT_NO]',ACCOUNT_NUMBER)awaitpage.type('input[name=PASSWORD]',PASSWORD)awaitPromise.all([page.waitForNavigation({waitUntil:'networkidle0'}),page.click('input[type=submit]')]).catch(asyncerr=>{console.log(err.response)process.exit(1)})// redirect to balance-page then get balanceawaitpage.goto('https://direct3.smbc.co.jp/servlet/com.smbc.SUPRedirectServlet')constbalance=awaitpage.$eval('.fRight',elm=>elm.textContent.replace(/\s/g,'')).catch(asyncerr=>{console.log(err)process.exit(1)})console.log(`Your current balance is ${balance}`)// close browserawaitbrowser.close()})()

実行(headless: falseなのでブラウザが立ち上がる)

$ node server.js

以下の赤枠が自動で入力され、
Image from Gyazo

残高部分を取得してコンソールに表示します
Image from Gyazo

Image from Gyazo

注意

  • 正しく.envを設定する。第一認証を一定回数以上間違えるとインターネットバンキングのロックがかかるようです
  • 入力部分のセレクタは2020/3/24現在のものです。サイトのHTML構造が変更したら適宜こちらも書き換える

おわり

[Node.js] request モジュール がDeprecated になっていた

homebrewでnodeモジュールのダウングレード

$
0
0

homebrewでダウングレードしたnodeを使用する際に、詰まった部分を残します。

環境

インストール済みのnodeバージョン:13.11.0
インストールしたいnodeバージョン:12.16.1

macOS:10.14.3

nodeバージョン12系をインストール

バージョン指定の方法が12系の場合@12で最新の12系のバージョンがインストールできます。

$ brew install node@12
==> Downloading https://homebrew.bintray.com/bottles/node@12-12.16.1.mojave.bott
==> Downloading from https://akamai.bintray.com/3d/3dee3426e2ea8928d0724a801591a
######################################################################## 100.0%
==> Pouring node@12-12.16.1.mojave.bottle.tar.gz
==> Caveats
node@12 is keg-only, which means it was not symlinked into /usr/local,
because this is an alternate version of another formula.

If you need to have node@12 first in your PATH run:
  echo 'export PATH="/usr/local/opt/node@12/bin:$PATH"' >> ~/.bash_profile

For compilers to find node@12 you may need to set:
  export LDFLAGS="-L/usr/local/opt/node@12/lib"
  export CPPFLAGS="-I/usr/local/opt/node@12/include"

==> Summary
🍺  /usr/local/Cellar/node@12/12.16.1: 4,293 files, 57.8MB

12系のバージョンはインストールが完了しました。

しかし、13系のバージョンを残したままインストールすると複数のノードパッケージをインストールできますが、同時に使用できるようにすることはできません。
したがって、既存のパッケージ(ここでは13系)が入っている場合は最初にその紐付きを解除する必要があります。

インストール済みのバージョンの紐付きを解除

以下のコマンドで現在紐付いているバージョンを解除できます。

$ brew unlink node
Unlinking /usr/local/Cellar/node/13.11.0... 0 symlinks removed

使用したいバージョンを紐付ける

今回は12系のnodeバージョンを紐付けたいので以下のように設定します。

$ brew link node@12
Warning: node@12 is keg-only and must be linked with --force

If you need to have this software first in your PATH instead consider running:
  echo 'export PATH="/usr/local/opt/node@12/bin:$PATH"' >> ~/.bash_profile

紐付けを行なった際に.bash_profileのnodeのパスを設定してくださいと警告が出ているので、設定します。

$ echo 'export PATH="/usr/local/opt/node@12/bin:$PATH"' >> ~/.bash_profile

.bash_profileを念のため更新

$ source ~/.bash_profile

最後にnodeのバージョンを確認

$ node -v
v12.16.1

成功です。


余談ですが、今回バージョンを落とした理由は
gatsby-cli v2.11.1をインストールする際に、nodeモジュールが13.11.0の場合インストールに失敗してしまう事がきっかけでした。

ダウングレードにより、無事gatsby-cliの導入もできました。

$ gatsby --version
Gatsby CLI version: 2.11.1

GraphQL Mesh は何を解決するのか? ~ Qiita API を GraphQL でラップして理解する GraphQL Mesh ~

$
0
0

image.png

GraphQL Mesh とは

The Guildから GraphQL Meshが発表されました。

GraphQL Mesh は REST API や gRPC などの既存のバックエンド API サービスと接続するプロキシとして機能します。
GraphQL Mesh は、開発者が他の API 仕様(gRPC、OpenAPI、Swagger、oData、SOAP、GraphQL など)で記述されたサービスに対して、GraphQL のクエリを通じて簡単にアクセス可能にすることを目的として作られました。

従来、GraphQL プロキシを実装するためには、バックエンド API サービスに対して以下の作業を行う必要がありました。

  • その API 仕様を読み解き、
  • GraphQL サーバを構築し、
  • スキーマ、リゾルバ、バックエンド API との通信処理を実装する

複数のバックエンド API をラップする GraphQL サーバを実装するためだけに多大な労力を割いていたのです。

もちろん、openapi-to-graphqlのように、OpenAPI 定義を GraphQL のスキーマに読み換えるツールや、スキーマ定義からモックサーバを構築する graphql-toolsなどは登場していました。
今回登場した GraphQL Mesh は革新的です。バックエンド API の API 仕様さえあれば、そのバックエンド API に対して GraphQL クエリが即座に実行できる GraphQL プロキシが手に入ります。

本記事では GraphQL Mesh の簡単な使用方法とアーキテクチャの構成パターンについて解説します。

※ 本記事は こちらの記事を参照しています。

使用方法

バックエンドに OpenAPI で記述された REST API サービスがあることを想定して、GraphQL Mesh によるプロキシサーバを構築します。今回バックエンドの API は Qiita APIを使用します。

1. インストール

GraphQL Mesh はいくつかのコアライブラリを組み合わせてインストールします。

$ yarn add graphql \
           @graphql-mesh/runtime \
           @graphql-mesh/cli \
           @graphql-mesh/openapi

使用可能な API(と実装予定の API)は 3/25 現在、以下の通りです。

PackageStatusSupported Spec
@graphql-mesh/graphqlAvailableGraphQL endpoint (schema-stitching, based on graphql-tools-fork)
@graphql-mesh/federationWIPApollo Federation services
@graphql-mesh/openapiAvailableSwagger, OpenAPI 2/3 (based on openapi-to-graphql)
@graphql-mesh/json-schemaAvailableJSON schema structure for request/response
@graphql-mesh/postgraphileAvailablePostgres database schema
@graphql-mesh/grpcAvailablegRPC and protobuf schemas
@graphql-mesh/soapAvailableSOAP specification
@graphql-mesh/mongooseAvailableMongoose schema wrapper based on graphql-compose-mongoose
@graphql-mesh/odataWIPOData specification

2. 設定ファイルにバックエンド API の API 仕様を記述する

次に、.meshrc.yamlというファイルを作成し、バックエンド API の API 仕様を記述しましょう。今回は OpenAPI を使用します。他にも gRPC、oData、SOAP、GraphQL などをサポートしています。.meshrc.yamlはプロジェクトのルートディレクトリに配置します。

sources:-name:Qiitahandler:openapi:source:./qiita.openapi.yaml

QiitaAPI の OpenAPI 定義 .qiita.openapi.yamlは以下のように記述しています。

Qiita APIの仕様 (OpenAPI)
swagger:"2.0"info:version:0.0.1title:Qiita APIhost:"qiita.com"basePath:"/api/v2"schemes:-httpsconsumes:-application/jsonproduces:-application/jsonpaths:"/tags/{tagId}/items":get:parameters:-in:pathname:tagIdtype:stringrequired:true-$ref:"#/parameters/pageParam"-$ref:"#/parameters/perPageParam"responses:"200":description:指定されたタグが付けられた投稿一覧を、タグを付けた日時の降順で返します。schema:title:タグ記事一覧type:arrayitems:$ref:"#/definitions/Item""/users/{userId}":get:parameters:-in:pathname:userIdtype:stringrequired:trueresponses:"200":description:ユーザを取得します。schema:$ref:"#/definitions/User""/users/{userId}/items":get:parameters:-in:pathname:userIdtype:stringrequired:true-$ref:"#/parameters/pageParam"-$ref:"#/parameters/perPageParam"responses:"200":description:ユーザの投稿の一覧を作成日時の降順で返します。schema:title:ユーザー記事一覧type:arrayitems:$ref:"#/definitions/Item""/items":get:parameters:-$ref:"#/parameters/pageParam"-$ref:"#/parameters/perPageParam"-name:queryin:querydescription:検索クエリrequired:falsetype:stringresponses:"200":description:投稿の一覧を作成日時の降順で返します。schema:title:記事一覧type:arrayitems:$ref:"#/definitions/Item"parameters:pageParam:in:queryname:pagedescription:ページ番号 (1から100まで)type:numberperPageParam:in:queryname:per_pagedescription:1ページあたりに含まれる要素数 (1から100まで)type:numberdefinitions:ErrorMessage:description:エラーの内容を説明するmessageプロパティと、エラーの種類を表すtypeプロパティで構成されますtype:objectproperties:message:type:stringtype:type:stringGroup:description:"Qiita:Teamのグループを表します。"type:objectproperties:created_at:type:stringid:type:integername:type:stringprivate:type:booleanupdated_at:type:stringurl_name:type:stringTag:description:タグproperties:name:type:stringexample:Rubyversions:type:arrayitems:type:stringexample:0.0.1User:properties:description:description:自己紹介文type:stringfacebook_id:type:stringfollowees_count:description:このユーザがフォローしているユーザの数type:integerfollowers_count:description:このユーザをフォローしているユーザの数type:integergithub_login_name:type:stringid:type:stringitems_count:description:"このユーザがqiita.com上で公開している投稿の数(Qiita:Teamでの投稿数は含まれません)"type:integerlinkedin_id:type:stringlocation:type:stringname:type:stringorganization:type:stringpermanent_id:description:ユーザごとに割り当てられる整数のIDtype:integerprofile_image_url:description:設定しているプロフィール画像のURLtype:stringtwitter_screen_name:type:stringwebsite_url:type:stringItem:type:objectproperties:rendered_body:type:stringbody:type:stringcoediting:type:booleancomments_count:type:integercreated_at:type:stringid:type:stringlikes_count:type:stringprivate:type:booleanreactions_count:type:integertitle:type:stringupdated_at:type:stringurl:type:stringpage_views_count:type:integertags:type:arrayitems:$ref:"#/definitions/Tag"user:$ref:"#/definitions/User"group:$ref:"#/definitions/Group"

3. GraphQL Mesh サーバを起動する

GraphQL Mesh サーバを起動します。以下コマンドは npm scriptsに設定しておくと良いでしょう。

$ yarn graphql-mesh serve
yarn run v1.22.4
info: 🕸️ => Serving GraphQL Mesh GraphiQL: http://localhost:4000/

http://localhost:4000/GrapiQLが起動します。ブラウザを開いて確認しましょう。

4. GraphQL クエリを実行する

Qiita 記事の情報と、記事に紐づくユーザ情報も合わせて取得します。複数の REST API で取得できる情報をネストして記述し、1回のクエリで取得できることこそが GraphQL の真骨頂です。

query getItems {
  getItems{
    title
    likesCount
    user {
      name
      itemsCount
      organization
      description
    }
  }
}

きちんと取得できているようです。

image

さらに OpenAPI のモデルの定義を正確に読み解き、GraphQL のスキーマ定義にもきちんと反映ができています。素晴らしい。

image

GraphQL Mesh の活用方法

GraphQL Mesh はバックエンド API のプロキシとして機能します。この性質から、クライアントに対する GATEWAY としてふるまい、複数のバックエンドを束ねた構成をとっても良いでしょう。

また、複数のマイクロサービスが内部で相互通信する際に、HUB とする構成を取ることもできます。

まだ開発初期段階らしく、GitHub の README には以下のように記されています。

Note: this project is early and there will be breaking changes along the way

今後大きく変更されることがあるかもしれません。ただ、このツールのコアコンセプトには非常に感銘を受けます。AWS の AppSync などの GraphQL マネージドサービス系がこの考え方を取り入れたら、Web API の業界に大きなインパクトがありそうだと感じました。


N予備校の教材でAjaxでCORSを試してみる

$
0
0

やりたいこと

N予備校のAjaxのページに「同一生成ポリシーを満たさない場合CORSにひかかって通信できません」と書かれているが、そのあたりの回避策について特に紹介されていなかったのでAccess-Control-Allow-Originを試してみる。

前提知識

環境

  • Vagrant ubuntu(Node.jsにて上記のリンクのサーバが稼働している)
  • 手元のマシン(Mac)

実験

  • 手元のマシンのブラウザから、Vagrant上のNode.jsに対してAjax通信を行い失敗することを確認する
  • CORSを回避するためにサーバー側にヘッダーを挿入する

修正前

  • LoadAvgが表示されない
  • コンソールログをみると以下のエラーが発生している
Access to XMLHttpRequest at 'http://localhost:8000/server-status' 
from origin 'http://localhost' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

コード修正

server-status.js
'use strict';constexpress=require('express');constrouter=express.Router();constos=require('os');router.get('/',(req,res,next)=>{res.setHeader('Access-Control-Allow-Origin','http://localhost') 👈追加res.json({loadavg:os.loadavg()});});module.exports=router;

Access-Control-Allow-Originを追加することで特定のOriginからの通信を許容する

修正後

  • LoadAvgが表示された
Access-Control-Allow-Origin: http://localhost 👈 追加されている
Connection: keep-alive
Content-Length: 19
Content-Type: application/json; charset=utf-8
Date: Tue, 24 Mar 2020 19:25:17 GMT
ETag: W/"13-78iQH47CLcJ0F+4s06WtpVC9PNc"
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block

Node.jsでファイル出力

Windows10にてReact公式チュートリアル用のローカル開発環境構築時、node最新化、npx実行で突っかかった備忘録

$
0
0

はじめに

React公式チュートリアルをやってみたところ
ローカル開発環境構築中にnode最新化、npx実行で突っかかったので、備忘録。

実施したチュートリアル
Reactチュートリアル(日本語)

なお、自分はReactは触ったことが無い。

環境

  • windows10 64bit
  • node 6.9.4 -> 13.11.0
  • npm 6.13.7

ローカル開発環境の構築

チュートリアルを進める方法には、2つのオプションがある。

オプション 1: ブラウザでコードを書く
始めるのに一番手っ取り早い方法です!
...
オプション 2: ローカル開発環境
これは完全にオプションであり、このチュートリアルを進めるのに必須ではありません!

今回はオプション2を実施した。

node最新化

既存nodeバージョン確認

  1. 最新の Node.js がインストールされていることを確かめる。

とあるので、nodeを最新化する。
現行入っているバージョンを確認。

$node -vv6.9.4

nodistでnodeインストール

入れ替え方法は下記を参考にした。
windowsでNode.jsをバージョン管理する

$nodist dist
...
  13.9.0
  13.10.0
  13.10.1
  13.11.0

$nodist 13.11.0
13.11.0
Installing 13.11.0
 13.11.0 [===============] 53396/53396 KiB 100% 0.0s
Installation successful.

$nodist + 13.11.0
13.11.0

$node -vv6.9.4

 
何故かnodeのバージョンが変わっていない。

既存nodeアンインストール

うーん。わからないので、「プログラムと機能」から「Node.js」をアンインストール。

再度nodeバージョン確認

その後、nodeのバージョンを確認。

$node -vv13.11.0

npm最新化

nodeが最新化された。npmも最新化しておく。

$nodist npm match
npm matc

$npm -v6.13.7

新しいプロジェクト作成

npxでcreate-react-app実行

  1. Create React App のインストールガイドに従って新しいプロジェクトを作成する

チュートリアルに従いnpxを実行する。

$npx create-react-app my-app
'npx' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

npxが認識されなかった。
nodistでnodeインストール時にnpxもインストールされるはずだが……。

npxを手動インストール

https://paradox-tm.hateblo.jp/entry/2018/04/25/115243
上記記事で同様の現象が出ていたので、記事を参考に強制的にnpxをインストール。

$npm install-g npx
...
added 493 packages from 654 contributors in XX.XXXs

$npx create-react-app my-app
...
Happy hacking!

プロジェクトの動作確認

以降、チュートリアルに従いソースファイル作成し、最後にnpm startでサーバ起動。
http://localhost:3000を開くと、空の三目並べの盤面が表示されることを確認できた。

以上です。

node.jsのテンプレートリテラルと+による文字列結合の速度差

$
0
0

はじめに

文字列を取り扱う際、毎回テンプレートリテラルと文字列連結だとどちらの方が早いのだろう。という疑問が毎回頭を過ぎってました。
ということで軽くですがテストしました。

環境

  • Windows 10 pro
  • node.js v13.0.1

試す

計測にはperformanceを使用しました。
10^7回文字列連結する処理を10回繰り返し、平均実行時間を出しました。
なお実行時間の単位はmsです。

まずは単純な文字列同士の結合です。

const{performance}=require("perf_hooks");consta="hogehoge";constb="fugafuga";/** concat_text */letconcat_sum=0;for(leti=0;i<10;i++){conststart=performance.now();for(letj=0;j<10**7;j++){consthoge=a+b;}consttime=performance.now()-start;console.log(time);concat_sum+=time;}console.log("concat_text average: "+concat_sum/10);/** template_literal */lettemplate_sum=0;for(leti=0;i<10;i++){conststart=performance.now();for(letj=0;j<10**7;j++){consthoge=`${a}${b}`;}consttime=performance.now()-start;console.log(time);template_sum+=time;}console.log(`template_literal average: ${template_sum/10}`);
result
34.08500099973753
39.77130000013858
11.797801000066102
11.937899000011384
11.332999000325799
11.727699999697506
11.424000999890268
12.096899999771267
11.576799999922514
11.971499999985099
concat_text average: 16.772190099954607
116.54179899999872
163.70389899984002
121.10849899984896
71.12280000001192
71.46999999973923
71.70690099988133
74.72199900029227
72.5004999996163
71.70279900031164
71.67179999966174
template_literal average: 90.62509959992022

単純な文字列の結合だと+で結合した方が若干早いようです。
連結する数を4個(a + b + c + d)に増やした場合以下のようになりました。

  • +は実行時間に然程影響は無かった
  • テンプレートリテラルは実行時間が倍になった。

次に連結の際にnumberの計算もするよう変更し、前テストと同じ回数行いました。

const{performance}=require("perf_hooks");consta="hogehoge";/** concat_text */letconcat_sum=0;for(leti=0;i<10;i++){conststart=performance.now();for(letj=0;j<10**7;j++){consthoge=a+(8+2);}consttime=performance.now()-start;console.log(time);concat_sum+=time;}console.log("concat_text average: "+concat_sum/10);/** template_literal */lettemplate_sum=0;for(leti=0;i<10;i++){conststart=performance.now();for(letj=0;j<10**7;j++){consthoge=`${a}${8+2}`;}consttime=performance.now()-start;console.log(time);template_sum+=time;}console.log(`template_literal average: ${template_sum/10}`);
result
270.05879899999127
226.13540100026876
226.2756000002846
230.80309899989516
245.48059999989346
221.4089999999851
226.89270000020042
227.69470000034198
225.0682989996858
220.0896009998396
concat_text average: 231.9907799000386
41.350199999753386
114.77740000002086
38.32340000011027
109.85499900020659
48.483599999919534
102.05140100000426
43.74809999996796
92.99629899999127
40.28349999990314
42.15730100031942
template_literal average: 67.40262000001967

テンプレートリテラルの方が早いですね。
連結する数を4個(a + (8 + 2) + (9 + 1) + (10 - 3))に増やした場合以下のようになりました。

  • テンプレートリテラルは実行時間に然程影響は無かった
  • +は実行時間が倍になった。

まとめ

  • +を用いた連結は複雑な処理を入れるかどうかでタイムに差が出た
  • テンプレートリテラルは複雑な処理をする際にタイムにブレが出にくい
  • 連結数を増やした場合、早い方のタイムはブレが出にくい

といってもここまで大量に処理しない限り誤差の範囲ですので個人の好みになって来ると思います。
私は見やすさ的にもタグ関数的にもテンプレートリテラルを使いたいと思います。

また、今回はnodeのみで実験しましたがブラウザ上だとまた違った結果になりそうです。

初学者ですので間違い等あれば訂正いただけると嬉しいです。

obniz+赤外線LED+TypeScriptでリモコンコンセント(OCR-05W)を動かす

$
0
0

はじめに

こんにちは。電気毛布エンジニアの@tmitsuoka0423です。

昨日書いた「obniz+赤外線LEDでリモコンコンセント(OCR-05W)を動かす」では、obnizのパーツライブラリページ上で動作確認しましたが、今回はTypeScriptで実装していきます。
ソースコードはhttps://github.com/tmitsuoka0423/obniz-ocr-05wで公開しています。

【クラウドファンディング実施中!】mouful(モウフル)

電気毛布につけるだけ!スマホで布団の温度調節できるmouful(モウフル)
というクラウドファンディングを実施しています。

電気毛布の
  △にして寝る→暑くて体がカラカラになってしまう
  △にして寝る→温まらなくて眠れない
  △にして寝る→面倒臭くてやらなくなる
という課題を解決します。

  ○1週間レンタルでフィードバックしてくれる方
  ○開発メンバー
を募集しています!

クラウドファンディングページはこちら→https://camp-fire.jp/projects/view/220261

今回使うもの

品名画像価格
obniz Board5,500円くらい
赤外線センサー OSRB38C9AA50円くらい
赤外線LED OSI5FU5111C-4020円くらい
リモコンコンセント OCR-05W1,200円くらい

obnizと素子を接続する

以下図のように接続します。
Screenshot from Gyazo

リモコンの赤外線信号を解析する

まずは、赤外線センサーを使ってリモコンの信号を受信するプログラムを作成します。
(確認してないですが、このプログラムを使ってOCR-05W以外のリモコンの信号も受信できると思います。)

receive.ts
importObnizfrom'obniz';constobniz=newObniz('OBNIZ_ID_HERE');obniz.onconnect=async()=>{constsensor=obniz.wired('IRSensor',{vcc:0,gnd:1,output:2});console.log('リモコンのボタンを短く押してください…');sensor.start(function(arr){console.log('受信したシグナル:');console.log(JSON.stringify(arr));obniz.close();});}

これをコンパイルして実行する。
Screenshot from Gyazo
と表示されるので、リモコンのボタンを押すと、めっちゃ長い信号が受信される。

無事受信できた。

リモコンコンセントにobnizから信号を送信する

プログラムは以下の通り。

send.ts
importObnizfrom'obniz';conston:(0|1)[]=[1,1,1,1,1,()];// 受信したリモコンの信号をコピペする。constoff:(0|1)[]=[1,1,1,1,1,()];// GitHubに全量を載せています。constobniz=newObniz('OBNIZ_ID_HERE');obniz.onconnect=async()=>{constled=obniz.wired('InfraredLED',{anode:3,cathode:4});led.send(on);// led.send(off);console.log('信号を送信しました。');obniz.close();}

動かしてみる。

まとめ

obniz+TypeScriptは補完が効くようになるのでオススメ。

Node.js を持ち歩けるようにして、どこでも簡易WEBサーバを起動出来るようにする

$
0
0

Node.jsで便利なライブラリをつくったとしても、お客さまのPCやサーバの本番環境に Node.jsの実行環境がないケースもあります。「Node.jsの実行環境を持ち歩けたらなぁ、、」ということでググってみたらこの記事が。。

インストーラなどの実行が不要な 解凍すればすぐに使えるNode.js実行環境の構築手順です。コレを使えば、実行環境ごとZipでアーカイブしたファイルを作成し作業環境でそれを解凍すればNode.js環境の構築完了、なんて事ができそうです。

ついでに、そのNode.js上でWebサーバを立ち上げることで、どこでも簡易WEBサーバが起動出来るようにしてみます。

Node.js実行環境の構築

公式からバイナリをダウンロード。Windows Binary (.zip) の64-bitを選択します。2020/03/15時点「node-v12.16.1-win-x64.zip」が最新版のようです。
ダウンロード後、任意の場所に解凍しましょう。たとえば T:\Tools\nodejs_portableなどなど。

さて上記ディレクトリには、node.exeなどが入っていると思いますが、同じディレクトリに起動コマンドrun.batを置いておきます。中身はテキストでこんな感じ。

@echo off
set PATH=%cd%;%PATH%
set NODE_PATH=%cd%\node_modules\npm\node_modules;%cd%\node_modules\npm
cmd

いわゆる、このディレクトリにパスを通しているだけです。
基本的な環境構築は以上です。

実行してみる

さてエクスプローラ上で run.batをダブルクリックして、node.js 実行環境を起動してみましょう。

T:\Tools\nodejs_portable>

こんな感じで起動すればOKです。

T:\Tools\nodejs_portable>node --versionv12.16.1

T:\Tools\nodejs_portable>npm --version6.13.4

T:\Tools\nodejs_portable>

一応 Hello Worldを。

T:\Tools\nodejs_portable>mkdir app
T:\Tools\nodejs_portable>cd app
T:\Tools\nodejs_portable\app>type index.js  ← あらかじめメモ帳とかで作成しておきましょう
console.log('Hello World.')

T:\Tools\nodejs_portable\app>node index.js  ←実行
Hello World.

T:\Tools\nodejs_portable\app>

実行OKですね。つづいて npm なども動作確認してみます。

(上でつくったappは一旦削除したとして)
T:\Tools\nodejs_portable>mkdir app
T:\Tools\nodejs_portable>cd app
T:\Tools\nodejs_portable\app>npm init -yWrote to T:\Tools\nodejs_portable\app\package.json:

{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

下記のような index.jsをつくってみました。

T:\Tools\nodejs_portable\app>typeindex.jsconstrequest=require('request')request.get('http://www.yahoo.co.jp',(err,res,body)=>{console.log(body)})

npm iでインストールします。

T:\Tools\nodejs_portable\app>> npm i --save request
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN app@1.0.0 No description
npm WARN app@1.0.0 No repository field.

+ request@2.88.2
added 47 packages from 58 contributors and audited 63 packages in 9.683s
found 0 vulnerabilities

T:\Tools\nodejs_portable\app>node index.js

....  ばばっってYahooのサイトがなんか返ってくればOK

T:\Tools\nodejs_portable\app>

OKそうですね!

-- 注意 --
いわゆるグローバルインストールnpm install -g xxについて。
グローバルインストールした際は、T:\Tools\nodejs_portable\node_modulesにインストールされる想定だったのですが、すでに Node.jsがインストールされている環境のばあいそちらのディレクトリにインストールされてしまいました。

すでにNode.jsがインストールされている環境では、グローバルインストールは実施しない方がよさそうです。
ご注意ください。

簡易WEBサーバの構築

さて、さいごにWEBサーバです。Node.jsを用いたWEBサーバの構築は
[Node.js] 簡単なWebサーバとして静的ファイル配信/ディレクトリ一覧機能のサンプルコード
ココをまるまる参考にさせていただきました!感謝です。

さてやってみます。

(上でつくったappは一旦削除したとして)
T:\Tools\nodejs_portable>mkdir app
T:\Tools\nodejs_portable>cd app
T:\Tools\nodejs_portable\app>npm init -yWrote to T:\Tools\nodejs_portable\app\package.json:

{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

T:\Tools\nodejs_portable\app>npm i --save express
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN app@1.0.0 No description
npm WARN app@1.0.0 No repository field.

+ express@4.17.1
added 50 packages from 37 contributors and audited 126 packages in 10.259s
found 0 vulnerabilities

さて index.js(WEBサーバのプログラム)は以下でOKです。単純です。

T:\Tools\nodejs_portable\app>typeindex.js'use strict';constexpress=require('express');constapp=express();app.use(express.static('./dist'));app.listen(process.env.PORT||3000);

ポート番号:3000 で、ドキュメントルートを ./distとしたWEBサーバを起動するためのJavaScriptコードが完成です。

疎通のため単純なhtmlを./dist配下に配置します。

T:\Tools\nodejs_portable\app>mkdir dist
T:\Tools\nodejs_portable\app>type dist\index.html
<html><body><h1>テスト</h1>
</body></html>T:\Tools\nodejs_portable\app>

実行してみる

さてWEBサーバを起動してみます。

T:\Tools\nodejs_portable\app>node index.js
...

起動したようです。

別のコマンドプロンプトからアクセスしてみます。(あもちろんWEBブラウザでもよいです)

C:\Users\xx>curl http://localhost:3000/index.html
<html><body><h1>テスト</h1>
</body></html>

OKそうですね!

例によって、インストールディレクトリ(例だとT:\Tools\nodejs_portableですね) をZip化して持ち運べば、任意の場所で解凍すればOKの、簡易WEBサーバの完成です。静的なWEBサーバですが、ちゃちゃっとつかうフロントのサーバとしては十分でしょう。

おつかれさまでした!

関連リンク

create-react-appを卒業して自分でReact + TypeScriptの開発環境を作れるようになるということ

$
0
0

これまでReactの環境構築をする時はcreate-react-appに頼りっきりでしたが、いい加減自分で作れないとまずいなと思い忘備録も兼ねて残しておきます。
また、せっかくTypeScriptも使うので webpack.config.jsもTypeScriptで書けるようにしたいと思います。


最終的なディレクトリ構成は次のようになります。

.
┃━━ public
┃  ┗━ index.html
┃━━ src
┃  ┃━ index.tsx
┃  ┗━ App.tsx
┃━━ package.json
┃━━ package-lock.json
┃━━ tsconfig.json
┗━━ webpack.config.ts

それではやっていきましょう。

1. ディレクトリの初期化

適当なディレクトリを作って npm initするだけです。

mkdir hogehoge
cd hogehoge
npm init -y

-yオプションはお好みで(つけると全部初期値で自動的に初期化されます)。
初期化が終了すると package.jsonができているはずです。

2. モジュールのインストール

React + TypeScriptの環境構築なので、とりあえず必要そうなモジュールをインストールしていきます。
バンドラにはWebpackを利用します。

npm i -d react react-dom
npm i -D typescript ts-loader webpack webpacl-cli webpack-dev-server @types/node @types/react @types/react-dom

@types/nodeの中に webpack.Configurationが定義されており、これがWebpackの設定情報の型です。
これを使って webpack.config.tsを書いていきます。

webpack.config.ts
import{Configuration}from'webpack'importpathfrom'path'constconfig:Configuration={mode:'development',entry:'./src/index.tsx',module:{rules:[{test:/\.tsx?$/,use:'ts-loader',exclude:/node_modules/,},],},resolve:{extensions:['.tsx','.ts','.js',],},devtool:'inline-source-map',output:{filename:'bundle.js',path:path.resolve(__dirname,'dist'),},}exportdefaultconfig

感のいい人はお気づきかもしれませんが、現段階だと開発用サーバーの設定である devServerプロパティがありません(書いてみるとエラーになるはずです)。
そこで @types/webpack-dev-serverをインストールします。

npm i -D @types/webpack-dev-server

これで webpack-dev-serverの設定ができるようになりました。 webpack.config.tsに設定を追記します。

webpack.config.ts
constconfig:Configuration={// ...devServer:{contentBase:path.resolve(__dirname,'dist'),},// ...}

これでエラーが消えたはずです。

HtmlWebpackPluginの設定

詳しくは割愛しますが、このプラグインを使うとテンプレートのHTMLのbodyタグの末尾にscriptタグを勝手に挿入してくれるので便利です。
というわけでインストールします。

npm i -D html-webpack-plugin @types/html-webpack-plugin
webpack.config.ts
importHtmlWebpackPluginfrom'html-webpack-plugin'constconfig:Configuration={// ...plugins:[newHtmlWebpackPlugin({template:'./public/index.html',}),],// ...}

テンプレートとなるHTMLファイルを public/index.htmlに作成します。

public/index.html
<htmllang="ja"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><title>your app title</title></head><body><divid="root"></div></body></html>

ここではテンプレートHTMLのパスしか設定しませんが、 titleなど他にもいくつかオプションがあるので気になる方は調べてみてください。

3. TypeScriptの設定

tsconfig.jsonを作成・編集します。

tsconfig.json
{"compilerOptions":{"outDir":"./dist/","noImplicitAny":true,"module":"es6","target":"es5","jsx":"react","allowJs":true,"allowSyntheticDefaultImports":true}}

srcディレクトリ内に、エントリポイントとなる index.tsxを作っていきます。とりあえず簡単なものだけ。

src/App.tsx
importReact,{FC}from'react'exportconstApp:FC=()=><div>HelloWorld!</div>
src/index.tsx
importReactfrom'react'importReactDOMfrom'react-dom'import{App}from'./App'ReactDOM.render(<App/>,document.getElementById("root"))

4. npm scriptsを設定する

package.jsonを編集します。
開発用サーバーを立ち上げるコマンドと、ビルド用のコマンドを用意します。

package.json
{//..."scripts":{"dev":"webpack-dev-server --open","build":"webpack"},//...}}

ここまででほぼ完成みたいなものですが、このままだとコンパイルできません。

5. 設定ファイルを読み込めるようにする

今のままだと設定ファイルが .tsファイルなので読み込めません。そこで ts-nodeというモジュールを使用します。
モジュール名の通りですが、Node.jsがTypeScriptを直接読めるようにするものです。

npm i -D ts-node

これでコンパイルが通る・・・ようにはなりません。もう1ステップ必要です。
作成した webpack.config.tsですが、import/export文を使用しているためこのままだと使えません。
これが使えるように tsconfig.jsonを編集します。

tsconfig.json
{"compilerOptions":{//..."esModuleInterop":true,"module":"commonjs",//...}}

詳細な説明は省きますが、「CommonJSモジュールだよ~」と教えてあげることで使えるようになります。

とりあえずはこれにて終了です。npm run devなり npm run buildして開発に励みましょう。

6. おわりに

ところどころ端折りましたが、これでReact + TypeScriptの開発環境は作れたはずです。
似たような記事は結構あるのですが、同じReact + TypeScriptでもWebpackの設定までTypeScriptで行っているものは少なかったので記事にしました。型定義があるぶん補完も効くので書きやすいです。

こんな記事でも誰かのお役に立てば幸いです。ご覧頂きありがとうございました。


[備忘録]Macの開発環境構築(anyenv + nodenv + Node.js)

$
0
0

構築の流れ

  1. Homebrewのインストール
  2. Gitのインストール
  3. anyenvのインストール
  4. nodenvのインストール
  5. Node.jsのインストール

環境

  • macOS Catalina
  • シェルは zsh

Homebrewのインストール

本家のスクリプトを実行。

% /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

Homebrewのバージョン確認。

% brew -v

Gitのインストール

Gitがインストールされているか確認する。
使い方が表示されればOK。

% git

HomebrewでGitをインストール

% brew install git

anyenvのインストール

HomebrewかGitでanyenvをインストールする。

Homebrew版

% brew install anyenv

Git版

% git clone https://github.com/anyenv/anyenv ~/.anyenv

anyenvを初期化する。

% anyenv init

.zshrcにパスを通す記述を追加する。

% echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.zshrc
% echo 'eval "$(anyenv init -)"' >> ~/.zshrc

パス通したあとにシェルを再起動。

% exec $SHELL -l

エラーが出た。

% ANYENV_DEFINITION_ROOT(/Users/[user-name]/.config/anyenv/anyenv-install) doesn't exist. You can initialize it by:
> anyenv install --init

指示通り anyenv install --initを実行。

% anyenv install --init

Manifest directory doesn't exist: /Users/[user-name]/.config/anyenv/anyenv-install
Do you want to checkout ? [y/N]: y
Cloning https://github.com/anyenv/anyenv-install.git master to /Users/[user-name]/.config/anyenv/anyenv-install...
Cloning into '/Users/[user-name]/.config/anyenv/anyenv-install'...
remote: Enumerating objects: 48, done.
remote: Total 48 (delta 0), reused 0 (delta 0), pack-reused 48
Unpacking objects: 100% (48/48), done.

Completed!

完了!
anyenv使えるか確認。

% anyenv

anyenv 1.1.1
Usage: anyenv <command> [<args>]

Some useful anyenv commands are:
   commands            List all available anyenv commands
   local               Show the local application-specific Any version
   global              Show the global Any version
   install             Install a **env
   uninstall           Uninstall a specific **env
   version             Show the current Any version and its origin
   versions            List all Any versions available to **env

See `anyenv help <command>' for information on a specific command.
For full documentation, see: https://github.com/anyenv/anyenv#readme

プラグインを入れる

anyenvの更新を楽にするプラグインをインストールしておく。
まずはインストールするフォルダを作成。
-p:上層のディレクトリが存在しなければ作成する。(この場合.anyenvディレクトリ)

% mkdir -p ~/.anyenv/plugins

anyenv-update

URL:https://github.com/znz/anyenv-update
anyenvで入れた**env系とプラグインの更新をする。

% git clone https://github.com/znz/anyenv-update.git ~/.anyenv/plugins/anyenv-update

anyenv-git

URL:https://github.com/znz/anyenv-git
anyenvで入れた**env系とプラグインのgitコマンドを実行する。

% git clone https://github.com/znz/anyenv-git.git ~/.anyenv/plugins/anyenv-git

nodenvのインストール

anyenvでnodenvをインストール

anyenvでインストール可能な「**env」一覧を表示。

% anyenv install -l

nodenvをインストール。

% anyenv install nodenv

シェルを再起動。

% exec $SHELL -l

nodenv使えるか確認。

% nodenv

Usage: nodenv <command> [<args>]

Some useful nodenv commands are:
   commands    List all available nodenv commands
   local       Set or show the local application-specific Node version
   global      Set or show the global Node version
   shell       Set or show the shell-specific Node version
   install     Install a Node version using node-build
   uninstall   Uninstall a specific Node version
   rehash      Rehash nodenv shims (run this after installing executables)
   version     Show the current Node version and its origin
   versions    List installed Node versions
   which       Display the full path to an executable
   whence      List all Node versions that contain the given executable

See `nodenv help <command>' for information on a specific command.
For full documentation, see: https://github.com/nodenv/nodenv#readme

Node.jsのインストール

nodenvでNode.jsをインストール

nodenv installコマンドの使い方を表示。

% nodenv install

インストール可能なNode.jsのバージョン一覧を表示。

% nodenv install -l

Node.jsの公式サイトを確認して、推奨バージョンをインストール。

% nodenv install *.**.*

インストールしたバージョンを確認。

% nodenv versions

グローバルとローカルのバージョンを指定。

# グローバル
% nodenv global *.**.*

# ローカル
% cd [プロジェクトのパス]
% nodenv local *.**.*

参考URL

【未完】楽天市場のAPIを使って買えるマスクを提案してくれるLINEBotを作ってみた

$
0
0

はじめに

ProtoOutStudioというイケイケなスクールの「LINE Bot+APIで表現してアウトプット」という課題で製作したものです。

こちらの1時間でLINE BOTを作るハンズオンの記事をベースにLINEBotを作成しました。
最近の新型ウィルスの影響で、ネットショッピングばかりしているのですが、ちょっと楽天市場のトップ画面を見るのに飽きてきたので、(マスクなどの)必要なものを、(売り切ればかりなので)在庫のある商品で、高額になりすぎて買う気がなくなってしまわない(金額の)範囲で、提案してもらえるLINEBotを考えました。
(Amazonは申請が大変そうに見えたのでお見送りしました)

概要と作れなかったところ

概要

  • LINEBotにほしいもの、「マスク」と入力したら
    1. 楽天市場のキーワード検索から「マスク」を検索
    2. 絞り込み検索で「購入可能」
    3. 「最安価でソート」
    4. 「最低金額○円以上」
    5. 「最高金額○円以下」
    6. 商品画像付き
    7. を5つくらい返してくれるBot #### 作れなかったところ
  • ほしいもの 「マスク」と入力したら 
    1. × → 楽天市場のキーワード検索から「マスク」を検索
    2. ○ → 絞り込み検索で「購入可能」
    3. ○ → 「最安価でソート」
    4. ○ → 「最低金額○円以上」
    5. ○ → 「最高金額○円以下」
    6. ○ → 商品画像付き
    7. × → を5つくらい返してくれるBot

入力したものをエンコードしてAPIのURLに入れて作成するところと、
(APIをベタでかくと動くBotはできたけど)これを複数表示するためにどこでFor文を回せばよいかわからなかった

環境

Node.js v13.7.0
MacBook Pro macOS Mojave
Visual Studio Code v1.43.1

できたもの

Image from Gyazo
「マスクある?」と質問することで、楽天市場の中で在庫あり商品、最安価の商品名と商品URLと商品画像のあるマスクを提案してくれます。

コード

node.js
'use strict';constexpress=require('express');constline=require('@line/bot-sdk');constaxios=require('axios');constPORT=process.env.PORT||3000;constguidecat='https://i.gyazo.com/97840790b257952c89c59e7c176e114c.png';constconfig={channelSecret:'',channelAccessToken:''};constapp=express();app.post('/webhook',line.middleware(config),(req,res)=>{console.log(req.body.events);Promise.all(req.body.events.map(handleEvent)).then((result)=>res.json(result));});constclient=newline.Client(config);functionhandleEvent(event){if(event.type!=='message'||event.message.type!=='text'){returnPromise.resolve(null);}letmes=event.message.text;if(mes.indexOf('')>-1){getNodeVer(event.source.userId);returnclient.replyMessage(event.replyToken,[{type:'image',originalContentUrl:guidecat,previewImageUrl:guidecat},{type:"text",text:'在庫があって安いのはこれだよ〜'}]);}else{mes=event.message.text;console.log(mes);returnclient.replyMessage(event.replyToken,[{type:'image',originalContentUrl:guidecat,previewImageUrl:guidecat},{type:"text",text:'〇〇?って聞いてほしいな'}]);}}constgetNodeVer=async(userId)=>{//キーワード部分にevent.message.textをエンコードしたものを入れたいconstres=awaitaxios.get('https://app.rakuten.co.jp/services/api/IchibaItem/Search/20170706?format=json&keyword='+'%E3%83%9E%E3%82%B9%E3%82%AF'+'&hits=3&applicationId=1003940711976508350');constitem=res.data;awaitclient.pushMessage(userId,{type:'text',text:item.Items[0].Item.itemName+"\n"+item.Items[0].Item.itemUrl+"\n¥"+item.Items[0].Item.itemPrice});awaitclient.pushMessage(userId,{type:'image',originalContentUrl:item.Items[0].Item.mediumImageUrls[0].imageUrl,previewImageUrl:item.Items[0].Item.mediumImageUrls[0].imageUrl});}app.listen(PORT);console.log(`Server running at ${PORT}`);

参考サイト

感想

深いAPIを操るの大変ですが自分好みの条件の商品をサクッと提案してもらえるBotはそれなりに便利そうなでちゃんと完成させねばです。
きっともっといい書き方や2回書かなくてもいいものとかたくさんある気がしますが、【未完】を取れるように早めにやっつけたいです。

JavaScriptでちょっと複雑なcliを作るのに便利なEnquirer

$
0
0

この記事は

LAPRAS アウトプットリレーの...何日目だっけ?3/25の記事です!
こんにちは!LAPRAS エンジニアの @rockymanobiです!

最近Node.jsでCLIを作る機会があり、その時に触ったEnquirerというライブラリが便利だったので、軽く紹介してみようというものです。ツールそのものについて軽くふれつつ、制作過程で出てきた「こんなことしたいけど、どう実現すれば良いんだろう」と試行錯誤して分かった使い方などを共有できればなと思います。

Enquirerとは

Enquirerは CLIアプリケーションにおける対話的インターフェイスの実装を楽にしてくれるライブラリです。単純なテキスト入力の受付はもちろん、リストからの選択、チェックボックス、パスワード、入力補完、など、様々な入力方式を手軽に組み込むことができます。Node.js製です。JavaScript(TypeScript)万歳!

公式サイトによるとこんなのもできるそうです(凄い!! いつ使うんだろう

類似のツールとしては先発の Inquirer.jsなどがあります。公式サイトに「Inquirerより速いぜ!」とあったり、Inquirerとほぼ同じような記述方式をサポートしていることから、かなり意識して作っているように見えます。どちらも利用者が多く、メンテも続いているのでどちらを使っても良いでしょう。あえて比較をするならば、Enquirerの方が多少全体や中身を把握しやすかったのと、少し凝ったことをやろうとしたときに素直そうな印象でした(個人の感想です)。

基本的な使い方

例えばこんなものを作りたい場合は...

constEnquirer=require('enquirer');(async()=>{constquestion={type:'select',name:'favorite',message:'好きな乗り物は?',choices:['パトカー','救急車','消防車'],};constanswer=awaitEnquirer.prompt(question);console.log(`僕も${answer.favorite}が好きだよ`);})();

このようにpromptメソッドにオプションを渡してやると、それに応じた方式で入力を受け付ける描画をしたのち、ユーザの入力が完了したときにPromiseresolveしてくれます。結果はオプションnameに指定したキーにぶら下がってきます。

この要領で公式ドキュメントを見ながら使えば、大抵のことは実現できると思います(少し違う使い方もありますが後で触れます)

少し複雑なケースの実装

ここからは少しだけ複雑な要件に対応してみます。
複雑な入力と言われて最初に思い浮かぶのはポケモンバトルの選出画面です。故にここからは「ポケモンバトルの選出画面をCLIで実装するとしたら」をテーマに少しづつ進めていきたいと思います。

リストから要素を複数選択(これは単純

ポケモンの対戦は、基本的に以下のような流れで進行します。

1. ポケモン6匹でパーティを構築する
2. 対戦前にお互いにパーティを見せ合う
3. 6匹のうち3匹を選出し、3vs3で対戦

今回の対応範囲である「選出」というのはこの3番めにある「6匹のうち3匹を選ぶ」作業のことを指します。

以上を踏まえると、選出画面のCLI版は以下のようなものになりそうです。

実装は以下のようになります。

constEnquirer=require('enquirer');constmyPokemonNames=['フシギバナ','リザードン','カメックス','ゴリランダー','エースバーン','インテレオン',];(async()=>{constquestion={name:'selections',type:'select',multiple:true,message:'誰を出す?',choices:myPokemonNames,validate:(selectedItems)=>{if(selectedItems.length===3){// true/falseを返すとOK/NGのみを表現returntrue;}// 文字列を返すとエラーメッセージになるreturn'3匹選んでください';},};constanswer=awaitEnquirer.prompt(question);console.log(`${answer.selections.join(',')}を選出しました`);})();

オプションmultiple: trueを渡すことで、複数選択可能なチェックボックス式の入力を受け付けます。

また、validateにバリデーション用の関数を渡すことで、ユーザ入力を検証して、不適合な入力を弾き、入力画面をキープすることができます。エラーメッセージを独自のものにしたい場合は、true/falseではなく文字列を返すようにすることで、判定をNGとしたうえで、関数が返した文字列をエラーメッセージとして表示してくれます(errorMessageってオプションあったほうがわかりやすいきがしますが)。

タイマーで入力をキャンセルする

ここまでは公式Readmeにもしっかり書いてあるので難なく対応できました。が、要件を一つ忘れていたのでここで追加します。

ポケモンの対戦では遅延行為を防ぐため、あらゆる行動に制限時間が設けられています。もちろん選出も例外ではなく、すべてのプレイヤーは1分30秒(記事執筆時点)以内で3匹のポケモンを選び出す必要があります。これに間に合わない場合は、強制的に上から順に3匹のポケモンが選出されます。

この要件をCLIに反映させてみます。

constEnquirer=require('enquirer');// (信じられないことに)配列 myPokemonNamesをchoicesオプションとして渡すと破壊的に配列が変更されるので、// 違うArrayインスタンスを返すようにFunctionに包んでいるconstmyPokemonNames=()=>{return['フシギバナ','リザードン','カメックス','ゴリランダー','エースバーン','インテレオン',];};(async()=>{constprompt=newEnquirer.MultiSelect({name:'selections',message:'誰を出す?',choices:myPokemonNames(),validate:(selectedItems)=>{if(selectedItems.length===3){returntrue;}return'3匹選んでください';},})lettimer;prompt.once('run',()=>{timer=setTimeout(()=>{prompt.cancel()},5000)})prompt.once('close',()=>{clearTimeout(timer);});constanswer=awaitprompt.run().catch(()=>{// 時間切れですreturnmyPokemonNames().slice(0,3);});console.log(`${answer.join(',')}を選出しました`);})();

これまでとは少し実装方法を変えています。これまではInquirer.js風の実装をしていましたが、ここではコチラのように、ライブラリにビルトインで入っているPromptの子クラスを用いた実装にしています。

コンストラクタの形式はこれまでpromptメソッドに渡していたものに似ていますが、各クラスごとに自明なものMultipleSelectクラスにおけるtypemultiple:trueなど)が不要になっています(TypeScriptなら型チェックも効いて快適)。

この方式ではPromptクラスのrun()メソッドを実行したときにユーザの入力を受け付けるようになり、同cansel()メソッドを呼んでやることで、強制的に入力を終了させることが可能です。

PromptクラスはEventEmitterを継承しており、上記コードでは入力受付開始時のrunイベントに反応してタイマーを作動させ、一定時間経過後にキャンセルするようにしています。入力終了時(キャンセル/タイムアウト含む)に発生するcloseイベントが発生したタイミングでは、タイマーを止める処理を実行するようにしています。

最後に、プロンプトをキャンセルしたときにはPromiserejectされるので、catch節でエラーを拾って、「時間切れの場合は先頭の3匹強制選出」を示す結果を返すようにしています(ちなみにcatch節のコールバックには何も引数が入って来ません)

カウントダウンを表示する

制限時間を超過すると時間切れになるようにはできましたが、選出中に「後何秒?」が分からないのは辛いものがあります。これも対応しましょう。

(async()=>{lettimeRemaining=10;constprompt=newEnquirer.MultiSelect({name:'selections',message:()=>{return`誰を出す? 残り ${timeRemaining}秒`},choices:myPokemonNames(),validate:(selectedItems)=>{if(selectedItems.length===3){returntrue;}return'3匹選んでください';},})letinterval;prompt.once('run',()=>{interval=setInterval(()=>{timeRemaining-=1;if(timeRemaining<=0){prompt.cancel()}else{prompt.render()}},1000)})prompt.once('close',()=>{clearInterval(interval);});constanswer=awaitprompt.run().catch(()=>{console.log('時間切れです');returnmyPokemonNames().slice(0,3);});console.log(`${answer.join(',')}を選出しました`);})();

タイマー処理をsetTimeoutsetIntervalに変えて、1秒毎にカウントダウンするように変更しつつ、メッセージに「残り n 秒」を表示させるために、以下の修正を施しています。

  • messageオプションに残り秒数を表示する文字列を返す関数を渡す
  • 1秒毎に prompt.render()メソッドを実行する

これにより、プロンプトの内容が毎秒再描画され、残り時間がカウントダウンされていく様子を表示することができました。カウントダウン部分のみを他のライブラリや独自実装などで代替しようとすると、カーソルの状態が衝突して表示がおかしくなったりするので、このあたりをサポートしてくれているのは有り難い限りです。

複数の質問をする & 前の回答を考慮して選択肢を変更する

これで完成かと思いましたが、そうはいきません。確かに選出は6匹から3匹を選び出す作業ですが、同時に「誰を先発させるか」を決める作業でもあることを忘れていました。

最初に選択する要素には特別な意味をもたせる必要がありそうなので、最初に先発を聞いて選んでもらった後に、控えの二匹を選出してもらうようにしてみます。

(async()=>{lettimeRemaining=10;letcurrentPrompt;letinterval;// Enquirerインスタンスの参照が欲しいのでstaticメソッドのpromptではなく// new Enquirer()して、そいつのpromptメソッドを呼ぶようにするconstenquirer=newEnquirer();enquirer.on('prompt',(prompt)=>{currentPrompt=prompt;prompt.once('run',()=>{interval=setInterval(()=>{timeRemaining-=1;if(timeRemaining<=0){currentPrompt.cancel()}else{currentPrompt.render()}},1000)});prompt.once('close',()=>{clearInterval(interval);});})constanswer=awaitenquirer.prompt([{type:'select',name:'starter',message:()=>{return`先発は誰にする? 残り ${timeRemaining}秒`},choices:myPokemonNames(),},{type:'select',multiple:true,name:'reserves',message:()=>{return`控えは誰にする? 残り ${timeRemaining}秒`},choices(){returnmyPokemonNames().filter((name)=>{returnthis.state.answers.starter!==name;})},validate:(selectedItems)=>{if(selectedItems.length===2){returntrue;}return'2匹選んでください';},}]).catch(console.error);if(answer){constselected=[answer.starter].concat(answer.reserves);console.log(`${selected.join(',')}を選出しました`);}else{console.log('時間切れです')console.log(`${myPokemonNames().slice(0,3).join(',')}を選出しました`);}})();

公式ドキュメントによると、Enquirerは複数の質問を連続して表示することに対応しているようですが、Enquirer.promptメソッドにオプションの配列を渡してあげる形式にする必要があります。このままだとタイマー処理によってキャンセルすることができないので、どうにかしてPromptクラスのインスタンスを参照する必要があります。

ということをやろうとしているのが上の方にあるこの処理です。

// Enquirerインスタンスの参照が欲しいのでstaticメソッドのpromptではなく// new Enquirer()して、そいつのpromptメソッドを呼ぶようにするconstenquirer=newEnquirer();enquirer.on('prompt',(prompt)=>{

Enquirerクラスは内部的に保持しているPromptインスタンスを処理するタイミングでpromptイベントを発火しつつPromptインスタンスを渡してくれるので、そこでこれまでのケースと同じようにイベントハンドルを仕掛けています。

先発で選んだポケモンを控えの選択肢に出さない

messageオプションなどと同様にchoicesオプションにも関数を指定することが可能です。そして、この関数内部でthis.state.answersを参照することで前の質問に対する入力の値を得ることができます。コレを利用して、控えポケモンの選択肢から、先発に選んだポケモンを除外しています。

choices(){returnmyPokemonNames().filter((name)=>{returnthis.state.answers.starter!==name;})},

その他

今回の例ではchoicesにはString配列を渡していましたが、{ name: '興梠', value: 'rocky' }のようなオブジェクトの配列を渡すことで、見た目上はnameに指定した値を表示しつつ、実際にanswerで得られるのはvalueに指定した値にする、ということも可能です(多分大体そうする)。

加えて、このようにオブジェクトを渡す方式にしている場合は、最後の例を実現するにあたってchoicesをフィルタする代わりに、各choiceのオブジェクトにdisabled: trueなどを渡すことで選択不能な状態にすることができるみたいです。

(というか複数聞きたいなら複数回prompt呼んでしまえばいいじゃないのって思ったけど違うのかな)

まとめ

無事、ポケモン選出画面の要件を満たすことができました。
技術的には以下のあたりがリポジトリ検索したり調べたりコード見てみたりしないと見えてこなかった印象があるので、実現したい方は参考にしてみると良いでしょう。(PullRequestチャンスでもある)

  • タイマーでプロンプトを終了させるためにはPrompt#cancel
  • Promptクラスの参照は、最初からPromptクラスを直接使った方法で実装するか、Enquirerクラスのpromptイベントをリスニングして降ってくるのを拾う

最後に

この謎チュートリアルはノンフィクションです(実際に勢いでポケモン対戦できるCLIを書いているときの展開をほぼそのまま再現しました)

話題のanalyzeコマンドを実装してみた

$
0
0

今、イケてるエンジニア界隈で話題沸騰中のanalyzeコマンドをご存知でしょうか。

こういうやつですね。

スクリーンショット 2020-03-26 9.41.49.png

Yet another analyze command

コレCLIっぽい見た目をしていますが、実はWebブラウザ上1でしか動作しません。不便ですね。
Shellでも使いたい!という声にお答えして、実装してみました!!

コチラです。
NPM

Source code: https://github.com/kaz/qiita-analyze

使い方

インストールは以下のコマンドを実行するだけです。nodejs/npmは別途インストールしてください。

$ npm i -g qiita-analyze

そしたらこうやって実行

$ analyze @sobaya007

すごい!!!! (実際は色もついてるよ)

$ analyze @sobaya007
 投稿した記事           読んだ記事            LGTMした記事
   D言語: 20%       Docker: 7%          dlang: 19%
   D言語くん: 20%     Python: 5%          D言語: 7%
   プログラミング: 10%   JavaScript: 3%      Vim: 4%
   GPGPU: 10%       docker-compose: 3%  C: 4%
   dlang: 10%       Node.js: 2%         C++: 4%

解説

NodeJSでのWebスクレイピングにはcheerioが便利です。(申し訳程度の技術要素)

LINE botを勉強会の受付に導入してみたい! ~connpass APIの紹介~

$
0
0

導入

 仕事は別に勉強会を開いているのですが、有志による運営のため手が回らないこところが多いです。その中でも受付業務にフォーカスして手助けになるシステムのプロトタイピングを行っています。

Noodlで受付嬢を創った

 私と同じくユーザコミュニティの運営やイベントの開催をされている方に読んでいただいて、少しでも負担が軽くなるシステム例としていただけたら嬉しいです。

 勉強会といえば、IT関連の勉強会支援サービスconnpassですね。
と、いうことでconnpassのAPIを利用してLINE botと連携してみようと思いました。
この記事ではconnpass APIの紹介と簡単な使い方を書いています。

connpass APIについて

 こちらのconnpass APIからURLと検索クエリ、レスポンスについて解説されています。開催日時や場所だけでなく、サイトのHTMLまで入手できるとは....。

実際にconnpass APIを使ってみる

 それでは試しにconnpass APIを使用し、どんなデータが返ってくるのか確かめてみようと思います。

 実行環境はノートPC上とし、ngrokでトンネリングしてLINEサーバと通信します。今回の動きとしては、LINE botにテキストを投げかけるとイベント名「Noodl」で検索し、そのレスポンスに含まれるイベントURLを返します。

LINE botにテキストを投げるとこのように返答が来ます。

LINE bot.jpg

準備

 使用するjsライブラリは次の通りです。npmコマンドでインストールしましょう。

  • axios
  • express
  • @line/bot-sdk

 npm initを行ったパスで次のコマンドを実行すると、必要なパッケージがインストールされます。

npm i axios express @line/bot-sdk

実装

 LINE botの準備とNode.jsはネットに沢山情報があるため割愛します。最終的に次のコードとなりました。

'use strict';constaxios=require('axios');constexpress=require('express');constline=require('@line/bot-sdk');// LINEconstPORT=process.env.PORT||3000;constconfig={channelSecret:'LINE MessagingAPIのチャンネルシークレット',channelAccessToken:'LINE Messaging APIのアクセストークン'};constapp=express();app.get('/',(req,res)=>res.send('Hello LINE BOT!(GET)'));//ブラウザ確認用(無くても問題ない)app.post('/webhook',line.middleware(config),(req,res)=>{console.log(req.body.events);//ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。if(req.body.events[0].replyToken==='00000000000000000000000000000000'&&req.body.events[1].replyToken==='ffffffffffffffffffffffffffffffff'){res.send('Hello LINE BOT!(POST)');console.log('疎通確認用');return;}Promise.all(req.body.events.map(handleEvent)).then((result)=>res.json(result));});constclient=newline.Client(config);functionhandleEvent(event){varevent_url;if(event.type!=='message'||event.message.type!=='text'){returnPromise.resolve(null);}// connpass APIにアクセス// このURLがconnpass APIにアクセスするURLaxios.get('https://connpass.com/api/v1/event/?keyword_or=Noodl').then(function(response){// handle success// イベントURLをevent_url=response.data.events[0].event_url;}).catch(function(error){// handle errorconsole.log(error);}).finally(function(){// always executedconsole.log(event_url);console.log(typeofevent_url);returnclient.replyMessage(event.replyToken,{type:'text',text:event_url//実際に返信の言葉を入れる箇所});});}app.listen(PORT);console.log(`Server running at ${PORT}`);

 重要なところだけ解説します。

URL指定

 クエリの”keyword_or”が検索ワードを挿入する部分です。

https://connpass.com/api/v1/event/?keyword_or=Noodl

URLを抜き出す

 次の処理で、大量にあるデータの中からイベントURLを抜き出しています。

event_url = response.data.events[0].event_url;

LINE botで返答

次の処理でイベントURLをユーザに送信しています。タイプはtextで問題ないです。

    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: event_url //実際に返信の言葉を入れる箇所
    });

おわりに

 今回はconnpass APIを使用し、LINE botと連携してイベントURLを返すデモとなりました。他にも面白そうなデータ(イベント開催場所の緯度経度、日時等)があるので拡張していきたいと思います。

 例えば、
- Google スプレッドシートと連携して参加者の出欠確認。
- GPSを拾って会場にいるときだけ受付可能にしたり。
- イベント開催場所の最寄りのラーメン屋さんを紹介してくれたり...。

考えれば考えるほど面白いのがAPIの醍醐味ですね(笑)

Viewing all 8808 articles
Browse latest View live