Shogo's Blog

Dec 20, 2015 - 1 minute read - python mecab

MeCabをPython3から使う(続報)

Python3からMeCabを扱おうとして挫折していたのですが (MeCabをPython3から使う(中間報告))、 改めて調査して、上手くいかなかった原因が分かったのでご報告します。

おさらい

Python3で以下のようにMeCabを使おうとすると

import MeCab
tagger = MeCab.Tagger('')
text = u'MeCabで遊んでみよう!'
node = tagger.parseToNode(text)
while node:
    print(node.surface + '\t' + node.feature)
    node = node.next

surfaceが全く読み取れないという現象に遭遇していました。

BOS/EOS,*,*,*,*,*,*,*,*
名詞,一般,*,*,*,*,*
助詞,格助詞,一般,*,*,*,で,デ,デ
動詞,自立,*,*,五段・バ行,連用タ接続,遊ぶ,アソン,アソン
助詞,接続助詞,*,*,*,*,で,デ,デ
Traceback (most recent call last):
  File "m.py", line 10, in <module>
  print( node.surface + '\t' + node.feature )
  UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa3 in position 1: invalid start byte

解決策

詳しい原因なんてどうでもいいからMeCabを使いたい人向けに、最初に解決方法を書いておきます。 以下のように本当に解析したい対象を解析する前に、一度parseをしておけばOKです。

import MeCab
tagger = MeCab.Tagger('')

tagger.parse('') # これ重要!!!!

text = u'MeCabで遊んでみよう!'
node = tagger.parseToNode(text)
while node:
    print(node.surface + '\t' + node.feature)
    node = node.next

解析結果を全く使わずに捨てていて無駄のように思えますが、この一行が重要です! これを入れると以下のように正常に解析ができます。

        BOS/EOS,*,*,*,*,*,*,*,*
MeCab   名詞,一般,*,*,*,*,*
で      助詞,格助詞,一般,*,*,*,で,デ,デ
遊ん    動詞,自立,*,*,五段・バ行,連用タ接続,遊ぶ,アソン,アソン
で      助詞,接続助詞,*,*,*,*,で,デ,デ
みよ    動詞,非自立,*,*,一段,未然ウ接続,みる,ミヨ,ミヨ
う      助動詞,*,*,*,不変化型,基本形,う,ウ,ウ
!       名詞,サ変接続,*,*,*,*,*
        BOS/EOS,*,*,*,*,*,*,*,*

解析を行うメソッドであればparseToNodeでも構いません。

原因

結果が壊れる直接的な原因はMeCabをPythonから使う注意点とかで紹介したように、 解析対象の文字列がPythonの管理下から外れGCされてしまったからです。 高速化のために余計なメモリーアロケーションを避けており、メモリ管理は利用者の責任というわけです。

なんとかならないものかと、よくソースコードを追ってみるとMECAB_ALLOCATE_SENTENCEというフラグをONにすれば メモリ管理をMeCabに任せることができるということがわかりました。 これはTaggerを作るときの引数から指定でき、-Cもしくは--allocate-sentenceというオプションがこのフラグに対応します。 これを有効にすれば解決だ!と思ったのですが、実は各種言語バインディングからMeCabを利用する場合はデフォルトで有効になっています

何故だ・・・とさらにコードを追ってみるとparseToNodeの実装が以下のようになっていることがわかりました。

const Node *TaggerImpl::parseToNode(const char *str, size_t len) {
  Lattice *lattice = mutable_lattice();
  lattice->set_sentence(str, len); // このなかでMECAB_ALLOCATE_SENTENCEフラグが立ってるか確認している
  initRequestType();               // このなかでMECAB_ALLOCATE_SENTENCEフラグを立ててる
  if (!parse(lattice)) {
    set_what(lattice->what());
    return 0;
  }
  return lattice->bos_node();
}

MECAB_ALLOCATE_SENTENCEフラグを立てる前に、立っているかを確認しています。

解析対象の文字列を渡す前にinitRequestType()を呼んでMECAB_ALLOCATE_SENTENCEフラグを立てれば良いのですが、 残念ながらinitRequestType()mutable_lattice()もprivateなメソッドなのでPythonから直接呼ぶことはできません。 そこでparse()を使ってinitRequestType()を間接的に呼び出せば問題解決というわけです。

別解

mutable_lattice()は触れなくても、自分で作ったlatticeなら自由にいじれるので、 以下のようにlatticeをPython側で作るのも手ですね。

lattice = MeCab.Lattice()
import MeCab
tagger = MeCab.Tagger('')
lattice = MeCab.Lattice()
text = u'MeCabで遊んでみよう!'
lattice.set_sentence(text)
tagger.parse(lattice)
node = lattice.bos_node()
while node:
    print(node.surface+"\t"+node.feature)
    node = node.next

いずれの方法でもnodeからlatticeやtaggerへの参照がない(実際はあるけどPythonはそのことを知らない)ので、 解析結果を読んでいる最中にlatticeやtaggerがGCで回収されないよう注意しましょう。

追記(2015-12-20)

MeCab自体の問題っぽいので、MeCabにpullreq送って直してもらおうとソースいじってたけど、すでにpatchあったpatchを取り込んだブランチを用意したので、 GCされて困っている方はgit cloneしてお試し下さい。

追記その2(2016-02-08)

なんとか取り込んでもらおうとPull Requestにしてマージしてもらいました。 まだリリースはされていませんが、2016-02-08現在のmasterブランチをビルドすれば、ガーベージコレクションの問題はなくなるはずです。 Twitterで作者に聞いてくれた人がいたみたいで、僕のpulllreq以外もたくさんマージされたようです。 よかったよかった。リリースを心待ちにしています。 (が、Python3対応のpullreqはマージされていない・・・一応試してみてから+1しておこうかな)