Goで自分のqiita投稿記事一覧を見れるコマンドラインツールを作ってみた。

# Goで自分のqiita投稿記事一覧を見れるコマンドラインツールを作ってみた。

Go言語の勉強をしたので、Cliアプリケーションを作成してみます。
何番煎じ目か知れませんが、QiitaのAPIを叩いて自分のqiitaの投稿記事一覧を見れるコマンドラインツールを作ってみます。
これが出来た暁には、qiitaを書く意欲がもりもりと湧いてくる筈である。

## 作ったもの
```bash
$ qi myqi
```
と打つと、
<img width="1027" alt="スクリーンショット 2019-11-12 17.21.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252625/53e1fcb3-0206-9ebb-26b2-4ca726bea3a4.png">
と出力されるというコマンドラインツール。
githubはこちらです。：https://github.com/yujiteshima/qi

## 使用するフレームワーク

`urfave/cli`というCLIアプリケーションフレームワークを使用してみる。
urfaveの使い方は公式のDocumentが一番分かりやすかったです。https://github.com/urfave/cli/blob/master/docs/v1/manual.md

## ディレクトリきったり、`go mod init` したりする。

Go 1.11から採用されている`modules`を使用する。

gopathの外でも開発が可能であるので、`home`直下に`workspace_golang`というディレクトリを作って、

その中に今回のプロジェクトのディレクリを作成しました。

golangはbuildした時の実行ファイル名がデフォルトではディレクトリ名になるので、

コマンドの名前を付けると思って考えます。

「qiitaの記事を取得するコマンドだから········`qi`だ。」
というような感じで考えました。悩みますが、勉強が全然進まなくなりますので、さっと考えて進めていきます。

```bash
$ mkdir workspace_golang
$ cd workspace_golang
$ mkdir qi <- 好きな名前を付ける、オプションを付けずに`build`するとフォルダ名がコマンド名になる
```

次にモジュールの初期化を行います。

```bash
$ cd qi
$ go mod init github.com/username/qi
```

これでプロジェクトフォルダに`go.mod`ファイルが作成されます。
このファイルが`npm`の`package.json`みたいなもののようだ。

buildすると新たに、`go.sum`ファイルが作成される。
このファイルはbuildした実行ファイルの依存パッケージのlockする為にバージョンが記載されている。
`npm`の`package-lock.json`みたいなもののようだ。

次にCLIフレームワーク`urfave/cli`を`go get`しておきます。

```bash
$ go get github.com/urfave/cli
```
## ディレクトリ構成
今回は`myqi`コマンドだけしか実装しないが、他にも好きなキーワードで最新記事や、人気記事を取ってきたり、様々なコマンド実装をしていきたいので、cmdパッケージへ分けていきたいと思います。

```bash
qi
├── cmd
│   └── myqi.go
├── go.mod
├── go.sum
└── main.go
```

## 全体のコード
まず全体のコードを載せ、続いてコードの説明をしていきたいと思います。

```go:main.go
package main

import (
	"os"

	"github.com/username/qi/cmd" 

	"github.com/urfave/cli"
)

func main() {
	app := cli.NewApp()
	app.Name = "qiitasearch"
	app.Usage = "search qiita articles"
	app.Version = "0.1.0"
	
	app.Commands = []cli.Command{
		{
			Name:  "myqi",
			Usage: "qiita + mine : you get yours articles",
			Action: func(c *cli.Context) error {
				qiitaToken := os.Getenv("QIITA_TOKEN")
				datas := cmd.FetchQiitaData(qiitaToken)
				cmd.OutputQiitaData(datas)
				return nil
			},
		},
	}
	
	app.Run(os.Args)
}
```

