はじめに
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サービス 利用規約」に同意し、メールアドレス認証を行います。
メールアドレス認証を行うと、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の応答を一部抜粋するとこんな形です。
{
"results": {
"api_version": "1.26",
"results_available": 15,
"results_returned": "2",
"results_start": 1,
"shop": [
{
"access": "すすきの駅徒歩1分。松岡ビル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分。松岡ビル1階の路面店です。",
"latitude": 43.0558780557,
"longitude": 141.3525926045
}
{
"name": "札幌キッチン SAPPORO KITCHEN",
"genre_catch": "札幌・ダイニングバー・貸切・パーティ",
"address": "北海道札幌市中央区北1条西3-3-24 札幌中央ビル1・2F",
"access": "札幌市営地下鉄南北線,札幌市営地下鉄東西線,札幌市営地下鉄東豊線大通駅6出口より徒歩約4分",
"latitude": 43.0628577388,
"longitude": 141.3518686377
}
...
欲しい項目だけフラットな形で抜き出すことができました。
Goプログラムで同じことをしてみる
まずは、gojqをインストールします。
go install github.com/itchyny/gojq/cmd/gojq@latest
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分。松岡ビル1階の路面店です。
catch:約30種の樽生クラフトビール&本格料理
address:北海道札幌市中央区南4条西4丁目松岡ビル1F
latitude:43.0558780557
longitude:141.3525926045
---------
name:札幌キッチン SAPPORO KITCHEN
access:札幌市営地下鉄南北線,札幌市営地下鉄東西線,札幌市営地下鉄東豊線大通駅6出口より徒歩約4分
catch:札幌・ダイニングバー・貸切・パーティ
address:北海道札幌市中央区北1条西3-3-24 札幌中央ビル1・2F
latitude:43.0628577388
longitude:141.3518686377
---------
name:BistroBON tabloid table ビストロボン タブロイドテーブル
access:地下鉄南北線さっぽろ駅より徒歩2分/チカホ直結“sitatte sapporo”1F
catch:ワイン&クラフトビール 本格肉ビストロ
address:北海道札幌市中央区北2条西3丁目 札幌フコク生命越山ビル 1F/sitattesapporoビル 1F
latitude:43.064226175
longitude:141.3516066346
---------
name:和食バル 和 およばれ
access:地下鉄南北線「大通」駅徒歩3分/「すすきの」駅徒歩3分/狸小路5丁目 ホテルビスタ札幌大通1F
catch:北海道名物×創作料理 狸小路で昼飲み♪
address:北海道札幌市中央区南3条西5丁目16番地1F
latitude:43.0566692888
longitude:141.3507624483
---------
name:クラフトビア食堂 VOLTA ボルタ
access:南北線すすきの駅徒歩3分
catch:NET予約した方限定でお得な情報をDMで配信
address:北海道札幌市中央区南4条西6丁目8-3 晴ばれビル1F
latitude:43.0555643836
longitude:141.3495476277
うまく取得することができました。
終わりに
jqコマンドはフィルタや集計関数など便利な関数が用意されています。
Goプログラムの中でごにょごにょやるよりも、jqをうまく使ってシンプルにクエリーに記載できると少し楽ができそうです。
以上、Goの小ネタでした。