iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0

1) 用介面定義能力:ICrawler

在專案裡,先用介面描述「會做什麼」,讓上層程式只依賴抽象而不綁定實作。ICrawler 就定義了兩個能力:取得股票主檔清單歷史日行情

    /// <summary>
    /// Data crawler capable of fetching stock lists and historical quotes.
    /// 資料擷取介面:用於取得股票清單與歷史報價。
    /// </summary>
    public interface ICrawler
    {
        /// <summary>
        /// Retrieves all stock profiles.
        /// 取得所有股票主檔資料。
        /// </summary>
        IEnumerable<StockProfile> GetStockList();

        /// <summary>
        /// Crawls historical daily quotes for a single stock.
        /// 取得單一股票於指定期間的每日行情。
        /// </summary>
        /// <param name="code">Stock code.</param>
        /// <param name="startDate">Inclusive start date.</param>
        /// <param name="endDate">Inclusive end date.</param>
        IEnumerable<DailyQuote> GetDailyQuotes(string code, DateTime startDate, DateTime endDate);
    }

好處:呼叫端只要面向 ICrawler 撰寫程式,將來要把資料來源換成別的 API(或離線檔案)時,不用改呼叫端程式碼。


2) 用 HttpClient 發送 HTTP 請求(GET 為主,POST 補充)

TwseOpenApiCrawler 透過建構子注入 HttpClient 與資料庫儲存端(Repository),符合前面學過的 DI 思惟。

public sealed class TwseOpenApiCrawler : ICrawler
{
    private readonly HttpClient httpClient;
    private readonly IDatabaseRepository repository;

    public TwseOpenApiCrawler(HttpClient httpClient, IDatabaseRepository repository)
    {
        this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
    }
}

2.1 GET:下載 JSON 並解析

  • 取得股票清單:以 GET 取得公開清單,再用 JsonDocument 逐筆解析。
public IEnumerable<StockProfile> GetStockList()
{
    const string url = "https://openapi.twse.com.tw/v1/opendata/t187ap03_L";
    var json = httpClient.GetStringAsync(url).GetAwaiter().GetResult();       // 下載
    using var doc = JsonDocument.Parse(json);                                 // 解析 JSON
    var list = new List<StockProfile>();

    foreach (var element in doc.RootElement.EnumerateArray())
    {
        // 範例:嘗試從不同欄位名取得 Code / Name(API 欄位可能變動)
        if (!TryGetString(element, new[] { "公司代號", "股票代號", "證券代號", "Code", "stockNo" }, out var code) ||
            !TryGetString(element, new[] { "公司簡稱", "Name", "stockName" }, out var name))
        {
            continue;
        }

        string? industry = null;
        if (TryGetString(element, new[] { "產業別", "Industry" }, out var industryCode) &&
            IndustryMap.TryGetValue(industryCode, out var mapped))
        {
            industry = mapped; // 代碼 → 中文產業名
        }

        list.Add(new StockProfile
        {
            Code = code,
            Name = name,
            Industry = industry,
            Market = MarketCode.TSE,
            Country = Country.TW,
            LastUpdatedUtc = DateTime.UtcNow,
        });
    }

    // 追加各產業指數(IX****),亦以清單形式回傳
    foreach (var idx in IndexIndustries)
    {
        list.Add(new StockProfile
        {
            Code = idx.Key,
            Name = $"{idx.Value}指數",
            Industry = idx.Value,
            Market = MarketCode.Index,
            Country = Country.TW,
            LastUpdatedUtc = DateTime.UtcNow,
        });
    }
    return list;
}

重點:

  • 容錯解析TryGetString 會嘗試多個備用欄位名稱,降低 API 欄位異動的風險。
  • 欄位對照表IndustryMapIndexIndustries 將代碼轉為產業名稱,讓資料更可讀。

2.2 GET(含錯誤處理與狀態碼判斷)

  • 取得單檔歷史日行情:透過 GetAsync→檢查 StatusCodeEnsureSuccessStatusCode→讀取內容→解析 data 陣列。遇 404/443 或停牌(TWSE 以「--」與 0 值表示)時,會記錄「缺漏日」供後續補資料用。
using var response = httpClient.GetAsync(url).GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode)
{
    var status = (int)response.StatusCode;
    if (IsMissingStatus(status)) { /* 記錄缺漏日並 continue */ }
    response.EnsureSuccessStatusCode(); // 其他狀態碼 → 丟例外
}
var json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("data", out var dataArray)) { /* 略過 */ }
// 逐筆解析:日期(民國年格式處理)、開高低收、量值 → DailyQuote

3) JSON 解析的實務細節

TwseOpenApiCrawler 中採用 低階 JsonDocument 逐節點解析,原因是:

  • TWSE 回傳欄位名稱不穩定,需要容錯(多個欄位名嘗試)。
  • data 常是「陣列或物件」混合格式,逐一判斷更保險。

若 API 欄位穩定,亦可使用 強型別反序列化JsonSerializer.Deserialize<T>())讓程式更簡潔。


4) 範例:用 TwseOpenApiCrawler 抓取股票清單(GET)

以下示範如何把 HttpClient 與 Repository(可先做一個假的 stub)注入,呼叫 GetStockList(),並將結果列印出來。(僅示意,請依你的專案命名空間調整)

using System;
using System.Collections.Generic;
using System.Net.Http;
using TwStockMaster.Core.Service;
using TwStockMaster.Core.Interface;
using TwStockMaster.Utils.Models;

class FakeRepository : IDatabaseRepository
{
    // 實作你需要用到的最小方法即可;若只示範 GetStockList,可留空或拋 NotImplementedException
    // ...
}

class Program
{
    static void Main()
    {
        HttpClient httpClient = new HttpClient();
        IDatabaseRepository repo = new FakeRepository();

        ICrawler crawler = new TwseOpenApiCrawler(httpClient, repo); // 透過介面使用實作
        IEnumerable<StockProfile> list = crawler.GetStockList();     // 呼叫 GET,解析 JSON

        foreach (var sp in list)
        {
            Console.WriteLine($"{sp.Code,-6} {sp.Name,-10} {sp.Industry}");
        }
    }
}

對照:ICrawler 介面定義了 GetStockList()GetDailyQuotes(...)TwseOpenApiCrawler 即為其以 TWSE Open API 的實作版本。


5) 小結

  • 介面ICrawler)先描述「能力」,上層只依賴抽象。
  • HttpClient + GET 取得 TWSE JSON,再用 JsonDocument 安全解析。
  • 若未來要切換資料源,只要提供新的 ICrawler 實作,呼叫端程式可不變。


上一篇
Day 11 - 非同步程式設計 (async/await) 入門
系列文
30天快速上手製作WPF選股工具 — 從C#基礎到LiteDB與Web API整合12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言