やったこと
一ヶ月ちょっと前にPython 3.5対応したMLB野球データスクリプト「py-retrosheet」をgolangで写経してみました。
Retrosheet(http://www.retrosheet.org/) 上にある、試合データと成績(主に打席のデータ)をダウンロード、集計や分析に使えるレベルまでParseしてCSVを吐き出す、というスクリプトです。
オリジナルの本当はDatabase(MySQLとか)に突っ込む機能もあるのですが、これは後日作ります(理由:睡眠時間削りたくないンゴ...w)。
どうでもいいですけど、「Go! Retrosheet」って名前に勢いがあってかっこいいですね(棒読み)
なぜGolangで作ろうと思ったのか
理由は2つ。
- Go Conference 2015 Winter参加記念
- Golangをちゃんと学びたかった
前回の#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で並列処理を書いたのですが、こんなにスッキリ書けるとは驚きでした。
// 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")」的なうまい書き方を知らなかったのもある)、つまらんハマり方したりと苦戦しました。
// 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を使ったことないのでそのお勉強もこのライブラリでしようと思います。