- 投稿日:2020-10-11T23:18:24+09:00
Unexpected end of JSON input while parsing near '...":"^3.5.0","concurren' のトラブルシューティング
- 投稿日:2020-10-11T23:08:08+09:00
LINEBotを使って海外現地金額から日本円を返してみた
目的
仕事で海外とやりとりすることが多くなってきました。
海外に行ったときに現地通貨が日本円ではいくらなのかをすぐに調べることができるツールがあれば便利だと思って今回、自分で作ってみました。実現方法
入力
入力についてはLINEを使用しました。
使用したAPI
Foreign exchange rates API
https://exchangeratesapi.io/ロジック
LINEに入力する文字の形式は固定にしました。
入力として受け付ける文字
(国名):(現地金額)は日本でいくら?
例
中国:1000は日本でいくら?LINEで入力した内容をもとにAPIを利用して日本円に換算しています。
1.LINEで入力した国名をもとに通貨名を取得します。
連想配列でkey:国名、value:通貨名として紐づけを持たせています。
2.1.で取得した通貨名からレートを求め、日本円を取得します。
今回使用したAPIについては1ユーロあたりのレートが返却される仕様となっていました。
そのため、まず、入力した現地金額を何ユーロか求めて、1ユーロあたりの日本円をかけて
求めています。コード
'use strict'; // ######################################## // 初期設定など // ######################################## // パッケージを使用します const express = require('express'); const line = require('@line/bot-sdk'); const axios = require('axios'); // ローカル(自分のPC)でサーバーを公開するときのポート番号です const PORT = process.env.PORT || 3000; // Messaging APIで利用するクレデンシャル(秘匿情報)です。 const config = { channelSecret: '作成したBotのチャネルシークレット', channelAccessToken: '作成したBotのチャネルアクセストークン' }; // 国名と通貨の紐づけをしています。 const currency = new Array(); currency["カナダ"] = "CAD:ドル"; currency["香港"] = "HKD:ドル"; currency["アイスランド"] = "ISK:クローナ"; currency["フィリピン"] = "PHP:ペソ"; currency["デンマーク"] = "DKK:クローネ"; currency["ハンガリー"] = "HUF:フォリント"; currency["チェコ"] = "CZK:コルナ"; currency["オーストラリア"] = "AUD:ドル"; currency["ルーマニア"] = "RON:レウ"; currency["スウェーデン"] = "SEK:クローネ"; currency["インドネシア"] = "IDR:ルピア"; currency["インド"] = "INR:ルピー"; currency["ブラジル"] = "BRL:レアル"; currency["ロシア"] = "RUB:ルーブル"; currency["クロアチア"] = "HRK:クナ"; currency["日本"] = "JPY:円"; currency["タイ"] = "THB:バーツ"; currency["スイス"] = "CHF:フラン"; currency["シンガポール"] = "SGD:ドル"; currency["ポーランド"] = "PLN:ズウォティ"; currency["ブルガリア"] = "BGN:レフ"; currency["トルコ"] = "TRY:リラ"; currency["中国"] = "CNY:人民元"; currency["ノルウェー"] = "NOK:クローネ"; currency["ニュージーランド"] = "NZD:ドル"; currency["南アフリカ共和国"] = "ZAR:ランド"; currency["アメリカ"] = "USD:ドル"; currency["メキシコ"] = "MXN:ポンド"; currency["イスラエル"] = "ILS:新シケル"; currency["イギリス"] = "GBP:ポンド"; currency["韓国"] = "KRW:ウォン"; currency["マレーシア"] = "MYR:リンギット"; // ######################################## // APIでデータを取得する部分 // ######################################## // APIを呼び出し日本円を求める const rateFunction = async (event) => { const userText = event.message.text; // ユーザーメッセージが「日の出」か「日の入り」かどうか if (userText.indexOf('日本でいくら?') === -1) { return client.replyMessage(event.replyToken, { type: 'text', text: '「(国名):(金額)は日本でいくら?」と話しかけてね' }); } else { // 「リプライ」を使って先に返事しておきます await client.replyMessage(event.replyToken, { type: 'text', text: '調べています……' }); let pushText = ''; try { const country_amt = userText.substring(0, userText.indexOf('は')); // LINEに入力した国名 const country = country_amt.substring(0, userText.indexOf(':')); // LINEに入力した金額 const amt = country_amt.substring(userText.indexOf(':')+1, userText.length); // axiosでレート取得のAPIを叩きます(少し時間がかかる・ブロッキングする) const res = await axios.get('https://api.exchangeratesapi.io/latest'); // LINEに入力した国名から通貨を取得する const country_tmp = currency[`${country}`]; // 通貨(英語名) const country_currency = country_tmp.substring(0, country_tmp.indexOf(':')); // 通貨(日本語名) const country_currency_jpn = country_tmp.substring(country_tmp.indexOf(':')+1, country_tmp.length); // LINEに入力した国名の1ユーロあたりのレート const country_rates = res.data.rates[`${country_currency}`]; // 1ユーロあたりの日本円 const jpy_rates = res.data.rates.JPY; // LINEに入力した金額の日本円 const jpy_yen = Math.round((amt / country_rates) * jpy_rates); pushText = `日本円で約${jpy_yen}円です!`; } catch (error) { pushText = '検索中にエラーが発生しました。ごめんね。'; // APIからエラーが返ってきたらターミナルに表示する console.error(error); } // 「プッシュ」で後からユーザーに通知します return client.pushMessage(event.source.userId, { type: 'text', text: pushText, }); } }; // ######################################## // LINEサーバーからのWebhookデータを処理する部分 // ######################################## // LINE SDKを初期化します const client = new line.Client(config); // LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます async function handleEvent(event) { // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } // 関数を実行します return rateFunction(event); } // ######################################## // Expressによるサーバー部分 // ######################################## // expressを初期化します const app = express(); // HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします app.post('/webhook', line.middleware(config), (req, res) => { // Webhookの中身を確認用にターミナルに表示します console.log(req.body.events); // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行 if (req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff') { res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します console.log('検証イベントを受信しました!'); // ターミナルに表示します return; // これより下は実行されません } // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、 // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result)); }); // 最初に決めたポート番号でサーバーをPC内だけに公開します // (環境によってはローカルネットワーク内にも公開されます) app.listen(PORT); console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);結果
LINEに
中国:1000は日本でいくら?
と入力することで
日本円で約15807円です!
と返信をもらうことができました。考察
目的である現地通貨が日本円ではいくらなのかを調べるツールを作ることができました。
ただ、下記の課題があると思っているので、改善していければと思ってます。1.国名と通貨名の紐づけを連想配列にしているが、もう少しスマートな書き方を検討する。
2.連想配列に存在しない国名がきたときの制御ができていない。
3.入力として受け付ける文字が(国名):(現地金額)は日本でいくら?と指定しづらい。
下記のように対話形式で実現させる。
国名を入力してね!
→中国
→何元?
→1000
→1000元は日本円で約15807円です!
- 投稿日:2020-10-11T22:03:22+09:00
LINE BOTでYouTubeの検索をしてみたよ
初めに
先日、LINE BOT の開発を教わったので、実際に作成してみました。
教わったのは簡単なテキストを返すものでしたが、LINE Messaging API でテンプレートメッセージのカルーセルを使ってみました。
LINE BOT の初期設定(事前準備)は以下を参考にしています。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest目的
私はよくYouTubeで動画を見るので、簡単なYouTube検索ボットを作ってみました。
単純メッセージのレスポンスだとつまらないと思ったので、カルーセル形式に挑戦してみました。環境
Visual Studio Code v1.49.0
node.js v14.9.0
ngrok v2.3.35概要
自PCを(Node.js)サーバに見立て、LINEサーバからWebhookを受け取る為のトンネリングサービスはngrokを利用しています。
データの流れについては、公式ページや上記の参考サイトをみると何となく分かります。今回、YouTubeのデータ検索にはYouTube API DATA v3 を利用し、LINE BOT に送ったメッセージでキーワード検索をしています。APIのレスポンスはaxiosで受け取り、関連する上位3件の動画情報から、サムネイル表示、タイトル表示、動画URLへ飛ぶようにしてみようと思いました。
コード
.js'use strict'; // おまじない // ######################################## // 初期設定など // ######################################## // パッケージを使用します const express = require('express'); const line = require('@line/bot-sdk'); const axios = require('axios'); const PORT = process.env.PORT || 3000; // ローカル(自分のPC)でサーバーを公開するときのポート番号 const YoutubeURL = 'https://www.youtube.com/watch?v='; // YouTubeURL const YoutubeAPIKey = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; //YouTubeAPIKey // Messaging APIで利用するクレデンシャル(秘匿情報)です。 const config = { channelSecret: '99999999999999999999999999999999', //作成したBotのチャネルシークレット channelAccessToken: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' //作成したBotのチャネルアクセストークン }; // ########## ▼▼▼ サンプル関数 ▼▼▼ ########## const MainFunction = async (event) => { const userText = event.message.text; // ユーザーメッセージ axios.defaults.baseURL = 'https://www.googleapis.com/youtube/v3'; // YouTube Data API // 「リプライ」を使って先に返事しておきます await client.replyMessage(event.replyToken, { type: 'text', text: '調べています……' }); let Title = []; let IdUrl = []; let ImageUrl = []; try { // axiosでAPIを叩きます(少し時間がかかる・ブロッキングする) const res = await axios.get('/search?part=snippet&q=' + encodeURIComponent(userText) + '&key=' + YoutubeAPIKey); // 関連の高い動画を3件返す for (let i=0; i<3; i++) { Title.push(res.data.items[i].snippet.title); // タイトル // 文字数が50より大きかったら末尾に...を付けたす ※MessagingApiの制限60文字を超えるとエラーになるので。 if (Title[i].length > 50) { Title[i] = Title[i].substr(0, 50) + '...'; } IdUrl.push(YoutubeURL + res.data.items[i].id.videoId); // 動画URL ImageUrl.push(res.data.items[i].snippet.thumbnails.medium.url); // サムネ画像 } } catch (error) { // APIからエラーが返ってきたらターミナルに表示する return client.pushMessage(event.source.userId, { type: 'text', text: '検索中にエラーが発生しました。ごめんね。', }); console.error(error); } // 「プッシュ」で後からユーザーに通知(カルーセル形式ガリ書き。) return client.pushMessage(event.source.userId, { "type": "template", "altText": "this is a carousel template", "template": { "type": "carousel", "columns": [ { "thumbnailImageUrl": ImageUrl[0], "text": Title[0], "defaultAction": { "type": "uri", "label": "動画を見に行く", "uri": IdUrl[0] }, "actions": [ { "type": "uri", "label": "動画を見に行く", "uri": IdUrl[0] } ] }, { "thumbnailImageUrl": ImageUrl[1], "text": Title[1], "defaultAction": { "type": "uri", "label": "動画を見に行く", "uri": IdUrl[1] }, "actions": [ { "type": "uri", "label": "動画を見に行く", "uri": IdUrl[1] } ] }, { "thumbnailImageUrl": ImageUrl[2], "text": Title[2], "defaultAction": { "type": "uri", "label": "動画を見に行く", "uri": IdUrl[2] }, "actions": [ { "type": "uri", "label": "動画を見に行く", "uri": IdUrl[2] } ] } ], "imageAspectRatio": "rectangle", "imageSize": "cover" } }); }; // ########## ▲▲▲ サンプル関数 ▲▲▲ ########## // ######################################## // LINEサーバーからのWebhookデータを処理する部分 // ######################################## // LINE SDKを初期化します const client = new line.Client(config); // LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます async function handleEvent(event) { // 受信したWebhookが「テキストメッセージ以外」であれば固定メッセージを返す if (event.type !== 'message' || event.message.type !== 'text') { //return Promise.resolve(null); return client.replyMessage(event.replyToken, { type: 'text', text: 'YouTubeで検索しようか?\n検索キーワードは?' }); } // サンプル関数を実行します return MainFunction(event); } // ######################################## // Expressによるサーバー部分 // ######################################## // expressを初期化します const app = express(); // HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします app.post('/webhook', line.middleware(config), (req, res) => { // Webhookの中身を確認用にターミナルに表示します console.log(req.body.events); // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行 if (req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff') { res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します console.log('検証イベントを受信しました!'); // ターミナルに表示します return; // これより下は実行されません } // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、 // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result)); }); // 最初に決めたポート番号でサーバーをPC内だけに公開します // (環境によってはローカルネットワーク内にも公開されます) app.listen(PORT); console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);苦戦した箇所
- ・YouTube APIの仕様理解
- 以下サイトを参考に、APIの有効化と仕様を確認しましたが、上手くデータを取れず少し苦戦しました。
原因は余計なパラメータを渡していた為でした。(APIの仕様書を読み解くのが、なかなか慣れない、、、)
Youtube Data API Key の取得手順
YouTube Data API | Google Developers
動画の検索 | YouTube Data API v2
- ・LINE Messaging APIのカルーセル表示
- テンプレートメッセージのカルーセル表示もかなり苦戦しました。YouTube APIからのデータは取れているのにエラーで上手くいかず、原因箇所特定に苦戦しました。
原因は、タイトルの文字数制限があり、超えていた為でした。なので、文字数判定を入れて無理やり切るように工夫してます。Title.push(res.data.items[i].snippet.title); // タイトル // 文字数が50より大きかったら末尾に...を付けたす ※MessagingApiの制限60文字を超えるとエラーになるので。 if (Title[i].length > 50) { Title[i] = Title[i].substr(0, 50) + '...'; }結果
実行結果は以下の通り、上手くデータを取ることができました。
テキストメッセージ以外は、固定メッセージを返すようにしています。
振り返り
課題はAPIの利用方法がまだ慣れない点だと強く感じました。
JavaScript、JSONがまだ良く分かっていない、自分の課題が見えました。
これからも書き続けてコツを掴めたら、もっとできることが増えていって楽しいだろうなぁと感じました。
(おすすめ提案とかもさせてみたかったけど、時間的に厳しかった。)
- 投稿日:2020-10-11T20:40:25+09:00
v0.10 voting が true になるヤツです..... (Symbol bootstrap)
ubutu 16.04/18.04/20.04 で OSインストール後にコレを実行して下しぃ
今のトコ 何故か 18.04/20.04 では起動しにくいw です(t4vpsでは...)この script で起動し切らない時は 下に update用の script を用意しますたので
一旦ログアウトしてから 設定した user /sshd で ログイン後
そっちを実行して下しぃ↓↓↓ voting が true になる 自動構築 script ↓↓↓
#!/bin/bash #new username echo "??? please input new username ???" read username #new sshd port echo "??? please input new sshd port ???" read sshd #ip echo "??? please input ip ???" read ip #friendly_name echo "??? please input friendly_name ???" read friendlyname #input beneficiaryAddress echo "??? please input beneficiaryAddress ???" read beneficiaryAddress #---------- #usernameset adduser $username gpasswd -a $username sudo #sshdset sed -i -e s/"#Port .*"/"Port 22"/ /etc/ssh/sshd_config sed -i -e s/"Port .*"/"Port $sshd"/ /etc/ssh/sshd_config sed -i -e s/"PermitRootLogin .*"/"PermitRootLogin no"/ /etc/ssh/sshd_config systemctl restart sshd #ufw set apt-get install ufw ufw allow $sshd/tcp #ufw start ufw enable ufw status #docker curl https://get.docker.com | sh usermod -aG docker $username systemctl start docker systemctl enable docker docker --version #docker-compose curl -L https://github.com/docker/compose/releases/download/1.27.3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose docker-compose --version systemctl restart docker #node js curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - apt-get install -y nodejs npm install -g npm node -v npm -v #install Symbol Bootstrap npm install -g symbol-bootstrap symbol-bootstrap -v #change userdir cd /home/$username #make workdir mkdir -p symbol-bootstrap cd symbol-bootstrap #make custom config file for voting cat <<EOF > vote.yml nodes: - voting: true EOF #make config file symbol-bootstrap config -p testnet -a dual -c vote.yml #config ip sed -i -e s/"host = .*"/"host = $ip"/ target/config/api-node/resources/config-node.properties #config friendlyname sed -i -e s/"friendlyName = .*"/"friendlyName = $friendlyname"/ target/config/api-node/resources/config-node.properties #config beneficiaryAddress sed -i -e s/"beneficiaryAddress = .*"/"beneficiaryAddress = $beneficiaryAddress"/ target/config/api-node/resources/config-harvesting.properties #check config echo "------------------------------------------------" sed -n 52,53p target/config/api-node/resources/config-node.properties echo "!!! Check IP/frendlyname !!!" echo "IP/frendlyname Ok? Ok:ENTER No:CTRL+C and restart script" read a sed -n 683p target/config/preset.yml echo "!!! Make sure voting is true !!! continue to ENTER" read a echo "!!! Check beneficiaryAddress !!!" echo "beneficiaryAddress Ok? Ok:ENTER No:CTRL+C and restart script" sed -n 9p target/config/api-node/resources/config-harvesting.properties read a echo "continue working......." #make docker-compose.yml symbol-bootstrap compose #start symbol-bootstrap run -d --timeout 600000 #show addresses cat target/config/generated-addresses/addresses.yml echo "" echo "------------------------------------" echo "!!! Push ENTER to show your harvesting address !!!" read a echo "!!! Your harvesting address is here !!!" sed -n 12,15p target/config/generated-addresses/addresses.yml echo "!!! beneficiaryAddress !!!" sed -n 9p target/config/api-node/resources/config-harvesting.propertiesupdate 用 (起動し切らなかった用w) script (node 固有アドレスとか変わっちまいやがりますので ソコ注意してね)
↑ の script で ubuntu 18.04/20.04 が 一発で起動せんかったの (特に 18.04)
api が up にならない事がありますた (http://"サーバのIPアドレス":3000/node/health に出て来ますよ)起動し切らんかった場合 コレ実行すると 上手く起動出来るかもかも
自動構築 script 実行時冒頭に設定した user/sshd port で再ログイン後
ディレクトリはそのままで これを実行して下しぃ↓↓↓ update script ↓↓↓
#!/bin/bash #input ip echo "??? please input ip ???" read ip #input friendlyname echo "??? please input friendly_name ???" read friendlyname #input beneficiaryAddress echo "??? please input beneficiaryAddress ???" read beneficiaryAddress echo "continue working......." #stop node cd symbol-bootstrap symbol-bootstrap stop #delete symbol-bootstrap dir cd sudo rm -rf symbol-bootstrap docker system prune -a #install Symbol Bootstrap sudo npm install -g symbol-bootstrap symbol-bootstrap -v #make workdir mkdir -p symbol-bootstrap cd symbol-bootstrap #make custom config file cat <<EOF > vote.yml nodes: - voting: true EOF #make config file symbol-bootstrap config -p testnet -a dual -c vote.yml #config ip sed -i -e s/"host ="/"host = $ip"/ target/config/api-node/resources/config-node.properties #config friendlyname sed -i -e s/"friendlyName = ......."/"friendlyName = $friendlyname"/ target/config/api-node/resources/config-node.properties #config beneficiaryAddress sed -i -e s/"beneficiaryAddress = "/"beneficiaryAddress = $beneficiaryAddress"/ target/config/api-node/resources/config-harvesting.properties #check config echo "------------------------------------------------" sed -n 52,53p target/config/api-node/resources/config-node.properties echo "!!! Check IP/frendlyname !!!" echo "IP/frendlyname Ok? Ok:ENTER No:CTRL+C and restart script" read a sed -n 683p target/config/preset.yml echo "!!! Make sure voting is true !!! continue to ENTER" read a echo "!!! Check beneficiaryAddress !!!" echo "beneficiaryAddress Ok? Ok:ENTER No:CTRL+C and restart script" sed -n 9p target/config/api-node/resources/config-harvesting.properties read a echo "continue working......." #make docker-compose.yml symbol-bootstrap compose #start symbol-bootstrap run -d --timeout 600000 #show addresses cat target/config/generated-addresses/addresses.yml echo "" echo "------------------------------------" echo "!!! Push ENTER to show your harvesting address !!!" read a echo "!!! Your harvesting address is here !!!" sed -n 12,15p target/config/generated-addresses/addresses.yml echo "!!! beneficiaryAddress !!!" sed -n 9p target/config/api-node/resources/config-harvesting.propertiesちゃんと起動出来たかは
http://"http://"サーバのIPアドレス":3000/node/info
http://"http://"サーバのIPアドレス":3000/chain/info
http://"http://"サーバのIPアドレス":3000/node/health
http://"http://"サーバのIPアドレス":3000/node/peers
で確認して下しぃ今のトコ ubuntu 16.04 なら 一発起動出来てます.....(time4vps の場合ですが)
あ あと
"beneficiaryAddress" ってのは
harvesting 設定した時に
収穫を送るアドレスを設定出来ます
ここは特に設定しなければ
収穫は node 固有アドレスに入ります
- 投稿日:2020-10-11T20:21:13+09:00
位置情報を送ると近くの宿泊施設を返すLINEBotを作る
初めに
LINEmessagingAPI + WebAPIで、位置情報を送ると近くの宿泊施設を返してくれるLINEBotを作成しました。
ついでに最寄り駅も返します。利用したAPI
■楽天トラベル施設検索API(宿泊施設の検索)
https://webservice.rakuten.co.jp/api/simplehotelsearch/■HeartRails Geo API(最寄り駅の検索)
http://geoapi.heartrails.com/コード
'use strict'; // おまじない // ######################################## // 初期設定など // ######################################## // パッケージを使用します const express = require('express'); const line = require('@line/bot-sdk'); const axios = require('axios'); // ローカル(自分のPC)でサーバーを公開するときのポート番号です const PORT = process.env.PORT || 10000; // Messaging APIで利用するクレデンシャル(秘匿情報)です。 const config = { channelSecret: '作成したBotのチャネルシークレット', channelAccessToken: '作成したBotのチャネルアクセストーク' }; // ########## ▼▼▼ サンプル関数 ▼▼▼ ########## const sampleFunction = async (event) => { // ユーザーメッセージが位置情報かどうか if (event.message.type == 'location') { let pushText = ''; const latitude = event.message.latitude; const longitude = event.message.longitude; const postNumber = event.message.address.split('〒')[1].split(' ')[0].replace('-',''); //楽天トラベル施設検索APIで、指定された位置情報から半径1km以内の宿を検索(おすすめ順・5件) client.pushMessage(event.source.userId, { type: 'text', text: '1km以内の宿泊施設を検索します。', }); try { const rakuten = await axios.get('https://app.rakuten.co.jp/services/api/Travel/SimpleHotelSearch/20170426?applicationId=アプリID&format=json&latitude=' + latitude + '&longitude=' + longitude + '&searchRadius=1&datumType=1&hits=5'); for(let i in rakuten.data.hotels){ client.pushMessage(event.source.userId, { type: 'text', text: rakuten.data.hotels[i]['hotel'][0]['hotelBasicInfo']['hotelInformationUrl'], }); } } catch (error) { pushText = '楽天検索中にエラーが発生しました。ごめんね。'; // APIからエラーが返ってきたらターミナルに表示する console.error(error); client.pushMessage(event.source.userId, { type: 'text', text: pushText, }); } //郵便番号から最寄駅を検索 try { const station = await axios.get('http://geoapi.heartrails.com/api/json?method=getStations&postal=' + postNumber); client.pushMessage(event.source.userId, { type: 'text', text: '最寄り駅は' + station.data['response']['station'][0].name + '駅です。', }); } catch (error) { pushText = '最寄り駅検索中にエラーが発生しました。ごめんね。'; // APIからエラーが返ってきたらターミナルに表示する console.error(error); client.pushMessage(event.source.userId, { type: 'text', text: pushText, }); } return await client.pushMessage(event.source.userId, { type: 'text', text: '良い旅を。', }); } }; // ########## ▲▲▲ サンプル関数 ▲▲▲ ########## // ######################################## // LINEサーバーからのWebhookデータを処理する部分 // ######################################## // LINE SDKを初期化します const client = new line.Client(config); // LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます async function handleEvent(event) { // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します //if (event.type !== 'message' || event.message.type !== 'text') { // return Promise.resolve(null); //} // サンプル関数を実行します return sampleFunction(event); } // ######################################## // Expressによるサーバー部分 // ######################################## // expressを初期化します const app = express(); // HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします app.post('/webhook', line.middleware(config), (req, res) => { // Webhookの中身を確認用にターミナルに表示します console.log(req.body.events); // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行 if (req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff') { res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します console.log('検証イベントを受信しました!'); // ターミナルに表示します return; // これより下は実行されません } // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、 // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result)); }); // 最初に決めたポート番号でサーバーをPC内だけに公開します // (環境によってはローカルネットワーク内にも公開されます) app.listen(PORT); console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);結果
実行結果です。
課題・考察
・最寄り駅の取得ができない場合がある
原因:LINEで位置情報を送る際、施設を指定すると郵便番号が送られない
対策:住所情報→郵便番号変換→最寄り駅検索の処理に変更する・対話型で検索条件をより柔軟にしたい
楽天トラベル施設検索APIを利用する際、検索条件は「おすすめ順」で5件表示としていますが、
Botとの会話で「値段が安い順」などを設定して検索できればより使いやすいなと感じました。位置情報を利用したAPIは他にもいくつもあるので、さらに組み合わせるともっと便利なBotになる気がします。
宿検索してそのまま予約までできるんじゃないかな。
- 投稿日:2020-10-11T19:34:10+09:00
WSL2でGUIアプリケーションを使えるようにする(ついでにPuppeteerでGoogle画像検索のスクレイピングに挑戦)
WSL2 で GUI アプリケーション実行
VcXsrv を使うことで WSL2 上で Linux GUI アプリケーションを実行することが可能です。
本稿では、GUIアプリケーションとして Google Chrome ブラウザを WSL2 上で実行できるように設定し、Puppeteer で Chrome ブラウザを自動制御してスクレイピングしてみます。
Environment
WSL2 + Ubuntu 20.04 + Docker 開発環境構築 で構築した環境を想定しています。
- OS:
Windows 10
(バージョン 2004, ビルド 19041 以上)- WSL2:
- OS:
Ubuntu 20.04
- Shell:
bash
Windows側の設定: VcXsrvの準備
Chocolateyパッケージマネージャ を用いてVcXsrv(Windows用Xサーバ環境)をインストールします。
Win + X
|>A
=> 管理者権限 PowerShell を起動します。# Chocolatey パッケージマネージャを導入していない場合は導入 > Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) # VcXsrv をインストール > choco install -y vcxsrvインストールしたら Windows スタートメニューから
XLaunch
を起動します。
- 起動時のダイアログ設定
- Select display settings:
Multiple Display
- Select how to start clients:
Start no client
- Extra settings:
- Clipboard (Primary Selection)
- Native opengl
- Disable access control
- Additional parameters for VcXsrv:
-ac
- ファイアウォールの設定
- VcXsrv の初回起動時にファイアウォールを聞かれたらパブリックネットワークで許可する
- ※ プライベートネットワークでは WSL2 と通信できず上手く行かない
- 初回起動時のファイアウォールの設定に失敗した場合:
Win + X
|>N
=> Windows 設定 > 更新とセキュリティ
- Windowsセキュリティ > ファイアウォールとネットワーク保護 > ファイアウォールによるアプリケーションの許可
- 「設定の変更」ボタンを押して設定編集する
VcXsrv windows xserver
の「プライベート」「パブリック」両方にチェックを入れるWSL2 (Ubuntu) 側の設定
# Xorg GUI 環境をインストール ## Ubuntu では様々な GUI 環境を利用できるため、好みに応じてインストールすれば良い $ sudo apt install -y libgl1-mesa-dev xorg-dev # DISPLAY 環境変数を Windows 側 VcXsrv IP にする ## シェルログイン時に一度設定されればよいため ~/.profile に設定を記述 ## << \EOS と書くことで内部テキストを変数展開せずに echo 可能 $ sudo tee -a ~/.profile << \EOS # WSL2 VcXsrv 設定 export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):0.0 EOS # シェル再起動 $ exec $SHELL -l動作確認
動作確認用に gedit (GNOMEデスクトップの標準テキストエディタ) をインストールして起動してみます。
# gedit インストール $ sudo apt install -y gedit # gedit 起動 ## 後ろに & をつけないと、GUI アプリケーション終了までコマンドを受け付けなくなる $ gedit &gedit が起動すれば OK です。
Windows側: VcXsrv のスタートアップ登録
ここまで設定をすると、VcXsrv が起動していないと WSL2 も起動しない状態になってしまいます。
そのため、Windows 起動時に VcXsrv も起動するようにしておきます。
Win + X
|>A
=> 管理者権限 PowerShell を起動します。# WSH を使って Windows スタートアップディレクトリに VcxSrv のショートカット作成 > $wsh = New-Object -ComObject WScript.Shell > $shortcut = $wsh.CreateShortcut("$env:USERPROFILE\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\vcxsrv.lnk") # ショートカット: vcxsrv.exe -multiwindow -ac > $shortcut.TargetPath = "C:\Program Files\VcXsrv\vcxsrv.exe" > $shortcut.IconLocation = "C:\Program Files\VcXsrv\vcxsrv.exe" > $shortcut.Arguments = "-multiwindow -ac" > $shortcut.Save()これで、Windows 起動時に VcxSrv が
-multiwindow -ac
オプションで起動するようになります。WSL2 (Ubuntu) 側: 日本語フォントを利用できるように設定
今のままでは日本語が表示できないため、日本語フォントを利用できるようにします。
# fc-cache コマンド等をインストール $ sudo apt install -y fontconfig # Windows側のフォントをシンボリックリンクすることで日本語フォントを使用できるようにする $ sudo ln -s /mnt/c/Windows/Fonts /usr/share/fonts/windows # フォントキャッシュクリア $ sudo fc-cache -fv # 日本語言語パックのインストール $ sudo apt -y install language-pack-ja # ロケールを日本語に設定 $ sudo update-locale LANG=ja_JP.UTF8 # いったん終了して再起動すればアプリケーションで日本語が使えるようになる $ exit # --- WSL2シェル再起動 --- # タイムゾーンをJSTに設定 $ sudo dpkg-reconfigure tzdata ## TUI で設定: Asia > Tokyo # 日本語 man (コマンドマニュアル) をインストール $ sudo apt install -y manpages-ja manpages-ja-devWSL2 (Ubuntu) 側: GUI で日本語入力可能にする
ここまでで日本語表示可能になりましたが、まだIMEが使えないため、日本語入力ができません。
そのため、mozc と fcitx を導入します。
- mozc: Googleが開発しているオープンソースのIME
- fcitx: Unix系OSにおけるインプットメソッドフレームワーク
# mozc と fcitx を導入 $ sudo apt -y install fcitx-mozc dbus-x11 x11-xserver-utils $ dbus-uuidgen | sudo tee /var/lib/dbus/machine-id # fcitx 設定 $ set -o noclobber # 必要な環境変数等を ~/.profile に追記 $ sudo tee -a ~/.profile << \EOS # fcitx 日本語入力設定 export GTK_IM_MODULE=fcitx export QT_IM_MODULE=fcitx export XMODIFIERS="@im=fcitx" export DefaultIMModule=fcitx if [ $SHLVL = 1 ] ; then # 半角全角点滅防止 xset -r 49 1>/dev/null 2>/dev/null # fcitx 起動 fcitx-autostart 1>/dev/null 2>/dev/null fi EOS # シェル再起動 $ exec $SHELL -l動作確認
gedit を起動して、日本語入力できるか確認してみましょう。
Google Chrome by Puppeteer on WSL2
ここまでの設定により WSL2 上で GUI アプリケーションを実行できるようになったため、Google Chrome ブラウザをインストールして、Puppeteer から操作できるようにしてみます。
(Chromeブラウザにはヘッドレスモードがあるため、GUIアプリケーションが実行できない環境でもPuppeteerによる操作はできるのですが、今回は挙動を確認しながら操作するためにヘッドレスモードは利用しません)# google-chrome 用リポジトリ登録 $ echo 'deb http://dl.google.com/linux/chrome/deb/ stable main' | sudo tee /etc/apt/sources.list.d/google-chrome.list $ wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - # google-chrome インストール $ sudo apt update && sudo apt install google-chrome-stable # google-chrome 起動 $ google-chrome & # => Google Chrome ブラウザが起動すればOKPuppeteer でブラウザ操作してみる
- Puppeteer:
- Chromeブラウザを Node.js から操作するためのライブラリ
- ブラウザそのものを操作するため、通常のスクレイピングでは難しい JavaScript の実行などもできる
Node.js のバージョンは安定版であればいくつでも良いと思いますが、ここでは
12.18.2
を利用することにします。# nodenv で Node.js 12.18.2 を導入 $ nodenv install 12.18.2 # Node.js バージョンを 12.18.2 に切り替え $ nodenv global 12.18.2 # バージョン確認 $ node -v v12.18.2 $ yarn -v 1.22.4 # プロジェクトディレクトリを ~/dev/nodejs/puppeteer/ とする $ mkdir -p ~/dev/nodejs/puppeteer/ $ cd ~/dev/nodejs/puppeteer/ # puppeteer インストール $ yarn add puppeteer # app.js 作成 $ touch app.js $ code app.jsapp.jsconst puppeteer = require('puppeteer') const fs = require('fs') const main = async () => { // headless: false => GUIブラウザ起動モードで Puppeteer 起動 const browser = await puppeteer.launch({ headless: false }) const page = await browser.newPage() // google.com に移動 await page.goto('https://www.google.com', {waitUntil: 'domcontentloaded'}) // スクリーンショット保存 fs.writeFileSync('screenshot.png', await page.screenshot({fullPage: true})) // 終了 await browser.close() } main()# 実行 $ node app.js # => Chromeブラウザが起動し、Googleホームページのスクリーンショットが撮影される # => ~/dev/nodejs/puppeteer/screenshot.png に保存される
PuppeteerでGoogle画像検索をスクレイピング
空前の機械学習ブームである昨今、自分でも画像分類の機械学習モデルを作りたいと思うことも少なくありません。
しかしながら、機械学習には大量の学習用データが必要であり、画像分類を行う場合、いかにして画像データを収集するかがネックになります。一つの手段としては、Googleが提供している検索用のAPIを利用して画像検索・収集する方法がありますが、このAPIは無料利用できるリクエスト数に制限があり、大量のデータ収集には向いておりません。
そのため、スクレイピング(HTTPリクエストを行ってそのレスポンスデータを解析する)という手段をとることが多いです。
本稿においても、Google画像検索を自動的に行い、そのレスポンスデータをスクレイピングして画像収集してみます。
※ スクレイピングは、サーバへの負荷を考え、短時間での大量リクエスト等を行わないよう、マナーを守って行う必要があります。
Repository
https://github.com/amenoyoya/scraping-tool/tree/master/google-images
Google画像検索の実行
app.jsconst puppeteer = require('puppeteer') /** * puppeteer 実行メイン関数 * @param {function(puppeteer.Page) => void} callback * @param {*} opt */ const puppet = async (callback, opt = {}) => { const browser = await puppeteer.launch(opt) const page = await browser.newPage() await page.emulate({ 'name': 'Windows', 'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3864.0', 'viewport': { 'width': 1024, 'height': 820, 'deviceScaleFactor': 1, 'isMobile': false, 'hasTouch': false, 'isLandscape': false } }) try { await callback(page) } catch (err) { console.log(err) } if (opt.close !== false) { await browser.close() } } /** * Google画像検索実行 * @param {puppeteer.Page} page * @param {string} keyword */ const searchGoogleImage = async (page, keyword) => { await page.goto('https://www.google.co.jp/imghp?hl=ja&tab=wi&authuser=0&ogbl', {waitUntil: 'domcontentloaded'}) await page.type('input[name="q"]', keyword) // フォーム送信してページ遷移を待つ await page.click('button[type="submit"]') await page.waitFor('img.rg_i', {timeout: 60000}) } /** * 動作確認: 「apple」というキーワードで画像検索実行 */ puppet(async page => { // 画像検索実行: keyword = 'apple' await searchGoogleImage(page, 'apple') }, { headless: false, // 挙動確認できるようにヘッドレスモードは利用しない slowMode: 500, // 挙動確認しやすいように+サーバ負荷を考えて、一つのアクションに 500 ミリ秒のインターバル close: false, // Chromeブラウザの開発ツールでレスポンスデータの解析を行いたいため、ブラウザ終了させない })# 実行 $ node app.js実行すると、Chromeブラウザが立ち上がり、「apple」というキーワードでGoogle画像検索が実行されるはずです。
F12
キーで、Chromeの開発ツールが表示されるので、それを見ながらHTMLソースコードを解析 → ダウンロードするべき画像データを特定します。検索結果ページから画像URLを抽出
2020年10月現在のGoogle画像検索は LazyLoad で画像を表示する仕様のため、画像URLの抽出には工夫が必要です。
今回は以下のように、「サムネイル画像をクリック」して出てくる「スライド画像URL」を抽出することで対応しています。
app.js/** * Google画像検索結果ページから画像URL取得 * @param {puppeteer.Page} page * @param {number} index * @return {string|boolean} url */ const getGoogleImage = async (page, index) => { try { // サムネイルをクリック => 自動でスクロールされるため、次のサムネイル画像もLazyLoadされる const thumbs = await page.$$('img.rg_i') if (thumbs.length <= index) { return false } await thumbs[index].click() } catch { return false } // サムネイルをクリックして出てくるスライド画像のURLを取得 try { await page.waitFor( 'img[jsname="HiaYvf"]:not([src^="data:image"]):not([src^="https://encrypted-tbn0.gstatic.com"])', {timeout: 5000} ) return await page.$eval( 'img[jsname="HiaYvf"]:not([src^="data:image"]):not([src^="https://encrypted-tbn0.gstatic.com"])', el => el.src ) } catch { // スライド画像がサムネイル画像と同じ場合: [前の画像, 今の画像, 次の画像] const imgs = await page.$$('img[jsname="HiaYvf"') if (imgs.length === 0) { return false } const img = imgs.length > 2? imgs[1]: imgs[0] return await img.evaluate(el => el.src) } }画像のダウンロード
画像URLを特定できればダウンロードは簡単です。
axios
を使うのが楽です。# axios インストール $ yarn add axiosapp.jsconst axios = require('axios') const fs = require('fs') const path = require('path') /** * 指定URLのリソースをバイナリデータとして取得 * @param {string} url * @return {Buffer|null} */ const getBinaryData = async url => { try { const res = await axios.get(url, {responseType: 'arraybuffer'}) return new Buffer.from(res.data) } catch (err) { console.log(err) return null } } /** * Base64画像データをバイナリデータとして取得 * @param {string} base64 * @return {Buffer} */ const decodeBase64Image = base64 => { return new Buffer.from(base64.replace(/^data:\w*\/\w+;base64,/, ''), 'base64') } /** * 指定URLのリソースをファイルにダウンロード * @param {string} url * @param {string} filename * @param {boolean} rename 同名ファイルを自動リネームするかどうか * @return {boolean} */ const download = async (url, filename, rename = false) => { const dir = path.dirname(filename) const ext = path.extname(filename) // 同名ファイルを自動リネームする場合: filename + '_' + ext const basename = (rename && isFile(filename))? path.basename(filename, ext) + '_' + ext: path.basename(filename) // base64デコード if (url.match(/^data:/)) { fs.writeFileSync(path.join(dir, basename), decodeBase64Image(url), 'binary') return true } // URLからダウンロード const buf = await getBinaryData(url) if (buf === null) { return false } fs.writeFileSync(path.join(dir, basename), buf, 'binary') return true }CLIツールとして完成させる
最後に
commander
を導入してCLIツールとして完成させます。# commander 導入 $ yarn add commander以下、全コードを記載します。
app.jsconst puppeteer = require('puppeteer') const axios = require('axios') const fs = require('fs') const path = require('path') const {program} = require('commander') /** * 指定パスがディレクトリか判定 * @param {string} target * @return {boolean} */ const isDirectory = target => { try { return fs.statSync(target).isDirectory() } catch (error) { return false } } /** * 指定パスがファイルか判定 * @param {string} target * @return {boolean} */ const isFile = target => { try { return fs.statSync(target).isFile() } catch (error) { return false } } /** * 指定URLのリソースをバイナリデータとして取得 * @param {string} url * @return {Buffer|null} */ const getBinaryData = async url => { try { const res = await axios.get(url, {responseType: 'arraybuffer'}) return new Buffer.from(res.data) } catch (err) { console.log(err) return null } } /** * Base64画像データをバイナリデータとして取得 * @param {string} base64 * @return {Buffer} */ const decodeBase64Image = base64 => { return new Buffer.from(base64.replace(/^data:\w*\/\w+;base64,/, ''), 'base64') } /** * 指定URLのリソースをファイルにダウンロード * @param {string} url * @param {string} filename * @param {boolean} rename 同名ファイルを自動リネームするかどうか * @return {boolean} */ const download = async (url, filename, rename = false) => { const dir = path.dirname(filename) const ext = path.extname(filename) // 同名ファイルを自動リネームする場合: filename + '_' + ext const basename = (rename && isFile(filename))? path.basename(filename, ext) + '_' + ext: path.basename(filename) // base64デコード if (url.match(/^data:/)) { fs.writeFileSync(path.join(dir, basename), decodeBase64Image(url), 'binary') return true } // URLからダウンロード const buf = await getBinaryData(url) if (buf === null) { return false } fs.writeFileSync(path.join(dir, basename), buf, 'binary') return true } /** * puppeteer 実行メイン関数 * @param {function(puppeteer.Page) => void} callback * @param {*} opt */ const puppet = async (callback, opt = {}) => { const browser = await puppeteer.launch(opt) const page = await browser.newPage() await page.emulate({ 'name': 'Windows', 'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3864.0', 'viewport': { 'width': 1024, 'height': 820, 'deviceScaleFactor': 1, 'isMobile': false, 'hasTouch': false, 'isLandscape': false } }) try { await callback(page) } catch (err) { console.log(err) } if (opt.close !== false) { await browser.close() } } /** * Google画像検索実行 * @param {puppeteer.Page} page * @param {string} keyword */ const searchGoogleImage = async (page, keyword) => { await page.goto('https://www.google.co.jp/imghp?hl=ja&tab=wi&authuser=0&ogbl', {waitUntil: 'domcontentloaded'}) await page.type('input[name="q"]', keyword) // フォーム送信してページ遷移を待つ await page.click('button[type="submit"]') await page.waitFor('img.rg_i', {timeout: 60000}) } /** * Google画像検索結果ページから画像URL取得 * @param {puppeteer.Page} page * @param {number} index * @return {string|boolean} url */ const getGoogleImage = async (page, index) => { try { // サムネイルをクリック => 自動でスクロールされるため、次のサムネイル画像もLazyLoadされる const thumbs = await page.$$('img.rg_i') if (thumbs.length <= index) { return false } await thumbs[index].click() } catch { return false } // サムネイルをクリックして出てくるスライド画像のURLを取得 try { await page.waitFor( 'img[jsname="HiaYvf"]:not([src^="data:image"]):not([src^="https://encrypted-tbn0.gstatic.com"])', {timeout: 5000} ) return await page.$eval( 'img[jsname="HiaYvf"]:not([src^="data:image"]):not([src^="https://encrypted-tbn0.gstatic.com"])', el => el.src ) } catch { // スライド画像がサムネイル画像と同じ場合: [前の画像, 今の画像, 次の画像] const imgs = await page.$$('img[jsname="HiaYvf"') if (imgs.length === 0) { return false } const img = imgs.length > 2? imgs[1]: imgs[0] return await img.evaluate(el => el.src) } } /** * Google画像検索: もっと表示 * @param {puppeteer.Page} page * @return {boolean} */ const loadMoreGoogleImages = async page => { try { await page.click('input[jsaction="Pmjnye"]') await page.waitFor(5000) return true } catch { return false } } /** * 画像URLからファイル名取得 * @param {string} url * @return {string} */ const getFilename = url => { if (url.match(/^data:/)) { // base64 データの場合は 'base64.拡張子' というファイル名にする return 'base64.' + url.match(/^data:image\/([^;]+)/)[1] } const filename = path.basename(url.match(/[^\?]+/)[0]) // クエリ文字列は削除 let ext = path.extname(filename) let stem = path.basename(filename, ext) // 拡張子抜きのファイル名 // 拡張子がない場合は .jpg とする ext = ext === ''? '.jpg': ext // ファイル名の長さは64文字までとする stem = stem.length > 64? stem.slice(0, 64): stem return stem + ext } /** * CLIオプションパース */ program .option('-d, --directory <string>', '保存先ディレクトリ', '.') .option('-l, --headless <boolean>', 'ヘッドレスモード', false) .option('-s, --slowmode <number>', '動作遅延[ms]', 500) .option('-C, --noclose <boolean>', 'ブラウザを自動で閉じない', false) .option('-n --numbers <number>', 'ダウンロード数', 100) .option('-r --rename <boolean>', '同名ファイルを自動リネーム', false) .requiredOption('-k, --keyword <string>', '検索キーワード') .parse(process.argv) /** * メインプログラム */ puppet(async page => { // 保存先ディレクトリ作成 if (!isDirectory(program.directory)) { if (!fs.mkdirSync(program.directory, {recursive: true})) { console.log(`failed to create directory: ${program.directory}`) return false } } // 画像検索実行 let maxdownloads = program.numbers await searchGoogleImage(page, program.keyword) for (let i = 0; i < maxdownloads; ++i) { const url = await getGoogleImage(page, i) // 画像が取得できない => もっと表示 => もう画像がないなら終了 if (!url) { if (await loadMoreGoogleImages(page)) { --i; // もっと表示できたらダウンロード再試行 continue; } break; } // ダウンロード const filename = path.join(program.directory, getFilename(url)) if (true === await download(url, filename, program.rename)) { console.log(`downloaded: ${filename}`) } else { // ダウンロードできなかった場合は maxdownloads を一つ増やす ++maxdownloads } } }, { headless: program.headless, slowMode: program.slowmode, close: program.noclose? false: true, })使い方
# Help $ node app.js -h # Usage $ node app.js -k <キーワード> [options] Options: -d, --directory <string> 保存先ディレクトリ (default: ".") -l, --headless <boolean> ヘッドレスモード (default: false) -s, --slowmode <number> 動作遅延[ms] (default: 500) -C, --noclose <boolean> ブラウザを自動で閉じない (default: false) -n --numbers <number> ダウンロード数 (default: 100) -r --rename <boolean> 同名ファイルを自動リネーム (default: false) # Example ## 「banana」という検索キーワードで画像を200枚 ./banana_images/ に保存 $ node app.js -k 'banana' -l true -n 200 -d './banana_images'
- 投稿日:2020-10-11T18:03:06+09:00
Electron上でVulkanを動かすことに成功しました
初投稿です。お手柔らかにお願いします。
今回はElectron上でVulkanを動かす事に成功した、という話です。
(ただし・・・)1. Electronとは?
早い話、「node.jsで書かれたウェブアプリをネイティブアプリにする」というフレームワークです。これにより簡単にクロスプラットフォームを実現できます。Visual Studio CodeやSlack、Discordなどがこの技術により作られています。
2.Vulkanとは?
早い話、最新式のクロスプラットフォームの3D APIです。同じクロスプラットフォームであるOpenGLと比べてより高性能な3Dプログラムを書くことが出来ます。
(その分難易度は物凄く高いですが。)3.で、ElectronでVulkanを動かすと何が嬉しいの?
まず、Electronは「ネイティブアプリを作る際にウェブアプリの資産をそのまま持ってこれる」という非常に便利な代物で最近のクロスプラットフォームのネイティブアプリはElectron(とスマホだとFlutter)が主流だと言われているぐらいです。ところが、Electronで3Dプログラミングを行おうとするとWebGL以外に選択肢がないという問題があります。
更にそのWebGLはテッセレーションシェーダが使えないなど通常の3D APIを比べて実装が古いという問題があります。
つまり、「ElectronでVulkanが使えれば最新の3D APIを使ったクロスプラットフォームアプリが簡単に作れるんじゃないか」という考えたわけで・・・。
4.実装方法
おおまかな方法を開くとこのようになります。
1.Vulkanで書いたプログラムをDLL化する
2.DLL化したVulkanで生成した画像をVulkanのAPIを使ってスクリーンショットとしてCPU側に移す
3.CPU側に移した画像をJpeg型のデータに加工し、さらにBase64の形にして文字列化
4.文字列化した画像をnode-ffi-napiを使って送信し、2Dのcanvasに表示するつまり「フレームごとに毎回スクリーンショットを撮ってElectron側にそのスクリーンショットを映す」というのが今回の手法です。
4.1 Vulkanで書いたプログラムをDLL化する。
まず、Vulkanで動くプログラムをDLL化します。今回は「三角形を表示するだけ」のプログラムをDLL化します。
4.2 DLL化したVulkanで生成した画像をVulkanのAPIを使ってスクリーンショットとしてCPU側に移す。
今回は下記のgithubにあるスクリーンショットのプログラムを元にしてスクリーンショットを撮るプログラムを作りました。
https://github.com/SaschaWillems/Vulkan4.3 CPU側に移した画像をJpeg型のデータに加工し、さらにBase64の形にして文字列化
ではこの画像をそのままElectronのcanvasに表示・・・と言いたいところですが、問題が発生しました。
Electron(というよりウェブブラウザ)はRGBAのデータが無圧縮で入ったconst char*型の配列を画像として表示できないのです。
この問題を解決するには二つ方法があります。
1.DLLで送信された画像をUint8Arrayに加工してWebGLでテクスチャとして表示する。
2.DLL内でJpeg方式(png方式でも可)のデータに変換、更にそのJpegのデータをBase64に変換して表示する。今回は2番めの方法を取りました。Jpegのデータの加工はlibjpeg-turbo、base64の変換には下記のブログを元にboostを用いました。
http://sanichi999.hateblo.jp/entry/2014/12/06/154356
ここで用いる画像をjpeg方式をデータに加工するライブラリは高速のものを使わなければいけない事がわかのったでlibjpeg-turboを使いました。
C++の画像ライブラリで有名であるlibpngやstbライブラリは今回、遅すぎて使い物になりませんでした4.4文字列化した画像をnode-ffi-napiを使って送信し、2Dのcanvasに表示する
後はこのBase64化した画像をElectron上に表示するだけです。Electronはnode-ffi-napiとnode-ref-napiというライブラリからDLLを使用できます。
DLL上には
start:Vulkanを始める関数
end:Vulkanを終了する関数
main_loop:Vulkanに生成した画像を映す関数
make_ss:スクリーンショットを映す関数
send_image:生成した画像をconst char*型として送信する関数
があります。これらをNode.js上で動作させます。javascript上のソースコードを書くとこのようになります。base64の文字列はまずpointerで取り、ref.readCStringという関数で文字列に修正します。
sample.jsconst sample1_elem = document.getElementById("sample1"); const ctx = sample1_elem.getContext("2d"); var chara = new Image(); var ffi = require('ffi-napi') var ref = require('ref-napi'); var bar = ffi.Library( 'DllHelloVulkan.dll', { 'start': ['void', [] ], 'end': ['void', [] ], 'main_loop': ['bool', [] ], 'make_ss': ['bool', [] ], 'send_image': ["pointer", [] ] } ); bar.start(); var foo; var buf; setInterval(() => { bar.main_loop(); bar.make_ss(); foo = bar.send_image(); buf = ref.readCString(foo); ctx.drawImage(chara, 0, 0,400,300); }, 32)5.だが、実際に動かすと
尋常じゃなく遅い!30fpsどころか10fpsも維持できてません。はっきり言って使い物になりません。
結局「Electronで最新の3D APIが使えるクロスプラットフォームのアプリ」は理論上可能でも実際は無理だという事がわかりました。
ネックとなっているのはJpegへの変換です。ここが遅すぎて使い物になりませんでした。
(NVJpegを試せばもっと早くなるかもしれないけど・・・。)最後に分かった事
1.やっぱりWebGLは凄いと思う
2.早くWebGPUを実装して欲しい
- 投稿日:2020-10-11T16:25:36+09:00
Node.jsでiniファイルを操る 「properties-reader」
properties-readerとは?
Node.js上でiniファイルを読み込めるようにしてくれるmodule。
(正確には、iniファイル形式 で書かれていれば拡張子はなんでもOK)何のために使うか?
Electronとかで実行ファイル形式に変換した際、あとから変更になった既定値(ファイルパスとか)のために わざわざソース編集して再コンパイル...なんてやってられないよ! って人が使うもの。
実際作成したソースを「コンパイルするのに30分かかります!」なんてのはざらで、ちょっとした変更の度に再コンパイル→検証→再コンパイル...ってのはかなりの手間。
そんな中でiniファイルやpropertiesファイルとしてパラメータもたせられたら便利じゃないですか???便利ですよね!!!
上記環境のような中でNodejs使ってるよ!って方は 今すぐproperties-readerの使用を検討してはいかがでしょうか?
使い方
インストール方法
npm install properties-reader使用するためのおまじない
index.js// iniファイルをimport const properties = propertiesReader('./test.ini'); // properties-readerをimport const propertiesReader = require('properties-reader');また、今回使用するiniファイルは下記の通り。
test.ini[hoge] fuga=hogefuga [foo] yeah=fooyeahtest2.ini[test] aaa=bbb指定のパラメータのみ取得
index.js// iniファイルから任意のパラメータを取得 const hf = properties.get('hoge.fuga'); // == hogehuga const fy = properties.get('foo.yeah'); // == fooyeah const hfPath = properties.path().hoge.fuga; // == hogehuga /syntax sugar全パラメータ取得
index.js// iniファイルから全パラメータを取得 properties.each((key, value) => { console.log(`${key}: ${value}`); });実行結果hoge.fuga: hogefuga foo.yeah: fooyeah複数のiniファイルをまとめて取得
index.js// 既に取得したiniに他のiniを追加 properties.append("./test2.ini"); // iniファイルから全パラメータを取得 properties.each((key, value) => { console.log(`${key}: ${value}`); });実行結果hoge.fuga: hogefuga foo.yeah: fooyeah test.aaa: bbb文字列から取得
index.js// 文字列を読み込み properties.read('ai.kaki = aikaki \n ue.kuke = uekuke'); properties.set('o.ko', 'oko'); // 読み込みたいパラメータが1つの場合はsetでもOK // iniファイルから全パラメータを取得 properties.each((key, value) => { console.log(`${key}: ${value}`); });実行結果hoge.fuga: hogefuga foo.yeah: fooyeah test.aaa: bbb ai.kaki: aikaki ue.kuke: uekuke o.ko: oko要素数取得
index.jsconst propertiesCount = properties.length;パラメータの保存
index.jsconst props = propertiesReader('./test.ini'); console.log(props.get('hoge.fuga')); props.set('hoge.fuga', 'hogehogefugafuga'); props.save('./test.ini') .then(data => { // save成功時処理 }, err => { // save失敗時処理 console.log(err); }); console.log(props.get('hoge.fuga'));test.ini[hoge] fuga=hogehogefugafuga [foo] yeah=fooyeah注意点
パラメータの上書き
既に存在するパラメータを指定してreadとかsetとかappendとかしちゃうと上書きされちゃうので注意。(read, set はまずないと思うがappendはありそう)
index.js// パラメータの上書き properties.each((key, value) => { console.log(`${key}: ${value}`); }); console.log(""); properties.append("./test3.ini"); properties.each((key, value) => { console.log(`${key}: ${value}`); }); console.log("");test3.ini[hoge] fuga=hogehogehoge [foo] yeah=fooyeah実行結果hoge.fuga: hogefuga hoge.fuga: hogehogehoge参考
公式: https://www.npmjs.com/package/properties-reader
また、上記を試してみたい方は keito-damaのgithub よりclone作ってみてください。
- 投稿日:2020-10-11T16:25:36+09:00
properties-reader について
properties-readerとは?
Node.js上でiniファイルを読み込めるようにしてくれるmodule。
(正確には、iniファイル形式 で書かれていれば拡張子はなんでもOK)何のために使うか?
Electronとかで実行ファイル形式に変換した際、あとから変更になった既定値(ファイルパスとか)のために わざわざソース編集して再コンパイル...なんてやってられないよ! って人が使うもの。
実際作成したソースを「コンパイルするのに30分かかります!」なんてのはざらで、ちょっとした変更の度に再コンパイル→検証→再コンパイル...ってのはかなりの手間。
そんな中でiniファイルやpropertiesファイルとしてパラメータもたせられたら便利じゃないですか???便利ですよね!!!
上記環境のような中でNodejs使ってるよ!って方は 今すぐproperties-readerの使用を検討してはいかがでしょうか?
使い方
インストール方法
npm install properties-reader使用するためのおまじない
index.js// iniファイルをimport const properties = propertiesReader('./test.ini'); // properties-readerをimport const propertiesReader = require('properties-reader');また、今回使用するiniファイルは下記の通り。
test.ini[hoge] fuga=hogefuga [foo] yeah=fooyeahtest2.ini[test] aaa=bbb指定のパラメータのみ取得
index.js// iniファイルから任意のパラメータを取得 const hf = properties.get('hoge.fuga'); // == hogehuga const fy = properties.get('foo.yeah'); // == fooyeah const hfPath = properties.path().hoge.fuga; // == hogehuga /syntax sugar全パラメータ取得
index.js// iniファイルから全パラメータを取得 properties.each((key, value) => { console.log(`${key}: ${value}`); });実行結果hoge.fuga: hogefuga foo.yeah: fooyeah複数のiniファイルをまとめて取得
index.js// 既に取得したiniに他のiniを追加 properties.append("./test2.ini"); // iniファイルから全パラメータを取得 properties.each((key, value) => { console.log(`${key}: ${value}`); });実行結果hoge.fuga: hogefuga foo.yeah: fooyeah test.aaa: bbb文字列から取得
index.js// 文字列を読み込み properties.read('ai.kaki = aikaki \n ue.kuke = uekuke'); properties.set('o.ko', 'oko'); // 読み込みたいパラメータが1つの場合はsetでもOK // iniファイルから全パラメータを取得 properties.each((key, value) => { console.log(`${key}: ${value}`); });実行結果hoge.fuga: hogefuga foo.yeah: fooyeah test.aaa: bbb ai.kaki: aikaki ue.kuke: uekuke o.ko: oko要素数取得
index.jsconst propertiesCount = properties.length;パラメータの保存
index.jsconst props = propertiesReader('./test.ini'); console.log(props.get('hoge.fuga')); props.set('hoge.fuga', 'hogehogefugafuga'); props.save('./test.ini') .then(data => { // save成功時処理 }, err => { // save失敗時処理 console.log(err); }); console.log(props.get('hoge.fuga'));test.ini[hoge] fuga=hogehogefugafuga [foo] yeah=fooyeah注意点
パラメータの上書き
既に存在するパラメータを指定してreadとかsetとかappendとかしちゃうと上書きされちゃうので注意。(read, set はまずないと思うがappendはありそう)
index.js// パラメータの上書き properties.each((key, value) => { console.log(`${key}: ${value}`); }); console.log(""); properties.append("./test3.ini"); properties.each((key, value) => { console.log(`${key}: ${value}`); }); console.log("");test3.ini[hoge] fuga=hogehogehoge [foo] yeah=fooyeah実行結果hoge.fuga: hogefuga hoge.fuga: hogehogehoge参考
公式: https://www.npmjs.com/package/properties-reader
また、上記を試してみたい方は keito-damaのgithub よりclone作ってみてください。
- 投稿日:2020-10-11T14:34:45+09:00
express-validatorの使い方メモ
はじめに
expressを使ってユーザー認証機能を作ったときに使った
express-validator
について使い方をメモ使い方
router.tsimport express, { Request, Response } from 'express'; // TSなので型もインポート。一応。 import { body } from 'express-validator'; const router = express.Router(); router.post( '/api/users/signup', [ body('email') // req.body.emailがメールかどうかをvalidateしてくれる .isEmail() .withMessage('Email must be valid'), // invaludであればメッセージを返す body('password') .trim() .isLength({ min: 4, max: 20 }) // req.body.passwordの文字数を指定 .withMessage('Password must be between 4 and 20 characters') // invaludであればメッセージを返す ], (req: Request, res: Response) => { // 一応TSなので型を明記 const { email, password } = req.body; if (!email || typeof email !== 'string') { res.status(400).send('Provide a valid email'); } // new User({ email, password }) // ここからユーザーを作成する } ); export { router };
- 投稿日:2020-10-11T14:07:24+09:00
nodebrewでNode.jsを最新化してnpm install -gし直すルーティンをスクリプトにした
nodebrew を使って Node.js のバージョン管理をしている場合、 npm のグローバルパッケージを移動させるのがしんどいです。
ふだんの手順
1. グローバルインストールされている npm のパッケージ一覧をメモする
こんなことしますよね。
$ npm ls -g --depth 0 /Users/kulikala/.nodebrew/node/v14.13.1/lib ├── @aws-amplify/cli@4.29.6 ├── @vue/cli@4.5.7 ├── aws-cdk@1.67.0 ├── firebase-tools@8.12.1 ├── nativescript@7.0.10 ├── npm@6.14.8 └── npm-check-updates@9.0.4コピペしてメモを残したりします。
2. nodebrew で最新の Node.js のバージョンをインストールする
$ nodebrew install latest $ nodebrew use latest3. 控えておいた npm パッケージたちを再インストールする
$ npm i -g @aws-amplify/cli @vue/cli aws-cdk firebase-tools nativescript npm-check-updatesnodebrew には、
migrate-package
というちょうど良さそうなコマンドがありますが、シンボリックリンクで処理してしまうので、ちょっと都合が悪かったりします。
クリーンに新規インストールした方がnode_modules
フォルダがキレイになるので、自分はいつもこの方法です。nodebrew migrate-package <version> Install global NPM packages contained in <version> to current version4. 古い Node.js のバージョンをサヨナラする
$ nodebrew uninstall <古いversion> $ nodebrew clean allルーティンをスクリプトにした
Bash 使いなので Bash で書きました。
.bash_profile
に次の内容を転記しておくと、update_node
コマンドで全てルーティンをやってくれます。.bash_profileupdate_node () { check_env () { if ! command_exists nodebrew; then echo 'This script updates node via nodebrew.' 1>&2 echo ' Please install nodebrew.' 1>&2 echo ' https://github.com/hokaccha/nodebrew' 1>&2 return 1 fi if ! command_exists jq; then echo 'This script uses jq as JSON parser.' 1>&2 echo ' Please install jq.' 1>&2 echo ' https://stedolan.github.io/jq/' 1>&2 return 1 fi } command_exists () { command -v "$@" > /dev/null 2>&1 } get_node_ver () { nodebrew ls \ | grep current: \ | cut -d ' ' -f 2 } npm_ls_global () { npm ls -g --depth 0 --json \ | jq --raw-output '.dependencies [].from | select(. != null)' } if ! check_env; then return 1 fi echo 'Updating node...' local NODE_CURRENT="$(get_node_ver)" echo "Current node: ${NODE_CURRENT}" local NPM_GLOBAL="$(npm_ls_global)" nodebrew install latest nodebrew use latest local NODE_LATEST="$(get_node_ver)" if [ "${NODE_CURRENT}" = "${NODE_LATEST}" ]; then echo 'Already up to date.' return 0 fi npm i -g ${NPM_GLOBAL} nodebrew uninstall "${NODE_CURRENT}" nodebrew clean all echo echo "Node is updated to: ${NODE_LATEST}" }nodebrew について
- 投稿日:2020-10-11T13:49:45+09:00
ESLint 7.11.0
前 v7.10.0 | 次 (2020-10-24 JST)
ESLint v7.11.0 has been released:https://t.co/znenTYhIQT
— ESLint (@geteslint) October 9, 2020ESLint
7.11.0
がリリースされました。機能追加は行われていませんが、ESLint の semver ポリシー (指摘が増えるかもしれないバグ修正はマイナーバージョンで行う) に従ってマイナーバージョンが上がっています。質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。
? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット (招待リンク)
? 本家リポジトリ
? 本家サポート チャット (招待リンク)
[PR] ESLint は開発リソースを確保するための寄付を募っています。
応援してくださると嬉しいです。
✨ 本体への機能追加
特になし
? 新しいルール
特になし
? オプションが追加されたルール
特になし
- 投稿日:2020-10-11T13:49:33+09:00
ESLint 7.10.0
ESLint v7.10.0 has been released:https://t.co/yfWn9gmynR
— ESLint (@geteslint) September 26, 2020ESLint
7.10.0
がリリースされました。小さな機能追加とバグ修正が行われました。質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。
? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット (招待リンク)
? 本家リポジトリ
? 本家サポート チャット (招待リンク)
[PR] ESLint は開発リソースを確保するための寄付を募っています。
応援してくださると嬉しいです。
✨ 本体への機能追加
特になし
? 新しいルール
特になし
? オプションが追加されたルール
no-inline-comments
ignorePattern
? #13029
正規表現で許可するインラインコメントを指定できるようになりました。
/*eslint no-inline-comments: [error, { ignorePattern: "^\\s*webpackChunkName:.+$" }]*/ //✓ GOOD import(/* webpackChunkName: "my-chunk-name" */ './locale/en');
- 投稿日:2020-10-11T13:49:19+09:00
ESLint 7.9.0
ESLint v7.9.0 has been released: https://t.co/yCHnnVmEdg
— ESLint (@geteslint) September 12, 2020ESLint
7.9.0
がリリースされました。機能追加は行われていませんが、ESLint の semver ポリシー (指摘が増えるかもしれないバグ修正はマイナーバージョンで行う) に従ってマイナーバージョンが上がっています。質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。
? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット (招待リンク)
? 本家リポジトリ
? 本家サポート チャット (招待リンク)
[PR] ESLint は開発リソースを確保するための寄付を募っています。
応援してくださると嬉しいです。
✨ 本体への機能追加
特になし
? 新しいルール
特になし
? オプションが追加されたルール
特になし
- 投稿日:2020-10-11T13:49:06+09:00
ESLint 7.8.0
ESLint v7.8.0 has been released:https://t.co/xrKRHJ8e6F
— ESLint (@geteslint) August 31, 2020ESLint
7.8.0
がリリースされました。小さな機能追加とバグ修正が行われました。質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。
? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット (招待リンク)
? 本家リポジトリ
? 本家サポート チャット (招待リンク)
[PR] ESLint は開発リソースを確保するための寄付を募っています。
応援してくださると嬉しいです。
✨ 本体への機能追加
env.es2021
が追加されました? #13603
.eslintrc.jsmodule.exports = { env: { es2021: true, }, }2つの新しいグローバル変数
WeakRef
とFinalizationRegistry
を定義します。論理演算の複合代入演算子をサポートしました
a1 &&= b1 a2 ||= b2 a3 ??= b3これらの演算子の正規化は
a1 = a1 && b1
ではなくa1 && (a1 = b1)
です。つまり、短絡する場合は代入演算が発生しません。これらの演算子には短絡動作 (short-circuit) があるため、今までは複合代入演算子をサポートしていませんでした。最近追加された
??
演算子によって需要が出たため、短絡動作があっても複合代入演算子をサポートすることになりました。この新しい構文を使用する場合、
parserOptions.ecmaVersion
オプションを2021
に指定してください。.eslintrc.jsmodule.exports = { parserOptions: { ecmaVersion: 2021, }, }数値リテラルの桁区切りをサポートしました
const binary = 0b0000_1010_0101_1111 const octal = 0o012_666_755 const hex = 0xDEAD_BEAF const decimal = 1_000_000_000桁数の多い数値をアンダーバーで区切って読みやすくできます。
この新しい構文を使用する場合、
parserOptions.ecmaVersion
オプションを2021
に指定してください。.eslintrc.jsmodule.exports = { parserOptions: { ecmaVersion: 2021, }, }? 新しいルール
特になし
? オプションが追加されたルール
no-magic-numbers
ignoreDefaultValues
? #12611
デフォルト値にマジックナンバーを許可するオプションが追加されました。
/*eslint no-magic-numbers: [error, { ignoreDefaultValues: true }]*/ //✓ GOOD const { PI = 3.14159265359 } = constantsid-length
exceptionPatterns
? #13576
正規表現で許可する名前を指定するオプションが追加されました。
/*eslint id-length: [error, { min: 3, exceptionPatterns: ["^(e|pi)$"] }]*/ //✓ GOOD const e = 2.71828182846 const pi = 3.14159265359 //✘ BAD const f = () => {}
- 投稿日:2020-10-11T13:48:53+09:00
ESLint 7.7.0
ESLint v7.7.0 has been released!https://t.co/ResBl1KtCo
— ESLint (@geteslint) August 14, 2020ESLint
7.7.0
がリリースされました。小さな機能追加とバグ修正が行われました。質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。
? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット (招待リンク)
? 本家リポジトリ
? 本家サポート チャット (招待リンク)
[PR] ESLint は開発リソースを確保するための寄付を募っています。
応援してくださると嬉しいです。
✨ 本体への機能追加
特になし
? 新しいルール
特になし
? オプションが追加されたルール
no-underscore-dangle
allowFunctionParams
? #13545
仮引数のアンダーバーで始まる名前を許可するためのオプションが追加されました。
これまで、このルールは仮引数を見逃していたため、破壊的変更を避けるためにデフォルト値は
true
になっています。次のメジャーバージョンでfalse
になる予定です。/*eslint no-underscore-dangle: [error, { allowFunctionParams: true }]*/ //✓ GOOD function f(_a) {}/*eslint no-underscore-dangle: [error, { allowFunctionParams: false }]*/ //✘ BAD function f(_a) {}
- 投稿日:2020-10-11T13:48:39+09:00
ESLint 7.6.0
ESLint v7.6.0 has been released!https://t.co/eb6Rf10fx4
— ESLint (@geteslint) July 31, 2020ESLint
7.6.0
がリリースされました。機能追加は行われていませんが、ESLint の semver ポリシー (指摘が増えるかもしれないバグ修正はマイナーバージョンで行う) に従ってマイナーバージョンが上がっています。質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。
? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット (招待リンク)
? 本家リポジトリ
? 本家サポート チャット (招待リンク)
[PR] ESLint は開発リソースを確保するための寄付を募っています。
応援してくださると嬉しいです。
✨ 本体への機能追加
特になし
? 新しいルール
特になし
? オプションが追加されたルール
特になし
- 投稿日:2020-10-11T01:25:52+09:00
React Material UI + Node.js expressのアプリ構築手順をまとめた
はじめに
フロントエンドがReact(Material UI)、バックエンドがNode.js expressのアプリを構築することがあったので、(何番煎じか分かりませんが)備忘録に手順を残します。
環境
- Windows 10
- Node.js v12.16.3
- npm 6.14.4
- yarn 1.22.4
手順
ディレクトリ作成
# アプリのコードを管理するディレクトリを作成 mkdir app cd app # クライアント、サーバーのコードを配置するディレクトリを作成 mkdir client serverフロントエンド:React, Material UI の導入
cd client npx create-react-app my-app ※my-app=アプリ名 ※npxはnpm 5.2 から利用できるパッケージランナーツール cd my-app yarn add @material-ui/core --save # アプリ内でアイコンを使用したい場合はこれもインストールする。回線が遅いとインストール時にタイムアウトが発生する可能性があるため、タイムアウトの時間を長めに設定しておく。 yarn add @material-ui/icons --save --network-timeout 1000000000※動作確認は後ほど実施
バックエンド:express の導入
cd ../.. cd server npm init (対話式で色々と入力するものがある。こだわりなければ全てEnterでよい。) npm install express --save type nul > server.js (macではtouch server.js かな? 単にsever.jsというファイルを作成しているだけ。)server.jsをテキストエディタで開いて下記のコードをコピペする。
server.jsconst express = require('express'); const path = require('path'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); // Reactのアプリ名によってはディレクトリ名を一部変更する app.use(express.static(path.join(__dirname, '../client/my-app/build'))); app.listen((process.env.PORT || 8000), () => { console.log(`Listening on port ${PORT}`); });動作確認
フロントエンド
- 開発用サーバーの起動
cd client/my-app yarn starthttp://localhost:3000 にアクセスしてこのような画面が表示されれば成功です。
yarn start
を実行中にApp.jsなどを変更して保存すると自動でコンパイルをしてページも自動でリロードされます。フロントの開発中は
yarn start
で開発用サーバーを起動させつつ、バックエンドのexpressのサーバーも起動させてREST APIの呼び出しをするのが良いと思います。
- ビルド
コードをビルドして、静的なファイル(html, jsファイルなど)を出力します。
yarn buildデフォルトでは
app/client/my-app/build
にビルドされたファイルが出力されます。
このフォルダをexpressのstaticフォルダとして設定しておくと、 http://localhost:8000 で表示することが出来ます。バックエンド
- サーバー起動
cd server node server.jshttp://localhost:8000 にアクセスして、http://localhost:3000 と同様の画面が表示されれば成功です。
おまけ
Material UIの Button component Icon componentを組み合わせた例です。
Material UIのIconの参照先: https://material.io/resources/icons/?icon=check_circle_outline&style=baseline
App.jsimport React from 'react'; import './App.css'; import { Button } from '@material-ui/core'; import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'; function App() { return ( <div className="App"> <Button variant="contained" color="primary"> <CheckCircleOutlineIcon /> please click! </Button> </div> ); } export default App;App.css.App { padding-top: 100px; text-align: center; }参考サイト