```go:myqi.go
package cmd

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
)

// jsonをパースする為の構造体を定義する

type Data struct {
	ID             string `json:id`
	Url            string `json:"url"`
	Title          string `json:"title"`
	LikesCount     int    `json:"likes_count"`
	PageViewsCount int    `json:"page_views_count"`
}

func FetchQiitaData(accessToken string) []Data {

	baseUrl := "https://qiita.com/api/v2/items?query=user:yujiteshima"
	// 様々な検索条件をかけるときはbaseUrlをv2/までにして他を変数で定義してurl.Parseで合体させる
	endpointURL, err := url.Parse(baseUrl)
	if err != nil {
		panic(err)
	}

	b, err := json.Marshal(Data{})
	if err != nil {
		panic(err)
	}

	var resp = &http.Response{}
	// qiitaのアクセストークンがない場合はAuthorizationを付与しない
	// 2パターン作っておく。
	// accessトークンは環境変数に入れておく。自分の場合は.bash_profileにexport文を書いている。

	if len(accessToken) > 0 {
		// QiitaAPIにリクエストを送ってレスポンスをrespに格納する。
		resp, err = http.DefaultClient.Do(&http.Request{
			URL:    endpointURL,
			Method: "GET",
			Header: http.Header{
				"Content-Type":  {"application/json"},
				"Authorization": {"Bearer " + accessToken},
			},
		})
	} else {
		fmt.Println("***** Access Token 無しでQiitaAPIを叩いています アクセス制限に注意して下さい*****")

		resp, err = http.DefaultClient.Do(&http.Request{
			URL:    endpointURL,
			Method: "GET",
			Header: http.Header{
				"Content-Type": {"application/json"},
			},
		})
	}
	defer resp.Body.Close()

	if err != nil {
		panic(err)
	}

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

	var datas []Data

	if err := json.Unmarshal(b, &datas); err != nil {
		fmt.Println("JSON Unmarshal error:", err)
		return nil
	}

	/*********一覧取得では、ページビューがnilになるので個別で取りに行ってデータを得る*****************/
	for i, val := range datas {
		
		article_id := val.ID
		baseUrl := "https://qiita.com/api/v2/items/"
		endpointURL2, err := url.Parse(baseUrl + article_id)
		if err != nil {
			panic(err)
		}

		b, err := json.Marshal(Data{})
		if err != nil {
			panic(err)
		}

		resp, err = http.DefaultClient.Do(&http.Request{
			URL:    endpointURL2,
			Method: "GET",
			Header: http.Header{
				"Content-Type":  {"application/json"},
				"Authorization": {"Bearer " + accessToken},
			},
		})

		if err != nil {
			panic(err)
		}

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

		var m map[string]interface{}

		if err := json.Unmarshal(b, &m); err != nil {
			fmt.Println("JSON Unmarshal error:", err)
			return nil
		}

		datas[i].PageViewsCount = int(m["page_views_count"].(float64))
	}
	return datas
}

// データの出力
func OutputQiitaData(datas []Data) {
	fmt.Println("************************自分のQiita投稿一覧******************************")
	for _, val := range datas {
		fmt.Printf("%-15v%v%v\n", "ID", ": ", val.ID)
		fmt.Printf("%-15v%v%v\n", "Title", ": ", val.Title)
		fmt.Printf("%-12v%v%v\n", "いいね", ": ", val.LikesCount)
		fmt.Printf("%-9v%v%v\n", "ページビュー", ": ", val.PageViewsCount)
		fmt.Printf("%-15v%v%v\n", "URL", ": ", val.Url)
		fmt.Println("-------------------------------------------------------------------------")
	}
}

```

## main.goでしている事
`urfave/cli`を使用して、コマンドラインツールのインターフェイスを定義する。
`urfave/cli`では、複雑なインターフェイスも定義できるが、今回は、

```
$ qi myqi
```
とうてば自身の`qiita`記事が出力されるようにしたいと思います。
他のコマンドもゆくゆくは作りたいので最低限、コマンドを１つとるツールとしていきます。

## Qiitaのトークンの取得

