Edited at

Goでcurl/jqライブラリを使う

More than 1 year has passed since last update.


はじめに

Goからcurlライブラリでjsonを取得し、jqライブラリでjsonをパースするまでの環境構築についてです。

標準ライブラリだけでも出来ることなんですが1、代表的なコマンドのライブラリを使う事で新たに覚える事を少なくして言語間での実装の差異をなるべく減らしたいと思います。

それぞれのライブラリはC言語で提供されていますので、Goで使用する時に直接使用するのもいいですが、利便性を考えてラッパーライブラリを使います。


環境構築

環境は以下の通りです。


  • OS: Windows 8.1 Pro 64bit


  • Go: 64bit版


  • MSYS2: 64bit版


    • GoからC言語製のcurl、jqライブラリを使う際に使用



ラッパーライブラリは以下の通りです、どちらも薄いラッパーで余計な事はしていません。


MSYS2の環境構築

curl、jqライブラリをインストールします。

なお、MSYS2のターミナル上で作業しています。


curlライブラリ、jqライブラリのインストール

インストールにはAlexpux/MINGW-packagesを利用します、これで各ライブラリをmakepkgコマンド(各ライブラリのソースダウンロード、MSYS2用パッチ適用、ビルドまでを全部自動でしてくれる)で手軽にインストールできます。

※curlはpacmanでインストールしたlibcurl-develだと不都合があったためソースからインストールします2

※パッケージ版でも問題ないとは思いますが、jqもcurlに合わせてソースからインストールします


インストール

# パッケージを最新へ

$ pacman -Syuu
# ※もし下記のような警告が出たら、ターミナルを再起動して再度実行
# 警告: terminate MSYS2 without returning to shell and check for updates again
# 警告: for example close your terminal window instead of calling exit
$ pacman -Syuu

# makepkgで使うビルド関連パッケージをインストール
$ pacman -S git base-devel mingw-w64-x86_64-toolchain

# MINGW-packagesをgit clone
$ cd /tmp
$ git clone --depth=1 https://github.com/Alexpux/MINGW-packages.git

# jqのビルド & インストール
$ cd /tmp/MINGW-packages/
$ cd `ls | grep jq`
# -sオプションで足りない依存パッケージもついでにインストール
$ makepkg -s
# ローカルパッケージのインストール
$ pacman -U ./mingw-w64-x86_64-jq-1.5-3-any.pkg.tar.xz
# 直接インストールの場合はこちら
#$ cp -r pkg/mingw-w64-x86_64-jq/mingw64/* /mingw64/

# curlのビルド & インストール
$ cd /tmp/MINGW-packages/
$ cd `ls | grep curl`
$ makepkg -s
# ここで、PGP鍵が検証できないというメッセージが出たら、公開鍵がないのが原因なのでインポートする
# ==> gpg でソースファイルの署名を検証...
# curl-7.52.1.tar.bz2 ... 失敗 (不明な公開鍵 5CC908FDB71E12C2) <- ※これをコピペ
# ==> エラー: PGP 鍵を検証できませんでした!
$ gpg --recv-keys 5CC908FDB71E12C2
# 再実行
$ makepkg -s
# ローカルパッケージのインストール
$ pacman -U ./mingw-w64-x86_64-curl-7.52.1-1-any.pkg.tar.xz
# 直接インストールの場合はこちら
#$ cp -r pkg/mingw-w64-x86_64-curl/mingw64/* /mingw64/



動作確認(C)

以下のソースをgccでコンパイル、実行して"ok"が出力されればcurlの環境構築は成功。

jqはこの後Goで使用するので省きます。


test_curl.c

// curlのSSL認証が動作するか確認する

#include <stdio.h>
#include <curl/curl.h>

size_t noop_function(char *buffer, size_t size, size_t nitems, void *instream) {
return 0;
}

int main() {
curl_global_init(CURL_GLOBAL_DEFAULT); // NOTE : libcurl-develのを利用するとここでこける

CURL *curl = curl_easy_init();
if (!curl) return 1;

// qiita.comにSSL接続
curl_easy_setopt(curl, CURLOPT_URL, "https://qiita.com");
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
// デフォルトだと標準出力にレスポンスが出力されてうっとおしいので、握りつぶす
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION , noop_function);
curl_easy_perform(curl);

