LoginSignup
3
3

More than 5 years have passed since last update.

【野球Hack】Python製MLBスコア&成績データ取得スクリプトを半日でGoで写経してみた

Last updated at Posted at 2015-12-06

やったこと

一ヶ月ちょっと前にPython 3.5対応したMLB野球データスクリプト「py-retrosheet」をgolangで写経してみました。

go-retrosheet

Retrosheet(http://www.retrosheet.org/) 上にある、試合データと成績(主に打席のデータ)をダウンロード、集計や分析に使えるレベルまでParseしてCSVを吐き出す、というスクリプトです。

オリジナルの本当はDatabase(MySQLとか)に突っ込む機能もあるのですが、これは後日作ります(理由:睡眠時間削りたくないンゴ...w)。

どうでもいいですけど、「Go! Retrosheet」って名前に勢いがあってかっこいいですね(棒読み)

なぜGolangで作ろうと思ったのか

理由は2つ。

前回の#goconでは、「せっかく行けるのにGolangやったことないのもアレなのでGolangで何か作って飛び入りLTしよう!」というモチベーションで半日でA Tour of GoやってDjangoのアプリケーションをMartiniで書き換えて見事飛び入りLTをキメた訳ですが、考えてみればそれ以来Golang書いてないやと思い(途中PyConもあったし。。。)、冬の#gocon参加記念って事で今回も書いてやるぜ!って感じで着手しました。LTは、、、間に合えばやろうぐらいに考えてました。

また、真面目な理由として、

  • Goの並列処理が〜

  • Goって◯◯に強いから野球もGoで〜

...と語れるほどGo知らない(というよりバリバリの初心者)なので、再び写経からのGoの良さとか特徴を再確認しよう!という「学習」の意味も含まれています。

作ったもの

gofmt, golintは通していますが全体的にコメントが少ない&適当かも...有識者の皆様、意見ツッコミお待ちしておりますm(_ _)m

あと、テストは書いてないのでバリバリのレガシーコードです。

download.go

RetrosheetのCSVを含んだアーカイブ(zipファイル)をfilesディレクトリ下にダウンロード、解凍するまでを行うコードです。

zipの解凍をするコードはこちらを参考に書いた&ちゃんと動いて感謝!なのですが、もっとスマートに書けないのかしら?と思いました。

あと、初めてGoで並列処理を書いたのですが、こんなにスッキリ書けるとは驚きでした。

download.go
// Copyright  The Shinichi Nakagawa. All rights reserved.
// license that can be found in the LICENSE file.

package main

import (
    "archive/zip"
    "flag"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "os"
    "path"
    "path/filepath"
    "sync"
)

const (
    // DirName a saved download file directory
    DirName = "files"
)

// IsExist return a file exist
func IsExist(filename string) bool {

    // Exists check
    _, err := os.Stat(filename)
    return err == nil
}

// MakeWorkDirectory a make work directory
func MakeWorkDirectory(dirname string) {
    if IsExist(dirname) {
        return
    }
    os.MkdirAll(dirname, 0777)
}

// DownloadArchives a download files for retrosheet
func DownloadArchives(url string, dirname string) string {

    // get archives
    fmt.Println(fmt.Sprintf("download: %s", url))
    response, err := http.Get(url)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println(fmt.Sprintf("status: %s", response.Status))

    // download
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    _, filename := path.Split(url)
    fmt.Println(filename)
    fullfilename := fmt.Sprintf("%s/%s", dirname, filename)
    file, err := os.OpenFile(fullfilename, os.O_CREATE|os.O_WRONLY, 0777)
    if err != nil {
        fmt.Println(err)
    }
    defer func() {
        file.Close()
    }()

    file.Write(body)

    return fullfilename

}

// Unzip return error a open read & write archives
func Unzip(fullfilename string, outputdirectory string) error {
    r, err := zip.OpenReader(fullfilename)
    if err != nil {
        return err
    }
    defer r.Close()
    for _, f := range r.File {
        rc, err := f.Open()
        if err != nil {
            return err
        }
        defer rc.Close()

        path := filepath.Join(outputdirectory, f.Name)
        if f.FileInfo().IsDir() {
            os.MkdirAll(path, f.Mode())
        } else {
            f, err := os.OpenFile(
                path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
            if err != nil {
                return err
            }
            defer f.Close()

            _, err = io.Copy(f, rc)
            if err != nil {
                return err
            }
        }
    }
    return nil
}

// GetEventsFileURL return a events file URL
func GetEventsFileURL(year int) string {
    return fmt.Sprintf("http://www.retrosheet.org/events/%deve.zip", year)
}

// GetGameLogsURL return a game logs URL
func GetGameLogsURL(year int) string {
    return fmt.Sprintf("http://www.retrosheet.org/gamelogs/gl%d.zip", year)
}

func main() {
    // Commandline Options
    var fromYear = flag.Int("f", 2010, "Season Year(From)")
    var toYear = flag.Int("t", 2014, "Season Year(To)")
    flag.Parse()

    MakeWorkDirectory(DirName)

    wait := new(sync.WaitGroup)
    // Generate URL
    urls := []string{}
    for year := *fromYear; year < *toYear+1; year++ {
        urls = append(urls, GetEventsFileURL(year))
        wait.Add(1)
        urls = append(urls, GetGameLogsURL(year))
        wait.Add(1)
    }

    // Download files
    for _, url := range urls {
        go func(url string) {
            fullfilename := DownloadArchives(url, DirName)
            err := Unzip(fullfilename, DirName)
            if err != nil {
                fmt.Println(err)
                os.Exit(1)
            }
            wait.Done()
        }(url)
    }
    wait.Wait()

}

dataset.go

chadwickという、スコアデータ専用のライブラリのコマンドを叩いてデータセットを作るスクリプトです。

chadwickライブラリに実装されている「cwgame」「cwevent」のコマンドを実行するのに苦戦しました。

Change Directoryしてコマンドを実行(引数のin/outは相対パス)ってところまではPython版と一緒なのですが、書きながらPathの設定がこんがらがったり、文字列のformatの引数が多くて(これは僕の設計ミスでもあるが、Pythonの「"{hoge}".format(hoge="fuga")」的なうまい書き方を知らなかったのもある)、つまらんハマり方したりと苦戦しました。

dataset.go
// Copyright  The Shinichi Nakagawa. All rights reserved.
// license that can be found in the LICENSE file.

package main

import (
    "flag"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "sync"
)

const (
    // ProjectRootDir : Project Root
    ProjectRootDir = "."
    // InputDirName : inputfile directory
    InputDirName = "./files"
    // OutputDirName : outputfile directory
    OutputDirName = "./csv"
    // CwPath : chadwick path
    CwPath = "/usr/local/bin"
    // CwEvent : cwevent command
    CwEvent = "%s/cwevent -q -n -f 0-96 -x 0-62 -y %d ./%d*.EV* > %s/events-%d.csv"
    // CwGame : cwgame command
    CwGame = "%s/cwgame -q -n -f 0-83 -y %d ./%d*.EV* > %s/games-%d.csv"
)

// ParseCsv a parse to eventfile(output:csv file)
func ParseCsv(command string, rootDir string, inputDir string) {
    os.Chdir(inputDir)
    out, err := exec.Command("sh", "-c", command).Output()

    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println(string(out))
    os.Chdir(rootDir)

}

func main() {
    // Commandline Options
    var fromYear = flag.Int("f", 2010, "Season Year(From)")
    var toYear = flag.Int("t", 2014, "Season Year(To)")
    flag.Parse()

    // path
    rootDir, _ := filepath.Abs(ProjectRootDir)
    inputDir, _ := filepath.Abs(InputDirName)
    outputDir := OutputDirName

    wait := new(sync.WaitGroup)
    // Generate URL
    commandList := []string{}
    for year := *fromYear; year < *toYear+1; year++ {
        commandList = append(commandList, fmt.Sprintf(CwEvent, CwPath, year, year, outputDir, year))
        wait.Add(1)
        commandList = append(commandList, fmt.Sprintf(CwGame, CwPath, year, year, outputDir, year))
        wait.Add(1)
    }
    for _, command := range commandList {
        fmt.Println(command)
        go func(command string) {
            ParseCsv(command, rootDir, inputDir)
            wait.Done()
        }(command)
    }
    wait.Wait()

}

成果と感想

思うようにデータセット(CSV)が作れるようになったので、うまくいったんじゃないかなと思います。

設計とか構成は改善の余地アリですが、Python版と較べてスッキリ書けてる気がします。

ちなみにエディターはIntelliJ IDEAのGoプラグインを使いました。

PyCharm含め、お布施を払って使ってる身なので使い倒そうと思って使ったのですが、やっぱいいですねー。今後もGoはIntelliJで行きます。

なお、先ほどv0.1を出したばっかり、という感じだったので飛び入りLTはやめにしました(悔しい)

宿題

データセットをMySQLに突っ込む実装まではやろうと思います。

その後、Python版とベンチとって比較したり、Docker上でGoを使ったことないのでそのお勉強もこのライブラリでしようと思います。

3
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
3
3