qiitaのトークンを取得しておき、環境変数に入れておきます。qiitaのトークンの取得の仕方は、この記事を書いている2019/11時点では、Qiitaのページ右上の自分のアカウントのアイコンから、

```
設定　-> 左側のメニューのアプリケーション -> 新しくトークンを発行する
```
となっています。
自分の環境の場合は、`.bash_profile`に取得したトークンを以下のように記述しています。

```bash:.bash_profile
# qiita_api
export QIITA_TOKEN='取得したqiitaのアクセストークンを入れてください'
```

## 別パッケージのimportは絶対パス指定で取得する

別のcmdディレクトリに作成した、cmdパッケージのimportは絶対パス指定にしないとならない。
`modules`を使用した時と,していない時で、記述方法が違い、古い情報のまま相対パスで記述するとパスを見つけられずエラーがでる。https://qiita.com/yujiteshima/items/8dc2f782f27f147a1e3e

```go:main.go·抜粋
import (
	"os"

	"github.com/yujiteshima/qiita/cmd" // 絶対パスで指定する。

	"github.com/urfave/cli"
)
```

## 初期化する
`go`には、オブジェクト指向プログラミングに見られる「コンストラクタ」機能は無いですが、
「型のコンストラクタ」というパターンの利用が多く使われている。
型のコンストラクタは`New + 型名`とする事が多い。また、型のコンストラクタは対象の型のポインタ型を返すように定義するのが望ましく、`urfave/cli`においてもそのようになっている。

```go:main.go·抜粋
func main(){

app := cli.NewApp()

}
```

app.Commandsにコマンド`myqi`を登録する。

```go:main.go·抜粋
app.Commands = []cli.Command{
		{
			Name:  "myqi", 
			Usage: "qiita + mine : you get yours articles",
			Action: func(c *cli.Context) error {
				qiitaToken := os.Getenv("QIITA_TOKEN")
				datas := cmd.FetchQiitaData(qiitaToken)
				cmd.OutputQiitaData(datas)
				return nil
			},
		},
	}
```

Nameに登録するコマンドを文字列で指定する。
Usageには使い方を登録する。helpで使われる。
Actionに実際の処理を定義する。
Action内で、
1. FectchMyQiitaData()を実行して、データを取得する。
2. OutputQiitaData()で出力する。

以上が`main.go`での処理です。続いて、cmdパッケージの説明をします。

## cmdパッケージ

cmdパッケージには、構造体`Data`の定義とFetchMyQiitaData関数とOutputQiitaData関数をを定義している。

## importしているパッケージ

まず各種パッケージを`import`する。

- "encoding/json" : QiitaAPIから取得したjsonを`go`のデータにパースする時に使用する。
- "fmt" : 標準出力に使用する。
- "io.ioutil" : QiitaAPIから取得したデータを読み込む時に使用する。
- "net/http" : QiitaAPIを叩きに行く時に使用する。
- "net/url" : 文字列からurlへパースする時に使用する。

```go:cmd.go·抜粋
package cmd

import (
	"encoding/json" 
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
)
```

## jsonをパースする為に構造体を定義する
`encoding/json`を用いて構造体のデータにパースする。
`encoding/json`は構造体に`Tag`でjson:`キー名`を定義しておくと、キー名を出力するjsonのキー名として使ってくれます。
書籍の`スターティングGOのChapter5の構造体とインターフェイス`が分かりやすかったです。

```go:cmd.go·抜粋
type Data struct {
	ID             string `json:id`
	Url            string `json:"url"`
	Title          string `json:"title"`
	LikesCount     int    `json:"likes_count"`
	PageViewsCount int    `json:"page_views_count"`
}
```

## `FetchMyQiitaData（）`を定義する。

`FetchMyQiitaData`は引数として`accessToken`を受け取って`Header`にセットしている。

