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

Google Spread Sheet のデータを毎分チェックし、変更があったら Firestore に保存する(Node.js)

$
0
0

TL;DR

タイトルから全てを察した方はこちらで十分かと思います。

import*asfunctionsfrom"firebase-functions";importadminfrom"firebase-admin";import{google,sheets_v4}from"googleapis";// 公開関数exportconstcheckSpreadSheet=functions.region("asia-northeast2").pubsub.schedule("every 1 minutes").onRun(async()=>{constsheetId="スプレッドシートのID"constranges=[{sheet:"シート",start:"A1",end:"B1"}];constsheetData=awaitgetSpreadSheetData(sheetId,ranges);constmodifiedTime=awaitgetModifiedTime(sheetId);awaitstore(modifiedTime,sheetData);});// 引数用定義exportinterfaceSpreadSheetRange{sheet:string;start:string;end:string;}// シートの情報を取得asyncfunctiongetSpreadSheetData(id:string,params:SpreadSheetRange[]):Promise<sheets_v4.Schema$Spreadsheet>{constauth=awaitgoogle.auth.getClient({scopes:["https://www.googleapis.com/auth/spreadsheets"]});constsheets=google.sheets("v4");constspreadsheetId=id;constranges:string[]=[];for(constrofparams){ranges.push(`${r.sheet}!${r.start}:${r.end}`);}constres=awaitsheets.spreadsheets.get({auth,spreadsheetId,ranges,includeGridData:true});returnres.data;}// 更新日取得asyncfunctiongetModifiedTime(fileId:string):Promise<string>{constauth=awaitgoogle.auth.getClient({scopes:["https://www.googleapis.com/auth/drive"]});constd=google.drive("v3");constres=awaitd.files.get({auth,fileId,fields:"modifiedTime"});returnres.data.modifiedTimeasstring;}// firestore に保存asyncfunctionstore(modifiedTime:string,data:sheets_v4.Schema$Spreadsheet):Promise<void>{if(data.sheets){constf=admin.firestore().collection("spreadsheet").doc(data.spreadsheetIdasstring);constss=awaitf.get();constssdata=ss.data();if(ssdata&&ssdata.lastUpdate===modifiedTime){return;}constd=ssdata?ssdata:{};d.lastUpdate=modifiedTime;constitems:string[]=[];for(constsofdata.sheets){if(!s.data)continue;for(constcolofs.data){if(!col.rowData)continue;for(constrowofcol.rowData){if(row.values&&row.values.length!==0){items.push(row.values[0].formattedValueasstring);}}}}d.items=items;awaitf.set(d);}}

Google Spread Sheet を簡易 CMS として使う

ウェブサイトを作っていると、「この部分は更新できるようにしたい!」と言われる事が良くあります。
大がかりなものであればCMSを入れて対応するのが良いと思うのですが、「お知らせだけ」とか、「トップページの文言だけ」が対象だった場合は、そのためだけにCMSを入れるのはためらいますし、かといって自分でCRADのUIを作るのは面倒です。

僕はそういった場合にGoogle Spread Sheetを使用して簡易的なCMSとしてしまう事が多いです。
以前はこのライブラリを介して、リクエストの度に直接 Google Sheets APIを叩いていました。

https://www.npmjs.com/package/google-spreadsheet

懸念

ただ、 Google Sheets API自体に下記の制限があります。

https://developers.google.com/sheets/api/limits

  • プロジェクト毎に 500リクエスト / 100秒
  • ユーザー毎に 100リクエスト / 100秒

小規模の案件であれば問題にならなそうですが、少し規模が大きくなると不安になってくる数字です。
何より、webという不特定多数のユーザーからアクセスされる環境の中、制限を下回る確実な補償がない以上「場合によっては制限を超えるかもしれない」という懸念を抱えながら運用しなければいけません。

Google Spread Sheet + Cloud Functions + Firestore

そこで、今回

  • Google Spread Sheetのデータを
  • Cloud Functionsで1分毎に更新チェックして
  • 変更があれば Firestoreに保存する

という方法を採ってみたので、記事として共有しようと思います。

サービスアカウントを取得する / APIを有効にする

こちらの記事を参考に、サービスアカウントの情報を取得します。

https://qiita.com/m_norii/items/63cc8f5eb91a3fc5505f

今回は更新日時を取得するために Google Drive APIも使用するので、 Google Sheets APIと合わせて有効にしておいてください。

必要な機能を実装する

ここから実際にコードを書いていきますが、順を追って説明していこうと思います。
また、全編を通して TypeScriptで記述しています。

Firebase を初期化

この辺りはやり方も流儀も色々なので、ここでは詳しく触れません。
公式ドキュメントの通りにするのが一番シンプルかなと思います。

https://firebase.google.com/docs/functions/get-started?hl=ja

依存パッケージのインストール

初期化が終わったら、今回必要な依存パッケージを入れます。

npm i -S googleapis firebase-admin

スプレッドシートの情報を取得

importは省略

