My Note

自己理解のためのブログ

Echo ( GoのWebフレームワーク ) on GAE で投資信託の基準価格を返すAPIを作成した

はじめに

今回は金融資産のポートフォリオ可視化するために投資信託の情報を返してくれるAPIが欲しかったので 勉強も兼ねてEchoで作成してみました。

yhidetoshi.hatenablog.com

データの取得先とISINコード

価格を取得するために、価格コムのサイト を利用しました。 kakaku.com

投資信託には ISINコードが割り当てられています。 このサイトでは クエリパラメータに ISINコード を渡せばその投資信託の基準価格のサイトを表示できます。

スクレイピング

スクレイピングは対象サイトに迷惑がかからないようにする必要があります。 価格は基本的に日時で更新されるので1日1回定期アクセスするようにしており、APIを叩く際はベーシック認証をかけて 外部から不用意にアクセスされないように制御しています。

Goでスクレイピングするために、 goquery を利用しました。

github.com

APIについて

e.GET("/investment-trust/:isin", api.FetchInvestTrust)
  • レスポンスについて
    • jsonで以下の情報を返す
      • 基準価格
      • 前日比価格
      • タイムスタンプ

■ レスポンス結果(例)

{
  "base_price": 17569,
  "day_before_price": -461,
  "time": "2022/02/21-22:25:42"
}
<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で自動デプロイするようにしています。 そちらについては以前に記事でまとめています。

yhidetoshi.hatenablog.com

yhidetoshi.hatenablog.com