Goのプログラム内でjqを使ってJSONを加工する

## はじめに

JSONデータをフィルタしたり、集計したりといった加工に`jq`コマンドは便利ですよね？

Goでプログラムを書いた場合も、プログラムの中でJSONを扱うケースは多々あると思いますが、
JSONの構造に合わせて構造体を定義してマッピングしたり、ちょっとした成形をしたりと面倒なことがあると思います。

今回は、`gojq`を使ってGoのプログラムの中でjqを使ってみましょう。
`gojq`はGoで書かれたjqコマンド実装で、Goのライブラリとしても使用できます。

gojq
https://github.com/itchyny/gojq


## 題材

ホットペッパーWebサービスに含まれる「グルメサーチAPI」の出力結果を一部取り出すGoプログラムを書いてみます。
https://webservice.recruit.co.jp/doc/hotpepper/reference.html#1

ホットペッパーWebサービスを呼び出す場合は、ユーザ登録が必要です。
「プライバシーポリシー」と「リクルートWEBサービス 利用規約」に同意し、メールアドレス認証を行います。

https://webservice.recruit.co.jp/register/

メールアドレス認証を行うと、APIキーが払い出されます。


## まずは cURL と jq で項目を抽出してみる

cURL と jq を使う場合のイメージはこんな感じです。

ホットペッパーのグルメサーチAPIを呼び出してJSON形式のレスポンスを取得します。
札幌時計台の緯度・経度と、キーワード「クラフトビール」を指定して周辺の掲載店を5件検索しています。


```
$ API_KEY=<YOUR_API_KEY>
$ curl -s "http://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=${API_KEY}&format=json&lat=43.06279741091979&lng=141.35356677093293&range=3&count=5&order=4&keyword=クラフトビール" |  jq '.'
```

グルメサーチAPIの応答を一部抜粋するとこんな形です。

```json
{
  "results": {
    "api_version": "1.26",
    "results_available": 15,
    "results_returned": "2",
    "results_start": 1,
    "shop": [
      {
        "access": "すすきの駅徒歩1分。松岡ビル１階の路面店です。",
        "address": "北海道札幌市中央区南4条西4丁目松岡ビル1F",
        "genre": {
          "catch": "約30種の樽生クラフトビール＆本格料理",
          "code": "G002",
          "name": "ダイニングバー・バル"
        },
        "id": "J001144710",
        "lat": 43.0558780557,
        "lng": 141.3525926045,
        "logo_image": "https://imgfp.hotp.jp/IMGH/84/63/P028438463/P028438463_69.jpg",
        "name": "THE CRAFT ザ クラフト",
        "name_kana": "ざ　くらふと　ザ　クラフト　すぱにっしゅあんどいたりあん",
        "photo": {
          "mobile": {
            "l": "https://imgfp.hotp.jp/IMGH/08/34/P035420834/P035420834_168.jpg",
            "s": "https://imgfp.hotp.jp/IMGH/08/34/P035420834/P035420834_100.jpg"
          },
          "pc": {
            "l": "https://imgfp.hotp.jp/IMGH/08/34/P035420834/P035420834_238.jpg",
            "m": "https://imgfp.hotp.jp/IMGH/08/34/P035420834/P035420834_168.jpg",
            "s": "https://imgfp.hotp.jp/IMGH/08/34/P035420834/P035420834_58_s.jpg"
          }
        },
        "station_name": "すすきの",
        "sub_genre": {
          "code": "G006",
          "name": "イタリアン・フレンチ"
        },
        "urls": {
          "pc": "https://www.hotpepper.jp/strJ001144710/?vos=nhppalsa000016"
        },
      },
      ...(件数分続く)...
    ],
  }
}
```


ここでjqを使って項目を抜粋します。genreの下のcatchも抽出します。

```
$ curl -s "http://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=${API_KEY}&format=json&lat=43.06279741091979&lng=141.35356677093293&range=3&count=5&order=4&keyword=クラフトビール" | \
jq '.results.shop[] | {name:.name, genre_catch:.genre.catch, address:.address, access:.access, latitude:.lat, longitude:.lng}'
{
  "name": "THE CRAFT ザ クラフト",
  "genre_catch": "約30種の樽生クラフトビール＆本格料理",
  "address": "北海道札幌市中央区南4条西4丁目松岡ビル1F",
  "access": "すすきの駅徒歩1分。松岡ビル１階の路面店です。",
  "latitude": 43.0558780557,
  "longitude": 141.3525926045
}
{
  "name": "札幌キッチン SAPPORO KITCHEN",
  "genre_catch": "札幌・ダイニングバー・貸切・パーティ",
  "address": "北海道札幌市中央区北１条西３－３－２４ 札幌中央ビル１・２F",
  "access": "札幌市営地下鉄南北線，札幌市営地下鉄東西線，札幌市営地下鉄東豊線大通駅6出口より徒歩約4分",
  "latitude": 43.0628577388,
  "longitude": 141.3518686377
}
...
```


欲しい項目だけフラットな形で抜き出すことができました。


## Goプログラムで同じことをしてみる

まずは、gojqをインストールします。

```
go install github.com/itchyny/gojq/cmd/gojq@latest
```

Goプログラムはこんな感じです。

