CH.03 · 財金程式設計互動教材

JSON、Markdown 與金融資料管理

把金融網站從單一計算工具,擴充成資料驅動的資訊系統——用 JSON 保存債券商品資料,透過 fetch + async/await 載入,再用 filter/sort 做出可搜尋、可篩選、可排序的資料庫。

fetch → filter → render

本章學習目標

  • 分辨 JSON、Markdown、CSV 的用途,依情境選擇格式
  • 設計具唯一識別碼的債券資料欄位
  • 用 fetch + async/await 非同步載入 JSON 資料
  • 用 filter、map、sort 完成搜尋、篩選與排序

01為何網站需要資料管理

前兩章的複利計算器與時間價值工具,主要依靠使用者即時輸入資料。但真正的金融知識網站還需要保存並管理大量資料——金融文章、債券商品、考照題目、書籍介紹、網站導覽設定。若每一筆資料都直接寫在 HTML 中,網站內容一多,程式會難以閱讀與維護:檔案過長、格式不一致(利率有時 3.5 有時 0.035)、無法搜尋篩選、同一筆資料散落多頁修改容易漏改、無法建立管理後台。

內容與畫面分離的架構——資料驅動網站將三個部分分開管理:資料內容(JSON/Markdown/CSV)保存資訊;顯示模板(HTML)決定呈現方式;程式邏輯(JavaScript)負責讀取、搜尋、篩選與產生畫面。

① 建立/修改資料JSON / Markdown / CSV
② 讀取與驗證JavaScript 或 Python 讀取,檢查格式
③ 搜尋排序filter/sort 依條件處理
④ 產生畫面轉成前台 HTML 呈現
i

同一份 JSON 可被多個頁面重複使用:首頁顯示最新 3 筆、債券專區顯示全部、搜尋頁顯示符合條件的資料,完全不需要複製貼上。

02JSON 的基本結構

JSON(JavaScript Object Notation)是一種文字格式,用來保存及交換結構化資料:文字格式可用任何編輯器開啟;可表示物件與陣列;適合 JavaScript 及 Python 處理;廣泛用於前後台資料交換;可透過 GitHub 進行版本控制。

// JSON 物件:用大括號 { } 表示單筆資料
{
  "id": "BOND001",
  "name": "甲公司五年期公司債",
  "coupon_rate": 0.035
}

// JSON 陣列:用中括號 [ ] 包含多筆物件
[
  { "id": "BOND001", "name": "甲公司五年期公司債" },
  { "id": "BOND002", "name": "乙銀行七年期金融債" }
]

JSON 常見資料型態

資料型態範例寫法金融應用場景注意事項
文字(string)"AA"名稱、分類、評等、日期必須用雙引號包住
數字(number)100000 / 0.035面額、票面利率、年數不可加千分位逗號
布林值(boolean)true / false是否上架、是否精選小寫,不加引號
陣列(array)["固定收益","ESG"]標籤、分類清單用中括號,逗號分隔
物件(object){"issuer":"甲公司"}多層巢狀資料用大括號
空值(null)null尚無資料的欄位小寫,不加引號

JSON 格式規則與常見錯誤

// ✓ 正確格式
{
  "face_value": 100000,
  "coupon_rate": 0.035,
  "active": true
}

// ✗ 錯誤:鍵沒有雙引號、數字有千分位、布林值大寫、末尾多逗號
{
  face_value: "100,000",   ← 鍵無引號 + 數字寫成文字
  coupon_rate: 0.035,
  active: True,            ← 布林值必須小寫
}                            ← 最後一個逗號是錯誤!

// ✗ 錯誤:JSON 不允許單引號、不允許註解
{ 'name': '甲公司' }       ← 必須用雙引號
{ "name": "甲公司" } // 備註  ← 不允許 // 或 /* */ 註解
!

JSON 格式非常嚴格。一個多餘的逗號或少一個引號,就會導致整份資料無法讀取,並出現 Unexpected token 錯誤。建議使用 VS Code 的 JSON 語法高亮功能,錯誤會以紅色底線標示。

03設計金融商品資料欄位

資料欄位若一開始缺乏規則,後續容易發生:同一欄位使用不同名稱(coupon_rate vs rate vs 票面利率)、百分比有時用 3.5 有時用 0.035、日期格式不一致、某些資料缺少唯一識別碼——這些問題都會讓搜尋與排序失效。

本章債券資料欄位規格

