- 投稿日:2020-07-20T23:56:21+09:00
Node でお手軽スクレイピング 2020 年夏
皆さんは Web ページのスクレイピングって書いた事ありますか?私はあります。だってどんなに平和で平穏な生活を送っていても数年に一度はスクレイピングってしたくなりますよね。「うわーまじか!API ないのかよ…。」的な。
そうしたら HTTP クライアントと HTML パーサのライブラリを探してきてインストールした上でごりごり書くことになると思います。でも実際に書いてみると、そうやってライブラリのインストールをしたりサンプルコードで動作確認している時間よりも、HTML を解析して実際にパースしたところから対象の要素を取得して欲しい値を取り出す試行錯誤の時間の方が長かったっていう事はないですか?
今日ご紹介する Node でお手軽スクレイピングは、その辺の試行錯誤の手間を極力減らすことが出来る方法です。2020 年夏の最新版です。
まずは環境から。特に古いものを使う理由もないので 2020-07-20 時点の最新版
14.5.0
を使っています。$ node -v v14.5.0そしてプロジェクトの初期化を行って、2 つほどライブラリをインストールします。
$ npm init $ npm install node-fetch jsdom --save-devnode-fetch は Node 上でウェブブラウザと同じような
fetch
を使えるようにするライブラリです。普段 Web ベースの JS を書いてると、HTTP アクセスするにもfetch
が直感的で楽だなーと思うので選びました。GitHub 上のスターは 5.3k。素晴らしいですね。jsdom はウェブブラウザと同様の API セットを持った HTML DOM ツリーをメモリ上に構築することが出来るライブラリです。Pure JavaScript で実装されたウェブブラウザのサブセットと思うと理解しやすいかも知れません。GitHub 上のスターは 14.4k。今回の記事の要です。
必要なライブラリが揃ったところで早速スクリプトを書いていきましょう。サンプルに気象庁の東京都の週間天気予報のページを選びました。
index.mjs#!/usr/bin/env node import fetch from 'node-fetch'; import jsdom from 'jsdom'; const { JSDOM } = jsdom; (async () => { const res = await fetch('https://www.jma.go.jp/jp/week/319.html'); const html = await res.text(); const dom = new JSDOM(html); const document = dom.window.document; const nodes = document.querySelectorAll('#infotablefont tr:nth-child(4) td'); const tokyoWeathers = Array.from(nodes).map(td => td.textContent.trim()); console.log(tokyoWeathers); })();これだけ見て「あー、なるほど!」ってならない方のために詳細な解説は後ほど加えていきますが、まず一番のポイントは
const nodes
から始まる行以降です。お気づきでしょうか?この行以降はそのままウェブブラウザ上でも実行可能なことに。従来のスクレイピングでは、必要な DOM 要素を取得するためのクエリを探したり、得られたノードを加工して必要なリストに変換する試行錯誤に時間がかかっていました。その試行錯誤自体を無くすことは不可能ですが、ウェブブラウザ上のデベロッパーツールであれば、リアルタイムに結果を見ながら試行錯誤することでその手間を大幅に減らすことが出来ます。
そしてデベロッパーツール上で欲しい結果が得られるようになったら、そのコードをスクリプトファイルに貼り付ければそれだけでもうスクレイピングの完成です。このスクリプトを実行すると以下のような結果が得られます。
$ ./index.mjs [ '曇', '曇一時雨', '曇一時雨', '曇', '曇', '曇時々晴', '曇時々晴' ]従来に比べると革命的に楽に書ける事がお分かりいただけたのではないでしょうか。
さて、では約束通り詳細な解説を加えていきましょう。
#!/usr/bin/env node
今回、コマンドラインから直接スクリプトを実行しようかなと思ったので追加しています。
node
コマンドにファイルを渡して実行するのであれば不要です。import fetch from 'node-fetch'; import jsdom from 'jsdom'; const { JSDOM } = jsdom;
import
記法が使えるようになったのは嬉しいのですが、v14 のデフォルトではファイルの拡張子を.mjs
にしておく必要があるので注意して下さい。またjsdom
に関しては直接import { JSDOM } from 'jsdom'
と書きたくなるところですが、現状ではjsdom
が ES2015 Modules 構文をサポートしていないため、こういったまどろっこしい書き方になります。(async () => { // ... })();非同期処理があるので
await
を使いたいのですが、await
自体も非同期関数の中じゃないと使えないので、非同期の無名関数を作って即時実行しています。const res = await fetch('https://www.jma.go.jp/jp/week/319.html'); const html = await res.text();Web プログラミングで見慣れた書き方ですね。非同期に
fetch
した結果から、HTML を文字列として取得しています。XHR
を使っていた期間が長かったので私もうっかり間違えがちですが、XHR
のresponseText
と違って、fetch
で得られるレスポンスのtext
メソッドは非同期なのでそこにも注意が必要です。const dom = new JSDOM(html); const document = dom.window.document;さあ本記事の最大の見せ場です。
JSDOM
コンストラクタに HTML を文字列で渡すと、それをパースして DOM ツリーにしてくれます。そこには Web プログラミングでおなじみ、window
オブジェクトがあり、その中にdocument
オブジェクトがあります。const nodes = document.querySelectorAll('#infotablefont tr:nth-child(4) td'); const tokyoWeathers = Array.from(nodes).map(td => td.textContent.trim()); console.log(tokyoWeathers);この部分は、デベロッパーツール上で動作確認したものを貼り付けると言っていた部分です。ウェブブラウザ上でだと
:nth-child(4)
に相当する部分を楽に探せるのがいいですね。そこで得られたNodeList
オブジェクトをArray.from
でArray
に変換するというのは、今どきなテクニックかもしれません。以上でスクリプトの解説は終わりです。
最後に、忘れてはならないのはスクレイピングは最終手段であるという事です。API が提供されているサービスであれば必ずそちらを使うべきですし、やむを得ずスクレイピングする際はサーバに過度な負荷を与えることの無いよう気をつけましょう。
- 投稿日:2020-07-20T23:56:21+09:00
Node.js でお手軽スクレイピング 2020 年夏
皆さんは Web ページのスクレイピングって書いた事ありますか?私はあります。だってどんなに平和で平穏な生活を送っていても数年に一度はスクレイピングってしたくなりますよね。「うわーまじか!API ないのかよ…。」的な。
そうしたら HTTP クライアントと HTML パーサのライブラリを探してきてインストールした上でごりごり書くことになると思います。でも実際に書いてみると、そうやってライブラリのインストールをしたりサンプルコードで動作確認している時間よりも、HTML を解析して実際にパースしたところから対象の要素を取得して欲しい値を取り出す試行錯誤の時間の方が長かったっていう事はないですか?
今日ご紹介する Node.js でお手軽スクレイピングは、その辺の試行錯誤の手間を極力減らすことが出来る方法です。2020 年夏の最新版です。
まずは環境から。特に古いものを使う理由もないので 2020-07-20 時点の最新版
14.5.0
を使っています。$ node -v v14.5.0そしてプロジェクトの初期化を行って、2 つほどライブラリをインストールします。
$ npm init $ npm install node-fetch jsdom --save-devnode-fetch は Node.js 上でウェブブラウザと同じような
fetch
を使えるようにするライブラリです。普段 Web ベースの JS を書いてると、HTTP アクセスするにもfetch
が直感的で楽だなーと思うので選びました。GitHub 上のスターは 5.3k。素晴らしいですね。jsdom はウェブブラウザと同様の API セットを持った HTML DOM ツリーをメモリ上に構築することが出来るライブラリです。Pure JavaScript で実装されたウェブブラウザのサブセットと思うと理解しやすいかも知れません。GitHub 上のスターは 14.4k。今回の記事の要です。
必要なライブラリが揃ったところで早速スクリプトを書いていきましょう。サンプルに気象庁の東京都の週間天気予報のページを選びました。
index.mjs#!/usr/bin/env node import fetch from 'node-fetch'; import jsdom from 'jsdom'; const { JSDOM } = jsdom; (async () => { const res = await fetch('https://www.jma.go.jp/jp/week/319.html'); const html = await res.text(); const dom = new JSDOM(html); const document = dom.window.document; const nodes = document.querySelectorAll('#infotablefont tr:nth-child(4) td'); const tokyoWeathers = Array.from(nodes).map(td => td.textContent.trim()); console.log(tokyoWeathers); })();これだけ見て「あー、なるほど!」ってならない方のために詳細な解説は後ほど加えていきますが、まず一番のポイントは
const nodes
から始まる行以降です。お気づきでしょうか?この行以降はそのままウェブブラウザ上でも実行可能なことに。従来のスクレイピングでは、必要な DOM 要素を取得するためのクエリを探したり、得られたノードを加工して必要なリストに変換する試行錯誤に時間がかかっていました。その試行錯誤自体を無くすことは不可能ですが、ウェブブラウザ上のデベロッパーツールであれば、リアルタイムに結果を見ながら試行錯誤することでその手間を大幅に減らすことが出来ます。
そしてデベロッパーツール上で欲しい結果が得られるようになったら、そのコードをスクリプトファイルに貼り付ければそれだけでもうスクレイピングの完成です。このスクリプトを実行すると以下のような結果が得られます。
$ ./index.mjs [ '曇', '曇一時雨', '曇一時雨', '曇', '曇', '曇時々晴', '曇時々晴' ]従来に比べると革命的に楽に書ける事がお分かりいただけたのではないでしょうか。
さて、では約束通り詳細な解説を加えていきましょう。
#!/usr/bin/env node
今回、コマンドラインから直接スクリプトを実行しようかなと思ったので追加しています。
node
コマンドにファイルを渡して実行するのであれば不要です。import fetch from 'node-fetch'; import jsdom from 'jsdom'; const { JSDOM } = jsdom;
import
記法が使えるようになったのは嬉しいのですが、v14 のデフォルトではファイルの拡張子を.mjs
にしておく必要があるので注意して下さい。またjsdom
に関しては直接import { JSDOM } from 'jsdom'
と書きたくなるところですが、現状ではjsdom
が ES2015 Modules 構文をサポートしていないため、こういったまどろっこしい書き方になります。(async () => { // ... })();非同期処理があるので
await
を使いたいのですが、await
自体も非同期関数の中じゃないと使えないので、非同期の無名関数を作って即時実行しています。const res = await fetch('https://www.jma.go.jp/jp/week/319.html'); const html = await res.text();Web プログラミングで見慣れた書き方ですね。非同期に
fetch
した結果から、HTML を文字列として取得しています。XHR
を使っていた期間が長かったので私もうっかり間違えがちですが、XHR
のresponseText
と違って、fetch
で得られるレスポンスのtext
メソッドは非同期なのでそこにも注意が必要です。const dom = new JSDOM(html); const document = dom.window.document;さあ本記事の最大の見せ場です。
JSDOM
コンストラクタに HTML を文字列で渡すと、それをパースして DOM ツリーにしてくれます。そこには Web プログラミングでおなじみ、window
オブジェクトがあり、その中にdocument
オブジェクトがあります。const nodes = document.querySelectorAll('#infotablefont tr:nth-child(4) td'); const tokyoWeathers = Array.from(nodes).map(td => td.textContent.trim()); console.log(tokyoWeathers);この部分は、デベロッパーツール上で動作確認したものを貼り付けると言っていた部分です。ウェブブラウザ上でだと
:nth-child(4)
に相当する部分を楽に探せるのがいいですね。そこで得られたNodeList
オブジェクトをArray.from
でArray
に変換するというのは、今どきなテクニックかもしれません。以上でスクリプトの解説は終わりです。
最後に、忘れてはならないのはスクレイピングは最終手段であるという事です。API が提供されているサービスであれば必ずそちらを使うべきですし、やむを得ずスクレイピングする際はサーバに過度な負荷を与えることの無いよう気をつけましょう。
- 投稿日:2020-07-20T22:31:06+09:00
「LINE Messaging API + Googleスプレッドシート + 猫画像API」による1問1答ではないチャットボットを作りました
通常のチャットボットは1問1答式ですが、データベースと連携することで1問1答式以外の可能性が広がると思い、試しにこんなものを作りました。
私が若かりし頃(30年前くらい)のコンパとかの宴会芸のネタの1つですww作ったもの
LINEチャットボット
「心を読む猫ボット」
ちなみに、iPhoneXへのハメコミ画像はこちらで作りました。
https://mockuphone.com/何をするチャットボットなの?
数回の質問で、頭の中にある数字をあてます。
つまり、あなたの心を読むことができるボットなのです。
なぜ猫なの?
テキストだけでは寂しいですし、老若男女に大人気の猫に便乗して少しでも利用してもらおうという
姑息な手マーケティング手法です。
それに、今思い付いたのですが、心を読むチャットボットに飽きても、猫画像に癒やされるチャットボットとして長く利用してもらえるという狙いもあるのです。
ちなみにリッチメニュー「タッチしてください」の画像は、我が家の猫ちゃんです。本当に人の心が読めるの?
チャットボットが人の心を読むなんて信じないでしょうから、その証拠動画をお見せいたしましょう。
ちなみにこの動画は、@tkyko13さんに教えてもらったGyazoというサービスを利用しました。
これで、興味津々になった方は、今すぐこちらのQRコードにスマホのカメラを向けましょう。
システム
環境
- Node 14.5.0
構成図
ボットが数回質問した内容からユーザーの頭の中にある数字を当てるという仕組みです。
過去のデータの蓄積が必要なのでデータベースとしてGoogleスプレッドシートを利用しました。
プログラムは node.jsで、herokuに構築しました。
node.jsをherokuに構築するには、実行プログラムと同じディレクトリに Procfile を追加するだけで、できました。Procfileweb: node app.js使用API
- LINE Messaging API
Node.jsでLINE BOTを作る方法はこちらを参考にしました
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017
- Google Sheets API
過去の情報を保有するためにGoogleスプレッドシートを使いました。
- Googleスプレッドシートの導入部分はこちらを参考
Node.js googleapis npmパッケージで Google スプレッドシートを await/async で読み取るメモ 〜1ft-seabass.jp.MEMO
- データの読み書きはこちらのサイトを参考
【Node.js + Sheets API v4】Googleスプレッドシートを読み書きする
- TheCatAPI
いろんな猫APIがあります。その中からURLをたたくだけでランダムにネコの画像を取得できるAPIを使用しました。なぜ心を読めるの?
超能力です。
というのは冗談で、簡単な算数問題です。
最後の「最初の数を引いてください」ってところがポイントです。答えはコードの中にもあります。
(コード読むより、普通に考えたほうが簡単ですが・・・)コード
app.js'use strict'; const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; const axios = require("axios"); const line = require('@line/bot-sdk'); //LINE Messaging API のいろいろ const config = { channelSecret: 'LINE_CHANNEL_SECRET', channelAccessToken: 'LINE_CHANNEL_ACCESS_TOKEN' }; //Googleスプレッドシートを使う設定 let {google} = require('googleapis'); const creds = { "type": "service_account", "project_id": "PROJECT_ID", "private_key_id": "PRIVATE_KEY_ID", "private_key": "PRIVATE_KEY", "client_email": "CLIENT_EMAIL", "client_id": "CLIENT_ID", "auth_uri": "AUTH_URI", "token_uri": "TOKEN_URI", "auth_provider_x509_cert_url": "AUTH_PROVIDER_X509_CERT_URL", "client_x509_cert_url": "CLIENT_X509_CERT_URL" }; // JSON Web Token(JWT)の設定 let jwtClient = new google.auth.JWT( creds.client_email, null, creds.private_key, ['https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive'] ); const sheet = 'SHEET名'; // スプレッドシートAPIはv4を使う let sheets = google.sheets('v4'); app.post('/webhook', line.middleware(config), (req, res) => { console.log(req.body.events); //ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。 if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){ res.send('Hello LINE BOT!(POST)'); console.log('疎通確認用'); return; } Promise .all(req.body.events.map(handleEvent)) .then((result) => res.json(result)); }); const client = new line.Client(config); async function handleEvent(event) { //GoogleスプレッドシートのJSON Web Token(JWT) の認証 let resultJwtClient; try { resultJwtClient = await jwtClient.authorize(); } catch (error) { console.log("Auth Error: " + error); } //シートを読み込む let responseGetSheet; try { responseGetSheet = await sheets.spreadsheets.values.get({ auth: jwtClient, spreadsheetId: sheet, range: "シート1", }); } catch (error) { console.log('The API returned an error: ' + error); } //シートから読み込んだデータ let data = responseGetSheet.data.values let bot_message; //返すセリフ let newComerFlag = 1; let userIndexNumber; //アクセス者のインデックス //アクセス者のインデックスを調べる(初めてかどうかも調べる) for (let i = 0; i < data.length ; i++ ) { if (data[i][0] == event.source.userId){ newComerFlag =0; userIndexNumber= i ; } } //最初ののアクセスのときの処理 if (newComerFlag == 1){ // googleスプレッドシートへの書き込み(ユーザーID、回数、数字の初期値) bot_message = "数字を1つ思い浮かべてください"; appendData("シート1!A1",event.source.userId,0,0) } //最後の1つ前のアクセスのときの処理 else if (data[userIndexNumber][1] == 4) { bot_message = "それに、最初に思った数字を引いてください"; data[userIndexNumber][1] = Number(data[userIndexNumber][1]) + 1; //googleスプレッドシートのデータをアップデート updateData("シート1!"+String(userIndexNumber+1)+":"+String(userIndexNumber+1),data[userIndexNumber][0],data[userIndexNumber][1],data[userIndexNumber][2]) } //最後のアクセスの処理 else if (data[userIndexNumber][1] == 5) { bot_message = "今あなたの頭にある数字は "+ data[userIndexNumber][2] + " ですね"; //googleスプレッドシートの内容を消去する updateData("シート1!"+String(userIndexNumber+1)+":"+String(userIndexNumber+1),"","","") } else{ //通常時のアクセスの処理 let plusMinus; let add_mes; let random1 = Math.floor( Math.random()*10 )+ 1; if (random1 > 2) {add_mes=" を足してください";plusMinus = 1;} else{add_mes=" を引いてください";plusMinus =- 1;} let random2 = Math.floor( Math.random()*10 )+ 1; bot_message = "それに " + random2 + add_mes; data[userIndexNumber][2] = Number(data[userIndexNumber][2])+ (random2*plusMinus); data[userIndexNumber][1] = Number(data[userIndexNumber][1]) + 1; //googleスプレッドシート更新(回数、現在の数値) updateData("シート1!"+String(userIndexNumber+1)+":"+String(userIndexNumber+1),data[userIndexNumber][0],data[userIndexNumber][1],data[userIndexNumber][2]) } //猫の写真をAPIからランダムにとってくる let cat_picture = await axios.get("https://api.thecatapi.com/v1/images/search"); let cat_url= cat_picture.data[0].url; //ボットのメッセージを返す処理 return client.replyMessage(event.replyToken, { "type": "template", "altText": "This is a buttons template", "template": { "type": "buttons", "thumbnailImageUrl": cat_url, "imageAspectRatio": "rectangle", "imageSize": "cover", "imageBackgroundColor": "#FFFFFF", "title": bot_message, "text": " ", "defaultAction": { "type": "uri", "label": "View detail", "uri": cat_url }, "actions": [ { "type":"message", "label":"OK", "text":"OK" } ] } }); } //googleスプレッドシートにデータを追加(行を追加) async function appendData(range0,value1,value2,value3) { let responseAppendSheet; try { responseAppendSheet = await sheets.spreadsheets.values.append({ auth: jwtClient, spreadsheetId: sheet, range: range0, valueInputOption: "USER_ENTERED", insertDataOption : "INSERT_ROWS", resource : { values : [[value1,value2,value3]] } }); } catch (error) { console.log('The API returned an error: ' + error); } } //googleスプレッドシートのデータをアップデート(上書き) async function updateData(range0,value1,value2,value3) { let responseAppendSheet; try { responseAppendSheet = await sheets.spreadsheets.values.update({ auth: jwtClient, spreadsheetId: sheet, range: range0, valueInputOption: "USER_ENTERED", resource : { values : [[value1,value2,value3]] } }); } catch (error) { console.log('The API returned an error: ' + error); } } app.listen(PORT); console.log(`Server running at ${PORT}`);フローチャート
ちなみに、このフローチャートはこちらで作りました。
draw.io
https://www.draw.io/可能性
LINEのチャットボットを外部のデータベースとやりとりできれば、可能性が広がります。
ぱっと浮かんだだけで、以下のようなことができそうなので、気が向いたらチャレンジしてみようと思います。
- しりとり、山手線ゲーム
- ポーカーなどのトランプ
- アドベンチャーゲーム、脱出ゲーム
- LINEから外部の機械やガジェットを操作
- センサーからマイコンを通じて情報取得
- 会話のやりとりをクラウドに保存して機械学習
- Unityと連携したゲームや、メディアアート
- 投稿日:2020-07-20T21:10:31+09:00
簡単レシート印刷 receiptline でバーコードと QR コードを作ってみた
日本発のオープンソース receiptline でレシート印刷に少しずつトライしています。
ネットオークションやフリマアプリでレシートプリンターを探していましたが・・・
とうとう何台か落札することができました!まだ手元にないので、前回利用した開発ツールを引き続き使います。
今回はバーコードと QR コードです。バーコード
バーコードアイコンをクリックすると、ダイアログボックスが開きます。
適当にデータを入れて、サイズと可読文字の有無を選びましょう。
ここは無難にデフォルト設定の CODE128 にしておきます。
CODE128 は「〇〇 Pay」のバーコードに使われているそうです。キャンセルしたいときは、ダイアログボックスの外をクリックします。
編集エリアに code プロパティと option プロパティが追加されました。
code プロパティは、バーコードを出力します。
名前はcode
またはc
、値はバーコードデータです。option プロパティは、この行以降のバーコードを設定します。
名前はoption
またはo
、値はバーコードオプションです。
複数のオプション値は,
or 1 つ以上のスペースで区切ります。ReceiptLine{code:WIND402; option:code128,2,72,hri}QR コード
二次元コードアイコンをクリックすると、ダイアログボックスが開きます。
二次元コードは QR コードのみです。
適当にデータを入れて、サイズと誤り訂正レベルを選択します。キャンセルしたいときは、ダイアログボックスの外をクリックします。
編集エリアに code プロパティと option プロパティが挿入されました。
二次元コードのプロパティはバーコードと同じです。
option プロパティの値は QR コード用になっていますね。ReceiptLine{code:Do it! Make it! Shake it!; option:qrcode,5,M}バーコードの位置揃え
文字列と同様に、テーブルの区切り文字
|
で位置揃えができます。
バーコードと QR コードを、左揃えと右揃えにしてみます。code プロパティと option プロパティを分離して、短縮名を使います。
また、クワイエットゾーンが必要なので、間隔を1行ずつ空けることにします。ReceiptLine{o:code128,2,72,hri} |{c:WIND402} {c:WIND402}| = {o:qrcode,5,M} |{c:Do it! Make it! Shake it!} {c:Do it! Make it! Shake it!}|ちなみに、バーコードと QR コードは、2 列にしたり罫線を引いたりすることができません。
この制約はレシートプリンターのコマンド仕様に由来するようです。バーコードの種類
code プロパティと option プロパティの対応をまとめました。
種類 option code 使い道 CODE128 code128 ASCII 文字列 コード決済 CODE93 code93 ASCII 文字列 ??? NW-7
(Codabar)nw7
codabar数字と一部の記号
(先頭と末尾は ABCD)宅配伝票 ITF
(Interleaved 2 of 5)itf 偶数桁の数字 段ボール箱 CODE39 code39 英数字と一部の記号 現品票 JAN
(EAN)jan
ean13 桁の数字
8 桁の数字商品 UPC-A
UPC-Eupc 12 桁の数字
7 桁の数字北米の商品 QR Code qrcode ASCII 文字列
漢字続きは Web で 次回は、変換ライブラリの API を試してみようと思います。
- 投稿日:2020-07-20T20:02:46+09:00
[React] useStateのセッターで配列を更新しても再レンダリングされない時
前置き
Reactのstate hookで配列を定義していて、更新したいと思いセッターに値を渡すと中身は更新されているのにコンポーネントが再レンダリングされない事案にぶち当たりました。
const [hoge,setHoge] = React.useState<SampleType>(InitialArray)これに悩まされて数時間無駄にしたので誰かのお役に立てば...
問題のコード
問題となるコードのサンプルを作ってみました
/*--------------------------------略*/ const LunchList: React.FC = () => { const [lunchlist,setList] = React.useState<Lunch[]>(InitialArray) React.useEffect(()=>{ const setvalue = lunchlist setvalue.push('パスタ') //重要 setList(setvalue) }) /*--------------------------------略*/ }パット見動きそうなのですがこれだと前置きで話した通り再レンダリングされません。
なぜ?
調べてみたところReactのstate hookは
object.is()を使って変更があったかどうかを判別しているので、
今回の例のようにlunchlist
をコピーしたsetvalue
を
push()
などで直接操作してセッターに渡しても再レンダリングされないようです。公式の記事
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)解決
オリジナルでもコピーでもダメなら新しい配列をつくってしまう
const setvalue = [...lunchlist, 'パスタ'] //解決 setList(setvalue)こうしてしまえば
object.is()
で変更があったか判別できるので再レンダリングしてもらえます以上
- 投稿日:2020-07-20T18:45:51+09:00
学びの積み重ねを続けよう
日々の学びを3つずつ積み重ねていきます。
詳細については時間のある時に書いていこうと思います。
目的はアウトプットを続けること。JavaScript編
7月20日
!! 二重否定は文字列 String から 真偽値を取得したい時に使われる。
返り値として、trueまたはfalseが欲しい時に使いましょう。
vuex/index.jsexport const getters = { isAuthenticated (state) { return !!state.user } }RFCとはインターネットにおける技術仕様
自動車の部品などに使われるJISのインターネット版という印象。
JSONのファイル形式やURLの仕様について公開されている。HTTPレスポンスは最初の一行目「ステータス・コード」をみることによってリクエストの結果を知ることができる
この中でもチェックしておきたいコードは以下
200 OK リクエストが正常に完了 みんな大好き 302 Found 探し物は見つかったけれど別の場所にあるためリダイレクトを要する 401 Unauthrized ユーザ認証に失敗したことを表す。 あまり見覚えがない 403 Forbidden アクセス制限による拒否。権限が必要となる。 404 Not Found クライアント側に起因する。URLの打ち間違いなど 500 Internal Server Error サーバ側に起因するエラー
- 投稿日:2020-07-20T08:15:18+09:00
ユーザーのセッション情報をスケーラブルに保つ 2 つの方法
セッション管理について誤った認識を持っていたり、サーバのスケーラビリティを考慮しないままアプリケーションの開発を進めてはいけません。開発の終盤でサーバの負荷分散ができない、なんということになりかねません。
本記事では、Web 初心者向けにセッション管理の主要な2種類の方法について説明します。具体的な実装例として、Web サーバに express を使用しますが、基本的な考え方はどの言語、どの Web サーバでも同じです。
セッション管理とは
そもそも、セッション管理とは何でしょうか。
ブラウザから Web サーバへの通信は HTTP で行われます。HTTP プロトコルはステートレスです。つまり「状態」を持てません。
クライアントから Web サーバへリクエストがあるたびにクライアントとサーバ間でコネクションが張られ、リクエストを受け、レスポンスを返却した後、コネクションは破棄されます。
もう一度リクエストをしても、Web サーバは同じクライアントからリクエストがあったと判断することはできません。そこで一般的に Web アプリケーションにおいては、ユーザがログインした後、そのユーザ情報をセッションデータとしては何らかの方法で保持することが求められます。このログインしたユーザの情報を何とか保持しようとする仕組みのことを「セッション管理」と呼びます。
しっかりと学びたい人は MDN web docs を読みましょう。セッションデータの管理方法
ユーザーのセッションデータを保持するには主に2つの方法があります(実際には他にもいくつかありますが、一般的にはこの2つがあげられるでしょう)。
- セッション ID を使う
クライアントではセッション ID のみを Cookie 内に保持し、
サーバでセッション ID に紐づくセッションデータを保持する方法。- Cookie のみでユーザ情報を保持する
クライアントにてセッションデータ全てを Cookie 内に保持する方法。1. セッションIDを使う
1 の方法を採用すると、クライアントとサーバは以下のような関係になります。
ユーザ 0001 が自身のセッション ID を Cookie にid=0001
で保持しており、サーバサイドで 0001 に紐づくユーザ情報を取得しています。
ただしこの方法ではいくつか問題があります。通常 Web システムではバックエンドのサーバは冗長構成をとり、LB(ロードバランサ)でリクエストが負荷分散されます。また、スケールアウト/スケールインすることも考慮しなければいけません。
単一サーバ内でセッションデータを保持していた場合、1つ前にリクエストしたサーバ以外へルーティングされてしまうとセッションデータが取得できません。そこで Redis などの Key/Value で取得できるデータベースを用意しておき、どのサーバからも取得できるようにしておく構成とすることが一般的です。
2. Cookie のみでユーザ情報を保持する
2 の方式を採用することで、サーバサイドのスケーラビリティを簡単に担保できます。クライアントにユーザのセッションデータを全て格納しておき、毎回サーバサイドに送信することでユーザのセッションデータを維持できます。以下の図では、Cookie には
name=田中, email=tanaka@gmail.com
を格納しており、リクエストごとにサーバへ送信しています。実際にこの方式を使用する場合は、Cookie には生身のデータを保持することはありません。暗号化した文字列を保存し、サーバサイドで復号化して取り出すことが一般的でしょう。この方式を採ることでサーバー側にデータベースやリソースを用意する必要が無くなります。ただし、セッションデータの合計がブラウザの最大 Cookie サイズ(4096バイト)を超えることはできないことに注意しましょう。
セッションデータの管理方法の判断基準
まとめると、セッションデータの取り扱い方法で考慮するのは以下のようになります。
アプリケーションの仕様や必要に応じて選択できるようにしておきましょう。
管理方法 メリット デメリット 1. セッション ID を使う ・セッションデータのサイズの上限を気にする必要がない
・セッションデータはクライアントに対して不可視・サーバサイドで Redis などのスケーラブルなセッションストアを用意する必要がある。
2. Cookie のみでユーザ情報を保持する ・Redis などのスケーラブルなセッションストアが不要 ・ブラウザには Cookie のサイズ上限があり、大きなデータを保持できない(4096 バイト)
・Cookie データがクライアントに見えてしまう主要なライブラリ
さて、express でセッションデータを保持する有名なライブラリとしては以下の2つがあります。
モジュール 概要 express-session
1 の方法に対応。
クライアント上のセッション識別子のみを Cookie 内に格納し、セッションデータはサーバーに格納します。通常は Redis などのデータベースに保存します。cookie-session
2 の方法に対応。
クライアント上のセッションデータを Cookie 内に格納します。express-session
express-session は、セッションデータをサーバーに保管します。Cookie にはセッションデータそのものではなく、セッション ID のみを保存します。デフォルトで、メモリー内のストレージを使用するため、本番環境向けには設計されていません。本番環境では、Redis などのスケーラブルなセッションストアをセットアップする必要があります。互換性のあるセッションストアのリストを参照してください。
サーバ内メモリにセッションデータを保持する場合、最もシンプルな例は以下になるでしょう。使用できるオプションの詳細は公式 GitHub リポジトリを参照ください。
Cookie にセッション ID を保持し、サーバに保持されたセッションデータがあれば、それをカウントアップして返却しています。
特定のユーザのリクエスト数を計測しています。const express = require("express"); const session = require("express-session"); const app = express(); app.use( session({ secret: "input your secret string", // 署名に使用するシークレット文字列 cookie: { maxAge: 10000 }, // 10秒間リクエストがなければセッションデータは削除されます。 }) ); app.get("/", function (req, res, next) { res.setHeader("Content-Type", "text/html"); if (req.session.views) { req.session.views++; res.write("<p>views: " + req.session.views + "</p>"); res.write("<p>expires in: " + req.session.cookie.maxAge / 1000 + "s</p>"); res.end(); } else { req.session.views = 1; res.end("<p>welcome to the session demo. refresh!</p>"); } }); app.listen("3000", () => { console.log("Application started"); });もちろん、異なるクライアントごとに一意なセッション ID が発行されます。以下は Chrome と Firefox を同時にたちあげて振る舞いを観測しています。セッション ID に紐づいたセッションデータをサーバサイドから取り出せています。
さて、セッションデータをメモリに保存していますが、このままでは不十分です。サーバを再起動するとセッションデータが全て消えてしまったり、サーバを複数台用意してロードバランサなどで負荷分散する構成をとることができません。セッションストアを利用できるようにしましょう。
以下は、Redis を使用した例です。connect-redis モジュールを使用しています。
const redis = require('redis') const session = require('express-session') const RedisStore = require('connect-redis')(session) const redisClient = redis.createClient() app.use( session({ store: new RedisStore({ client: redisClient }), secret: 'keyboard cat', resave: false, }) )cookie-session
cookie-session はセッション・キーだけでなく、セッション全体を Cookie に保存します。ブラウザは Cookie 当たり最小 4096 バイトをサポートするので、比較的小さいデータを取り扱う場合にのみ使用を検討してください。
const cookieSession = require("cookie-session"); const express = require("express"); const app = express(); app.use( cookieSession({ name: "session", keys: ["key1", "key2"], maxAge: 10000, }) ); app.get("/", function (req, res, next) { res.setHeader("Content-Type", "text/html"); if (req.session.views) { req.session.views++; res.write("<p>views: " + req.session.views + "</p>"); res.end(); } else { req.session.views = 1; res.end("<p>welcome to the session demo. refresh! -- cookie-session --</p>"); } }); app.listen(3000);
express-session
とは異なり、session.sig
,session
というキーの Cookie が保管されています。セッションデータの実体は{ views: 3 }
のようなオブジェクトですが、暗号化されて保持されています。まとめ
express で使用される主要な2つのライブラリとその使用方法を簡単に説明しました。
セッションの管理は Web 開発において最も基本的なところですので、しっかり理解しておきましょう。