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

【2021年】React / TypeScript使用のためのDocker + Vagrant環境を構築する

$
0
0

目的

下記の技術を使ったフロントのコンポーネントの作成を行うための環境構築を行いました。

  • React
  • TypeScript

目的は上記の利用ですが、本記事としてはReact/TypeScriptを触れることはありませんので、Node.jsを利用する環境の構築には利用が可能かと思います。

DockerだけでなくVagrantを採用している理由としては、自分の開発環境がmacであり、
参考記事に記載の理由で動作が遅いことを解消するためです。

参考記事

DXを大幅に低下させるDocker for Macを捨ててMac最速のDocker環境を手に入れる

前準備/筆者の環境

参考記事に記載のVagrantやMutagenのインストールが必要です。
筆者の環境としては下記となります。

  • MacOS 11.1
  • Vagrant 2.2.7
  • Mutagen 0.11.8

作成するもの

下記の4つのファイルを作成します。
それぞれのファイルは同一のディレクトリの配置でOKです。

  • Vagrantfile
  • mutagen.yml
  • Dockerfile
  • docker-compose.yml

起動をする際にはvagrantから立ち上げて、仮想マシンの中にsshしてdockerの立ち上げとなります。

Vagrantfile

作成する仮想マシンの構成の記載がされているファイルです。

Vagrant.configure('2')do |config|
  config.vm.box ='ubuntu/focal64'

  config.vm.hostname ='frontcomponent'

  config.vm.network :private_network, ip: '192.168.60.10'
  config.vm.network 'forwarded_port', guest: 8000, host: 8000

  config.vm.provider :virtualbox do |vb|
    vb.gui =false
    vb.cpus = 2
    vb.memory = 4096
    vb.customize ['modifyvm', :id, '--natdnsproxy1', 'off']
    vb.customize ['modifyvm', :id, '--natdnshostresolver1', 'off']
  end

  config.disksize.size ='15GB'
  config.mutagen.orchestrate =true

  config.vm.synced_folder './', '/home/vagrant/front_component', type: "rsync",
    rsync_auto: true,
    rsync__exclude: ['.git/', 'log/', 'tmp/']

  config.vm.provision 'shell', inline: <<-SHELL
    echo "fs.inotify.max_user_watches = 65536" >> /etc/sysctl.conf && sysctl -p

    curl -fsSL https://get.docker.com -o get-docker.sh
    sh get-docker.sh
    usermod -aG docker vagrant

    # dockerを更新する場合にはverを調整すること
    curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname-s)-$(uname-m)" -o /usr/local/bin/docker-compose
    chmod +x /usr/local/bin/docker-compose
  SHELL
end

portの設定は今後に自分が利用する環境に合わせて設定しています。
あと、vm.providerなどスペックの設定に関しては利用しているPCなどでチューニングすると良いかと思います。

他には自分がこれまでにハマったポイントや参考記事を拝見した後に追加した点としては下記となります。利用する場合には注意してください。

hostname

"_"の利用ができないようです。
サービス名などを入れた場合に間違えやすいので注意が必要です。

fs.inotify.max_user_watches

node_modulesなど外部のファイルを引いてくるディレクトリが存在する場合にファイル数が多くなりすぎて、エラーとなることがあります。その場合にはファイルのマウントから除外する方が正しいかとは思いますが、typescriptの方定義ファイルなどローカル上にマウントされてないとコード編集時にnode_modules内の型定義ファイルが見つからず加えました。

良い知見をお持ちの方がいたら意見を伺いたいです

mutagen.yml

sync:app:mode:"two-way-resolved"alpha:"./"beta:"frontcomponent:/home/vagrant/front_component"ignore:vcs:truepaths:-"/log"-"/tmp"

vagrantとのファイル同期にmutagenを利用します。
詳細に関しては記事にまとめるほどには理解が追いついていませんので、参考記事を参照していただけると良いかと思います。

Dockerfile

Docker Hubに置いてあるDockerのイメージファイルをベースとして、
今回利用するコンテナのベースイメージを作成します。

FROM node:15.5.1

ENV LANG C.UTF-8
ENV WORKSPACE=/var/www/front_component/

RUN curl -sL https://deb.nodesource.com/setup_15.x | bash - 
RUN apt install-y less build-essential nodejs
RUN apt install-y vim less
RUN npm install n -g
RUN n 14.15.4

WORKDIR $WORKSPACE

Docker Hubに置いてあるイメージを元に、今回の環境構築用のベースイメージを作成します。

今回は、今後にreact/typescriptを入れていくことを想定しているのでnodeのイメージをして、npmのインストールをしています。

docker-compose.yml

version:'3.7'services:front:build:.image:front:1.0container_name:fronttty:truestdin_open:trueports:-"8000:8000"environment:{}volumes:-.:/var/www/front_component# command: "bash -c 'npm i && npm run watch'"

コンテナ作成用の設定になります。
コメントアウト部分は今後にreact/typescriptを入れた後に利用する想定ですが、
ここまでの環境構築では利用をしないのでコメントアウトとしています。

vagrantおよびdockerの立ち上げ

立ち上げ方

# 先にcdコマンドで上記で作成したファイルの置いてあるディレクトリに移動する

vagrant up  # vagrantの立ち上げ
vagrant ssh # vagrant内へsshでアクセスcd front_component   # vagrantにローカルのファイルをマウントしているディレクトリに移動
docker-compose up -d# dockerの立ち上げ

以上で環境の立ち上げ完了です。

上記のコマンドと合わせて、自分がよく使うコマンドを記載しておきます。
簡易的な説明のみなので、ちゃんと使う老婆には調べて使ってください

vagrant系

vagrant up      # vagrantの立ち上げ
vagrant ssh     # 立ち上げたvagrantにsshアクセスする
vagrant halt    # vagrantの終了
vagrant destroy # vagrantで作成した仮装イメージごと削除

docker系

docker ps # 起動中のdockerのimageを確認する
docker exec-ti<コンテナ名> /bin/bash # コンテナの中にssh的にアクセスする
docker attach <コンテナ名> # コンテナで起動中のプロセスを表示する
docker logs <コンテナ名>   # 強制終了したコンテナの終了時のログなどを見るのに利用する

初投稿ε('∞'*)フゥー


LINEボットでGoogle Alertを通知

$
0
0

Google Alertは、キーワードを登録しておくと、ウェブ上の新着コンテンツを知らせてくれるサービスです。私は、今まで、メールで受信していたのですが、せっかくコンテンツを通知してくれたのに、時間がたつと埋もれてしまっていました。

そこで、新着コンテンツが来たタイミングでLINEボットで通知すると同時に、データベースに登録して、あとでも参照できるようにしてみます。

image.png

2か所でNode.jsを使います。
1つ目は、コンテンツの定期的な取得のため。
2つ目は、過去コンテンツの格納とLINEボット用のサーバです。

過去コンテンツは、MySQLサーバに格納しています。

もろもろはGitHubに上げておきました。

poruruba/GoogleAlert
 https://github.com/poruruba/GoogleAlert

流れ

①Googleアラートに、キーワードを登録しておきます。そうすると、RSSフィードのURLが取得できます。
②Node.jsなどで、定期的にRSSフィードからコンテンツを取得するとともに、Node.jsサーバにコンテンツ登録を依頼します。
③Node.jsサーバでは、コンテンツがすでにデータベースに登録されているか確認し、登録されていない場合はデータベースに登録します。それと同時に、LINEボットにコンテンツをメッセージとして送信します。
④ユーザは、LINEアプリにコンテンツがメッセージで送信されてきます。
⑤(必要に応じて)ユーザはLIFFアプリを起動し、Node.jsサーバからコンテンツ一覧を取得し表示します。

準備:Googleアラートにキーワードを登録

以下のサイトでキーワードを登録します。

Googleアラート
 https://www.google.co.jp/alerts

image.png

アラートを作成、と表示されているところにキーワードを入力します。
今回は、「ESP32」としてみました。オプションを表示となっている場合はクリックしてオプションを表示します。

image.png

ここで、配信先として、自身のGmailアドレスではなく、「RSSフィード」を選択します。
最後に、アラートを作成 を押下します。

そうすると、ESP32が追加され、無線のようなマークがでていますので、クリックします。
そうすると、RSSフィードが表示されました。
まだコンテンツの監視が始まったばかりで、コンテンツは1件もないです。ブラウザに表示されているこのURLを覚えておきます。

データベースの準備

以下のようなスキーマのテーブルを作成しました。

データベース名:googlealert

テーブル名:items
コンテンツを格納します。

image.png

テーブル名:members
LINEボットからコンテンツをメッセージ送信する先のユーザIDを格納します。

image.png

LINEボットの作成

すみませんが、以下の投稿を参考にしてください。

 LINEボットを立ち上げるまで。LINEビーコンも。

LINEボット名は「Googleアラート」にしてみました。

定期的なコンテンツの取得

定期的なコンテンツ取得は、GoogleアラートのRSSフィードを参照することで行います。
また、これから立ち上げるNode.jsサーバへコンテンツをHTTP Postしています。

RSSフィードの参照およびHTTP Postには以下のnpmモジュールを使っています。

rbren/rss-parser
 https://github.com/rbren/rss-parser

node-fetch/node-fetch
 https://github.com/node-fetch/node-fetch

