My Note

自己理解のためのブログ

Echo ( GoのWebフレームワーク ) on GAE で貴金属 ( 金 & プラチナ )の価格を返すAPIを作成した

はじめに

以前に 貴金属の価格を取得して可視化する内容で記事を作成しました。 今回は Echo( Goフレームワーク ) でデータを取得する部分についてアップデートした部分についてです。 金融資産のポートフォリオを可視化するために必要な情報を取得するためにデータ取得先、取得データ等を変更し、Echoのバージョンをアップデートしました。

yhidetoshi.hatenablog.com

データの取得先

価格を取得するために、マイゴールドパートナー を利用しました。

スクレイピング

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

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

github.com

APIについて

  • リクエストについて

    • ${ENDPOINT}/metal
      • リクエストパスは /metal と設定
  • レスポンスについて

    • jsonで以下の情報を返す
      • 金のweb買取価格
      • 金のweb前日比価格
      • プラチナのweb買取価格
      • プラチナのweb前日比価格
      • タイムスタンプ

■ レスポンス結果(例)

{
  "gold": 7697,
  "gold_day_before_price": 28,
  "platinum": 4301,
  "platinum_day_before_price": -71,
  "time": "2022/02/2-21:14:28"
}

ソースコード

参考までにディレクトリ構造は以下の通り

.
├── 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

■ 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
}

■ metal.go

package api

import (
    "encoding/json"
    "log"
    "net/http"
    "os"
    "strings"

    "github.com/PuerkitoBio/goquery"
    "github.com/labstack/echo/v4"
)

/* Response
{
    "gold": 7697,
    "gold_day_before_price": 28,
    "platinum": 4301,
    "platinum_day_before_price": -71,
    "time": "2022/02/21-21:14:28"
}
*/

const (
    targetURLMetal              = "https://gold.mmc.co.jp/"
    goldPriceCount              = 8
    goldDayBeforePriceCount     = 9
    platinumPriceCount          = 48
    platinumDayBeforePriceCount = 49
)

type Metal struct {
    GoldPrice              int    `json:"gold"`
    GoldDayBeforePrice     int    `json:"gold_day_before_price"`
    PlatinumPrice          int    `json:"platinum"`
    PlatinumDayBeforePrice int    `json:"platinum_day_before_price"`
    Date                   string `json:"time"`
}

func FetchMetal(c echo.Context) error {
    var goldPrice, goldDayBeforePrice, platinumPrice, platinumDayBeforePrice string
    var tdCount int

    res, err := http.Get(targetURLMetal)
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }
    defer res.Body.Close()

    doc, err := goquery.NewDocumentFromReader(res.Body)
    if err != nil {
        log.Println(err)
    }

    // Fetch gold and platinum price
    doc.Find("div.p-table-scroll--sticky > table > tbody > tr > td").Each(func(_ int, s *goquery.Selection) {
        if tdCount == goldPriceCount { //Gold Price
            goldPrice = s.Find("span.c-table__text--xl").Text()
        }
        if tdCount == goldDayBeforePriceCount { //Gold Price Day Before Price Count
            goldDayBeforePrice = s.Find("span.c-table__text--xl").Text()
        }
        if tdCount == platinumPriceCount { // Platinum Price
            platinumPrice = s.Find("span.c-table__text--xl").Text()
        }
        if tdCount == platinumDayBeforePriceCount { // Platinum Day Before Price Count
            platinumDayBeforePrice = s.Find("span.c-table__text--xl").Text()
        }
        tdCount++
    })
    // Format
    strGoldPrice := strings.Replace(goldPrice, ",", "", -1)
    strGoldDayBeforePrice := strings.Replace(goldDayBeforePrice, "+", "", -1)

    strPlatinumPrice := strings.Replace(platinumPrice, ",", "", -1)
    strPlatinumDayBeforePrice := strings.Replace(platinumDayBeforePrice, "+", "", -1)

    // Convert string to int
    intGoldPrice := convertStringToInt(strGoldPrice)
    intGoldDayBeforePrice := convertStringToInt(strGoldDayBeforePrice)
    intPlatinumPrice := convertStringToInt(strPlatinumPrice)
    intPlatinumDayBeforePrice := convertStringToInt(strPlatinumDayBeforePrice)

    // Set value to json
    responseJSON := Metal{
        GoldPrice:              intGoldPrice,
        GoldDayBeforePrice:     intGoldDayBeforePrice,
        PlatinumPrice:          intPlatinumPrice,
        PlatinumDayBeforePrice: intPlatinumDayBeforePrice,
        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}/metal | jq 
{
  "gold": 7698,
  "gold_day_before_price": 1,
  "platinum": 4338,
  "platinum_day_before_price": 37,
  "time": "2022/02/22-21:19:14"
}

デプロイについて

今回のGoアプリはGoogle App Engineにデプロイしているので、Github Actionsで自動デプロイするようにしています。 そちらについては以前に記事でまとめています。

yhidetoshi.hatenablog.com

yhidetoshi.hatenablog.com