# [Node.js]利用Google Sheets API操作Google試算表 ###### tags: `Node.js` `API` `Express.js` :::warning :bulb: 本站筆記已同步更新到我的[個人網站](https://simplydevs.netlify.app/)囉! 歡迎參觀與閱讀,體驗不同的視覺感受! ::: [TOC] ## :memo: 前言 當我們想要把資料放在Google Sheets上方便閱讀、計算等操作的時候,可以透過串接Google Sheets API製作、寫入資料。這裡介紹如何應用Node.js串接Google Sheets API,廢話不多說就開始實作吧! ## :memo: 取得Google憑證 首先,需要有Google憑證(credentials)才可以使用這個服務。 * **Creat New project:** 先進入[Google Cloud Console](https://console.cloud.google.com/getting-started),登入後先建立一個新的專案:`Select a project` > `New Project` * **Enable API:** 選擇側邊欄`APIs and services`> 接著點選 `Enabled APIs and Services`,再選擇畫面上方的 `+ ENABLE APIS AND SERVICES` > 在搜尋欄搜尋 google sheets API > 點進Google Sheets API,在Product details的畫面按 `Enable` > 稍後片刻等他開通 > 開通後跳轉到新的頁面,按側邊欄 `Credentials` * **OAuth consent screen:** 進入到Credentials頁面,選擇畫面上方的 `+ CREATE CREDENTIALS` > 選擇 `OAuth client ID`> 點選右邊的藍色按鈕 `configure consent screen` > 選擇User Type,此處我選External > 點選`CREATE` > 填入App information > `Save and Continue` > 接下來Scopes和Test Users都可以暫時不管他,一樣`Save and Continue` > 進入Summary頁面,按`Back to Dashboard` * **Create OAuth client ID:** 這時總算可以來建立 OAuth client ID了,一樣按側邊欄 `Credentials` 進入到Credentials頁面,選擇畫面上方的 `+ CREATE CREDENTIALS` > 選擇 `OAuth client ID` > Application Type選`Web Application`、填寫Name > 填寫 Authorized redirect URIs(例如:`http://localhost:3000/oauth2callback`)> 點選`CREATE` * **Download credentials:** 建立成功後,會跳轉到`OAuth client created`的畫面,上面有`Clinet ID`和`Client Secret`的資訊,下方有`Download JSON`選項,按一下就會下載OAuth Client ID的憑證資訊,這個檔案要保存好,等一下會用到(先重新命名為`credentials.json`)。 ## :memo: 專案實作 ### 1. **前置作業(環境設置)** * Node.js * npm ### 2. **初始化專案並安裝相關套件** ``` mkdir sheetAPI cd sheetAPI npm init -y npm i express @google-cloud/local-auth googleapis --save ``` 專案架構如下: ``` sheetAPI ├─ index.js ├─ package-lock.json └─ package.json ``` ### 3. **建立Express伺服器** 建立一個基本的Express伺服器: ```javascript= //index.js const express = require('express') const app = express() const port = 3000 app.listen(port, () => { console.log(`Express server is running on port ${port}`) }) ``` ### 4. **進行驗證並串接Google Sheets API** [Google官方文件](https://developers.google.com/sheets/api/quickstart/nodejs?hl=zh-tw)提供了一個快速設定的範例,稍微修改就可以: ```javascript=5 //index.js ... const fs = require('fs').promises; const path = require('path'); const process = require('process'); const {authenticate} = require('@google-cloud/local-auth'); const {google} = require('googleapis'); // If modifying these scopes, delete token.json. const SCOPES = ['https://www.googleapis.com/auth/spreadsheets']; // The file token.json stores the user's access and refresh tokens, and is // created automatically when the authorization flow completes for the first // time. const TOKEN_PATH = path.join(process.cwd(), 'token.json'); const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json'); // 檢查TOKEN_PATH中是否存放refresh token async function loadSavedCredentialsIfExist() { try { const content = await fs.readFile(TOKEN_PATH); const credentials = JSON.parse(content); return google.auth.fromJSON(credentials); } catch (err) { return null; } } // 讀取CREDENTIALS_PATH中的Credentials進行驗證,產生refresh token並放進TOKEN_PATH中 async function saveCredentials(client) { const content = await fs.readFile(CREDENTIALS_PATH); const keys = JSON.parse(content); const key = keys.installed || keys.web; const payload = JSON.stringify({ type: 'authorized_user', client_id: key.client_id, client_secret: key.client_secret, refresh_token: client.credentials.refresh_token, }); await fs.writeFile(TOKEN_PATH, payload); } // 驗證:如果有refresh token,就用refresh token驗證製作access token;如果沒有就先產生refresh token async function authorize() { let client = await loadSavedCredentialsIfExist(); if (client) { return client; } client = await authenticate({ scopes: SCOPES, keyfilePath: CREDENTIALS_PATH, }); if (client.credentials) { await saveCredentials(client); } return client; } ``` :pushpin: Points: - `SCOPES`是根據我們所要使用的功能選擇授權的範圍,Google Sheets API提供的SCOPES可以看[這裡](https://developers.google.com/sheets/api/scopes?hl=zh-tw)。 - `CREDENTIALS_PATH`是憑證資料的路徑,也就是前面設定OAuth Clinet ID下載的那個JSON檔案,剛才已經重新命名為`credentials.json`,這裡我直接放在根目錄。 - `TOKEN_PATH`是用來放`token.json`的路徑,`token.json`每次進行驗證取得refresh token會自動產生;如果更改SCOPE,記得要把舊的`token.json`刪除。 - 目前專案架構如下: ``` sheetAPI ├─ credentials.json ├─ index.js ├─ package-lock.json └─ package.json ``` ### 5. **操作Sheet檔案** 驗證完成後,就可以根據選擇的SCOPE對Sheet進行操作,這裡簡單示範幾個功能: #### **建立新的試算表** * [`spreadsheets.create`](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/create):官方提供的[範例](https://developers.google.com/sheets/api/guides/create) 在`index.js`中加入以下程式碼: ```javascript=62 // index.js ... async function createSheet (auth, title) { try { const service = google.sheets({ version: 'v4', auth }) const resource = { properties: { title } } const spreadsheet = await service.spreadsheets.create({ resource, fields: 'spreadsheetId' }) console.log(`Spreadsheet ID: ${spreadsheet.data.spreadsheetId}`) return spreadsheet.data.spreadsheetId } catch (error) { console.log(error) } } ``` :pushpin: Points: * `title`是sheet的標題,也可以加入其他內容,參考文件:https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#SpreadsheetProperties * `spreadsheetId`是每一個sheet獨有的ID,觀察sheet的網址,粗體字的部分就是sheetId: https://docs.google.com/spreadsheets/d/**<spreadsheet Id>**/edit?pli=1#gid=0 在`createSheet`函式下方再加入一個函式,測試看看: ```javascript=84 // index.js ... async function run () { try { const auth = await authorize() const sheetId = await createSheet(auth, 'test') } catch (err) { console.error(err) } } run() ``` 接著讓伺服器運作,成功的話,會在終端機上面看到: `Spreadsheet ID: <Your spreadsheet Id>` 同時根目錄也新增了一個`token.json`檔案,也可以到web介面看看,網址:https://docs.google.com/spreadsheets/d/**<Your spreadsheet Id>**/edit?pli=1#gid=0 應該可以看到一個標題是test的試算表建立成功,也可以進入[個人的Google Sheets頁面](https://docs.google.com/spreadsheets/u/0/),查看是否新增了一個新的試算表test: :bell: <span style="color: crimson;">需用取得Google憑證時的Google account去看,目前其他人還沒有權限</span> <br> ![](https://hackmd.io/_uploads/rJbMlkRa2.png) #### **在試算表新增資料** * [`spreadsheets.values.update`](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/update?hl=zh-tw):官方提供的[範例](https://developers.google.com/sheets/api/guides/values?hl=zh-tw) 在`index.js`中加入以下程式碼: ```javascript=62 // index.js ... async function updateValues (auth, spreadsheetId) { const service = google.sheets({ version: 'v4', auth }) const values = [ ['Item', 'Cost', 'Stocked', 'Ship Date'], ['Wheel', '$20.50', '4', '3/1/2016'], ['Door', '$15', '2', '3/15/2016'], ['Engine', '$100', '1', '3/20/2016'], ['Totals', '=SUM(B2:B4)', '=SUM(C2:C4)', '=MAX(D2:D4)'] ] const resource = { values } const result = await service.spreadsheets.values.update({ spreadsheetId, range: 'Sheet1!A1:D', valueInputOption: 'USER_ENTERED', resource }) console.log('%d cells updated.', result.data.updatedCells) return result } ``` :pushpin: Points: * `values`:試算表範圍內的資料,也就是要操作的資料,values是必填的參數,型態是陣列(array)。其他參數參考[文件](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values?hl=zh-tw#resource-valuerange)。 * `valueInputOption`是指系統如何解讀輸入資料。例如這裡使用`USER_ENTERED`,則系統會解析`Total`那列的`'=SUM(B2:B4)'`,加總B2:B4的值,就和在Excel資料表裡面操作一樣的方法。其他選項參考[文件](https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption)。 * `range`是指資料的範圍,可以指定多個儲存格 (例如 A1:D5) 或單一儲存格 (例如 A1)。詳細用法參考[文件](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values?hl=zh-tw#resource-valuerange)。或是[範例](https://developers.google.com/sheets/api/guides/values?hl=zh-tw)中也有提到: ![](https://hackmd.io/_uploads/SJhygyC6n.png) 在`run()`中加入: ```javascript=84 // index.js ... async function run () { try { ... await updateValues(auth, sheetId) } catch (err) { console.error(err) } } run() ``` 同樣讓伺服器運作,成功的話,會在終端機上面看到: ``` Spreadsheet ID: <Your spreadsheet Id> 20 cells updated. ``` 到web介面看看,這時可以看到test試算表中有資料了! ![](https://hackmd.io/_uploads/Sk4qS2663.png) #### **讀取試算表的資料** * [`spreadsheets.values.get`](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get?hl=zh-tw):官方提供的[範例](https://developers.google.com/sheets/api/guides/values?hl=zh-tw) 一樣根據範例稍微改寫,在`index.js`中加入以下程式碼: ```javascript=86 // index.js ... async function getValues (auth, spreadsheetId) { try { const service = google.sheets({ version: 'v4', auth }) const result = await service.spreadsheets.values.get({ spreadsheetId, range: 'Sheet1!A1:D2' }) const numRows = result.data.values ? result.data.values.length : 0 console.log(`${numRows} rows retrieved.`) return result.data.values } catch (error) { console.log(error) } } ``` 在`run()`中加入: ```javascript=84 // index.js ... async function run () { try { ... const result = await getValues(auth, sheetId) return console.log(result) } catch (err) { console.error(err) } } run() ``` 同樣讓伺服器運作,成功的話,會在終端機上面看到: ``` Spreadsheet ID: <Your spreadsheet Id> 20 cells updated. 2 rows retrieved. [ [ 'Item', 'Cost', 'Stocked', 'Ship Date' ], [ 'Wheel', '$20.50', '4', '3/1/2016' ] ] ``` 成功讀取了A1:D2這個範圍的資料,也就是Web介面上: ![](https://hackmd.io/_uploads/rkN8xkCT3.png) ## 小結 以上簡單介紹了Google Sheets API的一些用法,還有很多其他的功能都可以參考[API文件](https://developers.google.com/sheets/api/guides/concepts?hl=zh-tw)。前面介紹了使用OAuth Client ID進行驗證的方法,另外也可以使用`Service Account`進行驗證,驗證方式可以參考[這篇文章](https://www.section.io/engineering-education/google-sheets-api-in-nodejs/);使用`Service Account`驗證須注意權限,如要用自己的gmail帳號登入需要開啟權限,除了前面這篇文章中使用的方式(先用Gmail帳號開新的Sheet再分享權限給`Service Account`)之外,另外也可以使用`Google Drive API`給予權限,詳情可以參考[這篇文章](https://www.daimto.com/google-drive-api-with-a-service-account/#Create_Google_Drive_API_V3_service_object),以及`Google Drive API`文件[`permissions.create`](https://developers.google.com/drive/api/reference/rest/v3/permissions/create)或[`permissions.update`](https://developers.google.com/drive/api/reference/rest/v3/permissions/update),當然直接[使用Google Drive API建立Sheet](https://developers.google.com/drive/api/guides/create-file)也是可以的。 --- ## 參考資料 * [Google Sheets API文件](https://developers.google.com/sheets/api/guides/concepts?hl=zh-tw) * [使用 Node.js 操作 Google Sheets API 讀寫試算表資料庫](https://www.wfublog.com/2023/04/nodejs-google-sheets-api-read-write.html) * [How to use Google sheet as your database with Node.js](https://medium.com/@sakkeerhussainp/google-sheet-as-your-database-for-node-js-backend-a79fc5a6edd9) ::: success :crescent_moon:  本站內容僅為個人學習記錄,如有錯誤歡迎留言告知、交流討論! :::