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(或離線檔案)時,不用改呼叫端程式碼。
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));
}
}
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 欄位異動的風險。- 欄位對照表:
IndustryMap
、IndexIndustries
將代碼轉為產業名稱,讓資料更可讀。
GetAsync
→檢查 StatusCode
→EnsureSuccessStatusCode
→讀取內容→解析 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
在 TwseOpenApiCrawler
中採用 低階 JsonDocument
逐節點解析,原因是:
data
常是「陣列或物件」混合格式,逐一判斷更保險。若 API 欄位穩定,亦可使用 強型別反序列化(JsonSerializer.Deserialize<T>()
)讓程式更簡潔。
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 的實作版本。
ICrawler
)先描述「能力」,上層只依賴抽象。HttpClient
+ GET 取得 TWSE JSON,再用 JsonDocument
安全解析。ICrawler
實作,呼叫端程式可不變。