Back
PQCHub

串接中央氣象署

Node.js 與氣象數據:從 API 串接到你的第一個資料集


1. 核心工具:認識 axios

在我們開始抓取資料之前,我們需要一個工具來幫我們「發送網路請求」。

什麼是 axios axios 是一個非常受歡迎的 JavaScript 套件(庫),專門用來處理 HTTP 請求。您可以把它想像成 Node.js 的專屬「瀏覽器」,但它不是去抓網頁 (HTML),而是專門去跟 API 溝通並取回「純資料」(通常是 JSON)。

為什麼不用 Node.js 原生的 http 模組? Node.js 內建了 http 模組,但它使用起來非常繁瑣(需要手動處理資料流、chunk 等)。axios 幫我們把這一切都打包好了,讓我們能用更簡潔的語法完成任務。

axios 的基本使用 (Async/Await) axios 是基於 Promise 的。在 ES6+ 時代,我們最常用 async/await 語法來處理它,這讓非同步程式碼看起來像同步一樣好讀。

安裝:

npm install axios

基本範例:

import axios from 'axios';

async function fetchData() {
  try {
    // 1. 使用 await 等待 axios.get() 完成
    // 我們請求一個公開的測試 API
    const response = await axios.get('https://api.example.com/data');

    // 2. 資料會被 axios 自動打包在 .data 屬性中
    console.log('成功取得資料:', response.data);

  } catch (error) {
    // 3. 如果網路出錯或 API 回傳錯誤狀態 (404, 500...)
    console.error('抓取失敗:', error.message);
  }
}

fetchData();

2. 觀念建立:什麼是 API?fs 模組、JSON/CSV 處理

  • 網站 (Website) vs. API:

    • 網站 (HTML): 是設計給「人」看的,充滿了視覺設計、按鈕和圖片。
    • API (Application Programming Interface): 是設計給「程式」看的。它像是一個「資料的點餐窗口」,我們只要用固定的格式去「點餐」(發送請求),它就會回傳純粹的「資料」。
  • JSON (JavaScript Object Notation):

    • 一種輕量級的資料交換格式,也是 API 最常回傳的格式。
    • 它的語法和 JavaScript 的「物件」幾乎一模一樣,所以 Node.js 處理起來非常方便。

3. 準備工作:建立專案與設定 API 金鑰

步驟一:取得中央氣象署 API 金鑰 (Authorization Key)

這是最重要的第一步。API 需要「認證」,防止被濫用。

  1. 請學生至 中央氣象署開放資料平台 註冊會員。
  2. 登入後,至「會員中心」 > 「取得授權碼」。
  3. 複製這串「授權碼」,這就是我們的 API Key。

步驟二:建立 Node.js 專案 (ES6 模式)

  1. 建立一個新的專案資料夾,例如 cwa-data-project
  2. 打開終端機,進入該資料夾,並初始化專案:
    npm init -y
  3. [關鍵] 為了使用 ES6 的 import 語法,打開 package.json 檔案,在裡面加入一行 "type": "module"
    {
      "name": "cwa-data-project",
      "version": "1.0.0"Node.js 與氣象數據:從 API 串接到你的第一個資料集,
      "description": "",
      "main": "index.js",
      "type": "module",  <-- 加入這一行
      // ... 其他內容 ...
    }
  4. 安裝 axios
    npm install axios

步驟三:安全管理 API Key (原生模組方法)

我們不希望將 API Key 這麼敏感的資訊直接寫在 index.js 主程式中。我們使用一個獨立的 config.js 檔案來存放它,並利用 ES6 模組的 export 功能。

  1. 在專案根目錄下,建立一個新檔案叫做 config.js
  2. config.js 檔案中,貼上你的 API Key 並匯出:
    // config.js
    
    // 將你的金鑰貼在這裡
    export const CWA_API_KEY = '這裡是你的API金鑰';
    
    // (未來如果還有其他設定,也可以放這裡)

