Closed2

GoでJSONをパースしたい

ユータユータ

背景

dev.toの記事をGitHubで管理して、記事を自動投稿できる方法について紹介しました。
https://zenn.dev/yuta28/articles/dev-github-vscode

この中で手順7の記事IDの取得ですが、dev.toのAPIをcurlコマンドを叩いてjqコマンドでパースして取得する方法を紹介しました。
ただ毎回このコマンドをペーストするのは面倒でしたので、APIキーを環境変数化してスクリプトでもう少し簡単に記事IDを取得できるようにしたいと考えました。
当初はPythonを使ってスクリプト化しようと思いましたが、dev.toのAPIがまだベータ版なのかPythonだと値が取得できない問題がありました。

実行しても何もなし
import requests

response = requests.get('https://dev.to/api/articles/latest')

https://developers.forem.com/api

なのでせっかくなのでGoに挑戦しようと思い、Goで記事IDを取得できるようなスクリプトを作成してみることにしました。

スクリプト作成

まずはcurlコマンドのGETリクエスト部分をGoで実行できるようにします。
curl -H "api-key: API_KEY" https://dev.to/api/articles/me/unpublished
こちらにcurlコマンドをGoに変換してくれるサイトがありましたので、curlコマンドをGoに書き換えてみました。
https://curlconverter.com/

記事IDを取得
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
)

func main() {
	DEVAPIKEY := os.Getenv("DEVAPIKEY")
	client := &http.Client{}
	req, err := http.NewRequest("GET", "https://dev.to/api/articles/me/unpublished", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("api-key", DEVAPIKEY)
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", bodyText)
}

記述したGoファイルをjqコマンドでパースするとcurlコマンドと同じ結果が得られます。

go run .\get-blog.go | jq '.[].id'
1064616

うまくいきましたので次にjq部分もGoに組み込んでいきます。

gojqを活用

「Go jq」とググるとgojqと呼ばれるモジュールがあることがわかりました。
https://github.com/itchyny/gojq

Goで書かれたjqコマンドらしくライブラリ内に組み込むこともできるようです。
先ほどのGoスクリプトの関数をcurl()としてgojqのパースをmain()とすれば同じ結果が得られるのではないかと考えて、スクリプトの修正を行ないました。
main()部分をGitHubのサンプルコードを参考に修正してみました。

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"

	"github.com/itchyny/gojq"
)