cron_googlealert/index1.js
'use strict';constGOOGLE_ALERT_RSS_URL=process.env.GOOGLE_ALERT_RSS_URL||'【GoogleアラートのRSSフィードのURL】';constGOOGLE_ALERT_SEARCH_KEYWORD=process.env.GOOGLE_ALERT_SEARCH_KEYWORD||'【Googleアラートに指定したキーワード】';constbase_url="【Node.jsサーバのURL】";constfetch=require('node-fetch');const{URL,URLSearchParams}=require('url');constHeaders=fetch.Headers;constParser=require('rss-parser');constparser=newParser();(async()=>{varfeed=awaitparser.parseURL(GOOGLE_ALERT_RSS_URL);if(feed.items.length<=0)return;feed.items.forEach(item=>{console.log(item.title);});try{varcreated_at=newDate().getTime();for(vari=0;i<feed.items.length;i++){varitem=feed.items[i];console.log(item);varparam={keyword:GOOGLE_ALERT_SEARCH_KEYWORD,title:item.title,pubDate:item.pubDate,contentSnippet:item.contentSnippet,id:item.id,link:item.link,created_at:created_at};awaitdo_post(base_url+'/linebot-googlealert-push',param);}}catch(error){console.error(error);}})();functiondo_post(url,body){constheaders=newHeaders({"Content-Type":"application/json; charset=utf-8"});returnfetch(newURL(url).toString(),{method:'POST',body:JSON.stringify(body),headers:headers}).then((response)=>{if(!response.ok)throw'status is not 200';returnresponse.json();});}

以下の部分を、各自の環境に合わせて変更します。これから立ち上げるNode.jsサーバのURLです。

【Node.jsサーバのURL】

Node.jsサーバの実装

とりあえず、以下ダウンロードしてNode.jsサーバを立ち上げます。

unzip GoogleAlert-master.zip
cd GoogleAlert-master
mkdir cert
npm install

HTTPSである必要がありまして、SSL証明書をcertフォルダに置きます。フォルダ名は、app.jsを見ればわかります。
起動は以下の通りです。

$ node app.js

RSSフィードされたコンテンツを受信する部分を抜粋します。

server/api/controllers/linebot-googlealert/index.js
exports.handler=async(event,context,callback)=>{if(event.path=='/linebot-googlealert-push'){varbody=JSON.parse(event.body);varsql_query=`SELECT id FROM items WHERE id = '${body.id}'`;const[rows]=awaitdbconn.query(sql_query);varindex=rows.findIndex(rows_item=>body.id==rows_item.id);if(index<0){varsql_insert=`INSERT INTO items (id, keyword, content, pubDate, created_at) VALUES ('${body.id}', '${body.keyword}', '${JSON.stringify(body)}', '${newDate(body.pubDate).getTime()}', ${body.created_at})`;awaitdbconn.query(sql_insert);varsql_select=`SELECT memberId FROM members`;const[rows]=awaitdbconn.query(sql_select);varmessage=app.createSimpleCard(body.title,'キーワード: '+body.keyword,body.contentSnippet,'ブラウザで開く',{type:'uri',uri:body.link});rows.forEach(row=>{app.client.pushMessage(row.memberId,message);});}returnnewResponse({});}else

以下の部分を環境に合わせて変更します。

server/api/controllers/linebot-googlealert/index.js
constDB_HOST='【MySQLサーバのホスト名】';constDB_USER='【MySQLサーバのユーザ名】';constDB_PASSWORD="【MySQLサーバのパスワード】";constDB_PORT=3306;constDB_DATABASE="googlealert";

上記のうち、以下の部分がLINEボットとしてメッセージ送信する部分です。

server/api/controllers/linebot-googlealert/index.js
varmessage=app.createSimpleCard(body.title,'キーワード: '+body.keyword,body.contentSnippet,'ブラウザで開く',{type:'uri',uri:body.link});rows.forEach(row=>{app.client.pushMessage(row.memberId,message);});

以下の部分を環境に合わせて変更します。

server/api/controllers/linebot-googlealert/index.js
constconfig={channelAccessToken:'【LINEボットのチャネルアクセストークン(長期)】',channelSecret:'【LINEボットのチャネルシークレット】',};

上記のシークレットを変更しないと、LINEボットのWebhook設定で、Webhook URLの検証が成功しないです。

コンテンツ取得とLINE通知を試してみる。

それでは、LINEボットを自身のスマホのLINEアプリから登録しましょう。
登録が完了すると、LINEボットがそれを認識し、LINEユーザのユーザIDをデータベースに登録します。

image.png

以下の部分です。

server/api/controllers/linebot-googlealert/index.js
app.follow(async(event,client)=>{varmemberId=(event.source.type=='user')?event.source.userId:event.source.groupId;varsql_insert=`INSERT INTO members (memberId, type) VALUES ('${memberId}', '${event.source.type}')`;awaitdbconn.query(sql_insert);});app.unfollow(async(event,client)=>{varmemberId=event.source.type=='user'?event.source.userId:event.source.groupId;varsql_delete=`DELETE FROM members WHERE memberId = '${memberId}' AND type = '${event.source.type}'`;awaitdbconn.query(sql_delete);});exports.fulfillment=app.lambda();

そして、定期的なコンテンツ取得として用意したcron_googlealert/index1.jsを起動します。

起動に便利な、シェルスクリプトを用意しました。

cron_googlealert/index1.sh
#!/bin/shexport GOOGLE_ALERT_RSS_URL="【GoogleアラートのRSSフィードのURL】"export GOOGLE_ALERT_SEARCH_KEYWORD=" 【Googleアラートに指定したキーワード】"cd /home/XXXX/projects/node/cron_googlealert
/home/XXXX/.nvm/versions/node/v12.19.0/bin/node index.js

環境に合わせて以下を変更します。後者は、「ESP32」でした。

【Googleアラートに指定したキーワード】
【GoogleアラートのRSSフィードのURL】

$cd cron_googlealert
$chmod +x index1.sh
$ ./index1.sh

(まだコンテンツは見つかっていないかもしれません。気長に待ちましょう)

別のキーワードですが以下のようにDBに登録され、LINEにも通知されます。

image.png

同時に、LINEアプリにも通知が届いているかと思います。

image.png

あとは、これをCronで起動すればよいです。例えば、1時間ごとに。

$crontab -e
★以下を入力★
15 * * * * /home/XXXX/projects/node/cron_googlealert/index1.sh

コンテンツ一覧表示するLIFFアプリ

普通のWebページでもよいのですが、せっかくなのでLIFFアプリにして、LINEアプリ内で表示できるようにします。

LIFFアプリの登録には、LINE Developersで作ったMessaging APIのチャネルではなく、LINEログインのチャネルが必要です。

LINE Developers
https://developers.line.biz/console/

登録が完了すると、LIFF IDが割り当たります。

Node.jsサーバの以下の部分を書き換えます。

server/api/controllers/linebot-googlealert/index.js
constLIFF_ID="【LINEのLIFF-ID】";

image.png

画面はこんな感じです。

image.png

HTMLはこんな感じです。

public/googlealert/index.html
<!DOCTYPE html><htmllang="ja"><head><metahttp-equiv="Content-Type"content="text/html; charset=utf-8"/><metahttp-equiv="Content-Security-Policy"content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;"><metaname="format-detection"content="telephone=no"><metaname="msapplication-tap-highlight"content="no"><metaname="apple-mobile-web-app-capable"content="yes"/><metaname="viewport"content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"><!-- jQuery (necessary for Bootstrap's JavaScript plugins) --><script src="https://code.jquery.com/jquery-1.12.4.min.js"integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"crossorigin="anonymous"></script><!-- Latest compiled and minified CSS --><linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu"crossorigin="anonymous"><!-- Optional theme --><linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css"integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ"crossorigin="anonymous"><!-- Latest compiled and minified JavaScript --><script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd"crossorigin="anonymous"></script><linkrel="stylesheet"href="css/start.css"><script src="js/methods_bootstrap.js"></script><script src="js/components_bootstrap.js"></script><script src="js/vue_utils.js"></script><script src="dist/js/vconsole.min.js"></script><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script><linkrel="stylesheet"href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css"><script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script><title>Google Alert</title></head><body><divid="top"class="container"><buttonclass="btn btn-default pull-right"v-on:click="list_update">更新</button><h1>Google Alert</h1><br><h2>本日のアイテム</h2><divclass="panel panel-default"v-for="(value, index) in item_list_today"><divclass="panel-heading"><h3>{{value.content.title}}</h3></div><divclass="panel-body"><spanclass="pull-left">pubDate: {{new Date(value.pubDate).toLocaleString()}}</span><spanclass="pull-right">keyword: {{value.keyword}}</span><br><br>
            {{value.content.contentSnippet}}
          </div><divclass="panel-footer text-right"><aclass="pull-left"v-bind:href="value.content.link">ブラウザで開く</a>いいね数:{{value.likes}}
            <buttonclass="btn btn-default btn-sm"v-on:click="change_likes(value, true)"></button><buttonclass="btn btn-default btn-sm"v-on:click="change_likes(value, false)"></button></div></div><hr><h2>過去のアイテム</h2><divclass="form-inline"><buttonclass="btn btn-default"v-on:click="list_update_default">今月</button><selectclass="form-control"v-model.number="target_year"v-on:change="list_update"><optionv-for="(value, index) in target_year_list"v-bind:value="value">{{value}}年</option></select><selectclass="form-control"v-model.number="target_month"v-on:change="list_update"><optionvalue="0">通年</option><optionv-for="(value, index) in [1,2,3,4,5,6,7,8,9,10,11,12]"v-bind:value="value">{{value}}月</option></select><selectclass="form-control"v-model.number="has_likes"><optionvalue="1">いいね有のみ</option><optionvalue="0">すべて</option></select></div><tableclass="table table-striped"><thead><tr><th>keyworkd</th><th>title</th><th>pubDate</th><th>いいね</th></tr></thead><tbody><trv-for="(value, index) in item_list"v-if="has_likes==0||value.likes>0"><td>{{value.keyword}}</td><td><av-bind:href="value.content.link">{{value.content.title}}</a></td><td>{{new Date(value.pubDate).toLocaleString()}}</td><td>{{value.likes}}
                    <buttonclass="btn btn-default btn-xs"v-on:click="change_likes(value, true)"></button><buttonclass="btn btn-default btn-xs"v-on:click="change_likes(value, false)"></button></td></tr></tbody></table><!-- for progress-dialog --><progress-dialogv-bind:title="progress_title"></progress-dialog></div><script src="js/start.js"></script></body>

Javascriptはこんな感じです。

public/googlealert/js/start.js
'use strict';//var vConsole = new VConsole();constbase_url="【Node.jsサーバのURL】";varvue_options={el:"#top",data:{progress_title:'',// for progress-dialogitem_list_today:[],item_list:[],target_month:0,target_year:0,target_year_list:[],has_likes:0},computed:{},methods:{list_update_default:asyncfunction(){this.target_month=this.now.getMonth()+1;this.target_year=this.now.getFullYear();returnthis.list_update();},list_update_today:asyncfunction(today){varparam={};varlist=awaitdo_post(base_url+"/linebot-googlealert-list",param);for(vari=0;i<list.length;i++)list[i].content=JSON.parse(list[i].content);this.item_list_today=list;},list_update:asyncfunction(){varparam={year:this.target_year,month:this.target_month,};varlist=awaitdo_post(base_url+"/linebot-googlealert-list",param);for(vari=0;i<list.length;i++)list[i].content=JSON.parse(list[i].content);this.item_list=list;},change_likes:asyncfunction(target,increment){console.log(target);vartarget_likes=(increment)?(target.likes+1):(target.likes-1);if(target_likes<0)target_likes=0;varparam={id:target.id,likes:target_likes};awaitdo_post(base_url+"/linebot-googlealert-likes",param);vart1=this.item_list.find(item=>item.id==target.id);if(t1)this.$set(t1,"likes",target_likes);vart2=this.item_list_today.find(item=>item.id==target.id);if(t2)this.$set(t2,"likes",target_likes);},},created:function(){},mounted:asyncfunction(){proc_load();this.now=newDate();for(vari=0;i<5;i++)this.target_year_list.push(this.now.getFullYear()-i);this.target_month=this.now.getMonth()+1;this.target_year=this.now.getFullYear();this.list_update_today();this.list_update();}};vue_add_methods(vue_options,methods_bootstrap);vue_add_components(vue_options,components_bootstrap);varvue=newVue(vue_options);functiondo_post(url,body){constheaders=newHeaders({"Content-Type":"application/json; charset=utf-8"});returnfetch(newURL(url).toString(),{method:'POST',body:JSON.stringify(body),headers:headers}).then((response)=>{if(!response.ok)throw'status is not 200';returnresponse.json();});}

Node.jsサーバ側では、それにこたえられるように、以下のエンドポイントを用意しています。
一覧の取得といいねカウントです。一覧の取得では、本日のコンテンツ、月ごとのコンテンツ、年ごとのコンテンツ、のようにフィルタリングして返しています。

server/api/controllers/linebot-googlealert/index.js
if(event.path=='/linebot-googlealert-list'){varbody=JSON.parse(event.body);varstartTime;varendTime;if(!body.year||!body.month){vartoday=newDate();today.setHours(0,0,0,0);startTime=today.getTime();vartomorrow=newDate(today);tomorrow.setDate(today.getDate()+1);endTime=tomorrow.getTime();}elseif(body.year&&body.month==0){varthisYear=newDate();thisMonth.setFullYear(body.year);thisMonth.setMonth(0);thisMonth.setDate(1);thisMonth.setHours(0,0,0,0);startTime=thisYear.getTime();varnextYear=newDate(thisYear);nextMonth.setFullYear(thisYear.getFullYear()+1);endTime=nextYear.getTime();}else{varthisMonth=newDate();thisMonth.setFullYear(body.year);thisMonth.setMonth(body.month-1);thisMonth.setDate(1);thisMonth.setHours(0,0,0,0);startTime=thisMonth.getTime();varnextMonth=newDate(thisMonth);nextMonth.setMonth(thisMonth.getMonth()+1);endTime=nextMonth.getTime();}varsql_select=`SELECT * FROM items WHERE pubDate >= ${startTime} AND pubDate < ${endTime} ORDER BY pubDate DESC`;const[rows]=awaitdbconn.query(sql_select);returnnewResponse(rows);}elseif(event.path=='/linebot-googlealert-likes'){varbody=JSON.parse(event.body);varsql_update=`UPDATE items SET likes = ${body.likes} WHERE id = '${body.id}'`;awaitdbconn.query(sql_update);returnnewResponse({});}

以上

mongodb の BulkOp を使ってパフォーマンスをあげてみたで

$
0
0

公式

イメージ

  • 繰り返し処理などで一回一回 db に接続しにいく処理があったとする。
  • その処理は結構大変なので、パフォーマンスが激減する。

    • 例えば、洗濯物をタンスにしまうときに、
    • タンスを開ける -> 洗濯物を一枚入れる -> タンスを閉める...
    • この処理をその回数分繰り返しているような感じ。
    • 非常に能率が悪いことがわかる。
  • BulkOp を使うと、

    • Bulk に貯めといて、一回で db に全部入れておしまい ♪
    • みたいな感じになると思っている。

一例

asyncなんかの関数(なんかの引数){(省略)// mongodb に接続してなんかの db を取得する。 今回は page としよう。constpageCollection=mongoose.connection.collection('pages');// bulk を作る。 この時点では空っぽ。。。constunorderedBulkOp=pageCollection.initializeUnorderedBulkOp();// 繰り返し処理for(leti=0;i<pages.length;i++){if(originPages[i].redirectTo!==pages[i].path){thrownewError('The new page of to revert is exists and the redirect path of the page is not the deleted page.');}// 一回 bulk に貯めとく。unorderedBulkOp.find({_id:originPages[i]._id}).remove();}//繰り返し処理が終わったら,db に接続する。try{awaitunorderedBulkOp.execute();}catch(err){(省略)

どれくらい早くなんの?

  • 数十件ならばそんなに変わらない。
  • 数千件になると、約1分30秒位変わる。
  • つまり多ければ多いほど、この効果を実感できる。

正規表現を使って文字列判定をする。

$
0
0

変数を含んだpathでも特定のpathが含まれているかを確認する場合。

const noCheckPaths = /api/v1/user/${userId}などの場合

noCheckPaths.map(o => (`^${o}$`)).some(o => RegExp(o).test(ctx.path))) 

第9回:ニュースフィード投稿サーバIFを作る

$
0
0

目次:Webアプリ開発ロードマップ

第9回:ニュースフィード投稿サーバIFを作る

今回はニュースフィードを投稿するPOST /newsfeedsを作成します。

ニュースフィードを投稿するIFを登録する

下記のようにapp/rest.tsを修正し、expressにPOST /newsfeedsを追加します。
一旦受信したデータをログに出力するだけにしておきます。

        });
        app.post('/newsfeeds', (request, response, next) => {
            try {
                console.log(request);
                response.sendStatus(200);
            } catch (error) {
                response.sendStatus(500);
            }
        });
    }
    stop() {

作成したIFをクライアントから呼んでみる

Postmanを使って、作成したPOST /newsfeedsを呼び出してみます。
画像のようにPOST http://127.0.0.1:4300/newsfeedsを作成し送信するとレスポンスが受信できていることが確認できます。
1) 画面上部のタブ(+)をクリックし、新規のタブを追加します。
2) メソッドにPOSTを選択します。
3) request URLにhttp://127.0.0.1:4200/newsfeedsを入力します。
4) Bodyに画像のとおりJSONを設定します。
5) Sendボタンをクリックします。

