『我的App開發之路』 實做線上取得股票/貨幣報價

Kevin Chung
9 min readMar 18, 2020

--

由於我開發的 App 是屬於記帳類的,App 能夠線上取得股匯市的報價是很有用的,很多有在進行投資的使用者,透過這功能就可以馬上知道投資損益,進而得知目前的資產淨值。

要達到這個功能,設計上大概需要完成幾件事:

  1. 貨幣資料中需要有個文字欄位,用來儲存股票/貨幣的代碼
  2. 股價/貨幣報價的歷史紀錄,用來回溯資料
  3. 找到提供股票/貨幣報價功能的服務

前兩項只要在資料庫建立對應的表格或欄位即可,並不困難。

Yahoo 財經網頁

線上查詢股價/貨幣報價需要花點功夫,畢竟這只是個小小的記帳 App,沒有那樣的預算跟證交所買服務接 API,只能先找免費的服務。

因為一直以來都是在 Yahoo 上看股市報價,所以在規劃這個功能的時候,便把腦筋動到這上面來,透過點擊個股報價的連結網址,就可以知道它的 API query 語法,只要在最後放入想要查詢的股票代碼即可,例如想查詢台積電只要在網址列輸入:

https://tw.stock.yahoo.com/q/q?s=2330

就會顯示以下的網頁,成交價格就在HTML的表格裡。

實做上只需要用到 HTTP GET 來取得 HTML 檔,然後對內容做點簡單的解析即可。這裡使用 OkHttp 做範例:

OkHttpClient httpClient = new OkHttpClient();Request request = new Request.Builder()
.url("https://tw.stock/yahoo.com/q/q?s=" + code)
.build();

httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 錯誤處理
}

@Override
public void onResponse(Call call, Response response) throws
IOException {
// 報價網頁內容先存放在 String 變數
String html = response.body().string();
// 後續處理
...
}
});

拿到 HTML 檔後,接著就要研究怎麼拿到所需要的資料。先在瀏覽器上成交價格上點擊滑鼠右鍵,然後選擇選單上的 insepect 功能,就會出現下列的 HTML 結構,由於成交價格並沒有特別的 id 或 class 可以馬上定位到它,只能土法煉鋼先逼近再說。

觀察之後可以發現這個表格有個 “加到投資組合” 的固定字串,成交價就在之後的第一個 HTML 粗體標籤內 <b>,這時候可以先用第一個字串來搜尋索引,再用這個索引當作起點搜尋標籤。

程式大概長這樣,很直覺,拿到字串後可以再轉成 Double 型態做後續處理,當然在完整的實作上得注意例外處理。

// 先找 "加到投資組合" 字串,這裡使用 unicode
int index = html.indexOf("\u52a0\u5230\u6295\u8cc7\u7d44\u5408");
// 捨棄索引之前的字串
html = html.substring(index);
// 拿出粗體標籤之間的子字串
String price = html.substring(html.indexOf("<b>")+3, html.indexOf("</b>"));

這樣的設計方式雖然很陽春,但長久以來倒也一直有忠實的達成目的。

GoogleFinance 函數

雖然能透過 Yahoo 財經順利取得股票報價,不過靜態解析網頁的方式很容易因為格式改變而失效 (還好格式好幾年來都沒有改變..)。但另外一個問題是 Yahoo 財經只有台灣股市,使用者可能有投資美股或港股,線上更新就只能更新台股的投資項目,這功能讓人覺得美中不足啊..

恰好有使用者有在投資美股,並來信詢問是否可以 用 GoogleFinance 來抓取美股股價。咦!GoogleFinance 是什麼啊?感覺是盞明燈,通常有 Google 前綴的通常都是好東西,快速瀏覽了相關的資料,得知 GoogleFinance 原來是 Google 試算表中的眾多函數的其中之一,可以透過給訂股市名稱以及股票代碼給這個函數,就可以取得該股票的眾多資料,包含當前報價,交易量,歷史股價,…等。

