Echo ( GoのWebフレームワーク ) on GAE で投資信託の基準価格を返すAPIを作成した
はじめに
今回は金融資産のポートフォリオ可視化するために投資信託の情報を返してくれるAPIが欲しかったので 勉強も兼ねてEchoで作成してみました。
データの取得先とISINコード
価格を取得するために、価格コムのサイト を利用しました。 kakaku.com
- S&P500の場合のURl
投資信託には ISINコードが割り当てられています。
このサイトでは クエリパラメータに ISINコード
を渡せばその投資信託の基準価格のサイトを表示できます。
スクレイピング
スクレイピングは対象サイトに迷惑がかからないようにする必要があります。 価格は基本的に日時で更新されるので1日1回定期アクセスするようにしており、APIを叩く際はベーシック認証をかけて 外部から不用意にアクセスされないように制御しています。
Goでスクレイピングするために、 goquery
を利用しました。
APIについて
- リクエストについて
${ENDPOINT}/investment-trust/${ISIN_CODE}
- リクエストパスは
/nvestment-trust
と設定 - パスパラメータとして
ISINコード
を指定
- リクエストパスは
- パスパラメータの設定方法については以下を参考
e.GET("/investment-trust/:isin", api.FetchInvestTrust)
- レスポンスについて
- jsonで以下の情報を返す
- 基準価格
- 前日比価格
- タイムスタンプ
- jsonで以下の情報を返す
■ レスポンス結果(例)
{ "base_price": 17569, "day_before_price": -461, "time": "2022/02/21-22:25:42" }
- スクレイピングする対象のhtml(一部抜粋)
<div class="infoMainTop clearfix"> <dl> <dt>基準価額:</dt> <dd><span class="price"> 17,462 </span>円 前日比 <span class="minus">-107</span>円(-0.61%)</dd> </dl>
ソースコード
参考までにディレクトリ構造は以下の通り
. ├── README.md ├── api │ ├── base.go │ ├── client.go │ ├── crypto_coincheck.go │ ├── crypto_coincheck_test.go │ ├── crypto_gmo.go │ ├── crypto_gmo_test.go │ ├── healthcheck.go │ ├── investment_trust.go │ ├── metal.go │ └── staticcheck.conf ├── app.yaml ├── authentication │ └── basic_auth.go ├── go.mod ├── go.sum ├── main.go └── secret.yaml
- Goのバージョン
❯ go version go version go1.17.6 darwin/amd64
■ main.go
package main import ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "google.golang.org/appengine" "github.com/yhidetoshi/apiEchoGAE-private/api" "github.com/yhidetoshi/apiEchoGAE-private/authentication" ) var e = createMux() func createMux() *echo.Echo { e := echo.New() return e } func main() { // Echoインスタンス作成 // http.Handle("/", e) e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.Gzip()) e.Use(authentication.BasicAuth()) // Basic Auth cGmo := api.ClientCryptoGMO{} cGmo.NewClientCryptoGMO() cCc := api.ClientCryptoCoincheck{} cCc.NewClientCryptoCoincheck() // ルーティング // // ヘルスチェック e.GET("/api/healthcheck", api.Healthcheck) // 貴金属の価格を取得 e.GET("/api/metal", api.FetchMetal) // ISINコードを引数に基準価格を取得 // curl -sS localhost:1323/investment-trust/JP90C000GKC6 e.GET("/api/investment-trust/:isin", api.FetchInvestTrust) // 仮想通貨の価格を取得 e.GET("/api/crypto/gmo", cGmo.FetchCryptoGMO) e.GET("/api/crypto/coincheck", cCc.FetchCryptoCoincheck) // LocalとGAEの切り替え appengine.Main() // GAEの場合に有効化 //e.Start(":1323") // Localの場合に有効化 }
■ base.go
共通する設定や関数を集約
package handler import ( "log" "strconv" "time" ) const ( timezone = "Asia/Tokyo" offset = 9 * 60 * 60 timeFormat = "2006/01/02-15:04:05" ) var ( jst = time.FixedZone(timezone, offset) nowTime = time.Now().In(jst).Format(timeFormat) ) func convertStringToInt(value string) int { intValue, err := strconv.Atoi(value) if err != nil { log.Println("Error", err) } return intValue }
■ investment_trust.go
package api import ( "encoding/json" "fmt" "log" "net/http" "strings" "github.com/PuerkitoBio/goquery" "github.com/labstack/echo/v4" ) /* Response { "base_price": 17569, "day_before_price": -461, "time": "2022/02/21-15:25:42" } */ var ( targetURL = "https://kakaku.com/fund/detail.asp?si_isin=" ) type InvestmentTrustInfo struct { BasePrice int `json:"base_price"` DayBeforePrice int `json:"day_before_price"` Date string `json:"time"` } func FetchInvestTrust(c echo.Context) error { var strBasePrice, strDayBeforePrice string isin := c.Param("isin") res, err := http.Get(targetURL + isin) if err != nil { log.Println(err) } defer res.Body.Close() doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { fmt.Println("error res.Body reader") log.Println(err) } doc.Find("div.infoMainTop.clearfix").Each(func(_ int, s *goquery.Selection) { strBasePrice = s.Children().Find("span.price").First().Text() plusPrice := s.Children().Find("span.plus").First().Text() // 前日比がプラスの場合 minusPrice := s.Children().Find("span.minus").First().Text() // 前日比の価格 // htmlタグが結果で変化するため if minusPrice != "" { strDayBeforePrice = minusPrice } else if plusPrice != "" { strDayBeforePrice = plusPrice } if minusPrice != "" && plusPrice != "" { strDayBeforePrice = "0" } }) // trim "," repCommaBasePrice := strings.Replace(strBasePrice, ",", "", -1) repCommaDayBeforePrice := strings.Replace(strDayBeforePrice, ",", "", -1) // trim "\n" repNewLineBasePrice := strings.Replace(repCommaBasePrice, "\n", "", -1) repNewLineDayBeforePrice := strings.Replace(repCommaDayBeforePrice, "\n", "", -1) // trim "*" repPlusDayBeforePrice := strings.Replace(repNewLineDayBeforePrice, "+", "", -1) // Convert string to int intBasePrice := convertStringToInt(repNewLineBasePrice) intDayBeforePrice := convertStringToInt(repPlusDayBeforePrice) // Set value to json responseJSON := InvestmentTrustInfo{ BasePrice: intBasePrice, DayBeforePrice: intDayBeforePrice, Date: nowTime, } response, err := json.Marshal(responseJSON) if err != nil { log.Println(err) } return c.String(http.StatusOK, string(response)) }
■ 確認
❯ curl -sSu ${ID}:${PW} ${ENDPOINT}/investment-trust/JP90C0001MP2 | jq . { "base_price": 10001, "day_before_price": 0, "time": "2022/02/22-21:27:41" }
デプロイについて
今回のGoアプリはGoogle App Engineにデプロイしているので、Github Actionsで自動デプロイするようにしています。 そちらについては以前に記事でまとめています。