Text::MeCab(日本語変換系Acmeモジュールを支える偉大なモジュール)

tomi-ru
2010-12-21

こんにちは、とみたトミールです。

Text::MeCabに依存しているモジュールリストがAcmeばかり、しかもかなりぼくのモジュールじゃないかwと気づいたので、この偉大な日本語変換系Acmeモジュールを支えるText::MeCab様についてあらためて使い方を紹介してみます。


mecabとは

mecabとは、日本語形態素解析を高速に、未知語もいい感じで補足してくれるよくできたライブラリです。作者の方はGoogleにいて(今も、たぶん。)エイプリルフールに日本語お笑い系機能を出したりしています(それが本職じゃないと思いますけど)。

インストールすると入るmecabコマンドから、コマンドラインではこんな感じに使います。mecabを起動してから、文を入力してエンターです。

$ mecab
わたしはたわしをわたしました。
わたし  名詞,代名詞,一般,*,*,*,わたし,ワタシ,ワタシ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
たわし  名詞,一般,*,*,*,*,たわし,タワシ,タワシ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
わたし  動詞,自立,*,*,五段・サ行,連用形,わたす,ワタシ,ワタシ
まし    助動詞,*,*,*,特殊・マス,連用形,ます,マシ,マシ
た      助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。      記号,句点,*,*,*,*,。,。,。
EOS

たとえば、「わたし」を「それがし」に変換したいとした場合、普通に

$text =~ s/わたし/それがし/g;

すると、「わたします」みたいな別の単語にも影響してしまうので、形態素に区切って、その上で処理をしたりできるわけです。すばらしい。

ちなみにYahooのウェブサービスでもさいきん形態素のやつありますね。あれの方がウェブサービスを使い慣れている場合(WebService::Simpleとかで)は楽かもしれません。

ただ、毎回ネットワーク越しにリクエストが走るという厳しさと、あと後述するmecabのみの機能があるため、ネタ日本語変換で形態素解析使うならText::MeCabは必須でしょう。


Text::MeCabとは

Text::MeCabはmecabのPerlインターフェースです。mecabのページでMeCab.pmっていうperlモジュールも提供されているのですが、それはいかにもSWIGで自動生成しました!っていうインターフェースなので、Text::MeCabの方がいいです。

mecabを先にインストールしておく必要があります。Alien::MeCabっていうCPANの機能を利用してインストールできるパッケージもありますが、mecabの利用には本体だけじゃなくて辞書も必要です。辞書はAlien::MeCabについてこないので、普通にmecabのページに書いている方法で本体と辞書をインストールするといいです。辞書はutf-8でコンパイルするのが好みです。

Text::MeCabは、前はDevel::CheckLibが事前に入ってないと入らない、という話がありましたが昨日のアップデートで解消された模様です。あと、LD_LIBRARY_PATH環境変数が設定されてなくてこける、ということがありましたがぼくだけかもしれません。

export LD_LIBRARY_PATH=/usr/local/lib


使いかたとtips 基本編

「わたし」を「それがし」にするならこんな感じでしょうかね。

use utf8;
use Encode;
use Text::MeCab;

my $parser = Text::MeCab->new();
my $encoding = Encode::find_encoding( Text::MeCab::ENCODING ); # ポイントその1

my $input = "わたしはたわしをわたしました。";

my @result;
for my $text (split /(\s+)/, $input) { # ポイントその2
    if ($text =~ /\s/) {
        push @result, $text;
        next;
    }

    # ポイントその3
    foreach (
        my $node = $parser->parse($encoding->encode($text));
        $node;
        $node = $node->next
    ) {
        # ポイントその4
        next if $node->stat =~ /[23]/; # skip MECAB_(BOS|EOS)_NODE
        
        my $surface = $encoding->decode($node->surface);
        my $feature = $encoding->decode($node->feature);
        my @feature = split ',', $feature;
        
        if ($surface eq 'わたし' and $feature[0] eq '名詞') {
            push @result, 'それがし';
        } else {
            push @result, $surface;
        }
    }
}

print join '', @result;


ポイントその1

Text::MeCabは現状バイト列を受け取ってバイト列を返すため、parse()にはencodeしたものを、結果を受け取るsurface()やfeature()の結果はdecodeしてやる必要があります。Text::MeCab::ENCODINGで辞書のエンコーディングが取れるので、それを元にencode/decodeすればOKです。

これはオーバーヘッドを考慮してということなので、気が向いたらText::MeCabやYahoo形態素APIを同じAPIで使えるLingua::JA::MAParserみたいなのを作ってそれがうまいことラップするような感じにしようかなあとか思ってます。ネタモジュール作りたいだけなので文句言いません。どんなインターフェースでも喜んで使います。

