1
3

More than 1 year has passed since last update.

【Golang】海外サッカーリーグの試合情報を取得できるAPI「football-data.org」を使ってみた。

Posted at

はじめに

私があまり知らないだけかもしれないが、
海外サッカーリーグの試合情報(スケジュールや各種スタッツ等)を取り扱う国産サービスってあまり無い気がします。
(大手だとSportsnaviくらいしか知らない。。)

海外で有名なサービスだと
Sofascore
WhoScored
transfermarkt ※移籍情報がメイン

など沢山ありますが、それらサービスは独自に試合情報を収集しているか、
または外部の試合情報取得用のAPIから取得していると思われます。

今回は、外部の試合情報取得APIを使って
イングランド・プレミアリーグの試合情報
(APIコール時点の、第○節分の試合スケジュール、順位表、ゴールスコア上位10名)
を取得してWEB画面に表示させてみようと思います。

(私の勉強がてらに)Golangを使って実装してみます。
※ツッコミどころが多々あるかもしれませんが、ご了承ください。

対象読者

・WEBAPIを何となくでも理解している人
・Golang経験者、初学者
・海外サッカーが好きな人

主な使用技術

・Golang v1.17
・GJSON v1.14.1

使用するAPIについて

football-data.org
各国サッカーリーグの試合情報をJSON形式で取得できます。

※無償版、有償版があるようです。
 今回は無償版を使用しますが、以下のように機能が制限されます。
 ・取得データは、12のコンペティション(リーグ)のみ
 ・取得データは、基本データのみ(スケジュール、試合結果、一部スタッツ)
 ・APIコールは1分間に10回まで

※APIのリファレンスが以下にあります。ご参考までに。
football-data.org - API Reference Documentation

事前準備

APIを使用するため、アカウントを登録します。
右上「Get started」をクリック。
image.png

名前(適当でOK)とメールアドレスを入力します。
「Choose ur weapon if you like to:」は「Golang」を選択し、「Create account」をクリック。

 ②で入力したメールアドレス宛にAPIトークンが送られたことを確認する。
image.png

これで事前準備はOK。

実装

構成

構成は以下の通りとしました。

football
├── assets
│   ├── css
│   │   └── styles.css
│   ├── index.html                               // 試合情報の表示画面
│   └── js
│       └── jquery-3.6.0.min.js
├── cmd
│   ├── main.go                                      // サーバー&画面レンダリング機能
│   └── match
│       ├── getData.go                        // 試合情報取得
│       ├── schedule.go                       // 試合スケジュール取得 (getData.goから呼ばれる)
│       ├── score.go                             // ゴールスコア取得 (getData.goから呼ばれる)
│       └── standings.go                     // 順位表取得 (getData.goから呼ばれる)
├── go.mod
└── go.sum

フロントエンドの実装

css、jsについては、以下のサイトから入手したものを使用しています。
Start Bootstrap - Scrolling Nav v5.0.4

htmlも、各自適当にご用意ください。サンプルを置いておきます↓