この他にもサブコマンドに引数を受け取ってここで使用することも出来る。キーワードを引数で受け取って検索するコマンドを作るときに使える。

accessToken無しでリクエストを送った時には、accessToken無しでアクセスしているとメッセージを出すようにしておきました。
`json`で受け取ったデータをパースする為に、標準パッケージの`encoding/json`を使いました。`Unmarshal`メソッドを使って、jsonをGoの構造体データのスライスにパースする。とても簡単に出来ます。

## `OutputQiitaData()`を定義する

あとは出力する為の`OutputQiitaData`を定義するだけです。

```go:cmd.go·抜粋
func OutputQiitaData(datas []Data) {
	fmt.Println("************************自分のQiita投稿一覧******************************")
	for _, val := range datas {
		fmt.Printf("%-15v%v%v\n", "ID", ": ", val.ID)
		fmt.Printf("%-15v%v%v\n", "Title", ": ", val.Title)
		fmt.Printf("%-12v%v%v\n", "いいね", ": ", val.LikesCount)
		fmt.Printf("%-9v%v%v\n", "ページビュー", ": ", val.PageViewsCount)
		fmt.Printf("%-15v%v%v\n", "URL", ": ", val.Url)
		fmt.Println("-------------------------------------------------------------------------")
	}
}
```

forループで出力する。
見やすくフォーマットしておく。

## ページビューが取れない問題発生

しかし、ここで一度出力して確認すると、ページビューが0となってしまっている。
アクセストークンが外れてしまっているのか等様々調べた結果、

```go
"https://qiita.com/api/v2/items?query=user:yujiteshima"
```
このように一覧を取得する際には、ページビューは`null`になって返ってくる。つまり、個別の記事検索でしか、ページビュ０は取得できないという事でした。

## 個別のページからページビューを取得する
ページビューの表示を諦めそうになりますが、ページビュー欲しいです。個別のページから全て取得していきます。


```go
for i, val := range datas {
		//fmt.Println("id:", val.ID)
		article_id := val.ID
		baseUrl := "https://qiita.com/api/v2/items/"
		endpointURL2, err := url.Parse(baseUrl + article_id)
		if err != nil {
			panic(err)
		}

		b, err := json.Marshal(Data{})
		if err != nil {
			panic(err)
		}

		resp, err = http.DefaultClient.Do(&http.Request{
			URL:    endpointURL2,
			Method: "GET",
			Header: http.Header{
				"Content-Type":  {"application/json"},
				"Authorization": {"Bearer " + accessToken},
			},
		})

		if err != nil {
			panic(err)
		}

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

		var m map[string]interface{}

		if err := json.Unmarshal(b, &m); err != nil {
			fmt.Println("JSON Unmarshal error:", err)
			return nil
		}
		datas[i].PageViewsCount = int(m["page_views_count"].(float64))
	}
```

取得した一覧で得た`id`を使ってループで回して`page_view_count`を取得していき、0が入ってしまっている`page_view_count`を更新していきます。

これで、出力してみて完成です。
qiディレクトリで、`go install`します。

```
$ go install
```

これで、qiディレクリで無くても、コマンド`qi`が使えるようになった。

## まとめ

やはり、何か作って見ると、理解が深まると感じた。

今回のアプリケーションの今後の拡張としては、

1. さらに検索コマンドを増やしすこと。
2. テストを書く。
3. ページビューを取得する為に何回もAPIにアクセスする事になる事の改良。

3のページビューを取得する為に何回もAPIにアクセスする事になる事の改良については、`FaaS`と`CloudStorage`等のサービスを使って定期実行してページビューも取得済のデータをキャッシュさせたものを叩きに行くか、コマンドラインツールから、コマンドでデータをリフレッシュさせれて、そのデータを叩きにいったり出来るようにしてみたい。

たくさん`Go`を使って何か作って理解を深めていきたいと思います。

リファクタリングした方が良い所や、まずい書き方あれば指摘お願いします。