DEVELOPERS BLOGデベロッパーズブログ

  1. HOME > 
  2. 加藤 正人のデベロッパーズブログ > 
  3. 形態素解析システム MeCab (めかぶ) を CakePHP から利用する

加藤 正人のデベロッパーズブログ

加藤 正人

氏名
加藤 正人
役職
多分SE
血液型
秘密
出没
美味しいもののあるところ
特色
タヒチ大好き。ちょいメタボ。

加藤 正人

2015/05/09

形態素解析システム MeCab (めかぶ) を CakePHP から利用する

 

先の記事で形態素解析システム MeCab (めかぶ) を導入したのだが、それだけではつまらないので管理者権限なしで (module 等にすることなく) CakePHP から利用する方法を考えてみた。

実装方法

具体的にはコンポーネントとして実装し、MeCab 呼び出しは mecab コマンドを子プロセスとして実行し結果を受け取る方法とする。

コンポーネントのオプションとして、以下のものを用意する。

mecab_path

MeCab の実行ファイル mecab を指すフルパス文字列。省略不可

remove_spaces

mecab への入力文字列から空白を事前に除去するかどうかを指定する論理値。偽の場合は除去しない。それ以外の場合は除去する。省略時の既定値は TRUE。

spawn_type

mecab 処理の子プロセスをどのように生成するかを指定する文字列。利用可能な文字列とその意味は下記の通り。省略時の既定値は exec

proc_open

子プロセスを PHP の proc_open 関数を使用して生成する。子プロセスの入出力はともにパイプで行われ、オーバーヘッドが最も小さいが、デッドロックした場合 web サーバもロックされる恐れがある。web サーバがデッドロックに陥った場合は、サービスを再起動する必要がある。共用サーバ等では使用が禁止されていることもある。

exec

子プロセスを PHP の exec 関数を使用して生成する。子プロセスへの入力 (mecab で処理する入力文) は tempnam() 関数で名前生成した一時ファイルに保存してコマンドラインのパイプで mecab 処理に渡される。mecab 処理結果は exec() 関数の参照渡し変数に格納される。proc_open が利用できない (または禁止されている) サーバの場合でも、exec() の利用ができるのであればこちらを使用する。

result_type

mecab 処理結果をどのように取得するかを指定する文字列。指定可能な文字列とその意味は下記の通り。省略時の既定値は raw

raw

mecab の処理結果をそのまま文字列として返す。

'すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
EOS'
array

mecab の処理結果の形態素情報を連想配列で表現し、それらを要素に持つ配列として返す。

array(
	(int) 0 => array(
		'表層形' => 'すもも',
		'品詞' => '名詞',
		'品詞細分類1' => '一般',
		'品詞細分類2' => '*',
		'品詞細分類3' => '*',
		'活用形' => '*',
		'活用型' => '*',
		'原型' => 'すもも',
		'読み' => 'スモモ',
		'発音' => 'スモモ'
	),
	(int) 1 => array(
		'表層形' => 'も',
		'品詞' => '助詞',
		'品詞細分類1' => '係助詞',
		'品詞細分類2' => '*',
		'品詞細分類3' => '*',
		'活用形' => '*',
		'活用型' => '*',
		'原型' => 'も',
		'読み' => 'モ',
		'発音' => 'モ'
	),
	(int) 2 => array(
		'表層形' => 'もも',
		'品詞' => '名詞',
		'品詞細分類1' => '一般',
		'品詞細分類2' => '*',
		'品詞細分類3' => '*',
		'活用形' => '*',
		'活用型' => '*',
		'原型' => 'もも',
		'読み' => 'モモ',
		'発音' => 'モモ'
	),
	(int) 3 => array(
		'表層形' => 'も',
		'品詞' => '助詞',
		'品詞細分類1' => '係助詞',
		'品詞細分類2' => '*',
		'品詞細分類3' => '*',
		'活用形' => '*',
		'活用型' => '*',
		'原型' => 'も',
		'読み' => 'モ',
		'発音' => 'モ'
	),
	(int) 4 => array(
		'表層形' => 'もも',
		'品詞' => '名詞',
		'品詞細分類1' => '一般',
		'品詞細分類2' => '*',
		'品詞細分類3' => '*',
		'活用形' => '*',
		'活用型' => '*',
		'原型' => 'もも',
		'読み' => 'モモ',
		'発音' => 'モモ'
	),
	(int) 5 => array(
		'表層形' => 'は',
		'品詞' => '助詞',
		'品詞細分類1' => '係助詞',
		'品詞細分類2' => '*',
		'品詞細分類3' => '*',
		'活用形' => '*',
		'活用型' => '*',
		'原型' => 'は',
		'読み' => 'ハ',
		'発音' => 'ワ'
	),
	(int) 6 => array(
		'表層形' => 'もも',
		'品詞' => '名詞',
		'品詞細分類1' => '一般',
		'品詞細分類2' => '*',
		'品詞細分類3' => '*',
		'活用形' => '*',
		'活用型' => '*',
		'原型' => 'もも',
		'読み' => 'モモ',
		'発音' => 'モモ'
	)
)
object