欄位名稱資料型態必填說明與規則
idstring唯一識別碼,如 BOND001,不可重複、不可修改
namestring債券完整名稱
issuerstring發行機構名稱
bond_typestring公司債/金融債/政府公債/綠色債券
face_valuenumber面額,單位為元,必須大於 0
coupon_ratenumber票面利率,使用小數(3.5% → 0.035)
maturity_yearsnumber到期年限,必須大於 0
payment_frequencynumber每年付息次數(1=年付、2=半年付、4=季付)
credit_ratingstring信用評等,如 AAA/AA+/AA/A+/A/BBB
issue_date / maturity_datestring格式 YYYY-MM-DD
tagsarray標籤陣列,如 ["固定收益","ESG"]
featured / activebooleanactive 必填是否精選/是否上架(false = 前台不顯示)
i

唯一識別碼 id 的重要性——每筆資料都應有固定 id(如 BOND001)。名稱可能因更名而修改,但 id 永不改變,用於精確識別、建立詳細頁連結、避免名稱重複、建立收藏紀錄、與後台系統連接。好的 id 格式:類型前綴 + 序號,避免用名稱當 id。

百分比與日期的統一標準

項目建議方式不建議原因
票面利率"coupon_rate": 0.035(小數)3.5 或 "3.5%"小數可直接用 Intl.NumberFormat 轉換;文字格式無法排序
發行日期"issue_date": "2026-01-15""2026年1月15日"YYYY-MM-DD 字串排序即等於日期排序;中文格式無法比較大小

04JSON、Markdown 與 CSV 的差異與使用分工

比較項目JSONMarkdownCSV
✅ 適合債券商品清單、題庫、文章索引、網站設定與導覽、分類與標籤長篇文章正文、課程教材講義、操作說明文件、書籍介紹頁股票每日價格、基金淨值序列、大量交易紀錄、利率時間序列
❌ 不適合長篇文章正文、大量數值計算複雜欄位結構、排序搜尋困難多層巢狀結構、文字中包含逗號
使用時機需要程式搜尋、篩選、排序的結構化資料人工撰寫的長文,轉換為 HTML 呈現表格式、大量、可用試算表開啟的數值資料

三種格式的整合使用——實務上可以同時使用三種格式,各司其職:

// JSON:保存文章索引(標題、分類、Markdown 路徑)
[
  { "id": "ART001", "title": "債券投資入門", "category": "固定收益", "file": "articles/bond-intro.md" }
]

// Markdown:保存完整文章正文(articles/bond-intro.md)
# 債券投資入門
債券是發行機構向投資人借款所發行的有價證券。
## 一、債券的基本要素
1. 面額 2. 票面利率 3. 到期日

// CSV:保存每日價格資料(data/bond-prices.csv)
date,bond_id,price,yield
2026-01-02,BOND001,99.5,0.036
2026-01-03,BOND001,99.8,0.035

05母資料與前台索引資料

概念說明內容範例保存位置
母資料最完整、最具權威性的資料來源書名、ISBN、章節、完整介紹、上下架狀態data/bond-details.json
前台索引精簡版,只含前台顯示所需欄位id、名稱、封面、簡短摘要、排序data/bonds.json(程式產生)
i

母資料應只維護一份。若首頁、列表頁與後台各自保存一份,容易發生書名已修改但首頁仍顯示舊名、商品已下架但搜尋頁仍可找到等同步問題。只修改母資料 → 重新產生索引 → 前台自動更新。

06建立債券 JSON 資料

在 data/ 資料夾中建立 bonds.json,包含 6 筆債券,其中 BOND006 設定 active: false(下架),前台將只顯示 5 筆。

程式區塊 1data/bonds.json(節錄,完整 6 筆)
[
  {
    "id": "BOND001",
    "name": "甲公司五年期公司債",
    "issuer": "甲科技股份有限公司",
    "bond_type": "公司債",
    "face_value": 100000,
    "coupon_rate": 0.035,        ← 3.5%,使用小數
    "maturity_years": 5,
    "payment_frequency": 1,      ← 每年付息一次
    "credit_rating": "AA",
    "issue_date": "2026-01-15",  ← YYYY-MM-DD 格式
    "maturity_date": "2031-01-15",
    "currency": "TWD",
    "tags": ["固定收益", "科技業", "中期債券"],
    "featured": true,            ← 精選商品
    "active": true,              ← 上架中
    "description": "由甲科技發行的五年期普通公司債,每年付息一次。"
  },
  {
    "id": "BOND006",
    "name": "戊公司四年期公司債",
    "...": "...",
    "active": false              ← 下架!前台不顯示
  }
]

三個關鍵欄位說明

欄位前台行為初學者常見錯誤
activetrue / false(布林值)false → 程式篩選排除,前台不顯示"false"(字串)仍會通過 !== null 判斷,導致顯示異常
featuredtrue / false(布林值)true → 卡片顯示「精選」標籤,排序優先與 active 獨立:可以精選但下架,也可上架但不精選
tags["ESG","綠色金融"](陣列)搜尋時合併納入搜尋文字範圍寫成字串而非陣列,程式展開會失敗

07建立債券資料庫 HTML 頁面