スクリーンショット 2021-01-11 18.36.32.png

Sendを押すとレスポンスとして200 OKを受信していることが確認できます。

サーバ側で受信したデータを確認する。

PostmanからPOSTメッセージを呼び出したあと、サーバ側のログにリクエストの中身が出力されました。
bodyの中に送信したデータが入っていることが確認できます。

スクリーンショット 2021-01-11 18.41.22.png

joiを使ってパラメータチェックを行う。

モジュールをインストールする

下記のコマンドを実行し、joiをインストールします。

$ npm install joi

joiを読み込ませる

下記のようにapp/rest.tsを修正し、joiを読み込ませます。

app/rest.ts
importbodyParser=require('body-parser');importjoi=require('joi');import{environment}from'../environment';

受信したリクエストBODYをjoiを使ってバリデーションします。

app/rest.ts
try{constbody=request.body;console.log(body);// パラメータのフォーマットを定義constscheme=joi.object({message:joi.string().required(),createdAt:joi.date().required()});// バリデーションconst{error,value}=scheme.validate(body);if(error){response.sendStatus(400);}else{response.sendStatus(200);}}catch(error){

messageはstring型であること、createdAtはDate型であることをチェックしています。
また、どちらもrequiredで必須パラメータであることを指定しています。

Postmanから様々なパラメータを設定し呼び出してみて、
パラメータエラー(400)を正しく検知できていることを確認しましょう。

最後に

今回はニュースフィードを投稿するPOST IFを一つ作成してみました。
次回はフロントエンドからこのAPIを呼び出してみます。
今回開発したソースコードは GitHubに入っています。

receiptline で作った SVG の電子レシートを CodePen してみた

$
0
0

今回は「Webグラフィックス Advent Calendar 2020」に投稿した記事「SVG + CSS + Node.js + receiptline で電子レシートを発行してみよう」の補足です。

上記の記事では、作成した SVG の電子レシートを貼り付けることができず、仕方なくスクリーンショットを撮って PNG 形式で貼り付けていました。

ところが、翌日に投稿された、カレンダー作成者 warotarock さんの記事には、「動くもの」が CodePen で埋め込まれているではありませんか!

そこで、作成した SVG の電子レシート各種も CodePen でデモしてみたいと思います。

動くもの

See the Pen SVG + CSS + Node.js + receiptline = Digital Receipt by dopperi46 (@dopperi46) on CodePen.

何とかできました!
ハマりどころは、CodePen 埋め込みスクリプトの URL を変更しないと表示されないところ。

<script asyncsrc="https://production-assets.codepen.io/assets/embed/ei.js"></script>

また何か作ったら投稿します。ではまた!

Raspberry pi4へNode.jsのインストール手順

$
0
0

npm init -yでエラーが出る、、、

LINEBOTを作成してみようと思い、常時稼働させられるラズパイにNode.jsのインストールを試みた。
その中で初歩的な内容だが、インストール時にまとまってあれば嬉しかったなぁと思う気持ちで備忘録として残す。

そもそものNode.jsのインストールしようと思った流れは、
npmはインストールされていたが、npm init -yでエラーが出る、、、
Node.jsが適切にインストールされていないようだな、うん。

では、手順の説明です。

npmのアップデート

npmが入っていないときは以下のコードをターミナルにて実行。

$ sudo apt-get npm

次に、アップデート後に変化を確認するためにnpmのバージョンを確認。
以下のようにバージョンが出力されるのを確認する。

$ npm -v
6.14.10

さいごに、npmのアップデートを行う。

$ sudo npm update npm

これでnpmのアップデートは完了する。

次にNode.jsのインストールに移る。
Node.jsをインストールするためのバージョン管理ツールnを使った方法もあるようだが、自分はここで詰まって面倒になり諦めた。

詰まった内容としては、

  • そもそもnがなかった(入れはしました)
  • Node.js と npmに決まった対応があり、その設定がnでは死ぬほど面倒だった(決定的理由)

実際のバージョンの対応はNode.jsとnpmの対応表を参考にしてください。

なので、Node.jsのインストールとnpmとのバージョン対応を同時に行ってくれるnodebrewを使用する。

nodebrewのインストール

nodebrewのインストールには以下のコードを実行してください。

$ curl -L git.io/nodebrew | perl – setup

ここで、私のラズパイではあるローカルフォルダへアクセス許可がありませんとエラーが出てしまいました。
同じような場合には、

#777は「所有者」「所有グループ」「その他」に読み書き実行を許可します
$ chmod 777 エラーが起きている箇所

で解決できます。
その他にまで読み書き実行を許可しているので気をつけてください。
権限付与に関してはこちらの記事で確認できます。

次にPATHを通します。

#パスを通す、任意のエディタで開きます(ここではvi)
$ vi ~/.bash_profile

任意のエディタで開いたら
export PATH=$HOME/.nodebrew/current/bin:$PATH
と入力して保存してください。
viの操作コマンドはこちらを参考にされると良いかと思います。

ちゃんとインストールできたかを確認します。

nodebrew -v

いろいろ出ますが、nodebrew 1.0.1 のように出ればOKです。

Node.jsインストールとnpmとのバージョン対応付け

最後に、Node.jsをインストールし、対応付けます。

#バージョンは偶数番目が安定しているようです。
nodebrew install 任意のバージョン

#使用するNode.jsバージョンを選択。同時にnpmの対応も行われる。
nodebrew use 任意のバージョン

Installed successfullyと出れば終了です。

ここまで設定することで、npm init -y もエラーなく動作しました。

Node.jsをインストールする

$
0
0

何?Node.jsって?

Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動の JavaScript 環境です。
Node.js とは | Node.js

Windowsにインストールする

  • 環境 : Windows10 Pro バージョン1909
  1. Node.jsのダウンロードページを表示してインストーラをダウンロードする
    • 古めのバージョンは、リリース一覧 | Node.jsから探してダウンロード
    • 今回は古めのnode-v10.23.1-x64.msiをダウンロード
  2. インストーラを起動して[Next]ボタンでどんどん進んで[Install]ボタンでインストールするimage.pngimage.pngimage.pngimage.pngimage.pngimage.pngimage.png
  3. バージョンを確認する
バージョンを確認する
# インストーラが自動でパスを通してくれているのでnodeやnpmコマンドが使えるようになっている$ printenv PATH | sed-e's/:/:\n/g' | grep-i node
/c/apps/nodejs:

$ node -v
v10.23.1

$ npm -v
6.14.10

$ npm bin -g
C:\Users\ponsuke\AppData\Roaming\npm

ラズパイを使ってSwitchBotをWebUIから操作する

$
0
0

1. やったこと

SwitchBotを買いました。
スマホアプリから物理スイッチを押してくれる、Home IoTには欠かせないパーツです!
Hubを買ってもいいけど、ラズパイがあるのならばせっかくならラズパイから操作したい。
そして、WebUIを出して家の中からスマホで操作できるようにした。
というわけで、やってみました。

2. 必要なもの

  • SwitchBot Amazonで購入。キャンペーン期間中でちょっと安くなっていました(3,150円)
  • Raspberry Pi。我が家のは3
  • Python3 (インストールしておいてください)

3. 手順

3-1. ラズパイのコマンドラインからSwitchBotを操作する

公式のライブラリがあるので、ほぼそれを使うだけです。
自分は以下のページを参考にさせていただきました。

Raspberry Pi + SwitchBotで風呂を沸かす

軽く手順を書いておきます

3-1-1. Pythonの確認

※この通りのバージョンでなくても大丈夫

$ python --version
Python 2.7.16

$ python3 --version
Python 3.7.3

3-1-2. OpenWonderLabs/python-hostのクローン

$ mkdir switchbot
$ cd switchbot
$ git clone https://github.com/OpenWonderLabs/python-host.git

3-1-3. 各種ツールのインストール

gattlibのインストールで躓くことがあるようです。
風呂を沸かすのページに解決方法も書いてあります。
自分は大丈夫、友人は引っかかった。

$ sudo apt-get install python3-pip libboost-python-dev libboost-thread-dev
$ sudo apt-get install libbluetooth-dev # READMEにはないがこれも必要$ sudo pip3 install pybluez
$ sudo pip3 install gattlib

3-1-4. SwitchBotを探す

python-host付属のツールで探します
XX:XX:XX:XX:XX:XXの部分にあなたのSwitchBotのBLE MACが入ります。メモしておきましょう。

$ sudo python3 switchbot_py3.py --scan
Scanning for bluetooth low-energy devices
Discovering Switchbot services
 * Found Switchbot service on device XX:XX:XX:XX:XX:XX handle 22
Found 1 devices: ['XX:XX:XX:XX:XX:XX']
Enter the number of the device you want to control:
        0       XX:XX:XX:XX:XX:XX
0
Connected!
Command execution successful

3-1-5. SwitchBotでボタンを押す

XX:XX:XX:XX:XX:XXの部分にメモったBLE MACを入れましょう。

$ sudo python3 switchbot_py3.py --device XX:XX:XX:XX:XX:XX Press
Connected!
Command execution successful

お、動いた!

3-2. WebUIを作る

node.js + express でさくっと作ります。
まずは Hello World を出してみましょう。

3-2-1. node.js/npmのインストール

すでにインストールしてあればこの作業は不要です

sudo apt-get update
sudo apt-get install-y nodejs npm
sudo npm cache clean
sudo npm install n -gsudo-E n stable
sudo ln-sf /usr/local/bin/node /usr/bin/node
sudo apt-get purge -y nodejs npm
sudo apt -y autoremove
sudo npm install require

3-2-2. フロントエンドを作る

コマンド一発

pi@raspberrypi:~/switchbot/frontend $ npm init -y
Wrote to /home/pi/switchbot/frontend/package.json:

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

pi@raspberrypi:~/switchbot/frontend $ ls
package.json

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

expressをインストールします

$ npm install express --save
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN frontend@1.0.0 No description
npm WARN frontend@1.0.0 No repository field.

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

3-2-4. app.jsとindex.htmlでHello World (静的ルーティング)

ここのページを参考にしました。
Node.jsとExpressでローカルサーバーを構築する(2) ―Expressでルーティング―

$ mkdir public
$ cd public
$ vi index.html

※index.htmlの内容です

<!DOCTYPE html><html><head><title>test</title></head><body><h1>Hello World!</h1></body></html>
$ cd ..
$ vi app.js
/**
 * /app.js
 */constexpress=require('express')constapp=express();constpath=require('path')constport=10888;app.listen(port,()=>{console.log('Running at '+port+'...');});app.use(express.static(path.join(__dirname,'public')));app.use((req,res)=>{res.sendStatus(404);});

3-2-5. サーバー起動 (Hello World)

$ node app.js

ブラウザから localhost:8889 にアクセス
スマホを家のLANにWi-Fi接続して、スマホのブラウザから接続できればOK!

※恒久的起動

$ nohup node app.js &

殺し方

$ ps aux | grep node
pi        1105  0.0  3.9 150324 35644 ?        Sl   12:43   0:01 node app.js
$ kill 1105

3-3. SwitchBotとつなぐ

これはもうapp.jsindex.htmlを見てもらったほうが早いかと
execで 3-1-5 のコマンドを実行しているだけです。

/**
 * /app.js
 */constexpress=require('express');constapp=express();constpath=require('path');+constexec=require('child_process').exec;app.listen(8889,()=>{console.log('Running at 8889...');});// static page (top page)app.use('/',express.static(path.join(__dirname,'public')));// press (これを追加)app.get('/press',(req,res)=>{exec('sudo python3 /home/pi/switchbot/python-host/switchbot_py3.py --device XX:XX:XX:XX:XX:XX Press',(err,stdout,stderr)=>{if(err){res.write(err);}res.end(stdout);});});// not foundapp.use((req,res)=>{res.sendStatus(404);});
<!DOCTYPE html><html><head><title>Switchbot</title></head><body><h1>Switchbot</h1><ahref='press'>スイッチを押す</a><br></body></html>

これでラズパイからSwitchBotを動かせました!
これで何でもできるぞ~
Home IoTの第一歩です。

EOF

JavaScriptでPythonのenumerate関数を実行するには

$
0
0

はじめに

Node.jsを使ったWebアプリの開発にて、forEachでリストを回していたのですが、
急遽インデックスの情報も必要になりました。

そこで次のようなソースコードを書いたものの、「もっといい方法がある」と仲間に指摘されました。
実際に試したところ非常に便利だったことと、Pythonのenumerate関数に近いものを感じたので、
for文でインデックスとバリューを同時に取得する方法として、記事に残します。

ary=["bar","foo","hoge"]leti=0ary.forEach(function(value){console.log(i,value);i++});// 0 bar// 1 foo// 2 hoge

実行環境

  • Node.js v12.16.3

検証環境

  • Paiza.io(Python 3.8.2)
  • Paiza.io(Node.js v12.18.3)

そもそもenumerate関数とは

引数に指定したリストの、インデックスとバリューを取得する関数です。

ary=["bar","foo","hoge"]fori,iteminenumerate(ary):print(i,item)#  0 bar
#  1 foo
#  2 hoge

for文でインデックスとバリューを同時に取得する

forEachは、第二引数にインデックスを指定できる

forEach関数において 第一引数はリスト、第二引数はインデックスを取ります。

このインデックスは、ループ回数が増加するごとに自動的にインクリメントされるため、
i++i += 1と記述する必要はありません。

ary=["bar","foo","hoge"]ary.forEach(function(value,i){console.log(i,value);});// 0 bar// 1 foo// 2 hoge

for inを使う

for inはリストのインデックスを取得します。
次のようなソースコードにより、インデックスとバリューを取り出します。

ary=["bar","foo","hoge"]for(iinary){console.log(i,ary[i])}// 0 bar// 1 foo// 2 hoge

一方で、for inでは次のように予期しない挙動を示す場合もあるので、
利用には注意が必要そうです。

vardata=['apple','orange','banana'];//配列オブジェクトにhogeメソッドを追加Array.prototype.hoge=function(){}for(varkeyindata){console.log(data[key]);}// apple// orange// banana// function(){}

参考

【Node.js × LINE WORKS API】監査ログをダウンロードする

$
0
0

すんごいドマイナーな内容なのですが、やりたいことのために必要だったので調べました。( ゚Д゚)

ただ、勉強不足なせいか思ったより時間がかかってしまいました。。。
反省と自身の備忘録的にしっかりとアウトプットしておこうと思います(=_=)

LINE WORKS API

LINE WORKS で提供されている API です。
トーク Bot を操作したり、カレンダーやメールの内容を取ってきたりできます。
今回使用するのは 監査データのダウンロード APIです。

LINE WORKS Developers

実行環境

実行環境は以下の通りです。

  • Node.js
  • Visual Studio Code
  • LINE WORKS API

事前準備

  1. Node.js をインストール
  2. Visual Studio Code をインストール
  3. LINE WORKS Developer Console で認証情報を取得
  4. 各モジュールをインストール
npm install request --save
npm install csv --save

API を Request するコード

download.js
// LINEWORKS 認証情報constCONSTS=require("./consts.js");constrequest=require('request');constcsv=require('csv');constapiId=CONSTS.API_ID;constfeature='log';constservice='message';constserviceId='audit';constendDate=getUnixTime();conststartDate=Number(endDate)-86400;// 24時間前から現在時刻までのログを取得consttenantId=CONSTS.DOMAIN_ID;constdomainId=CONSTS.DOMAIN_ID;constrangeName='tenant';consturi=`https://audit.worksmobile.com/r/${apiId}/audit/v2/${feature}/${service}/logs.csv?apiId=downCsvLog&serviceId=${serviceId}&startDate=${startDate}&endDate=${endDate}&tenantId=${tenantId}&domainId=${domainId}&rangeName=${rangeName}`;constoptions={'method':'GET','url':uri,'headers':{'consumerKey':CONSTS.CONSUMER_KEY,'Authorization':`Bearer ${CONSTS.TOKEN}`}};request(options,function(error,response){if(error)thrownewError(error);csv.parse(response.body,{bom:true,columns:true},(err,data)=>{console.log(data)})});functiongetUnixTime(){constdate=newDate();constnow=date.getTime();returnMath.floor(now/1000);};

コードについて補足

  • 認証情報は CONSTS.js ファイルにまとめてあります。
CONSTS.js
constDOMAIN_ID='xxxxxxxxxxxx';constAPI_ID='xxxxxxxxxxx';constCONSUMER_KEY='xxxxxxxxxxx';constTOKEN='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'exports.DOMAIN_ID=DOMAIN_ID;exports.API_ID=API_ID;exports.CONSUMER_KEY=CONSUMER_KEY;exports.TOKEN=TOKEN;
  • 今回はトークログを取得しているので const service = 'message';を指定しています。ログイン状況やメールなどのログも取得できます。詳しくは公式ドキュメントを参照ください。

  • トークログのみ unix timestamp 形式で時間単位の指定が可能なので unix timestamp 形式で指定しています。

  • UNIX time stamp 形式で指定する場合 24時間以内のデータしか取得できません。

  • Request URL、ちょー長いですよね!( ゚Д゚)メンドイ

  • 取得した CSV データには BOM が入っているので csv.parseするときにオプションを指定しています。{ bom: true, columns: true }

おわりに

ここまでお付き合いいただきありがとうございました。

冒頭にも書きましたが、やりたいことがあって・・・
Bot の入っていないトークルームの発言をトリガーにしたいんですよね。
本当は GAS で実装したかったのですが GAS での csv ダウンロードがどうにもうまくできなくて。
GAS は Node.js での実装が終わったら再チャレンジしようと思います。

ではまた!(^^)/

参考にさせていただきましたm(_ _)m

LINE WORKS Developers
Visual Studio Codeをインストールする

yarnしたら The engine "node" is incompatible with this module.

$
0
0

環境

  • node: 12.10.0
  • yarn: 1.x

困りごと

久しぶりにプロジェクトを立ち上げようとしたら、、yarnができない:frowning2:

kazukinoMacBook-Pro:furien-web kazuki$ yarn
yarn install v1.22.10
[1/5] 🔍  Validating package.json...
error furien-web@1.0.0: The engine "node" is incompatible with this module. Expected version "12.10.0". Got "14.15.1"
error Found incompatible module.
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.

原因

  • どうやら利用しているnodeのバージョンが12.10.0であり、モジュールとの互換性がないことが原因みたいです。
  • チーム開発のため、勝手にバージョンを変えるわけにもいかないし、どうしよう。

解決策

kazukinoMacBook-Pro:furien-web kazuki$ yarn --ignore-engines

結果

無事通りました。
あくまでも一時的な対処法であります。悪しからず。:boy_tone1:

yarn install v1.22.10
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
warning "@nuxtjs/markdownit > raw-loader@2.0.0" has unmet peer dependency "webpack@^4.3.0".
warning "@nuxtjs/moment > moment-locales-webpack-plugin@1.2.0" has unmet peer dependency "webpack@^1 || ^2 || ^3 || ^4 || ^5".
warning "@nuxtjs/moment > moment-timezone-data-webpack-plugin@1.3.0" has unmet peer dependency "webpack@4.x.x || 5.x.x".
warning " > markdown-it-attrs@2.4.1" has unmet peer dependency "markdown-it@^8.4.2".
warning "nuxt-fontawesome > @fortawesome/vue-fontawesome@0.1.6" has unmet peer dependency "vue@~2".
warning " > sass-loader@7.1.0" has unmet peer dependency "webpack@^3.0.0 || ^4.0.0".
warning " > @vue/test-utils@1.0.0-beta.29" has unmet peer dependency "vue@2.x".
warning " > @vue/test-utils@1.0.0-beta.29" has unmet peer dependency "vue-template-compiler@^2.x".
warning " > eslint-loader@2.1.2" has unmet peer dependency "webpack@>=2.0.0 <5.0.0".
warning " > eslint-plugin-vue@4.7.1" has incorrect peer dependency "eslint@^3.18.0 || ^4.0.0".
warning " > vue-jest@3.0.4" has unmet peer dependency "vue@^2.x".
warning " > vue-jest@3.0.4" has unmet peer dependency "vue-template-compiler@^2.x".
[4/4] 🔨  Building fresh packages...
✨  Done in 154.62s.

APIを叩いていてTimestamp for this request is too fast.とエラーが出た時の解決策

$
0
0

APIを叩いていたらこんな応答が返ってきました

なぜ・・日本時間なのが悪いのか?と色々推測しました。

{
  "status": 1,
  "messages": [
    {
      "message_code": "ERR-5009",
      "message_string": "Timestamp for this request is too fast."
    }
  ],
  "responsetime": "2021-01-13T07:43:02.225Z"
}

結論 PCの時刻設定が違っている

特に設定をいじった覚えはないのですが、確かにずれがありました。
下記画像では、既に時間の修正を行っています。直す前は7.5秒のずれがありました。
スクリーンショット 2021-01-13 16.59.52.png
https://www.time-j.net/worldtime/country/jp

上記サイトと見比べながら人力で修正

時計の時間の部分をぽちぽちやります。
スクリーンショット 2021-01-13 17.02.09.png

ちゃんとデータが返ってくるようになりました

{
  "status": 0,
  "data": [
    {
      "amount": "4133",
      "available": "4133",
      "conversionRate": "1",
      "symbol": "JPY"
    },
  ],
  "responsetime": "2021-01-13T07:51:49.678Z"
}

Pure JavaScript 高速 SHA-256 ハッシュ実装

$
0
0

SHA-256 のダイジェスト・ハッシュ値を計算する高速なピュア JavaScript 実装のライブラリを npm で公開したので紹介します。
Uint8ArrayInt32Arrayを使うことで、このほかのピュア JavaScript 実装のライブラリよりも高速にハッシュ値を計算します。

SHA-2 (SHA-256) 版

SHA-256は、256ビット(16進数64桁)のハッシュ値です。
例:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

https://www.npmjs.com/package/sha256-uint8array

constcreateHash=require("sha256-uint8array").createHash;consttext="";consthex=createHash().update(text).digest("hex");// => "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"constdata=newUint8Array(0);consthash=createHash().update(data).digest();// => <Uint8Array e3 b0 c4 42 98 fc 1c 14 9a fb f4 c8 99 6f b9 24 27 ae 41 e4 64 9b 93 4c a4 95 99 1b 78 52 b8 55>

インターフェースは、Node.js の ネイティブの cryptoモジュールのサブセットです。
v0.9.0 時点のベンチマーク結果は以下の通り。(macOS 10.15.7 Intel Core i7 3.2GHz)
1KB 程度の JSON と日本語文字列を、各 10,000 回ずつハッシュ計算したときのミリ秒です。

moduleversionnode.js V14Chrome 87Safari 14minifiedbackend
crypto-103ms 👍---OpenSSL
sha256-uint8array0.9.0274ms446ms 👍243ms 👍3KB 👍Uint8Array
crypto-js4.0.0805ms910ms918ms108KBUint8Array
jssha3.2.0835ms892ms913ms10KBUint8Array
hash.js1.1.7635ms611ms1,577ms7KBArray
sha.js2.4.11356ms965ms3,512ms27KBBuffer
create-hash1.2.0381ms1,002ms3,502ms97KBBuffer
jshashes1.0.81,450ms2,239ms1,164ms23KBArray

1回あたりのハッシュ計算にかかる時間は、マイクロ秒単位なので、いろんな用途で使えそうです。
そのほかの特徴:

  • 入力は、文字列または Uint8Array
  • 出力は、16進数文字列または Uint8Array
  • 複数チャンクに分割された入力にも対応しています。
  • 絵文字など U+10000以降のサロゲートペア、UTF-8 で4バイトの文字に対応しています。
  • IE11 でも動きます。(IE10 ですら動くようだ)

SHA-1 版

同じインターフェースで SHA-1 版もあります。
SHA-1は、160ビット(16進数40桁)のハッシュ値です。
例:da39a3ee5e6b4b0d3255bfef95601890afd80709

https://www.npmjs.com/package/sha1-uint8array

constcreateHash=require("sha1-uint8array").createHash;consttext="";consthex=createHash().update(text).digest("hex");// => "da39a3ee5e6b4b0d3255bfef95601890afd80709"constdata=newUint8Array(0);consthash=createHash().update(data).digest();// => <Uint8Array da 39 a3 ee 5e 6b 4b 0d 32 55 bf ef 95 60 18 90 af d8 07 09>

v0.9.0 時点のベンチマーク結果は以下の通り。

moduleversionnode.js V14Chrome 87Safari 14minifiedbackend
crypto-70ms 👍---OpenSSL
sha1-uint8array0.9.0218ms346ms 👍192ms 👍2KB 👍Uint8Array
hash.js1.1.7513ms573ms908ms7KBArray
jssha3.2.0690ms782ms770ms9KBUint8Array
crypto-js4.0.0779ms829ms961ms108KBUint8Array
jshashes1.0.8686ms1,448ms727ms23KBArray
tiny-sha10.2.1209ms775ms3,573ms2KBUint8Array
sha.js2.4.11360ms930ms3,534ms26KBBuffer
create-hash1.2.0387ms976ms3,591ms97KBBuffer

古いライブラリだと ASCII 文字列のみ対応で、日本語すら使えないものもあったので、注意が必要。
上記リストの各ライブラリごとの minified したときの容量や、バックエンド(内部で使われる仕組み)は、ざっくり調査したものなので、厳密には違うかも。

高速化実装のキモ

JavaScript の実行を高速化するには、できるだけオブジェクトを作らない・メモリを確保しない実装が大切です。
V8 なら、確保されたメモリ内で済む処理ならば、かなり高速に動作してくれます。

本ライブラリでは、1回のハッシュ値計算ごとに、3つオブジェクトを作っています。
もし複数チャンク分割に対応せずに、1入力だけとすれば、再入を考慮せずに済むので、さらに減らせるのだが。
メモリは 8KB のプールを分割して利用することで、毎回はメモリを確保しないようにしています。

また、文字列からの入力時は、JavaScript の内部の文字コードは UTF-16 なので、UTF-8 への変換が必要。
文字列全体をいちどに TypedArray に展開するのではなくて、64・80バイト毎のブロックの範囲ごとに変換しています。
どちらかというと、この処理を書いてみたくて、このライブラリを作ったようなものだ。

ネイティブ実装との速度比較

上記のベンチマークの通り、ほかのピュア JavaScript 実装のライブラリよりは高速なものの、
Node.js だけで使うなら、ネイティブの cryptoモジュールを使ったほうが3倍くらい速いです。
内部で使われる OpenSSL の実装がカリカリで速すぎるみたい。

なお、ブラウザでは、TextEncodercrypto.subtle.digest()の両方が使える環境であれば、その方が速いです。
iOS 10.3 以降あるいは Android 5 以降かつ、HTTPS サーバ配下なら使えるようです。IE や古い方の Edge では使えない。

Node.js の cryptoとブラウザの crypto.subtleはインターフェースが異なるので注意が必要。
ブラウザでは、即値ではなく Promiseを返したり、まだチャンク分割入力できないようだし、ArrayBuffer専用のようだ。

Browserify

cryptoモジュール全体を呼んだアプリを、ブラウザ向けに普通に browserifyすると、minify した後でも +300KB 超の容量増になります。
あるいは大きめのライブラリを使うと 100KB になるところ、本ライブラリなら 3KB 前後と、コンパクトに利用できます。
もし、cryptoモジュールを crypto.createHash("sha256").update(data).digest("hex");形式のハッシュ計算でしか使っていないアプリなら、下記のような browserify 設定にすることで、『Node.js ならネイティブ実装』・『ブラウザなら本ライブラリ』と切り替えて利用することができます。

package.json
{"browser":{"crypto":"sha256-uint8array/dist/sha256-uint8array.min.js"},"devDependencies":{"browserify":"^17.0.0","sha256-uint8array":"^0.9.0","terser":"^5.5.1"}}

(備考)どのハッシュを使うか

ハッシュはいくつも種類があります。その昔は軽い MD5 を使っていました。

  • MD5 は弱いので、現代のサービスでは使われないです。
  • SHA-1 もすでに推奨されていないものの、用途によっては、まだまだみかけます。
  • SHA-256 を使うと SHA-1 よりも2〜3割ほど遅くなるものの、大量に使う用途でもなければ、無視できるレベルの差かなと。
  • SHA-368 や SHA-512 については、64ビット整数演算が必要なので、ピュア JavaScript で実用的な速度で実装するのは難しそう。

ネイティブ実装と、ピュア JavaScript 実装の両方を混在利用したい場合は、今なら SHA-256 を使うのが良さそう。

Node.jsとSeleniumを使ってChoromeのコンソールログを確認する

$
0
0

はじめに

Seleniumを使用してみる機会があったため,備忘録としてまとめようと思い,記事を書きました。
現在主にJavascriptを使用しているため,Node.jsを使ってSeleniumを動かそうと思います。
Node.jsの環境構築に関しては,こちらの手順で行なっています。

Seleniumとは

Webブラウザの操作を自動化するためのツール。
今回は,SeleniumをNode.js内で使用して,Webブラウザを操作してみたいと思います。

大まかな流れ

  1. 必要なパッケージ・ドライバのダウンロード
  2. 実行ファイル作成
  3. 実行

以下で詳細に説明していきます。

必要なパッケージ・ドライバのダウンロード

selenium-webdriverのインストール

$npm install--save selenium-webdriver
  • --save : package.jsonのdependenciesに追加される

各ブラウザのDriverのダウンロード

主要なブラウザで動作させるには,上記のパッケージに加えて以下のドライバをダウンロードし,実行ファイルと同じパスに配置する必要があります.

ブラウザドライバ
Chromechromedriver(.exe)
Internet ExplorerIEDriverServer.exe
EdgeMicrosoftWebDriver.msi
Firefoxgeckodriver(.exe)
Safarisafaridriver

今回はChromeを使用するので,上記表中のChromeドライバのページをクリックします.

スクリーンショット 2020-11-17 12.37.44.png

バージョン86を使用するので,対象バージョンの欄をクリックします.
(どのバージョンのドライバをダウンロードするかはこちらのページを参照)

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3533333031342f38326630643232302d626261342d653765662d386639382d3339363433353133333639352e706e67.png

Macのドライバ(chromedriver_mac64.zip)を選択し,ダウンロードします.
zip解凍後,Nodeの実行ファイルと同じパスに配置すれば,準備は完了です.

$cp ~/Downloads/chromedriver .

実行ファイル作成

今回は,Qiitaのトップページを開き,コンソールログを取得してみます。

実行ファイルの完成形

selenium_app.js
// ライブラリを呼び出すconstwebdriver=require("selenium-webdriver");constchrome=require("selenium-webdriver/chrome");const{Builder}=webdriver;const{Preferences,Type,Level}=require("selenium-webdriver/lib/logging");// await を使うため,async function 内で処理を記述する(asyncfunction(){// オプション付きでブラウザを立ち上げるconstcapabilities=webdriver.Capabilities.chrome();constlogPrefs=newPreferences();logPrefs.setLevel(Type.BROWSER,Level.ALL);capabilities.setLoggingPrefs(logPrefs);constoptions=newchrome.Options(capabilities);constdriver=awaitnewBuilder().forBrowser("chrome").setChromeOptions(options).build();// Qiitaのトップページへ遷移awaitdriver.get("https://qiita.com/");// 5秒待機awaitdriver.sleep(5000);// コンソールログの取得constconsoleLogs=awaitdriver.manage().logs().get(Type.BROWSER);for(leti=0;i<consoleLogs.length;i++){console.log(consoleLogs[i]);}// 終了awaitdriver.quit();})();

以下で詳細を説明します.

実行ファイルの作成

まずは新規ファイルを作成します

$touch selenium_app.js

webdriverを呼び出す

公式のドキュメントを参考に,実行ファイル内でchromeのwebdriverを呼び出します。
また,ログを取得するために必要なwebdriver内のライブラリも呼び出します。

selenium_app.js
// ライブラリを呼び出すconstwebdriver=require("selenium-webdriver");constchrome=require("selenium-webdriver/chrome");const{Builder}=webdriver;const{Preferences,Type,Level}=require("selenium-webdriver/lib/logging");

ブラウザを立ち上げる

コンソールログを取得するためのオプションを指定して,ブラウザを立ち上げます。

selenium_app.js
// オプション付きでブラウザを立ち上げるconstcapabilities=webdriver.Capabilities.chrome();constlogPrefs=newPreferences();logPrefs.setLevel(Type.BROWSER,Level.ALL);capabilities.setLoggingPrefs(logPrefs);constoptions=newchrome.Options(capabilities);constdriver=awaitnewBuilder().forBrowser("chrome").setChromeOptions(options).build();
  • 今回はコンソールログを取得するため,ログのタイプをType.BROWSERと指定していますが,デベロッパツールのNetworkの情報を取得したい場合はこの箇所をType.PERFORMANCEに変更すれば取得することができます。

ページ遷移

操作したいページのURLを指定し,そのURLへ遷移させます。

selenium_app.js
// Qiitaのトップページへ遷移awaitdriver.get("https://qiita.com/");

コンソールログの取得

selenium_app.js
// コンソールログの取得constconsoleLogs=awaitdriver.manage().logs().get(Type.BROWSER);
  • こちらもType.BROWSERの箇所をType.PERFORMANCEにすることで,デベロッパーツールのNetworkの情報を含むログ情報を取得することができます。

ドライバーの終了

selenium_app.js
// 終了awaitdriver.quit();

実行

実行ファイルが存在しているディレクトリに移動し,以下のコマンドを実行します。

$node selenium_app.js

以下のような結果が表示されれば成功

Entry {
  level: Level { name_: 'DEBUG', value_: 700 },
  message: 'https://qiita.com/ - [DOM] Input elements should have autocomplete attributes (suggested: "current-password"): (More info: https://goo.gl/9p2vKq) %o',
  timestamp: 1610609498344,
  type: ''
}

参考


Node.jsとMySQLの接続方法

$
0
0

mysqlモジュールをインストールする

Node.jsでMySQLに接続するには、mysqlモジュールを使います。
以下のコマンドを実行してインストールします。

$ npm install mysql

MySQLに接続する

MySQLを操作するjsファイルに追記していきます。
今回はMySQLを操作するjsファイルを main.jsとします。
main.jsに以下を追加して、mysqlのモジュールをインポートします。

main.js
const mysql = require('mysql');

次に、 createConectionメソッドで、使用するMySQLのデータベースとの接続の指定をし、 connectionを作成します。(MySQLでデータベースを作成しておきましょう。)

main.js
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'database-name'
});