// HTTP Responseを取得
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code == 200) printf("ok");

curl_easy_cleanup(curl);
return 0;
}



コンパイル&実行

$ gcc test_curl.c -I/mingw64/include -L/mingw64/lib -lcurl

$ ./a
ok


Goの環境構築

こちらはGoとMSYS2上のgcc(C:\path\to\msys64\mingw\bin\gcc)にパスが通ったコマンドプロンプトから行います。

※MSYS2へのパスは環境に合わせて下さい。


ラッパーライブラリをインストール

> @rem curl、jqライブラリをGoから見えるようにする

> set CGO_CFLAGS=-I C:\path\to\msys64\mingw64\include
> set CGO_LDFLAGS=-L C:\path\to\msys64\mingw64\lib
> @rem curl、jqラッパーライブラリをインストール
> go get -u github.com/andelf/go-curl
> go get -u github.com/mgood/go-jq
> @rem go get -u github.com/wordijp/go-jq

ここまでで環境構築は完了です。


Goでcurl/jqライブラリを使ってみる

(2017/2/11) jqの便利さについて追記しました

qiitaの投稿一覧をcurlで取得し、投稿者のIDとタイトルの一覧をjqで絞り込んで表示してみます。

ちなみに投稿一覧はQiitaホームのすべての投稿から確認出来ます。

すべての投稿.png

Qiita API v2で取得出来る投稿一覧のJSONは、下記のようなフォーマットになっています。


Qiita_API_V2_items.json

[

{
"rendered_body": "本文(長いので省略)",
"coediting": false,
"created_at": "2017-02-09T20:29:56+09:00",
"group": null,
"id": "acb70ae51c334aa1ee52",
"private": false,
"tags": [
{
"name": "C",
"versions": []
},
{
"name": "Go",
"versions": []
},
{
"name": "curl",
"versions": []
},
{
"name": "jq",
"versions": []
},
{
"name": "msys2",
"versions": []
}
],
"title": "Goでcurl/jqライブラリを使う",
"updated_at": "2017-02-09T21:24:05+09:00",
"url": "http://qiita.com/wordijp/items/acb70ae51c334aa1ee52",
"user": {
"description": "C++が好きです(C++の記事を投稿するとは言っていない)",
"facebook_id": "",
"followees_count": 7,
"followers_count": 9,
"github_login_name": "wordijp",
"id": "wordijp",
"items_count": 33,
"linkedin_id": "",
"location": "",
"name": "",
"organization": "",
"permanent_id": 51728,
"profile_image_url": "https://qiita-image-store.s3.amazonaws.com/0/51728/profile-images/1473692528",
"twitter_screen_name": "wordijp",
"website_url": ""
}
},
{
2記事目
}
]

ここから投稿者idとタイトルのグループへとパースするjq用文字列は、下記のようになります。


jq用文字列(投稿者idとタイトルのグループ)

'.[] | {user_id: .user.id, title: .title}'


パース後は下記のようになります。


投稿者idとタイトルのグループ.json

{

"user_id": "wordijp",
"title": "Goでcurl/jqライブラリを使う"
}
{
2記事目
}

ちなみに、配列を維持したままパースする時は下記のようになります、

実装時に使わないのですがついでなので紹介だけしておきます。


jq用文字列(投稿者idとタイトルのグループ、配列維持Ver)

'. | [{user_id: .[].user.id, title: .[].title}]'


パース後は下記のようになります。


投稿者idとタイトルのグループ、配列維持Ver.json

[

{
"user_id": "wordijp",
"title": "Goでcurl/jqライブラリを使う"
},
{
2記事目
}
]

Goのソースコードはこちら


test_curl_json.go

// Qiita API v2を呼び出し、投稿記事の投稿者IDとタイトルを列挙する

package main

import (
"bytes"
"encoding/json"
"fmt"
curl "github.com/andelf/go-curl"
"sync/atomic"
jq "github.com/mgood/go-jq" // <del>バグFixをPR中</del>直った!
//jq "github.com/wordijp/go-jq" // 本家のバグが修正されたので不要になった
)