新頁面 bond-database.html 包含五個區域:頁首導覽列、篩選卡片(關鍵字/評等/年限/排序)、結果計數列、債券卡片 Grid,以及無結果提示區。

程式區塊 2bond-database.html(完整版)
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="可依關鍵字、信用評等與到期年限篩選的教學用債券資料庫。">
  <title>金融商品資料庫</title>
  <link rel="stylesheet" href="css/bond-database.css">
</head>
<body>

<!-- ① 頁首導覽 -->
<header class="site-header">
  <div class="header-container">
    <a href="index.html" class="site-title">金融程式設計實作</a>
    <nav class="main-nav">
      <a href="index.html">複利工具</a>
      <a href="time-value.html">時間價值</a>
      <a href="bond-database.html" aria-current="page">債券資料庫</a>
    </nav>
  </div>
</header>

<main class="page-container">

  <!-- ② 頁面標題 -->
  <section class="page-introduction">
    <p class="category-label">金融資料管理</p>
    <h1>金融商品資料庫</h1>
    <p>本頁使用 JSON 保存債券資料,並透過 JavaScript 載入、搜尋及篩選。</p>
  </section>

  <!-- ③ 篩選卡片(4欄 Grid) -->
  <section class="filter-card">
    <div class="filter-grid">
      <div class="form-group">
        <label for="keywordInput">關鍵字搜尋</label>
        <input type="search" id="keywordInput" placeholder="輸入名稱、發行機構或標籤">
      </div>
      <div class="form-group">
        <label for="ratingFilter">信用評等</label>
        <select id="ratingFilter">
          <option value="all">全部評等</option>
          <option value="AAA">AAA</option>
          <option value="AA+">AA+</option>
          <option value="AA">AA</option>
          <option value="A+">A+</option>
          <option value="A">A</option>
          <option value="BBB">BBB</option>
        </select>
      </div>
      <div class="form-group">
        <label for="maturityFilter">到期年限</label>
        <select id="maturityFilter">
          <option value="all">全部年限</option>
          <option value="short">3 年以下</option>
          <option value="medium">4 年至 7 年</option>
          <option value="long">8 年以上</option>
        </select>
      </div>
      <div class="form-group">
        <label for="sortOrder">排序方式</label>
        <select id="sortOrder">
          <option value="featured">精選優先</option>
          <option value="rate-high">票面利率由高至低</option>
          <option value="rate-low">票面利率由低至高</option>
          <option value="years-short">年限由短至長</option>
          <option value="years-long">年限由長至短</option>
        </select>
      </div>
    </div>
    <div class="filter-actions">
      <button type="button" id="clearFiltersButton" class="secondary-button">清除篩選</button>
    </div>
  </section>

  <!-- ④ 結果計數列 -->
  <section class="result-summary">
    <p id="resultCount">資料載入中</p>
    <p id="statusMessage" class="status-message" aria-live="polite"></p>
  </section>

  <!-- ⑤ 債券卡片 Grid(由 JS 產生) -->
  <section id="bondList" class="bond-grid" aria-live="polite"></section>

  <!-- ⑥ 無結果提示 -->
  <section id="emptyState" class="empty-state hidden">
    <h2>找不到符合條件的資料</h2>
    <p>請調整關鍵字、信用評等或到期年限。</p>
  </section>

</main>

<footer class="site-footer">本頁資料為教學示例,不代表實際募集、報價或投資建議。</footer>

<script src="js/bond-database.js"></script>
</body>
</html>

08CSS 卡片與篩選介面

程式區塊 3css/bond-database.css(重點節錄)
* { box-sizing: border-box; }

:root {
  --primary-color: #2457a6;
  --card-color: #ffffff;
  --tag-background: #eef4fb;
  --success-background: #eaf7ef;
  --success-text: #1f6b3d;
  --warning-background: #fff5e6;
  --warning-text: #8a5310;
}

/* ── 篩選卡片:4 欄 Grid ── */
.filter-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 18px;
}

/* ── 債券卡片 Grid:桌面 3 欄 ── */
.bond-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 22px;
}

.bond-card { position: relative; padding: 24px; border-radius: 14px;
  background: var(--card-color); box-shadow: 0 6px 18px rgba(0,0,0,0.06); }
.bond-card.featured { border-top: 5px solid var(--primary-color); }