Bingo! 這不就是我想要的功能嗎?透過單一 function 就可以抓取到主要股市資料,貨幣匯率甚至共同基金(這裡可以查到支援的股市市場以及代號),真是太棒了!經過一些簡單測試,確認了功能之後,就擬定以下的流程:

一開始先利用 Google Sheets API 來建立一個新的試算表

fun createSheet():String {
val sheets = getSheets() // 取得 Sheets 物件

val sheet = sheets
.spreadsheets()
.create(Spreadsheet())
.setFields("spreadsheetId") // 請求 id 欄位
.execute()
return sheet.spreadsheetId
}

先使用一個 function 處理把要查詢的股票代碼(ex. NASDAQ:GOOG, TPE:2330)轉換成 GoogleFinance 公式字串,除了查詢成交價外,還另外查詢漲跌幅金額與幅度,構成試算表中的一列。

fun generateRow(code: String):List<String> {
return Arrays.asList(
"=GoogleFinance(\"${code}\",\"price\")",
"=GoogleFinance(\"${code}\",\"change\")",
"=GoogleFinance(\"${code}\",\"changepct\")"
)
}

接著把要查詢的公式字串,寫到試算表中。這個函數有兩個參數,一個是 spreadsheetId 指向之前建立的試算表,第二個參數是二維的 List,內容是多個要查詢的股價列,這樣可以一次取得所有數值。

fun updateValues(spreadsheetId: String, values: List<List<Any>>): UpdateValuesResponse {
val service = getSheets()
val body = ValueRange().setValues(values)
return service
.spreadsheets()
.values()
// 指定從 A1 開始寫入,要拿回數值時也是從一樣的地方
.update(spreadsheetId, "A1", body)
// 使用 USER_ENTERED 字串,這樣試算表才會執行函數
.setValueInputOption("USER_ENTERED")
.execute()
}

另外建立一個 function 把試算表的內容讀回來,range的範圍因為前面輸入了 3 行公式,所以指定為 “A:C”,這樣就可以把 A-C 這三行所有資料都取回來。

fun getValues(spreadsheetId: String, range: String): ValueRange {

val service = getSheets()

return service
.spreadsheets()
.values()
.get(spreadsheetId, range)
.execute()
}

因為 GoogleFinance 函數取得報價可能會花上一點時間,因此有必要在抓取的時候加入 retry 機制,當讀到任何一個的值是 null 時便重新讀取表格,程式大概就像下面這樣,最後讀取的結果會放回 list 變數裡,這樣就大功告成了。

fun queryStockPrice(list: ArrayList<HashMap<String,String>>) {    // 建立 Google 試算表
val spreadsheetId = createSheet()
val query = ArrayList<ArrayList<String>>() // 把要查詢的股票代碼轉換成 GoogleFinance 函數字串
for(m in list)
query.add(generateRow(m["code"]))
// 寫入試算表
updateValues(spreadsheetId, query)
var retryCount = 0
var retry: Boolean
do {
val values = getValues(spreadsheetId, "A:C")
retry = false
// 讀取並判斷資料內容
readLoop@ for ((row, l) in values.withIndex()) {
for ((col, v) in l.withIndex()) {
val value = v.toString()
if(value.toDoubleOrNull() == null) {
// 如果還沒有取得報價,延遲一段時間再度讀取
sleep(300)
retry = true
break@readLoop // 跳出迴圈,重新讀取表格
}
if (row < list.size) {
val m = list[row]
when (col) {
0 -> m["price"] = value
1 -> m["change"] = value
2 -> m["changepct"] = value
}
} else
break
}
}

} while(retry && retryCount++<MAX_RETRY)
}

要注意的是 GoogleFinance 函數雖然支援世界大部分的股市,不過卻不是每檔股票都有相關資料,經過測試,一些台灣的小型股是查不到的,因此我目前採用的是當 GoogleFinance 查詢不到,或是使用者不願授權的情形,還是會 fallback 到靜態網頁分析的方式,確保使用者都能取得所需資料。

--

--

No responses yet