Go言語+kagome+Ginで単語カウントツール（Webアプリ）を作成してみる

## はじめに

形態素解析を利用して単語（名詞のみ）の出現数をカウントする簡単なWebアプリを作成してみたので、その作成手順を紹介していきます。
同様のWebアプリをPython+MeCabで実装したことがあったので、今回はこれまで触れたことがなかったGo言語で挑戦してみました。

## 開発環境

開発環境は以下の通りです。

* OS: macOS Sierra 10.12.6
* Homebrew: 1.4.1
* goenv: 1.0.0
* Go言語: 1.9.2
* kagome
* gin

## Go言語のインストール

まずはGo言語をインストールするところからはじめます。
[こちらの記事](https://qiita.com/walkers/items/761b2a5e58849176a633)
を参考に、[goenv](https://github.com/syndbg/goenv)を利用してGo言語をインストールしていきます。

まずはHomebrewでgoenvをインストールします。

```console
$ brew install goenv
```

goenvを利用するため、`~/.zshrc`の末尾に以下の設定を追加します。（bashを利用している場合は`~/.bashrc`に読み替えてください）

```diff:~/.zshrc
+
+export PATH="$HOME/.goenv/bin:$PATH"
+eval "$(goenv init -)"
```

設定を追加したらsourceコマンドを実行して変更内容を反映させます。

```console
$ source ~/.zshrc
```

その後、goenvを利用してGo言語（バージョン1.9.2）をインストールしていきます。

```console
$ goenv install 1.9.2
```

goenvでインストールしたGo言語を使用するためには、以下のコマンドを実行して使用するGo言語のバージョンを設定する必要があります。

```console
$ goenv global 1.9.2
```

`go version`を実行して、インストールしたバージョン（1.9.2）が表示されればOKです。

```console
$ go version
go version go1.9.2 darwin/amd64
```

### Go言語でHello worldを試す

Go言語のインストールが完了したところで、まずは`main.go`というファイルを作成してHello worldを試していきます。

```go:main.go
package main

import "fmt"

func main() {
    fmt.Printf("Hello, world!\n")
}
```

`main`は特別なパッケージ名で、`main()`関数を定義することでエントリポイントとして扱われるようです。

```console
$ go run main.go
Hello, world!
```

また、Go言語はコンパイル言語であり**ソースコードをコンパイルして実行可能ファイルを生成する**こともできるので、こちらも試してみます。

```console
# main.goをコンパイル
$ go build main.go

# 実行可能ファイルが生成されたことを確認
$ ls
main    main.go

# 実行可能ファイルを実行
$ ./main
Hello, world!
```


## kagomeのインストール

次に、[こちらの記事](https://qiita.com/ikawaha/items/ff27ac03e22b7f36811b)を参考にして[kagome](https://github.com/ikawaha/kagome)というGo言語製の日本語形態素解析器をインストールしていきます。

```console
$ go get github.com/ikawaha/kagome/...
```

上記コマンド実行後、環境変数`$GOPATH`で設定しているディレクトリ以下にkagomeがインストールされます。（`$GOPATH`を設定していない場合、デフォルト値として`$HOME/go`を使用するようです）

### kagomeで形態素解析を実行

#### kagomeをコマンドとして実行

まずはターミナル上でコマンドとしてkagomeを実行してみます。

```console
$ ~/go/bin/kagome
すもももももももものうち
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS
```

#### kagomeをライブラリとして使用

ほぼ[こちらの記事](https://qiita.com/ikawaha/items/ff27ac03e22b7f36811b
)に記載されているサンプルコードのコピペとなりますが、`main.go`を修正してkagomeを利用した形態素解析を実行できるように修正していきます。
`tokenizer`インスタンス生成周りのコードについては`src/github.com/ikawaha/kagome/cmd/kagome/tokenize/cmd.go`の`command()`関数を参考に実装しました。

```diff:main.go
 package main

-import "fmt"
+import (
+    "fmt"
+    "strings"
+    "github.com/ikawaha/kagome/tokenizer"
+)

 func main() {
-    fmt.Println("Hello, world!\n")
+    dic := tokenizer.SysDicSimple()
+    t := tokenizer.NewWithDic(dic)
+    tokens := t.Tokenize("すもももももももものうち")
+    for _, token := range tokens {
+        features := strings.Join(token.Features(), ",")
+        fmt.Printf("%s\t%v\n", token.Surface, features)
+    }
 }
```

実行結果は以下の通りです。

```console
$ go run main.go
BOS
すもも	名詞,一般,*,*,*,*
も	助詞,係助詞,*,*,*,*
もも	名詞,一般,*,*,*,*
も	助詞,係助詞,*,*,*,*
もも	名詞,一般,*,*,*,*
の	助詞,連体化,*,*,*,*
うち	名詞,非自立,副詞可能,*,*,*
EOS
```

これで形態素解析が実行できるようになりました！

### 形態素解析の実行結果から名詞の単語のみ出現回数をカウント

形態素解析の結果から名詞の単語のみを抽出し、各単語の出現回数をカウントできるように修正します。

```diff:main.go
 import (
     "fmt"
-    "strings"
     "github.com/ikawaha/kagome/tokenizer"
 )

 ...

     dic := tokenizer.SysDicSimple()
     t := tokenizer.NewWithDic(dic)
     tokens := t.Tokenize("すもももももももものうち")
+    words := map[string]int{}
     for _, token := range tokens {
-        features := strings.Join(token.Features(), ",")
-        fmt.Printf("%s\t%v\n", token.Surface, features)
+        features := token.Features()
+        // 名詞以外ならスキップ
+        if len(features) == 0 || features[0] != "名詞" {
+            continue
+        }
+
+        // マップに既にキーが存在する場合はインクリメント、存在しない場合は1で初期化
+        _, ok := words[token.Surface]
+        if ok {
+            words[token.Surface]++
+        } else {
+            words[token.Surface] = 1
+        }
+    }
+
+    for word, count := range words {
+        fmt.Printf("%s\t%d\n", word, count)
     }
 }
```

実行結果は以下のようになります。

```console
$ go run main.go
すもも	1
もも	2
うち	1
```

※map（連想配列）を`range`でイテレーションする際に要素を取り出す順序は保証されない（**という仕様になっている**）ようなので、実行するごとに上記の実行結果（単語・出現回数の表示順序）は変わります。

## Ginのインストール

単語の出現回数をカウントする機能をWebアプリとして使用できるようにしていきます。
使用するWebフレームワークについては、[こちらの記事](https://furien.jp/columns/182/#2-3)]を参考にして**比較的軽量**かつ**レスポンスが高速**ということで[Gin](https://gin-gonic.github.io/gin/)を選択しました。

まずは以下のコマンドを実行してGinをインストールします。

```console
$ go get github.com/gin-gonic/gin
```

### 形態素解析の実行結果をブラウザで表示

形態素解析の実行結果をブラウザ上で表示できるようにするため、まずはテンプレートファイル（`index.tpl`）を作成していきます。

```html:index.tpl
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>go-word-counter</title>
</head>
<body>
    <ul>
        {{range $word, $count := .wordCount}}
            <li>{{$word}}：{{$count}}回</li>
        {{end}}
    </ul>
</body>
</html>
```

上記のテンプレートファイルを読み込み、形態素解析の実行結果を埋め込んだレスポンスをブラウザ上（デフォルトでは`localhost:8080`）で表示するように`main.go`を修正します。

なお、単語の出現回数をカウントする処理は`getWordCount()`関数を作成してそちらに移動しています。

```diff:main.go
 import (
-    "fmt"
+    "net/http"
     "github.com/ikawaha/kagome/tokenizer"
+    "github.com/gin-gonic/gin"
 )

-func main() {
+func getWordCount(text string) (map[string]int) {
     dic := tokenizer.SysDicSimple()
     t := tokenizer.NewWithDic(dic)
-    tokens := t.Tokenize("すもももももももものうち")
+    tokens := t.Tokenize(text)
     words := map[string]int{}
     for _, token := range tokens {
         features := token.Features()

 ...

         }
     }

-    for word, count := range words {
-        fmt.Printf("%s\t%d\n", word, count)
-    }
+    return words
+}
+
+func main() {
+    router := gin.Default()
+    router.LoadHTMLFiles("index.tpl")
+
+    router.GET("/", func(context *gin.Context) {
+        context.HTML(http.StatusOK, "index.tpl", gin.H{
+            "wordCount": getWordCount("すもももももももものうち"),
+        })
+    })
+
+    router.Run()
 }
```

これで`main.go`を実行して`localhost:8080`にアクセスすると、以下のような画面が表示されます。

![スクリーンショット 2018-01-01 23.05.07.png](https://qiita-image-store.s3.amazonaws.com/0/125531/70ab96e0-fc2d-9ad9-08ef-603950bf9414.png)

### 任意のテキストを形態素解析できるように修正

今までは固定されたテキストに対して形態素解析を実行した結果を表示していましたが、任意のテキストに対して実行できるように修正していきます。

まずは、任意のテキストを入力して`送信`ボタンを押下したらテキストを`/`に対してPOST送信するように`index.tpl`を修正します。

```diff:index.tpl
<body>
+    <form action="/" method="post">
+        <div>
+            <textarea name="text" placeholder="テキストを入力してください"></textarea>
+        </div>
+        <div>
+            <button type="submit">送信</button>
+        </div>
+    </form>
+    <hr>
    <ul>
```

そして、`/`にPOSTでリクエストされたら送信されたテキストに対して形態素解析を実行し、その結果を画面上に表示できるように`main.go`を修正します。

```diff:main.go
     router.GET("/", func(context *gin.Context) {
+        context.HTML(http.StatusOK, "index.tpl", gin.H{})
+    })
+
+    router.POST("/", func(context *gin.Context) {
+        text := context.PostForm("text")
         context.HTML(http.StatusOK, "index.tpl", gin.H{
-            "wordCount": getWordCount("すもももももももものうち"),
+            "wordCount": getWordCount(text),
         })
     })
```

これで任意のテキストに対して形態素解析が実行できるようになりました！

* `送信`ボタン押下前

![スクリーンショット 2018-01-01 23.14.19.png](https://qiita-image-store.s3.amazonaws.com/0/125531/2edf4f82-049f-4add-4ede-e6c315ffee9d.png)

* `送信`ボタン押下後

![スクリーンショット 2018-01-01 23.23.48.png](https://qiita-image-store.s3.amazonaws.com/0/125531/d7d66a30-926c-9cae-9ab4-82f488ba5739.png)

## ソースコード

最終的なソースコードは以下の通りです。

### index.tpl

```html:index.tpl
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>go-word-counter</title>
</head>
<body>
    <form action="/" method="post">
        <div>
            <textarea name="text" placeholder="テキストを入力してください"></textarea>
        </div>
        <div>
            <button type="submit">送信</button>
        </div>
    </form>
    <hr>
    <ul>
        {{range $word, $count := .wordCount}}
            <li>{{$word}}：{{$count}}回</li>
        {{end}}
    </ul>
</body>
</html>
```

### main.go

```go:main.go
package main

import (
    "net/http"
    "github.com/ikawaha/kagome/tokenizer"
    "github.com/gin-gonic/gin"
)

func getWordCount(text string) (map[string]int) {
    dic := tokenizer.SysDicSimple()
    t := tokenizer.NewWithDic(dic)
    tokens := t.Tokenize(text)
    words := map[string]int{}
    for _, token := range tokens {
        features := token.Features()
        // 名詞以外ならスキップ
        if len(features) == 0 || features[0] != "名詞" {
            continue
        }

        // マップに既にキーが存在する場合はインクリメント、存在しない場合は1で初期化
        _, ok := words[token.Surface]
        if ok {
            words[token.Surface]++
        } else {
            words[token.Surface] = 1
        }
    }

    return words
}

func main() {
    router := gin.Default()
    router.LoadHTMLFiles("index.tpl")

    router.GET("/", func(context *gin.Context) {
        context.HTML(http.StatusOK, "index.tpl", gin.H{})
    })

    router.POST("/", func(context *gin.Context) {
        text := context.PostForm("text")
        context.HTML(http.StatusOK, "index.tpl", gin.H{
            "wordCount": getWordCount(text),
        })
    })

    router.Run()
}
```

## おわりに

Go言語+kagome+Ginで単語（名詞のみ）の出現回数をカウントするWebアプリを作成する手順を紹介していきました。

同様のWebアプリを実装した経験はあったものの、Go言語をはじめ今回使用したkagomeやGinといったライブラリ自体は初めて触ったので非常に良い勉強になりました！
今回は**とりあえず動くコード**を書くことで精一杯でしたが、今後は**よりGo言語らしいコード**を書けるように勉強していきたいと思います。

## 参考記事

* [Golangをgoenvを使ってインストールしてみた](https://qiita.com/walkers/items/761b2a5e58849176a633)
* [パッケージ - はじめてのGo言語](http://cuto.unirita.co.jp/gostudy/post/go-package/)
* [GOPATH を build.Default.GOPATH で適切に扱う | eureka tech blog](https://developers.eure.jp/tech/build-context-gopath/)
* [Pure Go で辞書同梱な形態素解析器 kagome を公開してみました](https://qiita.com/ikawaha/items/ff27ac03e22b7f36811b)
* [逆引きGolang](https://ashitani.jp/golangtips/index.html)
* [Goで、mapをrangeでイテレーションすると、取り出す順番は実行ごとに異なる罠](https://qiita.com/yuki2006/items/5a43644e278c0777ca52)
* [【2017年版】Go言語のフレームワーク比較と需要の高まり｜フリエン](https://furien.jp/columns/182/#2-3)
* [Gin(Golang)におけるHTMLテンプレート記述方法](https://qiita.com/lanevok/items/dbf591a3916070fcba0d)