/* ── 債券資料列(dt/dd 標籤−值二欄對齊) ── */
.bond-data-row { display: grid; grid-template-columns: 100px 1fr; gap: 10px;
  padding: 7px 0; border-bottom: 1px solid #edf0f2; }
.bond-data-row dd { margin: 0; text-align: right; font-weight: 700; }

/* ── 標籤 pill 樣式 ── */
.tag { padding: 4px 10px; border-radius: 999px; background: var(--tag-background); font-size: 13px; }
.risk-label { display: inline-block; margin-top: 10px; padding: 4px 10px;
  border-radius: 6px; background: var(--warning-background); color: var(--warning-text); font-size: 13px; }

/* ── RWD 三斷點 ── */
@media (max-width: 980px) {
  .filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .bond-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 650px) {
  .filter-grid, .bond-grid { grid-template-columns: 1fr; }
  .bond-data-row { grid-template-columns: 1fr; gap: 2px; }
  .bond-data-row dd { text-align: left; }
}
CSS 設計說明初學者要點
.bond-card.featured精選卡片頂端加藍色邊框選擇器組合:代表同時有兩個 class 的元素
.bond-grid repeat(3,...)債券卡片 3 欄 Grid 排列980px 以下改 2 欄,650px 以下改 1 欄
.bond-data-row (dt/dd)標籤−值二欄對齊排版dt 左欄說明文字,dd 右欄數值(粗體)
.tag(圓角標籤)標籤 pill 樣式border-radius: 999px 讓任何寬度都顯示為橢圓形
.risk-label(橘色警示)根據評等自動顯示風險等級由 JS 的 getRiskLabel() 函數決定文字內容

09使用 fetch 讀取 JSON

fetch 可向指定路徑取得資料。讀取檔案需要時間,瀏覽器不會停住整個頁面等待,而會先繼續處理其他工作——這種方式稱為非同步操作。await 表示「等待此步驟完成再繼續」,async 表示「此函數包含非同步操作」。

// 方式一:傳統 .then() 鏈式寫法(較難讀)
fetch("data/bonds.json")
  .then(response => response.json())
  .then(data => console.log(data));

// 方式二:async/await(本章使用,較易讀)
async function loadBondData() {
  try {
    const response = await fetch("data/bonds.json");
    if (!response.ok) throw new Error(`HTTP 錯誤:${response.status}`);
    const data = await response.json();
    // 繼續處理資料...
  } catch (error) {
    console.error("載入失敗:", error);
  }
}
i

await 只能在 async 函數內使用。try...catch 是必要的,因為網路請求或 JSON 解析都可能失敗。

10JavaScript 完整資料處理程式

程式分為十六個功能區塊,每個區塊負責單一職責。

程式區塊 4js/bond-database.js(完整版含注解)
"use strict";

// ── ① 取得所有 HTML 元素 ─────────────────────────
const keywordInput = document.getElementById("keywordInput");
const ratingFilter = document.getElementById("ratingFilter");
const maturityFilter = document.getElementById("maturityFilter");
const sortOrder = document.getElementById("sortOrder");
const clearFiltersButton = document.getElementById("clearFiltersButton");
const resultCount = document.getElementById("resultCount");
const statusMessage = document.getElementById("statusMessage");
const bondList = document.getElementById("bondList");
const emptyState = document.getElementById("emptyState");

// ── ② 全域資料陣列(載入後保存,搜尋時不需重新讀取) ──
let allBonds = [];

// ── ③ 格式化函數 ────────────────────────────────
function formatCurrency(amount) {
  return new Intl.NumberFormat("zh-TW", {
    style: "currency", currency: "TWD", maximumFractionDigits: 0
  }).format(amount);
}
function formatPercent(rate) {
  return new Intl.NumberFormat("zh-TW", {
    style: "percent", minimumFractionDigits: 2, maximumFractionDigits: 2
  }).format(rate);
}
function formatPaymentFrequency(freq) {
  if (freq === 1) return "每年一次";
  if (freq === 2) return "每半年一次";
  if (freq === 4) return "每季一次";
  return `每年 ${freq} 次`;
}

// ── ④ 根據信用評等產生風險標示文字 ─────────────
function getRiskLabel(rating) {
  const low = ["AAA","AA+","AA","AA-"];
  const mid = ["A+","A","A-"];
  if (low.includes(rating)) return "相對較低信用風險";
  if (mid.includes(rating)) return "中度信用風險";
  return "信用風險相對較高";
}

// ── ⑤ 資料驗證:必要欄位是否存在 ──────────────
function validateBond(bond) {
  const required = ["id","name","issuer","bond_type",
    "face_value","coupon_rate","maturity_years",
    "credit_rating","active"];
  return required.every(f => bond[f] !== undefined && bond[f] !== null);
}

// ── ⑥ 安全輸出:防止 HTML 注入 ─────────────────
function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&")
    .replaceAll("<", "<")
    .replaceAll(">", ">")
    .replaceAll('"', """)
    .replaceAll("'", "'");
}

// ── ⑦ 產生標籤 HTML ─────────────────────────────
function createTagHtml(tags) {
  if (!Array.isArray(tags)) return "";
  return tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join("");
}

// ── ⑧ 產生單張債券卡片 HTML ─────────────────────
function createBondCard(bond) {
  const featuredClass = bond.featured ? "featured" : "";
  const featuredLabel = bond.featured ? '<span class="featured-label">精選</span>' : "";
  return `
    <article class="bond-card ${featuredClass}">
      ${featuredLabel}
      <p class="bond-type">${escapeHtml(bond.bond_type)}</p>
      <h2>${escapeHtml(bond.name)}</h2>
      <p class="issuer">發行機構:${escapeHtml(bond.issuer)}</p>
      <dl class="bond-data-list">
        <div class="bond-data-row"><dt>面額</dt><dd>${formatCurrency(bond.face_value)}</dd></div>
        <div class="bond-data-row"><dt>票面利率</dt><dd>${formatPercent(bond.coupon_rate)}</dd></div>
        <div class="bond-data-row"><dt>到期年限</dt><dd>${bond.maturity_years} 年</dd></div>
        <div class="bond-data-row"><dt>付息方式</dt><dd>${formatPaymentFrequency(bond.payment_frequency)}</dd></div>
        <div class="bond-data-row"><dt>信用評等</dt><dd>${escapeHtml(bond.credit_rating)}</dd></div>
        <div class="bond-data-row"><dt>到期日</dt><dd>${escapeHtml(bond.maturity_date)}</dd></div>
      </dl>
      <p class="description">${escapeHtml(bond.description)}</p>
      <div class="tag-list">${createTagHtml(bond.tags)}</div>
      <span class="risk-label">${getRiskLabel(bond.credit_rating)}</span>
    </article>
  `;
}

// ── ⑨ 三個篩選函數 ──────────────────────────────
function matchesKeyword(bond, keyword) {
  if (keyword === "") return true;
  const text = [
    bond.name, bond.issuer, bond.bond_type,
    bond.credit_rating, bond.description,
    ...(Array.isArray(bond.tags) ? bond.tags : [])  // 標籤展開
  ].join(" ").toLowerCase();
  return text.includes(keyword.toLowerCase());
}
function matchesRating(bond, rating) {
  if (rating === "all") return true;
  return bond.credit_rating === rating;
}
function matchesMaturity(bond, maturity) {
  if (maturity === "all") return true;
  if (maturity === "short") return bond.maturity_years <= 3;
  if (maturity === "medium") return bond.maturity_years >= 4 && bond.maturity_years <= 7;
  if (maturity === "long") return bond.maturity_years >= 8;
  return true;
}

// ── ⑩ 排序(用 [...bonds] 建立副本,不改原陣列)──
function sortBonds(bonds, selectedSort) {
  const sorted = [...bonds];  // 展開運算子建立副本
  if (selectedSort === "featured") sorted.sort((a,b) =>
    a.featured === b.featured ? a.name.localeCompare(b.name,"zh-Hant")
    : Number(b.featured) - Number(a.featured));
  if (selectedSort === "rate-high") sorted.sort((a,b) => b.coupon_rate - a.coupon_rate);
  if (selectedSort === "rate-low") sorted.sort((a,b) => a.coupon_rate - b.coupon_rate);
  if (selectedSort === "years-short") sorted.sort((a,b) => a.maturity_years - b.maturity_years);
  if (selectedSort === "years-long") sorted.sort((a,b) => b.maturity_years - a.maturity_years);
  return sorted;
}

// ── ⑪ 渲染:將資料陣列產生 HTML 並插入頁面 ─────
function renderBonds(bonds) {
  bondList.innerHTML = "";
  if (bonds.length === 0) {
    resultCount.textContent = "共找到 0 筆資料";
    emptyState.classList.remove("hidden");
    return;
  }
  emptyState.classList.add("hidden");
  resultCount.textContent = `共找到 ${bonds.length} 筆資料`;
  bondList.innerHTML = bonds.map(createBondCard).join("");
}

// ── ⑫ 套用篩選:整合搜尋+篩選+排序+渲染 ────────
function applyFilters() {
  const keyword = keywordInput.value.trim();
  const rating = ratingFilter.value;
  const maturity = maturityFilter.value;
  const sort = sortOrder.value;
  const filtered = allBonds
    .filter(b => matchesKeyword(b, keyword))
    .filter(b => matchesRating(b, rating))
    .filter(b => matchesMaturity(b, maturity));
  renderBonds(sortBonds(filtered, sort));
}

// ── ⑬ 清除所有篩選條件 ─────────────────────────
function clearFilters() {
  keywordInput.value = "";
  ratingFilter.value = "all";
  maturityFilter.value= "all";
  sortOrder.value = "featured";
  applyFilters();
}

// ── ⑭ 非同步載入 JSON(主要入口) ───────────────
async function loadBondData() {
  statusMessage.textContent = "資料載入中";
  try {
    const response = await fetch("data/bonds.json");
    if (!response.ok) throw new Error(`讀取失敗:${response.status}`);
    const data = await response.json();
    if (!Array.isArray(data)) throw new Error("JSON 最外層必須是陣列。");
    allBonds = data
      .filter(validateBond)              // 排除缺少必要欄位的資料
      .filter(b => b.active === true);   // 只保留上架資料
    statusMessage.textContent = "";
    applyFilters();
  } catch (error) {
    resultCount.textContent = "資料無法顯示";
    statusMessage.textContent = "載入債券資料時發生錯誤,請檢查 JSON 格式與檔案路徑。";
    console.error(error);
  }
}

// ── ⑮ 事件監聽:四個篩選條件 + 清除按鈕 ────────
keywordInput.addEventListener("input", applyFilters);
ratingFilter.addEventListener("change", applyFilters);
maturityFilter.addEventListener("change", applyFilters);
sortOrder.addEventListener("change", applyFilters);
clearFiltersButton.addEventListener("click", clearFilters);

// ── ⑯ 頁面載入後執行 ────────────────────────────
loadBondData();

11JavaScript 程式重點解析

函數職責使用的 JS 方法
validateBond()驗證必要欄位是否存在Array.every()
escapeHtml()防止 HTML 注入(安全輸出)String.replaceAll()
createTagHtml()將標籤陣列轉為 HTML 標籤Array.map() + join()
createBondCard()產生單張債券卡片 HTML模板字串(template literal)
matchesKeyword()關鍵字搜尋(含標籤)Array.includes() + toLowerCase()
sortBonds()依條件排序(不改原陣列)[...bonds] 展開 + Array.sort()
renderBonds()將資料陣列插入 DOMinnerHTML + map().join()
loadBondData()非同步載入與驗證 JSONasync/await + fetch + try/catch

五個關鍵 JavaScript 技術

// 1. filter():保留符合條件的元素
allBonds.filter(b => b.active === true)  // 只保留上架資料

// 2. map():將每筆資料轉換為 HTML 字串
bonds.map(createBondCard).join("")  // 所有卡片串接

// 3. 展開運算子 [...]:建立副本不改原陣列
const sorted = [...bonds];  // sort() 直接改原陣列,所以先複製

// 4. 搜尋時展開 tags 陣列並合併
const text = [bond.name, bond.issuer,
  ...(Array.isArray(bond.tags) ? bond.tags : [])
].join(" ").toLowerCase();
// 如果 tags = ["ESG", "綠色金融"],展開後合併為 "ESG 綠色金融"

// 5. escapeHtml():防止資料中的 HTML 標籤被執行
escapeHtml("<script>alert(1)</script>")
// 輸出:&lt;script&gt;alert(1)&lt;/script&gt; ← 安全文字
!

escapeHtml 是金融工具的資安基礎。若資料直接插入 innerHTML,惡意內容(如 script 標籤)可能執行不預期的程式。這在真實網站中屬於 XSS(跨站腳本攻擊)漏洞。

12為何直接開 HTML 可能讀不到 JSON

若直接雙擊開啟 bond-database.html,網址會顯示 file:///C:/...。部分瀏覽器基於安全限制(CORS 政策),不允許 fetch 讀取本機 JSON 檔案,Console 會出現與 CORS 相關的錯誤訊息。

方式操作步驟適合場景
VS Code Live Server(推薦)安裝 Live Server 延伸套件 → 在 HTML 按右鍵 → Open with Live Server → 網址變為 http://127.0.0.1:5500/日常開發,自動重新整理頁面
Python 本機伺服器在專案資料夾開啟 PowerShell → 輸入指令 → 瀏覽器開啟 http://localhost:8000/不需安裝 VS Code 延伸套件
# 在專案資料夾的 PowerShell 輸入(Python 3.12):
py -3.12 -m http.server 8000

# 若電腦的 Python 指令是 python,可改用:
python -m http.server 8000

# 瀏覽器開啟:
http://localhost:8000/bond-database.html

# 停止伺服器:回到 PowerShell 按 Ctrl + C

13操作實務與測試

開啟頁面後,預期顯示 5 筆資料(JSON 共 6 筆,但 BOND006 的 active 為 false 被排除)。resultCount 應顯示「共找到 5 筆資料」。

測試操作輸入/選擇預期結果
關鍵字搜尋輸入「能源」顯示丁能源六年期綠色債券
標籤搜尋輸入「ESG」顯示丁能源(因 ESG 在 tags 中)
評等篩選選 AA只顯示甲公司五年期公司債
年限篩選選 3 年以下只顯示丙建設三年期公司債
排序票面利率由高至低順序:4.80%→4.10%→3.50%→3.20%→1.80%
複合篩選關鍵字「金融」+ 評等「AA+」+ 年限「4-7年」只顯示乙銀行七年期金融債
i

錯誤資料測試——將 bonds.json 中的一個逗號暫時刪除,重新載入頁面。預期:資料無法顯示、頁面顯示載入錯誤訊息、F12 Console 顯示 JSON 解析錯誤。測試完成後務必恢復正確格式。

14實務案例

(一)資料欄位不一致的問題

若三筆債券的利率欄位各不相同(coupon_rate: 0.035、rate: 4.2、票面利率: "3.8%"),程式無法統一處理,前台排序錯誤、金融計算可能誤用百分比。解決方式:建立資料字典,全站統一使用 coupon_rate(小數格式)。

(二)票面利率高不代表投資較好

投資人可能只搜尋票面利率最高的債券,結果優先看到 BBB 評等的丙建設債券(4.80%)。票面利率較高,通常反映市場要求較高風險補償。金融資料卡片應同時呈現票面利率、信用評等、到期年限與風險提醒,讓使用者能全面判斷。

(三)下架資料仍出現在前台

常見原因:程式沒有篩選 active(遺漏 .filter(b => b.active === true));active 被錯誤保存為字串 "false" 而非布林值 false(字串 "false" 在 === true 判斷時會排除,但若用 if (bond.active) 判斷,字串 "false" 仍為 truthy!);首頁使用另一份舊的 JSON 索引資料,與母資料未同步;瀏覽器快取了舊版 JSON,需 Ctrl+F5 強制重新整理。

(四)日期格式造成排序錯誤

若資料中同時出現 2026-03-08、03/08/2026 與 2026年3月8日 三種格式,字串排序會產生錯誤結果。統一使用 YYYY-MM-DD 格式,字串排序即等於日期排序,不需額外轉換。

15常見錯誤與除錯

錯誤訊息 / 症狀最可能原因修正方式
Unexpected token 或 JSON parse errorJSON 格式錯誤(逗號、引號問題)用 VS Code JSON 格式化工具,或貼到 jsonlint.com 驗證
Failed to fetch直接以 file:// 開啟,或路徑拼錯啟動 Live Server 或 Python 本機伺服器
資料載入但畫面空白所有 active 為 false,或必要欄位缺失F12 Console 查錯,確認 validateBond 通過
票面利率顯示 350%JSON 用 3.5 但格式化時又乘以 100JSON 統一存 0.035,formatPercent 直接傳小數
搜尋不到標籤內容tags 不是陣列,或展開運算子邏輯錯誤確認 Array.isArray(bond.tags)
排序改變原始資料順序sort() 直接修改原陣列先用 [...bonds] 建立副本再排序

16資料安全與品質管理

概念說明本章對應程式
輸入驗證確認資料符合欄位規則(型態、範圍、必填)validateBond() 檢查必要欄位是否存在
輸出編碼避免資料中的特殊符號被瀏覽器當成 HTML 執行escapeHtml() 轉換特殊符號
i

兩者功能不同,不能只做其中一項。輸入驗證保護資料品質;輸出編碼保護前台安全。

資料字典範例

欄位型態必填驗證規則
idstring不可重複、不可空白、格式 BOND\d{3}
coupon_ratenumber小數格式,0 < coupon_rate < 0.5
activeboolean只接受 true 或 false,不接受字串
tagsarray必須為陣列,每個元素為字串

備份策略

data/
├─ bonds.json                  ← 現行版本
└─ backups/
   ├─ bonds_2026-06-01.json
   ├─ bonds_2026-06-10.json
   └─ bonds_2026-06-20.json

# 也可透過 GitHub 版本紀錄還原特定日期的 JSON

17關鍵字

JSON物件(object)陣列(array) 鍵(key)值(value)資料型態 唯一識別碼(id)結構化資料資料字典 母資料前台索引Markdown CSVfetchasync await非同步操作try...catch Array.filter()Array.map()Array.sort() 展開運算子(...)includes()toLowerCase() escapeHtml()XSS 防護active featuredtagsCORS Live Server本機伺服器資料備份 版本控制

18實作練習

(一)基礎操作
  • 新增一筆八年期公司債資料(BOND007),設定 active: true,加入三個標籤
  • 測試關鍵字是否可以搜尋到新資料(含標籤搜尋)
  • 將 BOND007 設定為精選(featured: true),確認卡片頂端出現藍色邊框
  • 將 BOND003 設定為下架(active: false),確認前台從 5 筆變為 4 筆
  • 測試「年限由長至短」排序,確認 10 年排在最前面
(二)功能修改
  • 增加債券種類篩選(公司債/金融債/政府公債/綠色債券)
  • 增加計價幣別篩選(TWD/USD)
  • 增加只顯示精選商品的勾選框(featured 篩選)
  • 增加票面利率區間篩選(< 2% / 2%–4% / > 4%)
  • 增加資料卡片的「查看詳情」按鈕(連結到 bond-detail.html?id=BOND001)
  • 增加目前篩選條件的文字摘要
(三)進階挑戰
  • 建立 bond-detail.html,透過 URL 參數 ?id=BOND001 讀取並顯示指定債券詳情
  • 使用 Python 撰寫欄位驗證腳本,自動檢查所有債券的 id 唯一性與欄位格式
  • 建立從 CSV 轉換為 JSON 的 Python 腳本(pandas read_csv → to_json)
  • 將 Markdown 文章與 JSON 索引連結,建立文章詳細頁
  • 將債券資料部署至 Cloudflare Pages,確認 fetch 在線上環境可正常運作

上面「功能修改」清單中的統計類需求,正是本章挑戰任務(04 分頁)任務一的兩個變體;「常見錯誤與除錯」表格中的症狀,則對應任務二、三——先在這裡讀懂需求與陷阱,再到挑戰任務分頁動手實作與除錯。

本章結語——本章將金融網站從單一計算工具,進一步擴充為資料驅動的金融資訊系統。JSON 適合保存結構化資料(債券、題庫、文章索引);Markdown 適合長篇文章;CSV 適合大量表格式數值資料。

讀者建立了完整的債券 JSON 資料,設計唯一識別碼、票面利率(小數格式)、信用評等、上下架控制、精選標籤等欄位,並使用 fetch + async/await 載入資料,再以 filter、map、sort 實現搜尋、篩選與排序功能。

良好的金融資料管理不只是將資料存入檔案,而是要建立一致的欄位名稱、資料型態標準、驗證規則與備份機制。只要底層資料結構穩定,前台功能才能持續擴充而不失控。

下一章進入 Python 金融運算與資料清理,學習使用 Python + Pandas 讀取 CSV 股票價格,計算每日報酬率、累積報酬率、平均報酬率、波動度及最大單日漲跌幅。

金融商品資料庫(教材原始範例)

輸入關鍵字、切換信用評等或到期年限、改變排序方式,資料會即時更新。這就是課本第六~十節的 bond-database.html/bond-database.css/bond-database.js 完全對應的成品——資料是用 fetch + async/await 從 JSON 載入的(示範環境用內嵌資料模擬檔案讀取)。等一下的挑戰任務就是在這份程式碼上動手修改。

  bond-database.html — 在瀏覽器中執行

觀念測驗

共 15 題選擇題,涵蓋本章所有核心觀念。每題作答後會立即顯示解析。

0 / 15 已作答

挑戰任務:修改程式碼,讓工具自己告訴你哪裡錯

下面三個練習都是「真的會執行」的程式碼,包含真實的 fetch 非同步資料載入。修改完成後按下「執行測試」,系統會把你的程式碼放進真實瀏覽器環境中運行(並自動等待資料載入完成),用白話文告訴你哪裡不對。

任務一:功能修改

對應教材「十八、實作練習(二)功能修改」

難度:入門
  • 在結果計數列旁邊,新增顯示目前「精選商品」的筆數
  • HTML 已經留好 #featuredCount 空欄位,只需要在 JavaScript 中賦值
js/bond-database.js — 可直接編輯
提示(卡住了再打開)
renderBonds() 函式中,resultCount.textContent = ... 之後、bondList.innerHTML = ... 之前,加入一行把統計結果寫進對應的空欄位。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己動手做一次,或按「換一題再練習」挑戰另一個類似的任務。

任務二:抓出隱藏的 id 錯誤

對應教材「十五、常見錯誤與除錯」— 資料載入但畫面空白

難度:除錯
  • 這份程式碼交接自另一位同學,執行後完全沒有債券卡片顯示出來
  • 找出問題並修正,讓資料庫恢復正常運作
js/bond-database.js — 藏有 1 個 bug
提示(卡住了再打開)
F12 → Console 如果出現「Cannot read properties of null」,代表 document.getElementById("...") 抓到的是 null。逐一比對 HTML 裡每個 id="..." 跟 JS 裡 getElementById("...") 的拼字是否一模一樣。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一個 id 錯誤。

任務三:資料邏輯錯誤

對應教材「十五、常見錯誤與除錯」— 下架資料仍出現 / 票面利率顯示 350%

難度:除錯
  • 這份程式碼可以正常執行、不會報錯,但顯示的資料不合理
  • 找出問題所在並修正
js/bond-database.js — 資料不合理,但沒有紅字錯誤
提示(卡住了再打開)
這是「跑得動、但答案錯」的類型,回頭檢查 loadBondData() 有沒有把下架資料篩選掉,以及 formatPercent() 傳入的到底是小數還是已經乘過 100 的數字。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一種資料邏輯錯誤。