4. 實戰編碼:擷取「臺北市 36 小時天氣預報」

我們的目標:抓取「一般天氣預報-未來36小時天氣預報」(資料代碼 F-C0032-001)中「臺北市」的資料。

在專案中建立 index.js 檔案:

// index.js

// 1. 載入 ES6 模組
import axios from 'axios';
// [修改] 從我們自訂的 config.js 載入 API Key
import { CWA_API_KEY } from './config.js'; 

// 2. 變數設定
const API_KEY = CWA_API_KEY; // API Key
const DATA_ID = 'F-C0032-001'; // 資料代碼:36小時天氣預報
const LOCATION = '臺北市'; // 目標地點

/**
 * 核心函式:使用 async/await 取得天氣資料
 */
async function getWeatherData() {
  console.log(`🚀 正在擷取 [${LOCATION}] 的天氣資料...`);

  // 組合 API 網址
  const url = `https://opendata.cwa.gov.tw/api/v1/rest/datastore/${DATA_ID}`;

  try {
    // 3. 使用 axios 發送 GET 請求
    const response = await axios.get(url, {
      headers: {
        'Authorization': API_KEY, // 4. 身份驗證
      },
      params: {
        'format': 'JSON', // 5. 指定回傳格式
        'locationName': LOCATION, // 6. 指定查詢地點
      },
    });

    // 7. 檢查 API 是否成功回傳
    if (response.data.success === 'true') {
      console.log('✅ 資料擷取成功!');
      // 8. 呼叫資料處理函式
      processForecast(response.data.records);
    } else {
      console.error('❌ API 請求失敗:', response.data.message);
    }
  } catch (error) {
    // 網路層級的錯誤 (例如:超時、404)
    console.error('❌ 擷取資料時發生錯誤:', error.message);
  }
}

/**
 * 協助函式:處理並印出預報資料
 * (這部分是資料科學的「資料清理」)
 */
function processForecast(records) {
  const location = records.location[0];
  console.log(`\n--- [${location.locationName}] 的未來 36 小時天氣預報 ---`);

  // API 回傳的資料是「依要素分類」的 (天氣、降雨機率、溫度...)
  // 我們需要把它們「依時間重組」

  // 取得所有天氣要素
  const weatherElements = location.weatherElement;
  const wx = weatherElements.find(el => el.elementName === 'Wx'); // 天氣現象
  const pop = weatherElements.find(el => el.elementName === 'PoP'); // 降雨機率
  const minT = weatherElements.find(el => el.elementName === 'MinT'); // 最低溫度
  const maxT = weatherElements.find(el => el.elementName === 'MaxT'); // 最高溫度

  // 預報資料分為 3 個時段 (每 12 小時一筆)
  for (let i = 0; i < wx.time.length; i++) {
    console.log(`\n🗓️ 時段 ${i + 1}:`);
    console.log(`   ${wx.time[i].startTime}`);
    console.log(`   至`);
    console.log(`   ${wx.time[i].endTime}`);
    console.log('---');
    console.log(`   天氣:${wx.time[i].parameter.parameterName}`);
    console.log(`   降雨:${pop.time[i].parameter.parameterName} %`);
    console.log(`   溫度:${minT.time[i].parameter.parameterName}°C - ${maxT.time[i].parameter.parameterName}°C`);
  }
}

// 執行主程式
getWeatherData();

執行程式

在終端機中執行:

node index.js

預期輸出:

🚀 正在擷取 [臺北市] 的天氣資料...
✅ 資料擷取成功!

--- [臺北市] 的未來 36 小時天氣預報 ---

🗓️ 時段 1:
   2025-10-27 18:00:00
   至
   2025-10-28 06:00:00
---
   天氣:多雲
   降雨:20 %
   溫度:22°C - 25°C

... (以此類推)

5. 資料科學應用與延伸