type wrdata struct {
ch chan []byte // 取得データの伝搬用
remain int32 // 取得データの残り数
// 並列処理の同期用
perform chan int
complete chan int
}

// cURLのデータ取得と戻り値用の処理を並列で実行する
// NOTE : 逐次よりちょっと速い
func easyParallelWrite(easy *curl.CURL, fWrite func([]byte)) {

// write function
easy.Setopt(curl.OPT_WRITEFUNCTION, func(ptr []byte, userdata interface{}) bool {
//println("ptr size:", len(ptr))
wd, ok := userdata.(*wrdata)
if !ok {
println("ERROR!")
return false
}

atomic.AddInt32(&wd.remain, 1)
wd.ch <- ptr
return true // ok
})

// write data
wd := &wrdata{
ch: make(chan []byte, 100),
remain: 0,
perform: make(chan int),
complete: make(chan int),
}
var buf bytes.Buffer
go func(wd *wrdata) {
performed := false
loop:
for {
if !performed {
select {
case <-wd.perform:
performed = true
// 既にデータ取得済み
if atomic.LoadInt32(&wd.remain) <= 0 {
break loop
}
default:
// no-op
}
}

data := <-wd.ch
atomic.AddInt32(&wd.remain, -1)
//println("Got data size=", len(data))
buf.Write(data)
// complete after performed
if performed && atomic.LoadInt32(&wd.remain) <= 0 {
break
}
}

//println("recv finished!")
wd.complete <- 1
}(wd)
easy.Setopt(curl.OPT_WRITEDATA, wd)

easy.Perform()

// データ取得の完了を待つ
wd.perform <- 1
<-wd.complete

// 結果を返す
fWrite(buf.Bytes())
}

// ヘルパ関数
// curl処理をここにまとめる
func curlExec(url string, fWrite func([]byte)) {
curl.GlobalInit(curl.GLOBAL_DEFAULT)
defer curl.GlobalCleanup()

easy := curl.EasyInit()
defer easy.Cleanup()

easy.Setopt(curl.OPT_URL, url)
//easy.Setopt(curl.OPT_SSL_VERIFYPEER, 0) // SSLの時に有効化
easyParallelWrite(easy, fWrite)
}

type Item struct {
UserID string `json:"user_id"`
Title string `json:"title"`
}

func main() {
curlExec("http://qiita.com/api/v2/items", func(buf []byte) {
j, _ := jq.NewJQ(".[] | {user_id: .user.id, title: .title}")

j.HandleJson(string(buf))
for j.Next() {
item := &Item{}
valuejson := j.ValueJson()
json.Unmarshal(([]byte)(valuejson), &item)

fmt.Println(valuejson) // JSON
fmt.Println(item.UserID + " : " + item.Title) // 構造体
fmt.Println()
}
})
}



実行結果

> go run test_curl_json.go

{"user_id":"kazuma1989","title":"DockerでとりあえずJettyを動かす最低限の構成をメモ"}
kazuma1989 : DockerでとりあえずJettyを動かす最低限の構成をメモ

{"user_id":"ironsand","title":"foo_BarHoge を camelize すると FooBarhoge になるので注意"}
ironsand : foo_BarHoge を camelize すると FooBarhoge になるので注意

{"user_id":"Teach","title":"ThirdPersonControllerでフラッシュジャンプ"}
Teach : ThirdPersonControllerでフラッシュジャンプ


取得したJSONからjqを使って欲しいデータを取り出しています、jqにはこのような強力な絞り込み機能がついています、これをjqを使わないで実装しようとすると大変だと思います、jq便利です。

それと、easyParallelWrite関数ですが、goroutineで並列化しているため、共有変数(remain)をスレッドセーフな処理のしたりと少々ややこしくなってますが、パフォーマンス目的で使っています。





  1. json取得は"net/http"で、jsonのパースは"encoding/json"で 



  2. pacmanでインストールされるlibcurlだと、curl_global_initやcurl_easy_initが、realloc関数呼び出し時のエラーにより実行できなかった。