index.htmlを表示
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <link href="assets/css/styles.css" rel="stylesheet"/>
        <title>{{ .Title}}</title>
    </head>
    <div id="page-top">
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top" id="mainNav">
            <div class="container px-4">
                <a class="navbar-brand" href="/">{{ .Title}}</a>
            </div>
        </nav>
    </div>
    <body>
        <!-- schedule section-->
        <section id="schedule">
            <div class="container px-4">
                <div class="row gx-4 justify-content-center">
                    <div class="col-lg-8">
                        <h2>イングランド・プレミアリーグ{{(index .MatchList 0).StartDate}}-{{(index .MatchList 0).EndDate}}シーズン 第{{(index .MatchList 0).CurrentMatchday}}節 試合日程</h2>
                        <table >
                            <thead>
                                <tr align="center">
                                    <th width="40%">ホーム</th>
                                    <th width="20%">試合開始日時(日本時間)</th>
                                    <th width="40%">アウェイ</th>
                                </tr>
                            </thead>
                            {{ range .MatchList }}
                            {{ if eq .CurrentMatchday .Matchday}}
                            <tbody>
                                <tr align="center">
                                    <td width="40%"><img src="https://crests.football-data.org/{{.HomeTeamId}}.svg" height="50"></br>{{.HomeTeamName}}</td>
                                    <td width="20%"><div class="text-center">{{.Month}}/{{.Day}}({{.WDays}})</div><div class="text-center">{{.StartTime}}</div><p class="fw-bolder fs-1">{{.HomeScore}} - {{.AwayScore}}</p></td>
                                   <td width="40%"><img src="https://crests.football-data.org/{{.AwayTeamId}}.svg" height="50"></br>{{.AwayTeamName}}</td>
                                </tr>
                            </tbody>
                            {{ end }}
                            {{ end }}
                        </table>
                    </div>
                </div>
            </div>
        </section>
        <!-- standings section-->
        <section id="standings">
            <div class="container px-4">
                <div class="row gx-4 justify-content-center">
                    <div class="col-lg-8">
                        <h2>イングランド・プレミアリーグ{{(index .MatchList 0).StartDate}}-{{(index .MatchList 0).EndDate}}シーズン 順位表</h2>
                        <table>
                            <thead>
                                <tr align="left">
                                    <th width="10%">順位</th>
                                    <th width="40%">チーム名</th>
                                    <th width="10%">試合数</th>
                                    <th width="10%">勝</th>
                                    <th width="10%">引</th>
                                    <th width="10%">負</th>
                                    <th width="10%">Pts</th>
                                </tr>
                            </thead>
                            {{ range .StandingsList }}
                            <tbody>
                                <tr align="left">
                                    <th width="10%">{{ .Position}}</th>
                                    <th width="40%"><img src="https://crests.football-data.org/{{.TeamId}}.svg" height="20"> {{ .TeamName}}</th>
                                    <th width="10%">{{ .PlayedGames}}</th>
                                    <th width="10%">{{ .Won}}</th>
                                    <th width="10%">{{ .Draw}}</th>
                                    <th width="10%">{{ .Lost}}</th>
                                    <th width="10%">{{ .Points}}</th>
                                </tr>
                            </tbody>
                            {{ end }}
                        </table>
                    </div>
                </div>
            </div>
        </section>
        <!-- scorers section-->
        <section id="scorers">
            <div class="container px-4">
                <div class="row gx-4 justify-content-center">
                    <div class="col-lg-8">
                        <h2>イングランド・プレミアリーグ{{(index .MatchList 0).StartDate}}-{{(index .MatchList 0).EndDate}}シーズン ゴールスコア</h2>
                        <table>
                            <thead>
                                <tr align="left">
                                    <th width="10%">順位</th>
                                    <th width="25%">選手名</th>
                                    <th width="25%">国籍</th>
                                    <th width="30%">所属チーム名</th>
                                    <th width="10%">ゴール数</th>
                                </tr>
                            </thead>
                            {{ range .ScoreList }}
                            <tbody>
                                <tr align="left">
                                    <th width="10%">{{.Standings}}</th>
                                    <th width="25%">{{.PlayerName}}</th>
                                    <th width="25%">{{.Nationality}}</th>
                                    <th width="30%"><img src="https://crests.football-data.org/{{.TeamId}}.svg" height="20"> {{ .TeamName}}</th>
                                    <th width="10%">{{.Goals}}</th>
                                </tr>
                            </tbody>
                            {{ end }}
                        </table>
                    </div>
                </div>
            </div>
        </section>
        <div class="container mb-4 text-center">
            <a class="btn btn-sm btn-success" href="#page-top">ページトップに戻る</a>
        </div>
        <!-- Bootstrap core JS-->
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
        <!-- Core theme JS-->
        <script src="assets/js/scripts.js"></script>
        <!-- Font Awesome -->
        <script src="https://kit.fontawesome.com/e6a049339c.js" crossorigin="anonymous"></script>
    </body>
</html>

バックエンドの実装

main.go

package main

import (
	"football/cmd/match"
	"log"
	"net/http"
	"os"
	"strings"
	"text/template"
)

func main() {

	// ルーティングパス設定
	dir, _ := os.Getwd()
	dir2 := strings.ReplaceAll(dir, "/cmd", "")

	// ルーティング
	http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir(dir2+"/assets"))))
	http.HandleFunc("/", index)

	// ListenAndServe
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

// メイン画面表示
func index(w http.ResponseWriter, r *http.Request) {

	// 試合情報取得
	p := match.GetMatchData(r, false)

	// 画面レンダリング
	renderTemplate(w, "index", p)
}

