上一篇,提到了可以在 tableView(_:willDisplay:forRowAt:) 中發動 URLRequest,這邏輯很正常,但真的不建議這麼做。
先來看上次的 API 接口
https://www.twse.com.tw/en/exchangeReport/STOCK_DAY?response=csv&date=20210928&stockNo=2330
我們要抽換的是 stockNo 後面的數字,而發動的時機,就是每次 tableViewCell 的 willDisplay。就這個畫面來說,大概在剛進入畫面的時候,就會發動五到六次不等的 API request。
而經過實測,這樣子發動 API Request ,會被證交所視為不正常的網路連線,然後會在接下來的一段時間,都無法拿到證交所的資訊,包含網頁也打不開。
既然這個方法走不通,那我們就要去找其他來源。去找是否有地方,可以一次性的下載所有上市股票的收盤價。
在政府的開放資料平台,是有這個資訊的。
https://data.gov.tw/dataset/11549
而下載所有資料的載點,也在這邊。
https://www.twse.com.tw/exchangeReport/STOCK_DAY_ALL?response=open_data
可拿取的資料如下圖
先宣告資料模型 StockDayTick
import Foundation
/// 這個 Data Model 會被公開資訊頁 和 開放資料共用,兩者的資料有差,如果另一邊沒有的,會用 default string = "-" 處理掉
struct StockDayTick: Codable {
private var dateUtility: DateUtility {
return DateUtility()
}
private var numberFormatter: NumberFormatter {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}
/// 如果是全市場台股的 dateString, format 為 yyyyMMdd
let dateString: String
let stockCode: String
/// 只有全市場台股的資料才有 stock name
var stockName = ""
/// 成交股數
let volumeString: String
/// 成交金額
let valueString: String
let openString: String
let highestString: String
let lowestString: String
let closeString: String
/// 與前天的漲跌差,但是在大盤的公開資訊,是沒有這個欄位的
var change: String = ""
/// 成交筆數,但是在大盤的公開資訊,是沒有這個欄位的
var transaction: String = ""
var date: Date? {
return dateUtility.getDate(from: dateString, format: "yyyy/MM/dd")
}
var open: Double? {
return numberFormatter.number(from: openString)?.doubleValue
}
var highest: Double? {
return numberFormatter.number(from: highestString)?.doubleValue
}
var lowest: Double? {
return numberFormatter.number(from: lowestString)?.doubleValue
}
var close: Double? {
return numberFormatter.number(from: closeString)?.doubleValue
}
/// 大盤 K 線的建構式
init(stockCode: String, stockName: String, dateString: String, openString: String, highestString: String, lowestString: String, closeString: String) {
self.stockCode = stockCode
self.stockName = stockName
self.dateString = dateString
self.openString = openString
self.highestString = highestString
self.lowestString = lowestString
self.closeString = closeString
self.volumeString = ""
self.valueString = ""
}
init(dateString: String,
stockCode: String,
stockName: String = "",
volumeString: String,
valueString: String,
openString: String,
highestString: String,
lowestString: String,
closeString: String,
change: String,
transaction: String) {
self.dateString = dateString
self.stockCode = stockCode
self.stockName = stockName
self.volumeString = volumeString
self.valueString = valueString
self.openString = openString
self.highestString = highestString
self.lowestString = lowestString
self.closeString = closeString
self.change = change
self.transaction = transaction
}
}
下載資料的程式碼
struct StockDayPriceManager {
private var alamofireAdapter: AlamofireAdapter {
return AlamofireAdapter.shared
}
private var dateUtility: DateUtility {
return DateUtility()
}
private var dateString: String {
return dateUtility.getString(date: Date(), format: "yyyyMMdd")
}
}
/// 這一區的程式碼,下載全台股所有個股單日行情,包含開高低收,成交筆數,成交股數,成交金額,價差
extension StockDayPriceManager {
/// 這一道 csv 不用拿掉第一行
func getAllTwMarketStockDayPrice(completion: @escaping ((Result<[StockDayTick], Error>) -> Void)) {
let urlString = "https://www.twse.com.tw/exchangeReport/STOCK_DAY_ALL?response=open_data"
alamofireAdapter.requestForStringWithRepsonseHeader(urlString) { string, allHeaders, error in
if let dateString = getTwMarketDateString(from: allHeaders),
let ticks = getTwMarketDayPriceList(rawString: string, dateString: dateString) {
completion(.success(ticks))
} else {
completion(.failure(GetAllMarketError()))
}
}
}
private func getTwMarketDateString(from responseHeaders: [AnyHashable: Any]?) -> String? {
if let headers = responseHeaders,
let fileName = headers["Content-Disposition"] as? String {
return fileName.slice(from: "STOCK_DAY_ALL_", to: ".csv")
}
return nil
}
private func getTwMarketDayPriceList(rawString: String, dateString: String) -> [StockDayTick]? {
if let csv = CSVAdapter(rawString: rawString) {
var ticks = [StockDayTick]()
for row in csv.namedRows {
let stockCode = row["證券代號"] ?? ""
let stockName = row["證券名稱"] ?? ""
let volume = row["成交股數"] ?? ""
let value = row["成交金額"] ?? ""
let open = row["開盤價"] ?? ""
let highest = row["最高價"] ?? ""
let lowest = row["最低價"] ?? ""
let close = row["收盤價"] ?? ""
let change = row["漲跌價差"] ?? ""
let transaction = row["成交筆數"] ?? ""
let tick = StockDayTick(dateString: dateString, stockCode: stockCode, stockName: stockName, volumeString: volume, valueString: value, openString: open, highestString: highest, lowestString: lowest, closeString: close, change: change, transaction: transaction)
ticks.append(tick)
}
return ticks
}
return nil
}
}
extension StockDayPriceManager {
struct GetAllMarketError: LocalizedError {
var errorDescription = "全市場K棒開高低收資料錯誤"
}
}
而呼叫的時機點,可以自由決定,我目前是放在 AppDelegate didFinishLaunch
StockDayPriceManager().getAllTwMarketStockDayPrice { [weak self] result in
switch result {
case .success(let ticks):
self?.save(twAllMarketTicks: ticks)
case .failure(let error):
Logger.log("拉取台股交易日全市場結果失敗: \(error.localizedDescription)")
}
}