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


# やったこと

一ヶ月ちょっと前にPython 3.5対応したMLB野球データスクリプト「[py-retrosheet](https://github.com/Shinichi-Nakagawa/py-retrosheet "py-retrosheet")」をgolangで写経してみました。

[go-retrosheet](https://github.com/Shinichi-Nakagawa/go-retorosheet "go-retrosheet")

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

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

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

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

理由は２つ。

* [Go Conference 2015 Winter](http://eventdots.jp/event/573121 "Go Conference 2015 Winter")参加記念
* Golangをちゃんと学びたかった

[前回の#gocon](http://gocon.connpass.com/event/14063/ "Go Conference 2015 summer")では、「せっかく行けるのにGolangやったことないのもアレなのでGolangで何か作って飛び入りLTしよう！」というモチベーションで[半日でA Tour of GoやってDjangoのアプリケーションをMartiniで書き換えて見事飛び入りLTをキメた](http://shinyorke.hatenablog.com/entry/2015/06/21/195656 "Golangはじめて半日後に野球のアプリ書いてLTしてきた ~ Go Conference 2015 summer Report #gocon")訳ですが、考えてみればそれ以来Golang書いてないやと思い（途中PyConもあったし。。。）、冬の#gocon参加記念って事で今回も書いてやるぜ！って感じで着手しました。LTは、、、間に合えばやろうぐらいに考えてました。

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

* Goの並列処理が〜

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

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

## 作ったもの

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

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

### download.go

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

zipの解凍をするコードは[こちら](https://gist.github.com/hnaohiro/4572580 "Golangでzipファイルを解凍するサンプル")を参考に書いた&ちゃんと動いて感謝！なのですが、もっとスマートに書けないのかしら？と思いました。

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

```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](http://chadwick.sourceforge.net/doc/index.html, "chadwick")という、スコアデータ専用のライブラリのコマンドを叩いてデータセットを作るスクリプトです。

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

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



```go: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を使ったことないのでそのお勉強もこのライブラリでしようと思います。