Shogo's Blog

たぶんプログラミングとかについて書いていくブログ

MeCabのGolangバインディングを書いてみた

GolangからMeCabを呼び出すライブラリ探せばあるにはあるのですが、 なんだかどれもメモリ管理がちょっと怪しいんですよね・・・。

メモリ管理はbluele/mecab-golangが一番しっかりしているっぽいですが、 libmecabの一番高機能だけど面倒な使い方しか対応していなくて、ちょっとカジュアルに遊ぶにはつらい。

というわけで、カジュアルな用途から高度な使い方まで対応したWrapperを書いてみました。

使い方

READMEとgodocのexamplesからのコピペになってしまいますが、 簡単に使い方の紹介です。

インストール

go getで取ってくることはできますが、事前にlibmecabとリンクするための設定が必要です。

1
2
3
$ export CGO_LDFLAGS="-L/path/to/lib -lmecab -lstdc++"
$ export CGO_CFLAGS="-I/path/to/include"
$ go get github.com/shogo82148/go-mecab

mecabコマンドと一緒にmecab-configがインストールされているはずなので、 それを使うのが楽でしょう。

1
2
3
$ export CGO_LDFLAGS="`mecab-config --libs`"
$ export CGO_FLAGS="`mecab-config --inc-dir`"
$ go get github.com/shogo82148/go-mecab

MeCabはデフォルトで/usr/local/以下に入るので、他の実装では決め打ちしている例が多いですが、 100%とは言い切れないので面倒ですが都度指定にしてあります。 cgoはpkg-configに対応しているで、MeCab側が対応してくれると環境変数の設定が不要になってもっと楽なんですけどね。

カジュアルに使う

Parseを使うとmecabコマンドと同等の結果を文字列として受け取れます。

1
2
3
4
5
6
7
8
9
10
11
tagger, err := mecab.New(map[string]string{})
if err != nil {
    panic(err)
}
defer tagger.Destroy()

result, err := tagger.Parse("こんにちは世界")
if err != nil {
    panic(err)
}
fmt.Println(result)

オプションの渡し方ですが、いろいろ考えた結果mapで渡すようにしてみました。 (PerlのText::MeCabからのインスパイア) 例えば、mecab.New(map[string]string{"output-format-type": "wakati"})のようにすると、分かち書きで出力されます。

ノードの詳細情報にアクセスする

ParseToNodeを使うと表層表現と品詞が最初から分かれた形で取得できます。 生起コストのようなより詳細な情報も取れます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tagger, err := mecab.New(map[string]string{})
if err != nil {
    panic(err)
}
defer tagger.Destroy()

// XXX: avoid GC problem with MeCab 0.996 (see https://github.com/taku910/mecab/pull/24)
tagger.Parse("")

node, err := tagger.ParseToNode("こんにちは世界")
if err != nil {
    panic(err)
}

for ; node != (mecab.Node{}); node = node.Next() {
    fmt.Printf("%s\t%s\n", node.Surface(), node.Feature())
}

以前紹介したMeCabをPython3から使う(続報)の件、 実はPythonに限ったことではなく、公式で提供されている全ての言語バインディングで発生します。 (例えばRubyでも発生するっぽい: Ruby + MeCab で Segmentation fault が発生した場合の対処) Pythonが参照カウント方式のGCを採用しているので、たまたま発見されるのが早かったというだけですね(Rubyだとメモリを圧迫するまで落ちないらしい)。

そして、公式で提供されているバインディングを参考に書いたので、今回のGo版でも発生します。 MeCab側で対応してもらったのでわざわざバインディング側で対応することもないだろうとの考えから、go-mecabでは特に対策をとっていません。 MeCab 0.996以下を使っている方は注意してください。(残念ながら0.996がまだ最新リリースだけど・・・)

Modelを共有する

MeCab ライブラリで紹介されている、マルチスレッド環境の場合での使い方にも対応しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
model, err := mecab.NewModel(map[string]string{})
if err != nil {
    panic(err)
}
defer model.Destroy()

tagger, err := model.NewMeCab()
if err != nil {
    panic(err)
}
defer tagger.Destroy()

lattice, err := mecab.NewLattice()
if err != nil {
    panic(err)
}
defer lattice.Destroy()

lattice.SetSentence("こんにちは世界")
err = tagger.ParseLattice(lattice)
if err != nil {
    panic(err)
}
fmt.Println(lattice.String())

複数のゴルーチンからmodeltaggerを共有できると思います。latticeだけはゴルーチン毎に生成してください。 (へいれつへーこーしょりとかよくわかってないですが、スレッドセーフならゴルーチンセーフという認識であってますよね?) メモリ効率もいいのでは(未検証なので誰か確かめて・・・)。

GoからCへ文字列を渡す方法について

一般的な方法

GoからCへ文字列を渡すには、Goの文字列をC.CStringを使ってCの文字列に変換する必要があります。

1
2
3
4
cstring := C.CString(gostring)
defer C.free(unsafe.Pointer(cstring))

C.some_useful_function(cstring)

ここで注意が必要なのはC.CStringの戻り値はGoのガーベージコレクションの対象から外れるということです。 C側での使用状況をGoのランタイムが把握しきれないからですね。 C.freeを使って明示的に開放してあげないとメモリーリークになります。 巷にあふれているMeCabバインディングはここがちょっと甘いものがほとんどでした。

黒魔術を使う

別にC.CStringでも十分だとは思ったのですが、 golang で string を []byte にキャストしてもメモリコピーが走らない方法を考えてみるを見て、つい魔が差してしまいました。 Goのstringをメモリーコピーを避けて[]byteにできるのなら、Cの文字列型(*C.char)でも同じことができるはず・・・!

1
2
cstring := *(**C.char)(unsafe.Pointer(&gostring))
C.some_useful_function2(cstring, len(gostring))

通常C言語の文字列は末尾に'\0'が番兵としてついており、C.CStringはそこら辺の事情を考慮してくれます。 しかし、この方法は番兵がいないため、文字列の長さを別途渡してあげる必要があります。 幸いMeCabは文字列長さを明示するインターフェースを備えているので、そちらを使えばOKでした。

GoのstringはもちろんGCの対象なので、GCには要注意です。 関数内で閉じた状態にするのが無難ですね。 また、空文字が渡されるとヌルポで死んでしまうようなので、そこにも注意しましょう。

まとめ

  • カジュアルな用途から高度な使い方まで対応したMeCabのWrapperを書いてみました
  • MeCab 0.996 と一緒に使う場合はGCに注意しましょう
  • GoからCへの文字列の渡し方を紹介しました
    • C.CStringを使った方法
    • unsafe.Ponterを使った方法

ピンポーン unsafe をご使用になる時は、用法・用量を守り正しくお使い下さい。

Comments