面倒なことは機械にやらせよう
こんな記事は死ぬほどあるけど他に書くこともない
動機
研究でめっちゃデータ取ってる。もともとデータの集計も手作業でやる系だったんだけど、この部分はシェルスクリプトとかで自動化してMarkdownの表を生成するまではやってた。しかし報告するときにMarkdownの表は見づらいみたいなことをBOSSと同期に言われたので集めてるデータをGoogle Spreadsheetで管理しようと思った。
今見たらこのMarkdownの表、自分でみてもわかりにくい
要件
書き込みたいデータ
double
int
int
int
int
int
int
int
int
int
先頭が少数、その後に9つの整数が縦に続く。これを書き込みたい。
いろんなパラメータを書く欄があるのでカラムはEから始まる。行は4から。Zまで書き込んだあとはEへ戻って下の行へ移動する。
できれば次の行への移動とかも自動でやってほしい。
使う言語
最近GoをはじめたのでGoでやってみようと思った。SpreadSheetはAPIを公開してて、Go向けにもライブラリがあったのでこれでいいやみたいな
developers.google.comhttps://developers.google.com/sheets/api/quickstart/go
作ってみる
ためす
まずは試す。
qiita.com
この解説を一通りやってみると大体の感覚が解る。ちなみにAPIを試すだけならAPIのリファレンスの各ページからも試せる。API試せるのめっちゃありがたい
リファレンスだと自動入力が効いてるのでRequestのbodyとかの書き方をチェックするのにも使えて最高
メソッドを選ぶ
developers.google.comhttps://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate
今回はspreadsheets.values.batchUpdate
を選んだUpdateをまとめてできるらしい。今の所1回につき1セットのデータしか書き込まないけど、そのうち拡張するかもしれないのでこっちにしておいた。
body
をみていると、data
っていうところがあった。data便利な単語だと思う。
それで、data
にはValueRange
型を好きな数書くといいみたい。ValueRange
型についてはリファレンスとか解説記事を見るとわかやすい
qiita.com
developers.google.comhttps://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values#ValueRange
今回最低限書き込みに必要な情報だけまとめるとこうなった
{
"valueInputOption": "USER_ENTERED",
"spreadsheetId": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
"responses": [
{
"range":"E4:E13",
"majorDimension":"COLUMNS",
"values":[
double,
int,
int,
int,
int,
int,
int,
int,
int,
int,
],
},
],
}
これをライブラリを使って投げてやればいいだけですね
実装してみる
とりあえずリファレンスのQuickStartをコピペ。かいてあるとおりにやったらAPIにアクセスできるようになった。便利な世の中やでホンマ
ここからはgoを雰囲気でかいているところが散見するのでマサカリが有ったらブンブン投げてほしいところ
変更したところはgetClient
を少し変えたぐらい
func getClient(ctx context.Context,credentialFile string) *http.Client {
b, err := ioutil.ReadFile(credentialFile)
if err != nil {
log.Fatalf("Unable to read client secret file: %v", err)
}
config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets")
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}
tokFile := "token.json"
tok, err := tokenFromFile(tokFile)
if err != nil {
tok = getTokenFromWeb(config)
saveToken(tokFile, tok)
}
return config.Client(ctx, tok)
}
context
を受け取れるようにしただけw
次にmain
部分をかいていく
func main() {
spreadsheetId := ""
ctx := context.Background()
client := getClient(ctx,"client_secret.json")
sheetService, err := sheets.New(client)
if err != nil{
log.Fatal(err)
}
data := []*sheets.ValueRange{
{
Range: "E4:E13"
Values: [][]interface{}{
0.123,
1,
2,
3,
4,
5,
6,
7,
8,
9,
},
MajorDimension: "COLUMNS",
},
}
reqest := &sheets.BatchUpdateValuesRequest{
ValueInputOption: "USER_ENTERED",
Data: data,
}
res, err := sheetService.Spreadsheets.Values.BatchUpdate(spreadsheetId,reqest).Context(ctx).Do()
if err != nil{
log.Fatal(err)
}
fmt.Printf("%#v\n",res)
}
typescript
をかいてたときも思ったけど。こうやってソースの途中にJSONみたいな記述ができるってのはすごく良いと思う。どう良いかっていうのは説明できない
とりあえずこのままgo run
すれば
0.123
1
2
3
4
5
6
7
8
9
みたいなデータがかけるようになったうれしい
もっと自動化する
書き込めて嬉しいけどこれだと書き込みたいデータが変わるごとにソースを更新しないといけない。しかも手作業
ぼくは人間なので手作業をN回すれば絶対1度はミスを犯すのでここも自動にしたいわけです
とりあえずbodyをJSONから読み込めるようにします。
type Body struct {
Start int `json:"Start"`
End int `json:"End"`
Column string `json:"Column"`
Data []interface{} `json:"Data"`
}
これはencoding/json
パッケージを使えば下みたいなJSONファイルと簡単にやり取りできます
{
"Start":4,
"End":13,
"Column":"E",
"Data":[
1,2,3,4,4,5,6,7,8,9
]
}
goはこういうところが良い感じですね。すき
リクエストを送るときは書き込みたいデータが既にまとまっているとして、終了時にはリセットしておきます。書き込みたいデータはシェルスクリプトからjq
を使って書き込むことにします
つまり
書き込みたいとき
{
"Start":4,
"End":13,
"Column":"E",
"Data":[
1,2,3,4,4,5,6,7,8,9
]
}
終わるとき。Data
にはjq
でデータを書き込むことにします
{
"Start":4,
"End":13,
"Column":"F",
"Data":[]
}
こうしたいので下みたいに書いてみたよ
func writeNext(b *Body) {
cur := b.Column
var next string
st := b.Start
ed := b.End
if cur == "Z" {
next = "E"
st = ed + 2
ed = st + 9
} else {
next = string([]byte(cur)[0]+1)
}
wd := Body{
Column: next,
Data: []interface{}{},
Start: st,
End: ed,
}
path := "next.json"
jb, err := json.Marshal(wd)
if err != nil{
log.Fatal(err)
}
ioutil.WriteFile(path,jb,0644)
}
読み込み側はこうだな
func readNextJson() *Body{
path := "next.json"
b, err := ioutil.ReadFile(path)
if err != nil {
log.Fatal(err)
}
var readData Body
if err := json.Unmarshal(b,&readData); err != nil{
log.Fatal(err)
}
return &readData
}
こうやって修正したmain
がこういう感じになった
func main() {
spreadsheetId := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
ctx := context.Background()
client := getClient(ctx,"client_secret.json")
sheetService, err := sheets.New(client)
if err != nil{
log.Fatal(err)
}
rj := readNextJson()
data := []*sheets.ValueRange{
{
Range: fmt.Sprintf("%s%d:%s%d",rj.Column,rj.Start,rj.Column,rj.End),
Values: [][]interface{}{
rj.Data,
},
MajorDimension: "COLUMNS",
},
}
reqest := &sheets.BatchUpdateValuesRequest{
ValueInputOption: "USER_ENTERED",
Data: data,
}
res, err := sheetService.Spreadsheets.Values.BatchUpdate(spreadsheetId,reqest).Context(ctx).Do()
if err != nil{
log.Fatal(err)
}
fmt.Printf("%#v\n",res)
writeNext(rj)
}
あとは好きなタイミングでこれをgo run
すればいいですね。はー楽になった
終わり
Vim向けのGoプラグインが優秀なこともあって、Goはなかなか楽に書けるな〜と思いました。そのうちデータの集計とかもGoにまとめたいですね