クエリを実行する

データベースからデータを引出します。
例えば以下のようになります。

main.js
connection.query(
    'SELECT * FROM database-name',
    (error, results) => {
      res.render('list.ejs', { databaseーname: results });
    }
  );

MySQLとの接続を切る

最後に endメソッドで connectionによるMySQLとの接続を切ります。

main.js
connection.end();

【基礎の基礎編】React + Express + GraphQLでHello World!

$
0
0

graphql-1200x630-960x504.png

はじめに

この記事は、GraphQLの基礎の基礎として、サーバーサイドとクライアントサイドを最低限実装し、GraphQLで接続する機能を実装しています。データの更新や、DB接続してデータを取得したりなどは行っていません。GraphQLでクライアントとサーバーの連携の概要を理解していただければと思います。

source

https://github.com/kyokozuka/react-node-express-graphql

GraphQLとは

GraphQLはAPIのための問い合わせ言語です。クエリを実行してデータを呼び出すためのランタイムをさすこともあります。

GraphQLの問い合わせ言語

GraphQLは、データベースに問い合わせるために開発されたSQLの考え方をインターネットに適用したものです。単一のGraphQLのクエリを使い、関連するデータをまとめて取得できます。また、データを変更したり削除したりすることもできます。SQLとGraphQLは問い合わせ言語という点で共通しています。