```src/main.go
// Powered by <a href="http://webservice.recruit.co.jp/">ホットペッパー Webサービス</a>

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"

	"github.com/itchyny/gojq"
)

const (
	URL_TEMPLATE = "http://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=%s&format=json&lat=%s&lng=%s&range=3&count=%d&order=4&keyword=%s"
	API_KEY      = "<YOUR_API_KEY>"
	LATITUDE     = "43.06279741091979"  // 検索起点の緯度
	LONGITUDE    = "141.35356677093293" // 検索起点の経度
	COUNT        = 5
	KEYOWORD     = "クラフトビール"
)

const JQ_QUERY = ".results.shop[] | {name:.name, address:.address,access:.access, genre_catch:.genre.catch, latitude:.lat, longitude:.lng}"

type Shop struct {
	Name       string  `json:"name"`
	Address    string  `json:"address"`
	GenreCatch string  `json:"genre_catch"`
	Latitude   float64 `json:"latitude"`
	Longitude  float64 `json:"longitude"`
}

func main() {
	url := fmt.Sprintf(URL_TEMPLATE, API_KEY, LATITUDE, LONGITUDE, COUNT, KEYOWORD)
	resp, err := http.Get(url)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}

	// (1) グルメサーチAPIのレスポンスJSONをパースしてinterface{}の型に格納する
	var apiResult interface{}
	if err := json.Unmarshal(body, &apiResult); err != nil {
		panic(err)
	}

	shopArr := []Shop{}

	// (2) jqのクエリ文字列をパースして、実行する
	query, err := gojq.Parse(JQ_QUERY)
	iter := query.Run(apiResult)
	for {
		v, ok := iter.Next()
		if !ok {
			break
		}

		result := v.(map[string]interface{})
		fmt.Printf("---------\n")
		fmt.Printf("name:%s\n", result["name"].(string))
		fmt.Printf("access:%s\n", result["access"].(string))
		fmt.Printf("catch:%s\n", result["genre_catch"].(string))
		fmt.Printf("address:%s\n", result["address"].(string))
		fmt.Printf("latitude:%g\n", result["latitude"].(float64))
		fmt.Printf("longitude:%g\n", result["longitude"].(float64))

		// (3) map[string]interface{} から struct に変換する
		jsonBytes, err := json.Marshal(v)
		if err != nil {
			fmt.Println(err)
		}
		//fmt.Printf("json: %s\n", jsonBytes)
		var shop Shop
		if err := json.Unmarshal(jsonBytes, &shop); err != nil {
			fmt.Println(err)
		}

		shopArr = append(shopArr, shop)
	}

	//fmt.Printf("-----\n")
	//fmt.Printf("shopArr: %#v\n", shopArr)

}
```

(1)の部分で、APIのレスポンスボディ(JSON)をパースして`interface{}`の形にします。

(2)の部分で(1)の結果に対してgojqのクエリを実行し、結果をイテレータの形で取得します。

gojqの各結果行は`map[string]interface{}`の形で取り出せるので、(3)の部分で、JSONの形にしてから独自定義のstructにマッピングしています。
今回のjqの結果はフラットな構造のJSONなので、リフレクションを使ってstructにマッピングしても良いでしょう。


では、実行してみましょう。

```
$ cd src
$ go run main.go
---------
name:THE CRAFT ザ クラフト
access:すすきの駅徒歩1分。松岡ビル１階の路面店です。
catch:約30種の樽生クラフトビール＆本格料理
address:北海道札幌市中央区南4条西4丁目松岡ビル1F
latitude:43.0558780557
longitude:141.3525926045
---------
name:札幌キッチン SAPPORO KITCHEN
access:札幌市営地下鉄南北線，札幌市営地下鉄東西線，札幌市営地下鉄東豊線大通駅6出口より徒歩約4分
catch:札幌・ダイニングバー・貸切・パーティ
address:北海道札幌市中央区北１条西３－３－２４ 札幌中央ビル１・２F
latitude:43.0628577388
longitude:141.3518686377
---------
name:BistroBON tabloid table ビストロボン タブロイドテーブル
access:地下鉄南北線さっぽろ駅より徒歩2分/チカホ直結“sitatte sapporo”1F
catch:ワイン＆クラフトビール　本格肉ビストロ
address:北海道札幌市中央区北２条西３丁目 札幌フコク生命越山ビル 1F/sitattesapporoビル 1F
latitude:43.064226175
longitude:141.3516066346
---------
name:和食バル 和 およばれ
access:地下鉄南北線「大通」駅徒歩３分/「すすきの」駅徒歩３分/狸小路５丁目 ホテルビスタ札幌大通1F
catch:北海道名物×創作料理　狸小路で昼飲み♪
address:北海道札幌市中央区南３条西５丁目１６番地１Ｆ
latitude:43.0566692888
longitude:141.3507624483
---------
name:クラフトビア食堂 VOLTA ボルタ
access:南北線すすきの駅徒歩3分
catch:NET予約した方限定でお得な情報をDMで配信
address:北海道札幌市中央区南４条西６丁目８－３ 晴ばれビル１Ｆ
latitude:43.0555643836
longitude:141.3495476277
```

うまく取得することができました。

## 終わりに

jqコマンドはフィルタや集計関数など便利な関数が用意されています。
Goプログラムの中でごにょごにょやるよりも、jqをうまく使ってシンプルにクエリーに記載できると少し楽ができそうです。

以上、Goの小ネタでした。