// 画面レンダリング
func renderTemplate(w http.ResponseWriter, tmpl string, p match.Page) {
	t := template.Must(template.ParseFiles(
		"../assets/" + tmpl + ".html",
	))
	t.ExecuteTemplate(w, tmpl+".html", p)
}

getData.go

package match

import (
	"encoding/json"
	"io/ioutil"
	"log"
	"net/http"
)

type Page struct {
	Title         string
	SubTitle      string
	MatchList     MatchLists
	ScoreList     ScoreLists
	StandingsList StandingsLists
}

const (
	SITE_TITLE string = "LIVESCORE"
	BLUNK      string = ""
)

func (p Page) MarshalBinary() ([]byte, error) {
	return json.Marshal(p)
}

// 試合データ取得
func GetMatchData(r *http.Request, fstLoginFlg bool) Page {

	c1 := make(chan MatchLists)
	c2 := make(chan ScoreLists)
	c3 := make(chan StandingsLists)

	// スケジュール取得
	go Schedule(c1)
	// ゴールスコア取得
	go Score(c2)
	// 順位表取得
	go Standings(c3)

	// 取得結果を一つにまとめる
	p := Page{SITE_TITLE, BLUNK, <-c1, <-c2, <-c3}

	return p
}

func ApiCall(param string) []byte {

	url := "https://api.football-data.org/v4/competitions/PL/" + param
	authHeaderName := "X-Auth-Token"
	authHeaderValue := "APIトークンをここに入力"

	req, _ := http.NewRequest(http.MethodGet, url, nil)
	req.Header.Set(authHeaderName, authHeaderValue)

	client := new(http.Client)
	resp, err := client.Do(req)
	if err != nil {
		log.Println("Error Request:", err.Error())
	}

	if resp.StatusCode != 200 {
		log.Println("Error Response:", resp.Status)
	}

	defer resp.Body.Close()

	byteArray, _ := ioutil.ReadAll(resp.Body)
	jsonBytes := ([]byte)(byteArray)

	return jsonBytes
}

※変数「authHeaderValue」に、事前準備で入手したAPIトークンを設定ください。

schedule.go

package match

import (
	"fmt"
	"strconv"
	"time"

	"github.com/tidwall/gjson"
)

type MatchList struct {
	Month           int
	Day             int
	WDays           string
	StartTime       string
	HomeTeamId      int
	AwayTeamId      int
	HomeTeamName    string
	AwayTeamName    string
	CurrentMatchday int
	StartDate       string
	EndDate         string
	Matchday        int
	HomeScore       int
	AwayScore       int
}

type GetSecretValueOutput struct {
	SecretString string
}

type MatchLists []MatchList

// スケジュール取得
func Schedule(ch chan MatchLists) {

	var rtnLists MatchLists

	jsonBytes := ApiCall("matches")
	games_num := int(gjson.GetBytes(jsonBytes, "resultSet.count").Int())
	wdays := []string{"日", "月", "火", "水", "木", "金", "土"}
	jst, _ := time.LoadLocation("Asia/Tokyo")

	for i := 1; i < games_num; i++ {

		arg := "matches." + strconv.Itoa(i)
		jdate := gjson.GetBytes(jsonBytes, arg+".utcDate").Time().In(jst)
		hour := fmt.Sprintf("%02d", jdate.Hour())
		minute := fmt.Sprintf("%02d", jdate.Minute())
		startTime := hour + ":" + minute

		homeTeamId := int(gjson.GetBytes(jsonBytes, arg+".homeTeam.id").Int())
		awayTeamId := int(gjson.GetBytes(jsonBytes, arg+".awayTeam.id").Int())
		homeTeamName := gjson.GetBytes(jsonBytes, arg+".homeTeam.name").String()
		awayTeamName := gjson.GetBytes(jsonBytes, arg+".awayTeam.name").String()
		currentMatchday := int(gjson.GetBytes(jsonBytes, arg+".season.currentMatchday").Int())
		startDate := gjson.GetBytes(jsonBytes, arg+".season.startDate").String()
		endDate := gjson.GetBytes(jsonBytes, arg+".season.endDate").String()
		matchday := int(gjson.GetBytes(jsonBytes, arg+".matchday").Int())
		homeScore := int(gjson.GetBytes(jsonBytes, arg+".score.fullTime.home").Int())
		awayScore := int(gjson.GetBytes(jsonBytes, arg+".score.fullTime.away").Int())

		rtnList := MatchList{
			int(jdate.Month()),
			int(jdate.Day()),
			wdays[jdate.Weekday()],
			startTime,
			homeTeamId,
			awayTeamId,
			homeTeamName,
			awayTeamName,
			currentMatchday,
			startDate[2:4],
			endDate[2:4],
			matchday,
			homeScore,
			awayScore}

		rtnLists = append(rtnLists, rtnList)
	}
	ch <- rtnLists
}