mecab の処理結果の形態素情報を stdClass オブジェクトで表現し、それらを要素に持つ配列として返す。

array(
	(int) 0 => object(stdClass) {
		表層形 => 'すもも'
		品詞 => '名詞'
		品詞細分類1 => '一般'
		品詞細分類2 => '*'
		品詞細分類3 => '*'
		活用形 => '*'
		活用型 => '*'
		原型 => 'すもも'
		読み => 'スモモ'
		発音 => 'スモモ'
	},
	(int) 1 => object(stdClass) {
		表層形 => 'も'
		品詞 => '助詞'
		品詞細分類1 => '係助詞'
		品詞細分類2 => '*'
		品詞細分類3 => '*'
		活用形 => '*'
		活用型 => '*'
		原型 => 'も'
		読み => 'モ'
		発音 => 'モ'
	},
	(int) 2 => object(stdClass) {
		表層形 => 'もも'
		品詞 => '名詞'
		品詞細分類1 => '一般'
		品詞細分類2 => '*'
		品詞細分類3 => '*'
		活用形 => '*'
		活用型 => '*'
		原型 => 'もも'
		読み => 'モモ'
		発音 => 'モモ'
	},
	(int) 3 => object(stdClass) {
		表層形 => 'も'
		品詞 => '助詞'
		品詞細分類1 => '係助詞'
		品詞細分類2 => '*'
		品詞細分類3 => '*'
		活用形 => '*'
		活用型 => '*'
		原型 => 'も'
		読み => 'モ'
		発音 => 'モ'
	},
	(int) 4 => object(stdClass) {
		表層形 => 'もも'
		品詞 => '名詞'
		品詞細分類1 => '一般'
		品詞細分類2 => '*'
		品詞細分類3 => '*'
		活用形 => '*'
		活用型 => '*'
		原型 => 'もも'
		読み => 'モモ'
		発音 => 'モモ'
	},
	(int) 5 => object(stdClass) {
		表層形 => 'は'
		品詞 => '助詞'
		品詞細分類1 => '係助詞'
		品詞細分類2 => '*'
		品詞細分類3 => '*'
		活用形 => '*'
		活用型 => '*'
		原型 => 'は'
		読み => 'ハ'
		発音 => 'ワ'
	},
	(int) 6 => object(stdClass) {
		表層形 => 'もも'
		品詞 => '名詞'
		品詞細分類1 => '一般'
		品詞細分類2 => '*'
		品詞細分類3 => '*'
		活用形 => '*'
		活用型 => '*'
		原型 => 'もも'
		読み => 'モモ'
		発音 => 'モモ'
	}
)

