はじめに
私があまり知らないだけかもしれないが、
海外サッカーリーグの試合情報(スケジュールや各種スタッツ等)を取り扱う国産サービスってあまり無い気がします。
(大手だと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」をクリック。
② 名前(適当でOK)とメールアドレスを入力します。
「Choose ur weapon if you like to:」は「Golang」を選択し、「Create account」をクリック。
③ ②で入力したメールアドレス宛にAPIトークンが送られたことを確認する。
これで事前準備は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/
最後に
リアルタイムに情報を取得してくれますし、個人で使う分には良いかもしれません。
ただ、無償版はAPIコール回数にキツイ制限があるため、そこのところを気をつけましょう。
(処理を工夫or有償版or他API検討などして)