MeCabバインディングとFFI
common lispからmecabを使う方法のメモ(sbcl依存)。
まずは、多分一番直截的な方法:
;;;;;;;; ;;;; FFI(Foreign Function Interface)用のpackageを使用する (use-package :sb-alien) ;;;;;;;; ;;;; so読み込みと関数定義 (load-shared-object "/usr/local/lib/libmecab.so") (define-alien-routine "mecab_new2" (* t) (param c-string)) (define-alien-routine "mecab_sparse_tostr" c-string (mecab (* t)) (text c-string)) (define-alien-routine "mecab_destroy" void (mecab (* t))) ;;;;;;;; ;;;; wrapper (defvar *mecab*) (defun mecab-parse (text &optional (*mecab* *mecab*)) (mecab-sparse-tostr *mecab* text)) (defmacro with-mecab ((&optional (option "")) &body body) `(let ((*mecab* (mecab-new2 ,option))) (unwind-protect (progn ,@body) (mecab-destroy *mecab*)))) ;;;;;;;; ;;;; 実行例 >(defvar *text* "MeCabはオープンソース形態素解析エンジンです. ") ;; デフォルト > (with-mecab () (mecab-parse *text*)) "MeCab 名詞,固有名詞,組織,*,*,*,* は 助詞,係助詞,*,*,*,*,は,ハ,ワ オープン 名詞,サ変接続,*,*,*,*,オープン,オープン,オープン ソース 名詞,一般,*,*,*,*,ソース,ソース,ソース 形態素 名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ 解析 名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ エンジン 名詞,一般,*,*,*,*,エンジン,エンジン,エンジン です 助動詞,*,*,*,特殊・デス,基本形,です,デス,デス . 名詞,サ変接続,*,*,*,*,* EOS " ;; 分かち書き > (with-mecab ("-Owakati") (mecab-parse *text*)) "MeCab は オープン ソース 形態素 解析 エンジン です . "
次は、mecab-sparse-tostrに文字列(c-string)ではなく、生の(?)バイト列(sbcl依存)を渡す版。(これをわざわざ作成した理由は後述)
;;;; ↓を、上のソースに追加 ;;;;;;;; ;;;; utility ;; signed -> unsigned (手抜き) (defun to-unsigned (b) (if (minusp b) (+ #x100 b) b)) ;; unsigned -> signed (手抜き) (defun to-signed (b) (if (>= b #x80) (- b #x100) b)) (defun char*->array (char*) (do ((i 0 (1+ i)) (acc '() (cons (to-unsigned (deref char* i)) acc))) ((zerop (deref char* i)) ; NULL terminate (coerce (nreverse acc) '(vector (unsigned-byte 8)))))) ;; 使用後に、free-alienする必要がある (defun string->char* (str) (let* ((bytes (string-to-octets str)) (len (length bytes)) (char* (make-alien char (+ 1 len)))) (dotimes (i len) (setf (deref char* i) (to-signed (aref bytes i)))) (setf (deref char* len) 0) ; NULL char*)) ;;;;;;;; ;;;; (* char)用のFF定義 (define-alien-routine "mecab_sparse_tostr" (* char) (mecab (* t)) (text (* char))) ;;;;;;;; ;;;; 実行例 >(defvar *char** (string->char* *text*)) >(with-mecab () (octets-to-string (char*->array (merab-parse *char**)))) "MeCab 名詞,固有名詞,組織,*,*,*,* は 助詞,係助詞,*,*,*,*,は,ハ,ワ オープン 名詞,サ変接続,*,*,*,*,オープン,オープン,オープン ソース 名詞,一般,*,*,*,*,ソース,ソース,ソース 形態素 名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ 解析 名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ エンジン 名詞,一般,*,*,*,*,エンジン,エンジン,エンジン です 助動詞,*,*,*,特殊・デス,基本形,です,デス,デス . 名詞,サ変接続,*,*,*,*,* EOS " ;; 解放 >(free-alien *char**)
とりあえず、ちゃんと動作するようだ。
で、わざわざ面倒な生のバイト列を使う実装も載せた理由だが、まあ単純にc-stringを渡す方法が遅い(オーバヘッドが大きい)からだ。
メモリ周りの調整や文字列のエンコード・デコードを行っているためか、初めの実装と二番目の実装では、mecab呼び出し速度に1.5倍程度の差が出る。
;;;;;;;; ;;;; [速度を比較] ;;;; 比較のためのテキストとしては、夏目漱石の『こころ』を使わせてもらう(青空文庫より入手し、utf8に変換) ;;;;;; ;;; mecabコマンドを直接使った場合(shell) > time mecab kokoro > /dev/null real 0m0.145s user 0m0.136s sys 0m0.008s ;;;;;; ;;; c-stringを使った場合 ;; ファイル読み込み関数 (defun read-file (path) (with-output-to-string (out) (with-open-file (in path) (let ((io (make-echo-stream in out))) (unwind-protect (loop while (read-line io nil nil)) (close io)))))) >(define-alien-routine "mecab_sparse_tostr" c-string (mecab (* t)) (text c-string)) >(let ((text (read-file "kokoro"))) (with-mecab () (time (mecab-parse text))) 'done) ;Evaluation took: ; 0.255 seconds of real time ; 0.252016 seconds of total run time (0.236015 user, 0.016001 system) ; 98.82% CPU ; 808,257,454 processor cycles ; 14,906,592 bytes consed DONE ;;;;;;;; ;;; (* char)を使った場合 ;; ファイルオープン (defmacro with-unix-read-open ((fd path) &body body) `(let ((,fd (sb-unix:unix-open ,path sb-unix:O_RDONLY 0))) (assert ,fd () "'~A' open failed!: ~A" ,path (sb-int:strerror (get-errno))) (unwind-protect (progn ,@body) (sb-unix:unix-close ,fd))) (defun alien-read-file (path) (let ((len (with-open-file (in path) (file-length in)))) (with-unix-read-open (fd-in path) (let* ((buf (make-alien char (1+ len))) (read-num (sb-unix:unix-read fd-in buf len))) (setf (deref buf read-num) 0) buf)))) >(define-alien-routine "mecab_sparse_tostr" (* char) (mecab (* t)) (text (* char))) >(let ((text (alien-read-file "kokoro"))) (with-mecab () (time (mecab-parse text))) (free-alien text) 'done) ;Evaluation took: ; 0.174 seconds of real time ; 0.176011 seconds of total run time (0.156010 user, 0.020001 system) ; 101.15% CPU ; 552,530,821 processor cycles ; 4,080 bytes consed DONE
二番目の方法の場合でも、mecabコマンドを直接使用する場合(及び、おそらくCから直接mecabライブラリを使う場合)に比べると遅いが、それでもc-stringを素直に使った場合に比べると、大分速度は改善している。
sbclは、Cなどで書かれたライブラリを手軽に呼び出せるのは良い(今回の例では、最低4行で準備が終わる)が、大量のテキストをやりとりする場合のオーバーヘッドが難だ。
FFIに限らずlispは、文字列をバイト列としてではなく、文字オブジェクトの配列*1として表現しているので、外部とのIOの際にエンコード・デコードのオーバーヘッドや文字コードが分からないとファイル(やsocket)の内容をstring型として読み込めない、という問題があるのが面倒だ。(逆にlisp内で処理が完結する場合は楽なのだが)
*1:C言語やRubyの場合は'あ'は、-utf8表現の場合-、227と129と130のバイト列として表現されているが、common lispだとcharacter型の'#\あ'という一つのオブジェクトだ。Javaと似てる?