GolangからMeCabを呼び出すライブラリ探せばあるにはあるのですが、 なんだかどれもメモリ管理がちょっと怪しいんですよね・・・。
メモリ管理はbluele/mecab-golangが一番しっかりしているっぽいですが、 libmecabの一番高機能だけど面倒な使い方しか対応していなくて、ちょっとカジュアルに遊ぶにはつらい。
というわけで、カジュアルな用途から高度な使い方まで対応したWrapperを書いてみました。
使い方
READMEとgodocのexamplesからのコピペになってしまいますが、 簡単に使い方の紹介です。
インストール
go get
で取ってくることはできますが、事前にlibmecabとリンクするための設定が必要です。
$ 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
がインストールされているはずなので、
それを使うのが楽でしょう。
$ 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
コマンドと同等の結果を文字列として受け取れます。
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
を使うと表層表現と品詞が最初から分かれた形で取得できます。
生起コストのようなより詳細な情報も取れます。
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 ライブラリで紹介されている、マルチスレッド環境の場合での使い方にも対応しています。
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())
複数のゴルーチンからmodel
やtagger
を共有できると思います。lattice
だけはゴルーチン毎に生成してください。
(へいれつへーこーしょりとかよくわかってないですが、スレッドセーフならゴルーチンセーフという認識であってますよね?)
メモリ効率もいいのでは(未検証なので誰か確かめて・・・)。
GoからCへ文字列を渡す方法について
一般的な方法
GoからCへ文字列を渡すには、Goの文字列をC.CString
を使ってCの文字列に変換する必要があります。
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
)でも同じことができるはず・・・!
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 をご使用になる時は、用法・用量を守り正しくお使い下さい。