ただ、どちらも問い合わせ言語ではあるものの、GraphQLとSQLは全く異なるものです。

  • SQLのクエリはデータベースに対して実行される(データベースのための問い合わせ言語)
  • GraphQLのクエリはAPIに対して実行される(インターネットのための問い合わせ言語)

構文

  • SQL: SELECT, GraphQL: Query
  • SQL: INSERT/UPDATE/DELETE, GraphQL: Mutation

GraphQLはインターネットのための問い合わせ言語なので、ソケット通信を使ってデータの変更を監視するためのコマンドも用意されている。そのコマンドを"Subscription"と呼びます。

GraphQLサーバーの実装

まず、サーバーサイドとして、簡単な(Hello Worldを表示する)APIを作成してみましょう。
今回はApollo ServerはOSSで、非常にシンプルにセットアップできる上、本番環境に投入できる水準の機能を多く提供しています。
代表的な機能としては、サブスクリプションのサポート、ファイルのアップロード、既存のサービスのAPIからデータを取得する機能、クラウドサービスのApollo Engineがあります。

環境構築

ディレクトリ構成

myappServer/
  ├ node_modules
  ├ src
    ├ resolbers
      ├ index.js
      └ Query.js
    ├ index.js
    └ typeDefs.graphql
  └ package.json 
