Hatena::ブログ(Diary)

mizchi log

@mizchiの雑記帳

2011-10-28

Node用MeCabバインディング あと非同期版つくろうとして失敗した話


似非原さんの自分で記事を読むのもだるくなってきたので、コンピューターに記事を読ませることにした。 - 蟲!虫!蟲! - #!/usr/bin/bugrammer を読んでて、スクレイピングならnodejsが楽だしnodeでmecab使う手段あったっけーと思いながら調べてたら、あった。

で、KOBA719さんのnode用MeCab(https://github.com/KOBA789/node-mecab)に

# これをforkしてpackage.jsonくっつけてnpmにでも投げてくれる人が現れないかなー(ボソッ

とあったので、とりあえず package.json書いてギッハブに置いといた。https://github.com/mizchi/node-mecab
で、他に登録している人もいないし、npm publishしようと思ったんだけど、自分のnpmのバージョンが古くて登録出来なくて、でよく考えると自分でメンテする気も無いので*1、cloneしてnpmのローカルインストールで使ってください。

インストール

たぶん、mecabをUTF8でインストールしておく必要がある、と思う。node-iconv噛ませたらいけるだろうが、多言語化めんどい。

$ git clone git://github.com/mizchi/node-mecab.git
$ cd node-mecab
$ npm install .  & npm link mecab

使い方:

元のリポジトリに書いてあった使い方だと、品詞とるのに自分でパースする必要があって面倒だったので、適当にインターフェースだけでっち上げた。

mecab = require("mecab");
console.log( mecab.parse("すもももももももものうち"));

[ [ 'すももももも',
'名詞',
'一般',
'*',
'*',
'*',
'*',
'すももももも',
'*',
'*',
'wikipedia' ],
[ 'も',
'助詞',
'係助詞',
'*',
'*',
'*',
'*',
'も',
'モ',
'モ' ],
[ 'もも',
'名詞',
'一般',
'*',
'*',
'*',
'*',
'もも',
'モモ',
'モモ' ],
[ 'の',
'助詞',
'連体化',
'*',
'*',
'*',
'*',
'の',
'ノ',
'ノ' ],
[ 'うち',
'名詞',
'非自立',
'副詞可能',
'*',
'*',
'*',
'うち',
'ウチ',
'ウチ' ] ]


どうでもいいけど、wikipediaから抽出したうちのオリジナル辞書だと「すもももももも」(漫画)が一単語として認識されてて辛い

追記


Twitterで非同期じゃないじゃん、と言われてC++の非同期版書こうとして、挫折した。
失敗した理由は、Taggerの初期化に失敗したか、Tagger#parseへの文字列の受け渡しが失敗してるか、どっちか。

僕はC++見よう見まねで書いてて、全く書けない状態からコピペで作ったので、限界がある。とくにchar* の扱いが苦手。

参考 C++ で node.js ライブラリを作る・その2 - Node.jsで遊ぶよ - Node.jsグループ


mecab_async.cc

#include <node.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <mecab.h>

using namespace v8;
using namespace node;

class AsyncObject : ObjectWrap {
public:
  static 
  void Initialize (const Handle<Object> target) {
    HandleScope scope;
    Local<FunctionTemplate> tmpl = FunctionTemplate::New(New);
    tmpl->InstanceTemplate()->SetInternalFieldCount(1);
    NODE_SET_PROTOTYPE_METHOD(tmpl, "exec", Exec);
    target->Set(String::New("AsyncObject"), tmpl->GetFunction());
  }

  ~AsyncObject () {}
  MeCab::Tagger *tagger;

private:
  AsyncObject (const char *refername) {}
  
  struct BufferedData {
    AsyncObject *refer;
    char *ret_param;
    Persistent<Value> callback;
    MeCab::Tagger *tagger;
  };

  // called at new AsyncObject();
  static Handle<Value> New(const Arguments& args) {
    HandleScope scope;

    BufferedData *buff = (BufferedData *)malloc(sizeof(BufferedData));
    // TODO : 初期化はこれでいい?
    buff->tagger = MeCab::createTagger("");
    // buff->tagger = MeCab::createTagger((args.Length() >= 1 && args[0]->IsString()) ? *String::Utf8Value(args[0]->ToString()) : "");

    if (!args.IsConstructCall()) return args.Callee()->NewInstance();
    try {
      (new AsyncObject(*String::Utf8Value(args[0])))->Wrap(args.This());
    } catch (const char *msg) {
      return ThrowException(Exception::Error(String::New(msg)));
    }

    return args.This();
  }

  static Handle<Value> Exec (const Arguments& args) {
    HandleScope scope;

    BufferedData *buff = (BufferedData *)malloc(sizeof(BufferedData));
    buff->refer = Unwrap<AsyncObject>(args.This());
    buff->ret_param = strdup(*String::Utf8Value(args[0]));
    buff->callback = Persistent<Value>::New(args[1]);

    eio_custom(EIO_Exec, EIO_PRI_DEFAULT, Callback, buff);

    buff->refer->Ref();
    ev_ref(EV_DEFAULT_UC);
    return Undefined();
  }

  int Exec (const char *arg) {
    return 1;
  }
  
  static int EIO_Exec(eio_req *req) {
    HandleScope scope;
    BufferedData *buff = (BufferedData*)(req->data);
    req->result = buff->refer->Exec(buff->ret_param);

    // TODO : パースに失敗しているのか、代入に失敗しているのか
    buff->ret_param =  const_cast<char*>(buff->tagger->parse(buff->ret_param));

    // 文字列を渡すだけ。これはちゃんと動く
    // sprintf( buff->ret_param , "%s", buff->tagger->parse(buff->ret_param));
    return 0;
  }

  // called as callback  
  static int Callback(eio_req *req) {
    HandleScope scope;
    ev_unref(EV_DEFAULT_UC);

    BufferedData *buff = (BufferedData*)(req->data);

    if (buff->callback->IsFunction()) {
      Local<Value> argv[2];
      argv[0] = req->result < 0 ?
        Local<Value>::New(String::New("Error")) :
        Local<Value>::New(Undefined());
      argv[1] = Local<Value>::New( String::New(buff->ret_param)  ) ;

      Persistent<Function>::Cast(buff->callback)->Call(
                       buff->refer->handle_, 2, argv);
    }

    buff->refer->Unref();
    buff->callback.Dispose();
    free(buff->ret_param);
    free(buff);
    return 0;
  }

  
};

extern "C" void init (Handle<Object> target) {
  AsyncObject::Initialize(target);
}



wscript

srcdir = "."
blddir = "build"
VERSION = "0.0.1"

def set_options(opt):
    opt.tool_options("compiler_cxx")

def configure(conf):
    conf.check_tool("compiler_cxx")
    conf.check_tool("node_addon")
    conf.check(lib='mecab', libpath=['/usr/lib', '/usr/local/lib'], uselib_store='MECAB')

def build(bld):
    obj = bld.new_task_gen("cxx",  "shlib",  "node_addon")
    obj.source = "mecab_async.cc"
    obj.target = "mecab_async"
    obj.uselib = "MECAB"

*1C++あんま読めないし

2010-06-10

DBCLS 6/10


引き続きバイトの内容
与えられた辞書データからMeCab辞書を作るところです

とりあえず前回のデータから末尾が名詞じゃない単語を省いた。

import MeCab
mecab = MeCab.Tagger("-Ochasen")
...
def is_nown(text):
    node = mecab.parseToNode(text)
    while node:
        if node.next.surface == "":
            langtype = node.feature.split(",")[0]
            if not langtype in ("名詞","接頭辞","記号"):
                return False
            else :
                return True
        node = node.next

最初うまくいかなくて、なんかおかしいなーと思ったら前回作った不完全な辞書をユーザー辞書に登録したままMeCab用のdicファイルを生成していた。そりゃそうだ。


前回と同じ文章を使ってみる

[mizchi]% echo アデニンの構造とカシューナッツ |mecab [mizchin:~/work/dbcls]
アデニン	名詞,一般,*,*,*,*,アデニン,*,*,dbcls
の	助詞,連体化,*,*,*,*,の,ノ,ノ
構造	名詞,一般,*,*,*,*,構造,*,*,dbcls
と	助詞,並立助詞,*,*,*,*,と,ト,ト
カシュー	名詞,一般,*,*,*,*,カシュー,カシュー,カシュー
ナッツ	名詞,一般,*,*,*,*,ナッツ,ナッツ,ナッツ
EOS

とりあえず辞書に含まれてた格助詞とかは除くことができた。それにしても使ってる元データは「構造」なんてのもいれていて、ちょっと過剰な気配がする

TF*IDF用に青空文庫を大量にパースしておく必要がありそう。元データは青空文庫をローカルに保持しているやつがあるのでそれを使う。

おまけ


最近彼女ができたid:syou6162さんにソースの解説をしてもらいたいのですが、僕はDBCLSにおいて最近彼女ができたid:syou6162 さんの仕事を引き継いでいることになるそうです。
最近彼女ができたid:syou6162さんは最近彼女ができたので幸せそうですね!


という文章をマルコフ連鎖すると

ですが、僕はDBCLSにおいて最近彼女ができたid:syou6162さんは最近彼女ができたid:syou6162さんの仕事を引き継いでいることになるそうです。最近彼女ができたid:syou6162さんは最近彼女ができたid:syou6162さんにソースの解説をしてもらいたいのですが


不毛


使ったのはこれ
MeCabとPythonでマルコフ連鎖を書いてみる(改) | Weboo! Returns.

結構エラー吐くのでスクラッチしたい

2010-06-03

MeCabでユーザー辞書を登録する


バイトでやってる自然言語処理のお仕事です。

  1. データをMeCab辞書のフォーマットにパースしてcsvで保存
  2. /usr/local/libexec/mecab/mecab-dict-index でコンパイル
  3. /usr/local/etc/mecabrc を編集してユーザー辞書を指定する

  4. 参考: MeCab: 単語の追加方法

MeCab辞書のフォーマット

例:

工藤,1223,1223,6058,名詞,固有名詞,人名,名,*,*,くどう,クドウ,クドウ

具体的には

表層形,左文脈ID,右文脈ID,コスト,品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音


今回は名詞を扱うので、ある程度決め打ちにしておく。

{単語},-1,-1,10,名詞,一般,*,*,*,*,{単語},*,*,{登録タグ}

  • 文脈ID:

左文脈IDは, その単語を左から見たときの内部状態IDです. 通常システム辞書と同一場所にある left-id.def から該当する ID を選択します. -1 としておくと, mecab-dict-index が自動的に ID を付与します.



  • コスト:

コストは,その単語がどれだけ出現しやすいかを示しています. 小さいほど, 出現しやすいという意味になります. 似たような単語と 同じスコアを割り振り, その単位で切り出せない場合は, 徐々に小さくしていけばいいと思います.



csvファイルを作る

とりあえずpythonで。エンコーディングはmecabのエンコーディングにあわせておく必要がある。
自分はutf-8決め打ち。

class Term(object):
    def __init__(self,title,option="*" ):
        self.title = title.replace(","," ")
        self.left = -1
        self.right = -1
        self.cost = 10
        self.langtype = "名詞"
        self.type1 = "一般"
        self.type2 = "*"
        self.type3 = "*"
        self.actx = "*"
        self.acttype = "*"
        self.base = title
        self.read = "*"
        self.anounce = "*"
        self.option = option

    def dump(self):
        return ",".join([self.title ,
                         str(self.left ),
                         str(self.right ),
                         str(self.cost ),
                         self.langtype ,
                         self.type1 ,
                         self.type2 ,
                         self.type3 ,
                         self.actx ,
                         self.acttype ,
                         self.base ,
                         self.read ,
                         self.anounce ,
                         self.option ]
                        )
# 泥臭い

def write(fname,terms):
    f = open(fname,"w")
    for i in terms:
        f.write(i.dump()+"\n")
    print "make it :" + fname
    f.close()

あとはTermオブジェクトの配列を作ってwriteに渡してやればいい。

コンパイル

作ったcsvに対して /usr/local/libexec/mecab/mecab-dict-index で dicファイルにコンパイルする。mecab-dict-index はmecabが動いているならインストールされているはず。
エンコーディングに注意。

$ /usr/local/libexec/mecab/mecab-dict-index -d /usr/local/lib/mecab/dic/ipadic -u なんとか.dic -f utf-8 -t utf-8 今作った.csv

これはipadicを使ってる場合。だいたいipadicかnaistの辞書を使っていると思われる。後述するmecabrcファイルの編集で使用するファイルを変更する

ipadic

/usr/local/etc/mecabrc に userdicを追加

  • mecabrc
; Configuration file of MeCab
; $Id: mecabrc.in,v 1.3 2006/05/29 15:36:08 taku-ku Exp $;
dicdir =  /usr/local/lib/mecab/dic/ipadic
userdic = /Users/mizchi/dbcls/mydb.dic ; さっきのcsvを追加

; userdic = /home/foo/bar/user.dic

; output-format-type = wakati
; input-buffer-size = 8192

; node-format = %m\n
; bos-format = %S\n
; eos-format = EOS\n

dicdirを変更すればシステム辞書を変更できる

使ってみた


dbclsで扱う文書を辞書化したものを使った

[mizchi]% echo アデニンの構造とカシューナッツ |mecab 
アデニン	名詞,一般,*,*,*,*,アデニン,*,*,dbcls
の	助詞,連体化,*,*,*,*,の,ノ,ノ
構造	名詞,一般,*,*,*,*,構造,*,*,dbcls
と	名詞,一般,*,*,*,*,と,*,*,dbcls
カシュー	名詞,一般,*,*,*,*,カシュー,カシュー,カシュー
ナッツ	名詞,一般,*,*,*,*,ナッツ,ナッツ,ナッツ
EOS

辞書に含まれている単語がちゃんと適用されてるし、そうじゃないものはシステム辞書が使われている。
「と」まで含まれているので、TF/IDFなどを使って専門用語のフィルターを掛けた方がよさそうだ。