たいちょーの雑記

ぼくが3日に一度くらい雑記をかくところ

GoでGoogle Spreadsheetにデータを書き込みたい

面倒なことは機械にやらせよう

こんな記事は死ぬほどあるけど他に書くこともない

動機

研究でめっちゃデータ取ってる。もともとデータの集計も手作業でやる系だったんだけど、この部分はシェルスクリプトとかで自動化して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にまとめたいですね