$ npm init -y // 初期化処理
$ npm install express apollo-server-express graphql-playground-middleware-express // モジュールのインストール

プログラム

index.js
constexpress=require("express");const{ApolloServer}=require('apollo-server-express');const{readFileSync}=require('fs');constresolvers=require('./resolvers');consttypeDefs=readFileSync('./src/typeDefs.graphql','utf-8');constapp=express();constserver=newApolloServer({typeDefs,resolvers});server.applyMiddleware({app})app.get('/',(req,res)=>res.end(`Welcome to the PhotoShare API`))app.listen({port:4000},()=>console.log(`GraphQL Server runnning @ http://localhost:4000{server.graphqlPath}`))
typeDefs.graphql
typeQuery{helloWorld:String!}
resolvers/index.js
constQuery=require('./Query');constresolvers={Query}module.exports=resolvers
resolvers/Query.js
module.exports={helloWorld:()=>'Hello World'}

サーバーの起動

$ cd myappServer
$ node src/index.js

>>>
GraphQL Server runnning @ http://localhost:4000{server.graphqlPath}

GraphQL Playgroundで実行してみましょう。
スクリーンショット 2021-01-14 171105.png

GraphQLクライアントの実装

次に、先ほど作成したGraphQLサーバーにクライアントからアクセスしてみましょう。