ポイントその2

二重にforを回しているように見えるのですが、mecabは改行を文の区切りとして判断するため、改行が含まれているテキストを内側だけのループでまわして、surfaceをくっつけると改行が消えてしまいます。それで、事前にwhitespaceで区切って、whitespace以外のものをmecabに渡しています。ネタモジュール作りたいだけなので文句言いません。どんなインターフェースでも喜んで使います。

ポイントその3

Text::MeCabのparseは各形態素ごとにText::MeCab::Nodeオブジェクトを返します。nextで次の形態素に進み、最後にfalseを返すので、Cスタイルのforでループするのが良く使われます。

ポイントその4

Text::MeCab::Nodeオブジェクトのメソッドでよく使うのはこんなかんじです。

$node->stat;    # 0 MECAB_NOR_NODE ふつうのノード
                # 1 MECAB_UNK_NODE 未知語ノード(未知語ONで使った場合)
                #         ^ unknownの意味であってウンコの意味ではない
                # 2 MECAB_BOS_NODE 最初フラグ(surfaceはない)
                # 3 MECAB_EOS_NODE 最後フラグ(surfaceはない)
$node->surface; # 「わたし」とか、もともとの文を区切った断片
$node->feature; # 「名詞,代名詞,一般,*,*,*,わたし,ワタシ,ワタシ」みたいな要素詳細

使いかたとtips 発展編

userdicについて!

これが言いたかったのですが、mecabのuserdicオプションは強力です。日本語変換ネタモジュール界では伝家の宝刀と呼ばれています([要出典])。

これは、もともとの辞書にプラスしてカスタム辞書を加味して形態素解析させるというものです。

たとえば、以下のようなcsvをユーザー辞書ファイル user.csvとして用意します。

わたし,1306,1306,1000,名詞,代名詞,一般,*,*,*,わたし,ワタシ,ワタシ,それがし

これをこんな感じでコンパイルします。

/usr/local/libexec/mecab/mecab-dict-index \
    -d /usr/local/lib/mecab/dic/ipadic \ # mecabで使ってる辞書のディレクトリ
    -f utf-8 \    # ユーザー辞書ファイルの文字コード
    -t utf-8 \    # ipadic辞書の文字コード
    -u user.dic \ # コンパイルした辞書の出力先ファイル名
    user.csv      # ユーザー辞書ファイル

mecabからはこんな風に使います。

$ mecab --userdic=user.dic
わたしはたわしをわたしました。
わたし  名詞,代名詞,一般,*,*,*,わたし,ワタシ,ワタシ,それがし
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
たわし  名詞,一般,*,*,*,*,たわし,タワシ,タワシ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
わたし  動詞,自立,*,*,五段・サ行,連用形,わたす,ワタシ,ワタシ
まし    助動詞,*,*,*,特殊・マス,連用形,ます,マシ,マシ
た      助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。      記号,句点,*,*,*,*,。,。,。
EOS

「わたし」の要素に、末尾に「それがし」が付いている=「わたし」の要素がもともとのipadicにある要素ではなく、ユーザー辞書の要素に入れ替えられているのがわかります。

このuserdic機能を利用し、Text::MeCabのオプションでそれを指定して、

my $parser = Text::MeCab->new({
    userdic => 'user.dic',
});

以下のように、もしその追加要素があればそれに入れ替える、みたいな使い方ができます。Acme::Samuraiの単語入れ替えはほぼこれを使っています。

    my $feature = $encoding->decode($node->feature);
    my @feature = split ',', $feature;
    
    push @result, $feature[9] || $surface;


あと既存の要素を入れ替える以外に、新たに要素を追加することもできます。たぶんこっちが本来のuserdicの使い方と思われます。Acme::Ikamusumeでは、HTMLタグを記号と判断してほしかったので(ipadic辞書デフォルトのままだと、未知語として名詞と判断されてします)以下のような追加をしています。

<,6,6,1000,記号,一般(GESO可),*,*,*,*,<,<,<
>,6,6,1000,記号,一般(GESO可),*,*,*,*,>,>,>
</,6,6,1000,記号,一般(GESO可),*,*,*,*,</,</,</

一般(GESO可)っていう分類は、記号は記号でも直前要素にGESOルールを適用するかどうか、っていう勝手要素としてつくったものです。

オリジナル辞書の作り方は、公式ページの資料と、実際のipadicの中のcsvを研究するといいです。


mecab作者の工藤さん、Text::MeCab作者の牧さん、おかげでだいぶ楽してネタモジュールを作れています。ありがとうございます。