取得「乾淨」的資料只是第一步。資料科學的價值在於「應用」。

應用一:將資料儲存為 CSV 檔案 (使用原生 fs 模組)

學會將資料轉換成表格格式 (CSV),以便後續用 Excel, Python (Pandas) 或 R 進行分析。

(修改 index.js,加入 fs 模組)

// index.js
import axios from 'axios';
import { CWA_API_KEY } from './config.js';
import fs from 'fs'; // [新增] 載入 Node.js 內建的檔案系統模組

// ... (getWeatherData 函式保持不變) ...

/**
 * 協助函式:處理預報資料並存檔
 */
function processForecast(records) {
  // ... (同上,找到 location 和 weatherElements) ...
  const locationName = records.location[0].locationName;
  const wx = records.location[0].weatherElement.find(el => el.elementName === 'Wx');
  const pop = records.location[0].weatherElement.find(el => el.elementName === 'PoP');
  const minT = records.location[0].weatherElement.find(el => el.elementName === 'MinT');
  const maxT = records.location[0].weatherElement.find(el => el.elementName === 'MaxT');

  let csvContent = '地點,開始時間,結束時間,天氣現象,降雨機率(%),最低溫(C),最高溫(C)\n'; // CSV 標頭

  for (let i = 0; i < wx.time.length; i++) {
    // 組合 CSV 的每一列
    const row = [
      locationName,
      wx.time[i].startTime,
      wx.time[i].endTime,
      wx.time[i].parameter.parameterName,
      pop.time[i].parameter.parameterName,
      minT.time[i].parameter.parameterName,
      maxT.time[i].parameter.parameterName,
    ].join(','); // 用逗號分隔
    csvContent += row + '\n';
  }

  // [新增] 寫入檔案
  try {
    fs.writeFileSync('forecast.csv', csvContent, 'utf8');
    console.log('✅ 預報資料已儲存為 forecast.csv');
  } catch (err) {
    console.error('❌ 儲存 CSV 檔案時發生錯誤:', err.message);
  }
}

// ... (執行主程式 getWeatherData()) ...

應用二:簡易資料分析

  • 自動天氣提醒器: 檢查未來 36 小時的 PoP (降雨機率),如果任何一個時段的 parseInt(pop.time[i].parameter.parameterName) > 50,就在主控台印出「提醒:未來 36 小時內可能下雨,記得帶傘!」。
  • 計算平均溫差: 找出 MaxT 的最大值和 MinT 的最小值,計算溫差。
// index.js

// 1. 載入 ES6 模組
import axios from 'axios';
import { CWA_API_KEY } from './config.js'; // 從 config.js 載入 API Key
import fs from 'fs'; // 載入 Node.js 內建的檔案系統模組

// 2. 變數設定
const API_KEY = CWA_API_KEY; // API Key
const DATA_ID = 'F-C0032-001'; // 資料代碼:36小時天氣預報
const LOCATION = '臺北市'; // 目標地點

/**
 * 核心函式:使用 async/await 取得天氣資料
 */
async function getWeatherData() {
  console.log(`🚀 正在擷取 [${LOCATION}] 的天氣資料...`);

  const url = `https://opendata.cwa.gov.tw/api/v1/rest/datastore/${DATA_ID}`;

  try {
    const response = await axios.get(url, {
      headers: { 'Authorization': API_KEY },
      params: {
        'format': 'JSON',
        'locationName': LOCATION,
      },
    });

    if (response.data.success === 'true') {
      console.log('✅ 資料擷取成功!');
      // 呼叫 *更新後的* 資料處理函式
      processForecast(response.data.records);
    } else {
      console.error('❌ API 請求失敗:', response.data.message);
    }
  } catch (error) {
    console.error('❌ 擷取資料時發生錯誤:', error.message);
  }
}

/**
 * 協助函式:處理預報資料、存檔並執行簡易分析
 * (*** 這是更新後的函式 ***)
 */