reactプロジェクトの作成

$ create-react-app myappClient

インストール

$ npm install graphql graphql-request
  • graphql-request

GraphQLオペレーションをAPIに送信するために利用できるフレームワークがいくつか存在します。その中で有名な"graphql-request"を使用します。これはfetchリクエストをPromiseでラップして、GraphQLサーバーへのリクエストを送信します。また、リクエストの構築とデータのパースに関する細かい処理を引き受けます。

プログラム

作成されたApp.jsを下記のように書き換えてください。

App.js
import{useState}from'react';import'./App.css';import{request}from'graphql-request'consturl='http://localhost:4000/graphql'constquery=`
  query helloWorld {
    helloWorld
  }
`functionApp(){const[hello,setHello]=useState('');constgraphQL=async()=>{constresult=awaitrequest(url,query)setHello(result.helloWorld)}return(<divclassName="App"><buttononClick={graphQL}>Click</button>
<div>{hello}</div>
</div>
);}exportdefaultApp;

request()でurlとqueryを引数として受け取ってサーバーへのリクエストを構築して、結果のデータを返します。ここでレスポンスされるデータは期待した通りのJSONデータ { "helloWorld": "Hello World" } がレスポンスされます。

サーバーの起動

$ cd myappClient
$ npm start

サーバーが起動したら、ブラウザが立ち上がり、「click」ボタンをクリックすると「Hello World!」と表示されるはずです。

スクリーンショット 2021-01-14 172910.png

【Express.js】PUGでサーバーサイドの環境変数を参照する

$
0
0

はじめに

PUGなどのフロントサイドのファイル内でサーバーサイドの環境変数を参照したい時に使える豆知識です。

前提条件

  • dotenvがインストールされていること
  • 環境変数ファイル(.envなど)で適当な値が宣言されていること

実装

Express.jsのapp.localsオブジェクトを使用します。

app.localsオブジェクトは、アプリケーション内のローカル変数である特性を持っており、一度設定されると、app.localsプロパティの値は、リクエストの存続期間中のみ有効なres.localsプロパティとは対照的に、アプリケーションの存続期間を通じて存続します。

簡単にいうと、アプリケーションレベルの環境変数を管理するので、PUGからも参照できるということです。

詳しくは公式ドキュメントを参照してください。
https://expressjs.com/en/api.html#app.locals

実装はすごく簡単です。
app.js内で下記のように宣言します。

SAMPLE_ENV='This is sample environment'
app.js
constexpress=require('express');require('dotenv').config();constapp=express();app.locals.SAMPLE_ENV=process.env.SAMPLE_ENV;

あとはPUGのなかでSAMPLE_ENVで呼び出すことができます。

sample.pug
p 結果 : #{SAMPLE_ENV}

出力結果

結果 : This is sample environment

node.jsでTCP通信 (serverからの送信内容をclientでエコーバックする)

$
0
0

node.jsでTCP通信をする方法を調べていると

  1. cilent側で文字を入力して
  2. それをserver側で折り返して
  3. それをclient側でコンソールに表示

というサンプルが山ほど見つかるのですが、

  1. server側で文字を入力して
  2. それをclient側で折り返して
  3. それをserver側でコンソールに表示

という例が見つからなかったので、書いてみました。

server.js

varnet=require('net');varserver=net.createServer(conn=>{console.log('connected.');conn.on('data',data=>{console.log('server-> '+data);});conn.on('close',()=>{console.log('closed');});process.stdin.resume()process.stdin.on('data',data=>{conn.write(data)});}).listen(3000);console.log('listening on port 3000');