※APIから受け取ったJSONデータから試合スケジュール(APIコール時点の第◯節分)を抜き出しています。
※抜き出す際は「GJSON」と呼ばれるGoライブラリでJSONデータをパースしています。

score.go

package match

import (
	"strconv"

	"github.com/tidwall/gjson"
)

type ScoreList struct {
	Standings   int
	PlayerName  string
	Nationality string
	TeamId      int
	TeamName    string
	Goals       string
}

type ScoreLists []*ScoreList

// ゴールスコア取得
func Score(ch chan ScoreLists) {

	var rtnLists ScoreLists

	jsonBytes := ApiCall("scorers")
	games_num := int(gjson.GetBytes(jsonBytes, "count").Int())

	for i := 0; i < games_num; i++ {

		arg := "scorers." + strconv.Itoa(i)

		playerName := gjson.GetBytes(jsonBytes, arg+".player.name").String()
		nationality := gjson.GetBytes(jsonBytes, arg+".player.nationality").String()
		teamId := int(gjson.GetBytes(jsonBytes, arg+".team.id").Int())
		teamName := gjson.GetBytes(jsonBytes, arg+".team.name").String()
		goals := gjson.GetBytes(jsonBytes, arg+".goals").String()

		rtnList := ScoreList{
			i + 1,
			playerName,
			nationality,
			teamId,
			teamName,
			goals}

		rtnLists = append(rtnLists, &rtnList)
	}
	ch <- rtnLists
}

※APIから受け取ったJSONデータからゴールスコア(APIコール時点のゴールスコア上位10名分)を抜き出しています。
※こちらも同様に「GJSON」と呼ばれるGoライブラリでJSONデータをパースしています。

standings.go

package match

import (
	"strconv"

	"github.com/tidwall/gjson"
)

type StandingsList struct {
	Position    int
	TeamId      int
	TeamName    string
	PlayedGames int
	Won         int
	Draw        int
	Lost        int
	Points      int
}

type StandingsLists []StandingsList

// 順位表取得
func Standings(ch chan StandingsLists) {

	var rtnLists StandingsLists
	jsonBytes := ApiCall("standings")

	for i := 0; i < 20; i++ {

		arg := "standings.0.table." + strconv.Itoa(i)

		position := int(gjson.GetBytes(jsonBytes, arg+".position").Int())
		teamId := int(gjson.GetBytes(jsonBytes, arg+".team.id").Int())
		teamName := gjson.GetBytes(jsonBytes, arg+".team.name").String()
		playedGames := int(gjson.GetBytes(jsonBytes, arg+".playedGames").Int())
		won := int(gjson.GetBytes(jsonBytes, arg+".won").Int())
		draw := int(gjson.GetBytes(jsonBytes, arg+".draw").Int())
		lost := int(gjson.GetBytes(jsonBytes, arg+".lost").Int())
		points := int(gjson.GetBytes(jsonBytes, arg+".points").Int())

		rtnList := StandingsList{
			position,
			teamId,
			teamName,
			playedGames,
			won,
			draw,
			lost,
			points}

		rtnLists = append(rtnLists, rtnList)
	}
	ch <- rtnLists
}

※APIから受け取ったJSONデータからゴールスコア(APIコール時点の第○節までの順位)を抜き出しています。
※こちらも同様に「GJSON」と呼ばれるGoライブラリでJSONデータをパースしています。

稼働確認

ターミナルからmain.goを実行する。

go run main.go

ブラウザからローカルホストにアクセスする。

http://localhost:8080/

以下、稼働結果
試合スケジュール
スクリーンショット 2023-02-20 7.58.41.png
順位表
スクリーンショット 2023-02-20 7.59.17.png
ゴールスコア
スクリーンショット 2023-02-20 8.00.41.png

最後に

リアルタイムに情報を取得してくれますし、個人で使う分には良いかもしれません。
ただ、無償版はAPIコール回数にキツイ制限があるため、そこのところを気をつけましょう。
(処理を工夫or有償版or他API検討などして)

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3