function processForecast(records) {
  const location = records.location[0];
  const locationName = location.locationName;
  console.log(`\n--- [${locationName}] 的未來 36 小時天氣預報 ---`);

  // 1. 取得所有天氣要素
  const weatherElements = location.weatherElement;
  const wx = weatherElements.find(el => el.elementName === 'Wx');
  const pop = weatherElements.find(el => el.elementName === 'PoP');
  const minT = weatherElements.find(el => el.elementName === 'MinT');
  const maxT = weatherElements.find(el => el.elementName === 'MaxT');

  // 2. 準備 CSV 內容
  let csvContent = '地點,開始時間,結束時間,天氣現象,降雨機率(%),最低溫(C),最高溫(C)\n';

  // 3. 準備分析用的變數
  let willRain = false; // 降雨提醒旗標
  let overallMinT = Infinity; // 36小時內的最低溫 (初始設為無限大)
  let overallMaxT = -Infinity; // 36小時內的最高溫 (初始設為無限小)

  // 4. 迭代處理 3 個時段
  for (let i = 0; i < wx.time.length; i++) {
    // --- (A) 取得時段資料 ---
    const startTime = wx.time[i].startTime;
    const endTime = wx.time[i].endTime;
    const weatherDesc = wx.time[i].parameter.parameterName;
    const rainProbStr = pop.time[i串接中央氣象署].parameter.parameterName;
    const minTempStr = minT.time[i].parameter.parameterName;
    const maxTempStr = maxT.time[i].parameter.parameterName;

    // 印出時段預報 (原功能)
    console.log(`\n🗓️ 時段 ${i + 1}: ${startTime} 至 ${endTime}`);
    console.log(`   天氣:${weatherDesc}`);
    console.log(`   降雨:${rainProbStr} %`);
    console.log(`   溫度:${minTempStr}°C - ${maxTempStr}°C`);

    // --- (B) 組合 CSV (原功能) ---
    const row = [
      locationName,
      startTime,
      endTime,
      weatherDesc,
      rainProbStr,
      minTempStr,
      maxTempStr,
    ].join(',');
    csvContent += row + '\n';

    // --- (C) 【新功能】執行資料分析 ---
    
    // C-1: 檢查降雨機率 (將字串轉為數字)
    const rainProbNum = parseInt(rainProbStr, 10);
    if (rainProbNum > 50) {
      willRain = true;
    }

    // C-2: 檢查高低溫 (將字串轉為數字)
    const minTempNum = parseInt(minTempStr, 10);
    const maxTempNum = parseInt(maxTempStr, 10);

    // 更新36小時內的極端溫度
    overallMinT = Math.min(overallMinT, minTempNum);
    overallMaxT = Math.max(overallMaxT, maxTempNum);
  }

  // 5. 寫入 CSV 檔案 (原功能)
  try {
    fs.writeFileSync('forecast.csv', csvContent, 'utf8');
    console.log('\n✅ 預報資料已儲存為 forecast.csv');
  } catch (err) {
    console.error('❌ 儲存 CSV 檔案時發生錯誤:', err.message);
  }

  // 6. 【新功能】印出分析結果
  console.log('\n--- 簡易資料分析結果 ---');
  
  // 6-1: 降雨提醒
  if (willRain) {
    console.log('☔️ 天氣提醒:未來 36 小時內有時段降雨機率超過 50%,記得帶傘!');
  } else {
    console.log('☀️ 天氣提醒:未來 36 小時降雨機率皆低於 50%。');
  }

  // 6-2: 溫差計算
  const tempDifference = overallMaxT - overallMinT;
  console.log(`🌡️ 溫差分析:未來 36 小時最大溫差為 ${tempDifference}°C (最高 ${overallMaxT}°C, 最低 ${overallMinT}°C)`);
}

// 執行主程式
getWeatherData();