なお、mecab コマンドには標準辞書以外の辞書指定や出力項目のチューニング機能等もあるが、これらは実装していない。

コンポーネントの宣言

CakePHP のコントローラ内で、下記のようにする。(この例では、LambdaTools プラグインのコンポーネントとして実装している)

01<?php
02class HogeHogeController extends AppController
03{
04    // …【中略】…
05 
06    public $components = array(
07        'LambdaTools.MeCab' => array(
08            'mecab_path' => '/home/hoge/my_mecab/bin/mecab',
09            'spawn_type' => 'exec',
10            'result_type' => 'array',
11        ),
12    }
13 
14    // …【以下略】…
15}
16?>
17    

なお、コンポーネントの設定はアクション内で MeCab::setOption() メソッドで設定することも可能。

パブリックメソッド

MeCab コンポーネントには以下のパブリックメソッドが用意されている。

analyze($text)

$text に与えられた日本語文字列を形態素解析した結果を返す。解析結果の形式はコンポーネントの 'result_type' オプションに従う。

setOptions($options)

連想配列として指定された MeCab コンポーネントオプションを設定する。連想配列は {オプション名} => {値} となる。このメソッドを使うことで、アクション処理時に MeCab コンポーネントの動作を変更することができる。

以下の簡単なサンプルは、入力フォームで入力テキストと解析結果のタイプを読み込み、テキストを mecab で処理して結果を配列で表示させるアクションの例である。(他のオプションはコンポーネント宣言時に与えているとする。)

01<?php
02class HogeHogeController extends AppController
03{
04    // …【中略】…
05 
06    public function mecab()
07    {
08        $form_name = 'MeCabTest';
09        if ( ($tmp=@$this->request->data[$form_name]) )
10        {
11            // 入力があった ⇒ MeCab 呼び出し
12            $this->MeCab->setOptions(array('result_type' => $tmp['result_type']));  // result_type をラジオボタンの値で設定する
13            $result = $this->MeCab->analyze($this->request->data[$form_name]['input']);
14            $this->set(compact('result'));  // 結果をビュー変数 $result に設定して表示させる
15        }
16    }
17 
18    // …【以下略】…
19}
20?>
21    

コンポーネントのコード

最後に、MeCab コンポーネントのソースを示す。