func curl() []byte {
	DEVAPIKEY := os.Getenv("DEVAPIKEY")
	client := &http.Client{}
	req, err := http.NewRequest("GET", "https://dev.to/api/articles/me/unpublished", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("api-key", DEVAPIKEY)
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	return bodyText
}

func main() {
	// Parse JSON
	query, err := gojq.Parse(".[].id")
	if err != nil {
		log.Fatalln(err)
	}
	input := curl()
	iter := query.Run(input) // or query.RunWithContext
	for {
		v, ok := iter.Next()
		if !ok {
			break
		}
		if err, ok := v.(error); ok {
			log.Fatalln(err)
		}
		fmt.Printf("%#v\n", v)
	}
}

このGoファイルを実行すると以下のようになりました。

go run .\get-blog2.go
2022/05/02 12:43:35 %!v(PANIC=Error method: invalid value: [91 123 34 116 121 112 101 95 111 102 34 58 34 97 114 116 105 99 108 101 34 44 34 105 100 34 58 49 48 54 52 54 49 54 44 34 116 105 116 108 101 34 58 34 73 32 109 97 110 97 103 101 32 109 121 32 100 101 118 46 116 111 32 98 108 111 103 32 105 110 32 71 105 116 72 117 98 32 114 101 112 111 115 105 116 111 114 121 34 44 34 100 101 115 99 114 105 112 116 105 111 110 34 58 34 65 32 112 111 115 116 32 98 121 32 89 117 116 97 114 111 32 77 111 114 105 34 44 34 112 117 98 108 105 115 104 101 100 34 58 102 97 108 115 101 44 34 112 117 98 108 105 115 104 101 100 95 97 116 34 58 110 117 108 108 44 34 115 108 117 103 34 58 34 116 101 115 116 45 52 108 99 56 45 116 101 109 112 45 115 108 117 103 45 49 55 50 49 51 51 56 34 44 34 112 97 116 104 34 58 34 47 121 117 116 97 50 56 47 116 101 115 116 45 52 108 99 56 45 116 101 109 112 45 115 108 117 103 45 49 55 50 49 51 51 56 34 44 34 117 114 108 34 58 34 104 116 116 112 115 58 47 47 100 101 118 46 116 111 47 121 117 116 97 50 56 47 116 101 115 116 45 52 108 99 56 45 116 101 109 112 45 115 108 117 103 45 49 55 50 49 51 51 56 34 44 34 99 111 109 109 101 110 116 115 95 99 111 117 110 116 34 58 48 44 34 112 117 98 108 105 99 95 114 101 97 99 116 105 111 110 115 95 99 111 117 110 116 34 58 48 44 34 112 97 103 101 95 118 105 101 119 115 95 99 111 117 110 116 34 58 48 44 34 112 117 98 108 105 115 104 101 100 95 116 105 109 101 115 116 97 109 112 34 58 34 34 44 34 98 111 100 121 95 109 97 114 107 100 111 119 110 34 58 34 45 45 45 92 110 116 105 116 108 101 58 32 73 32 109 97 110 97 103 101 32 109 121 32 100 101 118 46 116 111 32 98 108 111 103 32 105 110 32 71 105 116 72 117 98 32 114 101 112 111 115 105 116 111 114 121 92 110 112 117 98 108 105 115 104 101 100 58 32 102 97 108 115 101 92 110 100 101 115 99 114 105 112 116 105 111 110 58 92 110 116 97 103 115 58 32 103 105 116 104 117 98 44 32 118 115 99 111 100 101 92 110 45 45 45 92 110 34 44 34 112 111 115 105 116 105 118 101 95 114 101 97 99 116 105 111 110 115 95 99 111 117 110 116 34 58 48 44 34 99 111 118 101 114 95 105 109 97 103 101 34 58 110 117 108 108 44 34 116 97 103 95 108 105 115 116 34 58 91 34 103 105 116 104 117 98 34 44 34 118 115 99 111 100 101 34 93 44 34 99 97 110 111 110 105 99 97 108 95 117 114 108 34 58 34 104 116 116 112 115 58 47 47 100 101 118 46 116 111 47 121 117 116 97 50 56 47 116 101 115 116 45 52 108 99 56 45 116 101 109 112 45 115 108 117 103 45 49 55 50 49 51 51 56 34 44 34 114 101 97 100 105 110 103 95 116 105 109 101 95 109 105 110 117 116 101 115 34 58 48 44 34 117 115 101 114 34 58 123 34 110 97 109 101 34 58 34 89 117 116 97 114 111 32 77 111 114 105 34 44 34 117 115 101 114 110 97 109 101 34 58 34 121 117 116 97 50 56 34 44 34 116 119 105 116 116 101 114 95 117 115 101 114 110 97 109 101 34 58 34 89 48 117 50 56 49 34 44 34 103 105 116 104 117 98 95 117 115 101 114 110 97 109 101 34 58 34 89 117 104 116 97 50 56 34 44 34 119 101 98 115 105 116 101 95 117 114 108 34 58 34 104 116 116 112 115 58 47 47 122 101 110 110 46 100 101 118 47 121 117 116 97 50 56 34 44 34 112 114 111 102 105 108 101 95 105 109 97 103 101 34 58 34 104 116 116 112 115 58 47 47 114 101 115 46 99 108 111 117 100 105 110 97 114 121 46 99 111 109 47 112 114 97 99 116 105 99 97 108 100 101 118 47 105 109 97 103 101 47 102 101 116 99 104 47 115 45 45 108 121 65 115 65 121 99 115 45 45 47 99 95 102 105 108 108 44 102 95 97 117 116 111 44 102 108 95 112 114 111 103 114 101 115 115 105 118 101 44 104 95 54 52 48 44 113 95 97 117 116 111 44 119 95 54 52 48 47 104 116 116 112 115 58 47 47 100 101 118 45 116 111 45 117 112 108 111 97 100 115 46 115 51 46 97 109 97 122 111 110 97 119 115 46 99 111 109 47 117 112 108 111 97 100 115 47 117 115 101 114 47 112 114 111 102 105 108 101 95 105 109 97 103 101 47 53 52 52 54 53 52 47 52 54 102 53 56 102 52 54 45 101 48 100 57 45 52 55 99 53 45 56 54 99 53 45 98 100 101 56 99 57 50 56 97 100 49 56 46 106 112 103 34 44 34 112 114 111 102 105 108 101 95 105 109 97 103 101 95 57 48 34 58 34 104 116 116 112 115 58 47 47 114 101 115 46 99 108 111 117 100 105 110 97 114 121 46 99 111 109 47 112 114 97 99 116 105 99 97 108 100 101 118 47 105 109 97 103 101 47 102 101 116 99 104 47 115 45 45 45 69 67 104 108 85 98 117 45 45 47 99 95 102 105 108 108 44 102 95 97 117 116 111 44 102 108 95 112 114 111 103 114 101 115 115 105 118 101 44 104 95 57 48 44 113 95 97 117 116 111 44 119 95 57 48 47 104 116 116 112 115 58 47 47 100 101 118 45 116 111 45 117 112 108 111 97 100 115 46 115 51 46 97 109 97 122 111 110 97 119 115 46 99 111 109 47 117 112 108 111 97 100 115 47 117 115 101 114 47 112 114 111 102 105 108 101 95 105 109 97 103 101 47 53 52 52 54 53 52 47 52 54 102 53 56 102 52 54 45 101 48 100 57 45 52 55 99 53 45 56 54 99 53 45 98 100 101 56 99 57 50 56 97 100 49 56 46 106 112 103 34 125 125 93])
exit status 1

値がすべてバイト変換されていました。
fmt.Printfでデバッグしてみましたが、iter := query.Run(input)部分で変数iterがバイト変換されていました。
おそらくこの後にString変換して必要な部分を取得すればいいのですが、残念ながらこの先でつまずいていてわからない状態です。

GoはわからなすぎるのでとりあえずチュートリアルやProgateでもやろうかなと考えています。

ユータユータ

解決

Twitterにてアドバイスをいただきましたので解決方法を記します。

JSONにエンコードするためにencoding/jsonパッケージをインポートし、curl()を[]byteではなくinterface{}で返す必要があるみたいです。

get-blog-id.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/itchyny/gojq"
)
       // interface{}に変換
