iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0

🥳終於可以使用Google Sheets API啦!
相比設置GCP,使用API超 ‧ 簡 ‧ 單~

前言

昨天(Day27)已經取得了access token,今晚終於可以開始使用Google Sheets API。

正文

實作目標

Deno執行TypeScript腳本,以Google Sheets API抓取指定Google Sheet的資料,並以JSON格式存放於專案目錄內,供Vue Component使用。

專案架構

# Runtime Environments
├── Node.js (dev/build)
│   └── pnpm-managed toolchain
│       ├── Vite
│       ├── Vue
│       ├── TypeScript
│       └── Sass
└── Deno (prebuild)
    ├── get-gcp-token.ts    (Day27)
    └── fetch-sheet-data.ts (Day28)

實作步驟

⓪查閱文件

既然是要抓取資料,直覺上就是找get的方法,於是在官方文件查到了spreadsheets.values.get這個方法。[^1]

它的API endpoint是:
https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetId}/values/{range}

①撰寫文件

首先,稍微了解它會回傳什麼,並寫TypeScript文件:[^2]

interface GoogleSheetsResponse {
  range: string;
  majorDimension: string;
  values?: string[][];
}

在這裡,當我使用spreadsheets.values.get時,必定會回傳range,但其它方法則不一定會有range

同樣的,spreadsheets.values.get預設FORMATTED_VALUE,所以values會被限縮為string;但如果額外要求UNFORMATTED_VALUEvalues還有可能是number、boolean或null。

總之,由於我不是真的很熟悉TypeScript,為了個人實作方便,這裡把interface限縮於我這次實作目標的型別。

上述是成功時會回傳的;由於原生JavaScript直至2025年的Error還是很陽春,這裡還要自己準備一個HTTP Error:

class GoogleSheetsError extends Error {
  constructor(
    // parent parameters
    message: string,
    // subclass properties
    public readonly status?: number,
    public readonly statusText?: string
  ) {
    super(message);
    this.name = "GoogleSheetsError";
  }
}

雖然印象中有現成的社群套件提供各種各樣的Error,由於這裡我只需要一個簡單的,並且我個人很討厭追加套件,所以僅讓GenAI幫我生成HTTP Error後再微調。

②封裝Fetcher

《實作回顧:doGet(下)》,事先於GAS Web App環境設計好資料處理邏輯,只需要call一次就可以拿到所需資料。

而這次實作,則改在本地處理資料,所以必須多次call Goolge Sheets API。

為了方便我重複fetch,讀完官方文件,確定會用到的參數後,寫一個helper function:

async function fetchSpecificSheetData(
  accessToken: string,
  spreadsheetId: string,
  range: string,
  majorDimension: "ROWS" | "COLUMNS"
): Promise<GoogleSheetsResponse> {
  const apiUrl: string = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}?majorDimension=${majorDimension}`;

  try {
    const response: Response = await fetch(apiUrl, {
      method: "GET",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
    });

    if (!response.ok) {
      throw new GoogleSheetsError(
        `Google Sheets API error: ${response.status} ${response.statusText}`,
        response.status,
        response.statusText
      );
    }

    const data: GoogleSheetsResponse = await response.json();
    return data;
  } catch (error) {
    if (error instanceof GoogleSheetsError) {
      throw error;
    }
    if (error instanceof Error) {
      throw new GoogleSheetsError(
        `Failed to fetch sheet data: ${error.message}`
      );
    }
    throw new GoogleSheetsError(
      "Unknown error occurred while fetching sheet data"
    );
  }
}

③使用Fetcher

這樣一來,我在main function填入參數,就可以多次使用spreadsheets.values.get

const toolstackLinkData: GoogleSheetsResponse = await fetchSpecificSheetData(
  accessToken,
  SPREADSHEET_ID,
  "party-tools",
  "COLUMNS"
);

腳本示例

以下是我當時練習的腳本的pseudocode:

=== Setup ===
- Define constants:
    SPREADSHEET_ID = "<Google Sheet ID>"
    OUTPUT_DIR = "./src/contentData/fetched/"
- Load your sacred GCP token from "./.env/gcp-token.json" 🔑

=== Helper Functions ===
createOutputPath(fileName):
  → build "<OUTPUT_DIR>/<fileName>.json" like a civilized dev

fetchSheetData(token, id, range, dim):
  → politely ask Google Sheets API for data
  → return JSON (if Google says yes)

transformDataToJson(columns, id):
  → turn ['name', 'Acme'] into { name: 'Acme' }
  → keep arrays if there’s a party of values 🎉
  → add { id } on top if given
  → return JSON object with good vibes

=== Main Flow ===
main():
  token = getAccessToken()
  key = fetchSheetData(token, SPREADSHEET_ID, "query!A2", ROWS)

  if key:
    partyData = fetchSheetData(token, SPREADSHEET_ID, `party-${key}`, COLUMNS)
    save(transformDataToJson(partyData, key), "party.json")

  toolstackData = fetchSheetData(token, SPREADSHEET_ID, "toolstack", ROWS)
  save(transformDataToJson(toolstackData), "toolstackLink.json")

=== Entry Point ===
if run directly:
  main()

其它實作方案

如果不想像我一樣自己封裝fetcher,其實官方有提供諸如PythonNode.js等等封裝好的套件,比我自己寫的腳本代為處理了更多細節。

我自己沒有選擇用googleapis套件,主要是因為我當時覺得我所應用的場景很單純,用不到太複雜的機制,其次是我想了解一下基本的處理過程。使用套件長期來看,例如未來如果Google Sheets API大改版,維護上可能比較方便。

後話

相比doGet,我必須在不同專案目錄維護doGetfetcher兩組腳本,使用Google Sheets API則可以集中在同一個專案目錄管理所有邏輯,而且不用重複部署GAS Web App,所以顯然Google Sheets API比GAS HTTP endpoint更符合我的需求。

對我個人而言是小小的里程碑,從此以後,我就可以更有效率地sync資料。工作上會非常大量處理試算表,比起firebase,就我自己的工作場景,Google Sheets API已經堪用,且能很好地跟團隊內一般使用者所慣用的工具配合。

最後分享一個,我自己常常浪費時間、把腳本的log訊息排版成我覺得爽的格式😆:

Saved beach token to ./.env/deno-coconut.json  
✓ Ocean access granted 🌴🌊  

Fetching community id...  
✓ Community id retrieved: "deno-beach-club" 🦕🏖️  

Fetching full data from deno-beach-club sheet...  
✓ Community sheet data fetched 🍹🌺  

Transforming data into JSON object...  
✓ Transformed JSON object:
{
  "id": "deno-beach-club",
  "name": "Deno 海灘俱樂部",
  "projects": [
    "sunset-jam-session",
    "yakitori-drum-circle",
    "tempura-surf-bot"
  ],
  "toolstackLink": "vertical_tools_tropical"
}
✓ Community data saved to ./src/contentData/fetched/community.json 🏝️  

Fetching data from TOOLSTACK_LINK sheet...  
✓ TOOLSTACK_LINK sheet data fetched 🌈  

Transforming data into JSON object...  
✓ Transformed JSON object:
{
  "general_tools": "https://beach.party/gen",
  "dev_tools": "https://beach.party/dev",
  "vertical_tools_tropical": "https://beach.party/tropical"
}
✓ ToolstackLink data saved to ./src/contentData/fetched/toolstackLink.json 🏄‍♂️  

Annotations

[^1]:最近跟在菲律賓主修CS的大學生閒聊到,他經歷過自己讀文件和後GenAI時期,他現在都傾向讓GenAI生成就好。我自己則是,以Deno為例,找到整理好的llms.txt,用穩定性比較高的 NotebookLM查找。希望未來每個官方文件網站都可以提供內建的、基於LLM的搜尋服務。

[^2]:當時實際上是讓GenAI依官方文件生成interface,我再進一步依我的實作目標做限縮。


上一篇
Day27|Google Sheets API:取得Access Token
下一篇
Day29|我的自學之旅
系列文
我只是不想加班:一名客服人員的GAS自救之路30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言