001<?php
002// 京都大学情報学研究科、日本電信電話株式会社コミュニケーション科学基礎研究所 共同研究ユニットプロジェクトを通じて開発されたオープンソース形態素解析エンジン
003// MeCab (めかぶ) を利用するためのコンポーネント。
004// MeCab の詳細については、http://taku910.github.io/mecab/ を参照。
005 
006class MeCabComponent extends Component
007{
008    private $default_options = array(   // 既定値オプション設定を保持する変数 (Read Only)
009        // 必須オプション
010        'mecab_path' => NULL,   // MeCab がインストールされているパス。必須。
011        // 省略可能オプション
012        'remove_spaces' => TRUE,    // PHP の「偽」でない場合、入力文中の空白文字 (半角スペース、TAB、CR、LF、全角スペース) を除去する。既定値は TRUE。
013        'spawn_type' => 'exec', // 子プロセス処理方法 ('proc_open': mecab 処理を proc_open で行い、入出力は全てパイプで処理する。 'exec': 入力は一時ファイルに保存し、mecab 処理は 'exec' 関数で行ない結果を exec 関数の戻り値として取得する。
014        'result_type' => 'raw', // 解析結果タイプ ('raw': mecab の出力テキストそのまま。 'array': 項目名付連想配列。 '
015    );
016    private $actual_options;    // 既定値オプションに宣言時オプションを上書きした「実際に適用するオプション」を保持する変数。
017 
018    private $controller;    // 呼び出し側コントローラを覚えておく変数。
019 
020    private $spawn; // mecab を子プロセスとして実行する処理を保持するプライベート変数。
021    private $post_process// mecab の結果を変換する処理を保持するプライベート変数。
022 
023    private function _init_setting(ComponentCollection $collection, $settings)
024    {
025        return array_merge($this->default_options, $settings);
026    }
027 
028 
029    private function _option_check()
030    {
031        // mecab_path
032        if ( is_null($this->actual_options['mecab_path']) )
033        {
034            throw new Exception('Option \'mecab_path\' is not specified.');
035        }
036        else if ( !file_exists($this->actual_options['mecab_path']) )
037        {
038            throw new Exception('Specified \'mecab_path\' doesn\'t exist.');
039        }
040        // spawn_type
041        switch ( strtolower(@$this->actual_options['spawn_type']) )
042        {
043        case 'proc_open':   // プロセスをオープンし、入出力を全てパイプで処理する (オーバーヘッドは小さいが、デッドロックすると Web サーバも応答しなくなるので注意)
044            $this->spawn = function ($text) {
045                $descriptors = array(
046                    0 => array('pipe', 'r'),    // stdin
047                    1 => array('pipe', 'w'),    // stdout
048                    2 => array('pipe', 'w'),    // stderr
049                );
050                $proc = proc_open($this->actual_options['mecab_path'], $descriptors, $pipes, NULL, NULL);
051                if ( is_resource($proc) )
052                {
053                    // プロセスのオープンに成功 ⇒
054                    try
055                    {
056                        fwrite($pipes[0], $text);   // 子プロセスの stdin に入力文を書き込む
057                        fclose($pipes[0]);
058                        $ret = stream_get_contents($pipes[1]);  // 子プロセスの stdout を全て読み取る
059                        fclose($pipes[1]);
060                        fclose($pipes[2]);
061                    }
062                    catch ( Exception $e )
063                    {
064                        // 子プロセスと連結したパイプの後始末
065                        for ( $i=0; $i<3; $i++ )
066                        {
067                            if ( is_resource($pipes[$i]) ) fclose($pipes[$i]);  // パイプが閉じられていない場合は close する。
068                        }
069                    }
070                    // 子プロセスをクローズする
071                    proc_close($proc);
072                }
073                else
074                {
075                    // プロセスのオープンに失敗
076                    $ret = FALSE;
077                }
078                return $ret;
079            };
080            break;
081        case 'exec':    // mecab への入力は一時ファイルに保存し、子プロセスへの入力とする。結果は exec の戻り値で取得。ファイルを経由する分オーバーヘッドは大きい。
082            $this->spawn = function ($text) {
083                $fpath = tempnam(sys_get_temp_dir(), 'LambdaTools');
084                $fp = fopen($fpath, 'w');
085                fwrite($fp, $text);
086                fclose($fp);
087                $ret = NULL;
088                exec("cat {$fpath} | {$this->actual_options['mecab_path']}", $ret);
089                unlink($fpath);
090                return implode("\n", $ret);
091            };
092            break;
093        default:
094            throw new Exception('Invalid \'spawn_type\' specified.');
095        };
096        // result_type
097        switch ( strtolower(@$this->actual_options['result_type']) )
098        {
099        case 'raw':
100            $this->post_process = function ($result) {
101                // 取得したデータをそのまま返す
102                return $result;
103            };
104            break;
105        case 'array':
106            $this->post_process = function ($result) {
107                // 取得したデータを配列構造に変換して返す (既定値の出力フォーマットは {表層形}\t{品詞},{品詞細分類1},{品詞細分類2},{品詞細分類3},{活用形},{活用型},{原型},{読み},{発音}
108                $ret = NULL;
109                foreach ( explode("\n", $result) as $line )
110                {
111                    $tmp = NULL;    // 初期化
112                    if ( $line === 'EOS' )
113                        break;
114                    else
115                    {
116                        list($surface, $rest) = explode("\t", $line);
117                        $tmp = explode(',', $rest);
118                        $ret[] = array(
119                            '表層形' => $surface,
120                            '品詞'          => @$tmp[0],
121                            '品詞細分類1' => @$tmp[1],
122                            '品詞細分類2' => @$tmp[2],
123                            '品詞細分類3' => @$tmp[3],
124                            '活用形'     => @$tmp[4],
125                            '活用型'     => @$tmp[5],
126                            '原型'          => @$tmp[6],
127                            '読み'          => @$tmp[7],  // 未知語の場合、該当欄がない場合がある ⇒ NULL で表す
128                            '発音'          => @$tmp[8],  // 未知語の場合、該当欄がない場合がある ⇒ NULL で表す
129                        );
130                    }
131                }
132                return $ret;
133            };
134            break;
135        case 'object':
136            $this->post_process = function ($result) {
137                // 取得したデータを stdClass オブジェクトに変換して返す (既定値の出力フォーマットは {表層形}\t{品詞},{品詞細分類1},{品詞細分類2},{品詞細分類3},{活用形},{活用型},{原型},{読み},{発音}
138                $ret = NULL;
139                foreach ( explode("\n", $result) as $line )
140                {
141                    $tmp = NULL;
142                    if ( $line === 'EOS' )
143                        break;
144                    else
145                    {
146                        list($surface, $rest) = explode("\t", $line);
147                        $tmp = explode(',', $rest);
148                        $obj = new stdClass();
149                        $obj->表層形      = $surface;
150                        $obj->品詞        = @$tmp[0];
151                        $obj->品詞細分類1 = @$tmp[1];
152                        $obj->品詞細分類2 = @$tmp[2];
153                        $obj->品詞細分類3 = @$tmp[3];
154                        $obj->活用形     = @$tmp[4];
155                        $obj->活用型     = @$tmp[5];
156                        $obj->原型          = @$tmp[6];
157                        $obj->読み          = @$tmp[7];   // 未知語の場合、該当欄がない場合がある ⇒ NULL で表す
158                        $obj->発音          = @$tmp[8];   // 未知語の場合、該当欄がない場合がある ⇒ NULL で表す
159                        $ret[] = $obj;
160                    }
161                }
162                return $ret;
163            };
164            break;
165        default:
166            throw new Exception('Imnalid \'result_type\' specified.');
167        }
168    }
169 
170    public function __construct(ComponentCollection $collection, $settings=array())
171    {
172        // 呼び出し側コントローラを覚えておく。
173        $this->controller = $collection->getController();
174 
175        $this->actual_options = $this->_init_setting($collection, $settings);
176 
177        ////////////////////////
178        // オプションチェック
179        ///////////////////////
180        $this->_option_check();
181 
182    }
183 
184    // proc_open で mecab コマンドを子プロセスとしてオープンし、データを処理した結果を取得する。【TODO】
185    public function analyze($text)
186    {
187        // 空白除去オプションが指定されている場合は除去する。
188        if ( @$this->actual_options['remove_spaces'] )
189        {
190            $text = mb_ereg_replace('[\s\n\r\t]', '', $text);
191        }
192        $ret = call_user_func($this->spawn, $text);
193        return call_user_func($this->post_process, $ret);
194    }
195 
196    // 実行時にオプションを設定するメソッド
197    // 戻り値: 設定後の実際のオプション連想配列
198    public function setOptions($options=array())
199    {
200        foreach ( $options as $key => $value )
201        {
202            $this->actual_options[$key] = $value;
203        }
204        // オプションチェック
205        $this->_option_check();
206 
207        return $this->actual_options;
208    }
209}
210?> 

以上。

関連タグ: CakePHP2  MeCab  めかぶ  全文検索 

関連エントリー

CakePHP のレンダリング結果を保存したい

CakePHP 2.x の Cookie と js.cookie.js

時刻入力用 jQuery Plugin TimePicki の不具合調整

CakePHP プラグインで HTTPS 判定

作業用モデルビヘイビア