func curl() interface{} {
	DEVAPIKEY := os.Getenv("DEVAPIKEY")
	client := &http.Client{}
	req, err := http.NewRequest("GET", "https://dev.to/api/articles/me/unpublished", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("api-key", DEVAPIKEY)
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	var data interface{}
	err = json.NewDecoder(resp.Body).Decode(&data)
	if err != nil {
		log.Fatal(err)
	}
	return data
}

func main() {
	// Parse JSON
	query, err := gojq.Parse(".[].id")
	if err != nil {
		log.Fatalln(err)
	}
	input := curl()
	iter := query.Run(input) // or query.RunWithContext
	for {
		v, ok := iter.Next()
		if !ok {
			break
		}
		if err, ok := v.(error); ok {
			log.Fatalln(err)
		}
		fmt.Printf("%#v\n", v)
	}
}

ただこのまま実行しますとIDの値が指数表記されたものになりました。

go run get-blog3.go
1.064616e+06

fmtパッケージの非指数は%fをつかえばいいみたいですので試してみるとデフォルトで小数6桁まで表示されました。

func main() {
	// Parse JSON
	query, err := gojq.Parse(".[].id")
	if err != nil {
		log.Fatalln(err)
	}
	input := curl()
	iter := query.Run(input) // or query.RunWithContext
	for {
		v, ok := iter.Next()
		if !ok {
			break
		}
		if err, ok := v.(error); ok {
			log.Fatalln(err)
		}
		fmt.Printf("%f\n", v)   // %fに変更
	}
}
go run get-blog3.go
1064616.000000

調べると桁数の変更ができるみたいですので以下のように修正しました。

func main() {
	// Parse JSON
	query, err := gojq.Parse(".[].id")
	if err != nil {
		log.Fatalln(err)
	}
	input := curl()
	iter := query.Run(input) // or query.RunWithContext
	for {
		v, ok := iter.Next()
		if !ok {
			break
		}
		if err, ok := v.(error); ok {
			log.Fatalln(err)
		}
		fmt.Printf("%1.0f\n", v)
	}
}

これで無事に記事IDを取得することができました💡

go run get-blog3.go
1064616
このスクラップは2022/08/25にクローズされました