Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

GoでJSONから値を探索する時にgojqが便利だった

はじめに

こんにちは、サーバーサイドエンジニアの中川(@tkdev0728)です。
JSONから特定のキーを使って値を抽出するというユースケースは珍しくないと思います。言語やツールによっていくつかやり方は変わると思うのですが、 今回はリクエスト先と抽出用のキーが動的に変わるのでさまざまなパターンが想定されていました。
Goで抽出を行いたく、探索ロジックを自前で実装しなきゃいけないかなと思ったのですが調べてみるとgojqというライブラリがあることを知り使ってみました。 めちゃくちゃ便利だと思ったので今回はgojqについて紹介してみようと思います。

gojqとは

リポジトリはこちら

github.com

名前の通りですが、要はjqコマンドの内容をGoで使えるようにしたものです。一部違う点もありますが、基本的な使い方はjqと同じです。 jqコマンドについては本記事では割愛するので、詳しく知りたい方はjqコマンドのリポジトリを参照してください。

github.com

基本的な使い方

サンプルコード

package main

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

    "github.com/itchyny/gojq"
)

type Products interface{}

func main() {
    url := "https://dummyjson.com/products"
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        fmt.Println("Error: ", err)
    }
        
    client := new(http.Client)
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error: ", err)
    }

    if resp.StatusCode != 200 {
        fmt.Println("Error: ", resp.Status)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error: ", err)
    }
    var products Products
    json.Unmarshal(body, &products)

    query, err := gojq.Parse(".products[].title")
    if err != nil {
        fmt.Println("Error: ", err)
    }
    iter := query.Run(products)

    for {
        v, ok := iter.Next()
        if !ok {
            break
        }
        if err, ok := v.(error); ok {
            fmt.Println("Error: ", err)
            break
        }
        fmt.Printf("%#v\n", v)
    }
}

https://dummyjson.com/products にリクエストし、レスポンスのJSONからtitleキーの要素を一覧で取得します。 実行結果は以下です。

$ go run main.go 

"Essence Mascara Lash Princess"
"Eyeshadow Palette with Mirror"
"Powder Canister"
"Red Lipstick"
"Red Nail Polish"
"Calvin Klein CK One"
"Chanel Coco Noir Eau De"
"Dior J'adore"
"Dolce Shine Eau de"
"Gucci Bloom Eau de"
"Annibale Colombo Bed"
"Annibale Colombo Sofa"
"Bedside Table African Cherry"
"Knoll Saarinen Executive Conference Chair"
"Wooden Bathroom Sink With Mirror"
"Apple"
"Beef Steak"
"Cat Food"
"Chicken Meat"
"Cooking Oil"
"Cucumber"
"Dog Food"
"Eggs"
"Fish Steak"
"Green Bell Pepper"
"Green Chili Pepper"
"Honey Jar"
"Ice Cream"
"Juice"
"Kiwi"

上記は以下と同じです

$ curl https://dummyjson.com/products | jq '.products[].title'

仕組み

query, err := gojq.Parse(".products[].title")

JSONから抽出するためのクエリを作成するために、まずは抽出したい値に対応するキーを字句解析して意味のある最小単位(トークン)に分割します。 上記の例でいうと、.products[].title[ . , products , [] , . , title ]のように解析します。。 ここで不正な値や解析できない値などがあればエラーを返してくれるようです。

iter := query.Run(products)

次に字句解析したトークンを構文解析器によって解析し、抽象構文木を構築します。要は抽出ロジックを最適化するために入力値の構造をツリー形式で表現しています。そして生成した抽象構文木を実行可能な形式にコンパイルします。この辺りは完璧に理解したわけではないので半分想像ですが、おそらく構築した抽象構文木からどのように抽出すれば効率的かを計算して最適化したのちに実行ファイルを生成しているという理解です。 日本語で説明しようとすると難しい話になりますが、その辺りはquery.Run()の呼び出し先で行っているようです。

字句解析と構文解析によって構築された抽象構文木から最適化を行なって抽出用のクエリが作成できました。
ここまでくれば実際にJSONデータを作成したクエリを使って探索を開始し結果が出力されます。これを入力されたキーの数だけ繰り返し行います。

感想

仕組みについて調べるまでは「キーを使って幅優先探索か深さ優先探索のどちらかで抽出してるのかな」という浅い考えをしていたのですが、 調べてみるとただ抽出するだけではなくて、最適に探索、抽出するためにめちゃくちゃ考えられているというのが理解できました。
詳細まで追わないとわからないこともあるので深く知れたいい機会でしたが、同時に難しいことを知らなくてもキーを渡すだけで抽出できるのは開発者にとってはありがたいですね。 そういう利点もありますし、自前で探索ロジックを実装するのは難易度が高いと思うので車輪の再発明はしないように心がけるのは大事だと思いました。
余談ですが、「抽象構文木」は学生の頃授業で一度習ったのですが、本記事を執筆するまで完全に失念してました。授業で聞いてた時は座学として学んだだけで正直すぐ頭から抜けてしまっていたのですが、こういうふうに有効な場面や使われているコードを知った後で授業で習うとより理解しやすいだろうなと思ったので、学生の時は授業早く終わらないかなと思ってたのですが今はもう一度授業受けてみたいと思いましたw

最後に

ヤプリで実際にどういう場面でgojqが使われているのかを知りたいと思った方やヤプリに興味を持ったという方はぜひカジュアル面談にお越しください!

open.talentio.com