client.js

varnet=require('net');varclient=newnet.Socket();client.setEncoding('utf8');client.connect('3000','localhost',()=>{console.log('connected');});client.on('data',data=>{console.log('client-> '+data);client.write(data);});client.on('close',()=>{console.log('closed');});

以上です

参考
https://nodejs.org/api/net.html#net_net_createserver_options_connectionlistener

JavaScript Puppeteerで絶対ハマらないブラウザ操作2021

$
0
0

はじめまして。マーティンです。Twitterフォロワー20人記念記事、爆誕です。
スクレイピングライブラリはいろいろありますがブラウザ操作用ライブラリはPuppeteer一択じゃぁ!
個人的にはPythonよりJSでブラウザ操作する方が好みですが、このPuppeteerというライブラリは少々くせ者ですのでハマりやすいところを解説します。
JavaScriptの文法を理解した初心者の方向けです。

目次

1.そもそもPuppeteerとは
2.始め方
3.おすすめ起動オプションと注意点
4.ページ遷移
5.よく使うpageのメソッドと注意点

1. そもそもPuppeteerとは

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

Nodeで動くChromium操作用のライブラリ。「ヘッドレス」なので、デスクトップにChromiumを表示させずに動かすこともできるということですね。
Chromiumというやつがオープンソースで、それをPuppeteerで操作するという流れ。自分のパソコンに入ってるChromeを操作するわけではない。Chromiumはpuppeteerをインストールすると一緒についてくる。

Async AwaitのおかげでPuppeteerを使うのはめちゃくちゃ簡単になりました。
Puppeteerは操り人形という意味らしい。

2. 始め方

$npm install puppeteer

installするものはこれだけです。
Puppeteerで何かするときは以下の雛形を用います。

constpuppeteer=require('puppeteer');constURL="https://example.com";(async()=>{constbrowser=awaitpuppeteer.launch();//あとでオプション設定するよconstpage=awaitbrowser.newPage();awaitpage.goto(URL);//あとでオプション設定するよ//ここでなんかするよawaitbrowser.close();//開発時はコメントアウトしておくと良いよ})();//即実行する関数よ

3. おすすめ起動オプションと注意点

オプションの指定が可能です。

Puppeteerはデフォルトでヘッドレスが有効になっていますが、無効に設定できます。
slowMoを設定しないと処理が超高速で行われてしまいます。非同期処理に自身がない方は必ずslowMoしましょう。非同期処理に自身がある方も基本的にslowMoしたほうが良いかと。slowMo:50くらいにするといい感じの速度でブラウザが操作されるので見ていて面白い。

またwindowサイズを決定することも重要です。レスポンシブデザインのサイトでは閲覧するブラウザのwindowサイズによってHTMLの要素が変化する場合があるため、windowサイズを明示的に固定することでこれを防ぎます。
defaultViewportを設定しましょう。defaultViewportを設定することによりHMTLページのサイズを指定します。下にある --window-size はHTMLページのサイズではなくブラウザのサイズのみ指定します。defaultViewportなしではレスポンシブデザインによるHMTL要素の変化を防止できません。

goto(URL)するときはwaitUntilも一緒につけちゃいましょう。
https://pptr.dev/#?product=Puppeteer&version=v5.5.0&show=api-pagegotourl-options
ページ遷移を待ってくれます。

//開発時はheadless:falseが良いawaitpuppeteer.launch({headless:false,slowMo:50,args:['--window-size=1900,1080'],defaultViewport:{width:1900,height:1080}});awaitpage.goto(URL,{"waitUntil":'networkidle0'})

学生向けコーナー(需要あるのか?

Googleにログインした状態でブラウザ操作したいときがありますがPuppeteerでGoogleにログインすることは基本的にできません。詳しくは以下の記事がよくまとまっていますのでご覧あれ。
https://qiita.com/vicugna-pacos/items/a52e22d08856d1041316
自動でブラウザを操作していることがバレて弾かれちゃいます。
しかしなんと、教育用Googleアカウント: xxxxxx@ed.jp等を持っている学生ならそのアカウントを使ってGoogleにログインできます。謎ですね!ところが一つ問題点があります。
Headless trueでGoogleにログインしたい場合は必ずUserAgentを設定しなければならないということです。(個人的にハマったところ)
Puppeteerではフォームに値を入力するとき、フォームのHTML要素を指定します。よってheadless falseでブラウザを起動させてからデベロッパーツールを使ってHTML要素を調べるわけです。
headless falseではこちらが指定した通りGoogleにログインできるのですが、trueにした途端できなくります。原因はGoogleログインフォームとUserAgentの関係。
下を試してみると...

//headless trueのときconsole.log(awaitbrowser.userAgent())// => Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4298.0 Safari/537.36//headless falseのときconsole.log(awaitbrowser.userAgent())// => Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4298.0 Safari/537.36

headlessがfalseの場合はuserAgentに「Chrome」とあり,headless trueにするとuserAgentに「HeadlessChrome」とあることからheadlessか否かによってUserAgentが異なることがわかります。
UserAgentが異なると何が問題なのでしょうか?
Googleのログイン画面を見てみましょう。

[headlessがtrue UserAgentがHeadlessChromeの場合]
headless.png
[headlessがfalse UserAgentがChromeの場合]
chrome.png

ご覧の通りHeadlessChromeの場合とChromeの場合でGoogleのログインフォームのHTML構造が異なるため、メールアドレスを打ち込む際の(後に述べますが)inputフィールドを指定するセレクターの違いが生まれることがエラー発生の原因でした。

で解決策は?

Headless trueの状況下でUserAgentをChromeに無理やり設定することで解決!UserAgentはご自分のChromiumで「UserAgent 確認」と検索するといろいろなUserAgent確認用サイトがヒットしますのでそこで調べてください。

awaitpage.setUserAgent("あなたのUserAgent")

4. ページ遷移

非同期処理って何?って方はこちらの記事が非常に分かりやすいので参考までに。
https://qiita.com/soarflat/items/1a9613e023200bbebcb3
https://qiita.com/klme_u6/items/ea155f82cbe44d6f5d88

ページ遷移はPuppeteerの肝。
「async await つけるだけでしょ」と思っていると少し危険です。
Puppeteerでページ遷移を待つときには page.waitForNavigation()が使われますが、これがよく誤用されがち。

waitForNavigationはページ遷移の前に宣言

awaitPromise.all([page.waitForNavigation(),page.click("button")]);

buttonタグをクリックするpage.click()よりもあとに宣言された場合、たまにエラーが発生します。clickによるページ遷移がwaitForNavigationが呼ばれる前に完了した場合、waitForNavigationはページ遷移を認識できない。

waitForNavigationよりwaitForSelector

waitForNavigationにはいくつかオプション指定ができますが、どのオプションを持ってしても確実にページ遷移の非同期処理が行われるという保証はありません。そこで登場するのがwaitForSelector。

awaitPromise.all([page.waitForSelector("input[name='email']"),page.click("button")])

読んで字の如く、指定したセレクターが出現するのを待ちます。セレクターの指定方法はquerySelectorと同じです(後述)。
waitForSelectorを使えば非同期処理エラーはまずないだろうと思いますが、それでも不安な方はwaitForTimeoutを使いましょう。

awaitPromise.all([<省略>])awaitpage.waitForTimeout(500)///500ミリセカンド待つ

余談ですが少し前まで使われていたwaitForは非推奨となりましたので代わりにwaitForTimeoutを使いましょう。

waitFor is deprecated and will be removed in a future release. See https://github.com/puppeteer/puppeteer/issues/6214 for details and how to migrate your code.

5. よく使うpageのメソッドと注意点

page.click(selector[, options])

ページ内のHMTL要素をCSSセレクターを用いて指定します。
CSSセレクターはどうやって指定するのか?
デベロッパーツールを開いてお目当てのHTMLタグを右クリック、「Copy」=>「copy selector」で一発です。オプションはあまり使わないので言及しません。

page.type(selector,text[,options])

指定したCSSセレクターにテキストを入力します。

page.evaluate(pageFunc[,...args])

具体例を見てみましょう。

lettext=awaitpage.evaluate((selector)=>{returndocument.querySelector(selector).innerHTML;},"div > h1")

page.evaluateの第一引数にpageFuncとして渡すアロー関数を、第二引数にセレクター"div > h1"を渡していることがわかります。第二引数に渡したセレクターがpageFuncの引数selectorに代入され処理が実行されています。
注意したいことが3つ。
1つ目がpageFuncはブラウザ内で実行される関数であるため、スクリプトファイル内で定義された変数を直接関数内で使うことはできないこと。しっかりpage.evaluateの第二引数として変数をわたしてあげましょう。
2つ目がreturnをすること。returnしないとtextに値がを代入されません。
3つ目はmapとforeachについて。evaluate内でquerySelectorAllするときはmapを使いましょう。
return document.querySelectorAll("div").map(() => (do stuff here))
mapは新しい配列を返すがforeachは配列を書き換えるだけ。またmap内でもreturnを忘れないこと。
初歩的なことですが、よく忘れてしまうので自戒も込めて。

 その他

const URL = "https://------"のところで行末にセミコロンをつけないとエラーになる
・Puppeteerを使ってGoogleForm自動回答マシーンを作りたい方はhttps://stackoverflow.com/questions/59328036/how-to-select-an-opion-from-a-google-forms-popup-dropdown-puppeteer-nodejs 
が参考になります。
・Puppeteerでブラウザ操作を定期的に行いたいwindowsユーザーの方にはタスクスケジューラの使用をおすすめします。.batファイルでnode実行コマンド記述->タスクスケジューラで定期的に.batファイルを実行。
・間違っているところがあれば指摘していただけると幸いです。

最後まで読んでいただきありがとうございました。

Viewing all 8839 articles
Browse latest View live