// 引数用の定義exportinterfaceSpreadSheetRange{sheet:string;start:string;end:string;}// シートの情報を取得asyncfunctiongetSpreadSheetData(id:string,params:SpreadSheetRange[]):Promise<sheets_v4.Schema$Spreadsheet>{constauth=awaitgoogle.auth.getClient({scopes:["https://www.googleapis.com/auth/spreadsheets"]});constsheets=google.sheets("v4");constspreadsheetId=id;constranges:string[]=[];for(constrofparams){ranges.push(`${r.sheet}!${r.start}:${r.end}`);}constres=awaitsheets.spreadsheets.get({auth,spreadsheetId,ranges,includeGridData:true});returnres.data;}

getSpreadSheetDataが本体で、 SpreadSheetRangeはその引数のための定義になります。
sheetで指定したシートの内容を、 startで指定したセルから、 endで指定したセルまで読み込みます。また、引数を配列で取っている通り、複数のシートから同時にデータの取得ができます。

基本的には素直に Google Sheets APIを呼び出しており、戻り値もAPIが返したデータそのままです。
呼び出しは以下のようになります。

constsheetData=awaitgetSpreadSheetData("対象のスプレッドシートID",[{sheet:"シート",start:"A1",end:"B1"}]);

返り値は Schema$Spreadsheetになります。
定義は以下にソースがありますが、VSCode等でインテリセンスを見た方が早いと思います。

https://github.com/googleapis/google-api-nodejs-client/blob/master/src/apis/sheets/v4.ts#L3972

更新日を取得

importは省略

// 更新日取得asyncfunctiongetModifiedTime(fileId:string):Promise<string>{constauth=awaitgoogle.auth.getClient({scopes:["https://www.googleapis.com/auth/drive"]});constd=google.drive("v3");constres=awaitd.files.get({auth,fileId,fields:"modifiedTime"});returnres.data.modifiedTimeasstring;}

Google Sheets APIだけでスプレッドシート自体の最終更新日が分かれば良かったのですが、僕にはその方法が見つけられなかったので、Drive APIを使用してドキュメント自体の更新日を取得しています。
ここで、fieldsになにも指定しないと最低限の情報しか取得できなかったので、ご注意ください。

使い方は、説明するまでもありませんが以下の通りです。

constmodifiedTime=awaitgetModifiedTime("対象のスプレッドシートID");

Firestore に保存

importは省略

// firestore に保存asyncfunctionstore(modifiedTime:string,data:sheets_v4.Schema$Spreadsheet):Promise<void>{if(data.sheets){constf=admin.firestore().collection("spreadsheet").doc(data.spreadsheetIdasstring);constss=awaitf.get();constssdata=ss.data();if(ssdata&&ssdata.lastUpdate===modifiedTime){return;}constd=ssdata?ssdata:{};d.lastUpdate=modifiedTime;constitems:string[]=[];for(constsofdata.sheets){if(!s.data)continue;for(constcolofs.data){if(!col.rowData)continue;for(constrowofcol.rowData){if(row.values&&row.values.length!==0){items.push(row.values[0].formattedValueasstring);}}}}d.items=items;awaitf.set(d);}}

最初に更新日判定をしていて、更新日が同じだった場合は何も処理をしないようにしています。
それ以降の部分は、スプレッドシートがどういったフォーマットで書かれているかの仕様次第になるので、ケースによって最適解は大きく異なってくると思います。
今回は指定の列にあるテキストだけを拾ってくれば良い仕様だったので、スプレッドシートIDを用いてドキュメントを作り、その中の Arrayにデータを入れるような作りになっています。

前に出てきた2つの関数と合わせて使う事しか想定していないので、呼び出し方は割愛します。

Cloud Functions に登録する

前章で書いた機能を合わせ、以下のようなコードで今回の目的が達成できます。

constsheetId="スプレッドシートのID"constranges=[{sheet:"シート",start:"A1",end:"B1"}];constsheetData=awaitgetSpreadSheetData(sheetId,ranges);constmodifiedTime=awaitgetModifiedTime(sheetId);awaitstore(modifiedTime,sheetData);

これを、Cloud Functions に登録できる形にすると

exportconstcheckSpreadSheet=functions.region("asia-northeast2").pubsub.schedule("every 1 minutes").onRun(async()=>{constsheetId="スプレッドシートのID"constranges=[{sheet:"シート",start:"A1",end:"B1"}];constsheetData=awaitgetSpreadSheetData(sheetId,ranges);constmodifiedTime=awaitgetModifiedTime(sheetId);awaitstore(modifiedTime,sheetData);});

このような形になります。
ここまでの全ての定義を1つのファイルに書き、importを足すと冒頭のコードになります。regionscheduleその他定数は要件に合わせて変更して下さい。

終わりに

今後も良く使いそうだったので、自分用のメモも兼ねて今回まとめてみました。
ざっとググってみても同じような考えの方はたくさんいらっしゃるので、これが何番煎じの記事かは分かりませんが、同じような事をしようとしている人の労力を少しでも減らせれば幸いです。


Viewing all articles
Browse latest Browse all 8691

Trending Articles