DEVELOPERS BLOGデベロッパーズブログ
- HOME >
- 加藤 正人のデベロッパーズブログ >
- 形態素解析システム MeCab (めかぶ) を CakePHP から利用する
加藤 正人のデベロッパーズブログ
- 氏名
- 加藤 正人
- 役職
- 多分SE
- 血液型
- 秘密
- 出没
- 美味しいもののあるところ
- 特色
- タヒチ大好き。ちょいメタボ。
- 2020/12/15
- CakePHP3 のリダイレクトがうまく行かない例
- 2020/11/12
- Apache Bench
- 2020/10/05
- PhpSpreadsheet でExcel ファイルを読み込む (CakePHP3)
先の記事で形態素解析システム 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 |
02 | class 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 |
02 | class 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 |
006 | class 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 | ?> |
以上。
関連エントリー
- 2018/04/14
- CakePHP のレンダリング結果を保存したい
- 2017/09/06
- CakePHP 2.x の Cookie と js.cookie.js
- 2017/07/08
- 時刻入力用 jQuery Plugin TimePicki の不具合調整
- 2017/06/18
- CakePHP プラグインで HTTPS 判定
- 2016/02/27
- 作業用モデルビヘイビア