php-mecabを使わずに自力でMeCabの処理結果を取り込んでみる
更新日:2019/12/23
僕が以前作成した文字数と単語数をカウントするツール(仮名)で、単語数の出現頻度を計測するためにMeCabというオープンソースの形態素解析エンジンを利用しています。
そしてMeCabの処理結果をPHPで使用するために、php-mecabという拡張モジュールが必要です。
しかしPHPのバージョンアップが原因で、拡張モジュールが一時的に使用できなくなってしまいました。
この問題は上の記事内で解決しています。
しかし、
- 今後同じような状況になったときに対処するのが面倒=>拡張モジュール再生成が面倒
- 僕のツール内では簡単な機能しか使用していない=>簡単な処理でいい
ということで、MeCabの処理結果を取得するPHPライブラリを作成してみようと思います。
完成イメージ
今回は文字数と単語数をカウントするツール(仮名)の修正を最低限に抑えるために、php-mecabのクラス名やメソッド名をそのまま使用します。
実際のコードは次のようになっていて、単語数の出現回数をカウントしています。
$options = array('-d', '/usr/lib64/mecab/dic/mecab-ipadic-neologd'); //辞書ディレクトリの指定
$mecab = new MeCab\Tagger($options); // Taggerクラスの初期化
$nodes = $mecab->parseToNode($m_str); // 先頭ノードの取得
$uay = []; // カウント用配列の初期化
foreach ($nodes as $n) { ノードを順番に処理
if ($n->getLength() > 0) { // 念のため単語の長さが0でないか判定
$pi=$n->getPosId(); // 品詞IDを取得
if( $pi >= 3 && $pi < =9){continue;}
if( $pi >= 13 && $pi <= 25){continue;}
$sf = $n->getSurface(); // 単語を取得
array_key_exists($sf, $uay) ? $uay[$sf]++ : $uay[$sf]= 1; // 単語をキーにしてカウント
}
}
品詞IDを調べているのは、句読点や記号などをカウントしないように、除外するためです。
このソースが機能するように、次のクラスとメソッドを作成していきます。
クラス | メソッド | 内容 |
---|---|---|
MeCab\Tagger | __construct($option) | オプションを受け付けてクラスを初期化 |
parseToNode($m_str) | 文字列を解析しノードを返す | |
MeCab\Node foreach()を利用できるようにする | getLength() | 単語の長さ(バイト)を返す |
getSurface() | 単語を返す | |
getPosId() | 品詞IDを返す |
基本的なロジックとしてはMeCabコマンドをexec関数で実行して、結果をノードに変換します。
やっていることは、初歩的なものだと思います。
完成ソース
namespaceを指定
まずはnamespaceを指定して、php-mecabとの競合を回避します。
namespace Mylib\MeCab;
利用側のソースでは、useを使用することで他のコードを変更せずに済みます。
利用側(既存)のソース
use Mylib\MeCab as MeCab;
$mecab = new MeCab\Tagger($options);
MeCab\Taggerクラス
MeCab\Taggerクラスでは、MeCabのオプション設定および文章を解析してノードを作成します。
まずはクラスの宣言をします。
class Tagger
{
}
このクラス内にコンストラクタを作成します。
class Tagger
private $options='';
function __construct(array $options=null)
{
$this->options = ($options===null) ? '' : implode(' ',$options);
}
オプションは後程使用しやすいように、スペースで連結しておきます。
次はparseToNode関数です。
次の手順でノードを作成します。
(1) 一時ファイルに文章を保存
(2) exec関数でMeCabコマンドを実行し、結果を取り込む
(3) 結果を一行ずつ取り出しMeCab\Nodeクラスを作成する
class Tagger - parseToNode
private $cmd = DEFAULT_COMMAND;
private $_error = false;
public function parseToNode($string = ''){
$this->_error=false;
if( ($tmpfile=self::CreateTemp($string)) === false){
$this->_error='一時ファイルが作成できませんでした';return false;
}
try{
$cmd = $this->cmd . ' '
. (($tmpfile===null) ? '' : $tmpfile) . ' '
. $this->options;
return self::CreateNode($cmd);
} catch( \Exception $e) {
$this->_error=$e->getMessage();return false;
}finally { // 後始末(一時ファイルの削除)
@unlink($tmpfile);
}
}
DEFAULT_COMMANDは、ソースの冒頭で次のように宣言してあります。
<?php
namespace Mylib\MeCab;
const DEFAULT_COMMAND='mecab';
ノード作成に失敗した場合、falseを返し $this->_errorにエラー内容をセットします。
今回は例外処理でExceptionを捕捉しています。
Exceptionの前に\がついているのは、冒頭でnamespaceを指定しているためです。
useを使用することで、\を省略できます。
<?php
namespace Mylib\MeCab;
use Exception;
次にparseToNode内で呼び出している関数を作成します。
class Tagger - parseToNode
private static function CreateTemp($string){ // 一時ファイルの作成
$tmpfile = tempnam(sys_get_temp_dir(),'mymecab');
return ( file_put_contents($tmpfile, $string) === false) ? false : $tmpfile;
}
private static function CreateNode($cmd){ // Nodeの作成
exec($cmd,$lines);
$bfnode=null;
$startNone=false;
foreach($lines as $line){
$node = new Node($line);
if($node->dataCount() >= MINIMUM_FIELD_LENGTH) {
if($startNone===false) $startNone=$node;
if($bfnode!==null) $bfnode->setNextNode($node);
$bfnode=$node;
}
}
return $startNone;
}
最後にエラー内容を外部に公開する関数を作成します。
class Tagger
public function error(){
return $this->_error;
}
MeCab\Nodeクラス
MeCab\Noderクラスは、MeCabコマンドの実行結果を一行一クラスで取り込みます。
foreach()を使用できるように、IteratorAggregateをインプリメントします。
詳しくは次のページをご覧ください。
MeCab\Node
class Node implements \IteratorAggregate
{
private $nextNode=false;
public function getIterator() {
return new NodeIterator($this);
}
public function getNext(){
return $this->nextNode;
}
}
getNextはNodeIteratorクラスから呼び出される関数ですが、php-mecabで定義された関数でもあります。
そのため次のようなループ処理でも、ソースを修正せずに利用可能です。
do{
// 何らかの処理
}while($node = $node->getNext());
冒頭でnamespaceを指定しているため、IteratorAggregateの前には\が必要です。
またはuseを使用します。
<?php
namespace Mylib\MeCab;
use IteratorAggregate;
次にコンストラクタを作成します。
MeCab\Node - __construct
private $data;
private $dataCount=0;
function __construct($line)
{
$this->data = explode( ',' , str_replace("\t" , ',' , $line));
$this->dataCount = count($this->data);
}
取り込んだデータの形式は次のようになっています。
青い 形容詞,自立,*,*,形容詞・アウオ段,基本形,青い,アオイ,アオイ空 名詞,一般,*,*,*,*,空,ソラ,ソラ
一文字目と二文字目の区切りがタブ文字。
それ以降の区切りがカンマ(,)なので、最初にタブ文字をカンマに変換。
その後、カンマ毎に配列に格納しています。
次に、Taggerクラスから呼び出されるdataCount関数とsetNextNode関数を作成します。
MeCab\Node
public function dataCount(){
return $this->dataCount;
}
public function setNextNode($nextNode){
$this->nextNode = $nextNode;
}
MeCab\Node
private $posId = null;
public function getSurface()
{
return (($text = $this->GetData(POS_SURFACE)) === false) ? '' : $text;
}
public function getLength()
{
return strlen($this->getSurface());
}
public function getPosId()
{
if($this->posId !== null) return $this->posId;
return ($this->posId=\Mylib\MeCabLib\MecabType::GetId(
array_map(
function ($v){return $this->GetData($v);},
[POS_TYPE1,POS_TYPE2,POS_TYPE3,POS_TYPE4]
)));
}
private function GetData($pos)
{
return ($pos >= $this->dataCount) ? false : $this->data[$pos];
}
赤文字はMeCabコマンドの実行結果を格納した配列の位置です。
次のように定義してあります。
const POS_SURFACE =0;
const POS_TYPE1 =1;
const POS_TYPE2 =2;
const POS_TYPE3 =3;
const POS_TYPE4 =4;
配列から直接データを取得しても問題ないと思われますが、念のためGetData関数で配列個数チェックを行っています。
NodeIteratorクラス
Nodeクラスでforeach()を利用可能にするイテレーターです。
class NodeIterator
class NodeIterator implements \Iterator
{
private $org;
private $cur;
private $key=0;
function __construct($p)
{
$this->org = $p;
$this->cur = $p;
}
function rewind()
{
$this->cur = $this->org;
$this->key=0;
}
function current()
{
return $this->cur;
}
function key()
{
return $this->key;
}
function next()
{
$this->cur=$this->cur->getNext();
++$this->key;
}
function valid()
{
return false !== $this->cur;
}
}
MecabTypeクラス
MeCabの実行結果から品詞IDを取得するクラスです。
require_once './mecab-data.php';
class MecabType
{
public static function GetId($keys)
{
if(in_array(false,$keys)) return false;
return self::CheckArray(MecabTypeData,$keys,0,count($keys) - 1);
}
protected static function CheckArray($data,$keys,$pos,$endpos)
{
$key = $keys[$pos];
if(array_key_exists($key, $data)){
return ($pos >= $endpos) ? $data[$key]
: self::CheckArray($data[$key],$keys,$pos+1,$endpos);
}
return false;
}
}
MeCabの実行結果のうち、2~5までの4つのキーワードから品詞IDを取得しています。
次の例で赤い文字がキーワードになります。
青い 形容詞,自立,*,*,形容詞・アウオ段,基本形,青い,アオイ,アオイ空 名詞,一般,*,*,*,*,空,ソラ,ソラ
品詞IDの情報はMeCabの辞書ディレクトリ内の pos-id.def ファイルで定義されています。
辞書ディレクトリ内のデフォルトは、/etc/mecabrcファイルにdicdirとして定義されています。
pos-id.def
# cat /usr/lib64/mecab/dic/ipadic/pos-id.def
その他,間投,*,* 0
フィラー,*,*,* 1
感動詞,*,*,* 2
記号,アルファベット,*,* 3
記号,一般,*,* 4
記号,括弧開,*,* 5
記号,括弧閉,*,* 6
このままだと使いにくいので、mecab-data.phpを用意してPHPの連想配列に落とし込んでいます。
mecab-data.php
<?php
namespace Mylib\MeCab;
const MecabTypeData =['その他'=>['間投'=>['*'=>['*'=>'0']]],'フィラー'=>['*'=>['*'=>['*'=>'1']]],'感動詞'=>['*'=>['*'=>['*'=>'2']]],'記号'=>['アルファベット'=>['*'=>['*'=>'3']],'一般'=>['*'=>['*'=>'4']],'括弧開'=>['*'=>['*'=>'5']],'括弧閉'=>['*'=>['*'=>'6']],'句点'=>['*'=>['*'=>'7']],'空白'=>['*'=>['*'=>'8']],'読点'=>['*'=>['*'=>'9']]],'形容詞'=>['自立'=>['*'=>['*'=>'10']],'接尾'=>['*'=>['*'=>'11']],'非自立'=>['*'=>['*'=>'12']]],'助詞'=>['格助詞'=>['一般'=>['*'=>'13'],'引用'=>['*'=>'14'],'連語'=>['*'=>'15']],'係助詞'=>['*'=>['*'=>'16']],'終助詞'=>['*'=>['*'=>'17']],'接続助詞'=>['*'=>['*'=>'18']],'特殊'=>['*'=>['*'=>'19']],'副詞化'=>['*'=>['*'=>'20']],'副助詞'=>['*'=>['*'=>'21']],'副助詞/並立助詞/終助詞'=>['*'=>['*'=>'22']],'並立助詞'=>['*'=>['*'=>'23']],'連体化'=>['*'=>['*'=>'24']]],'助 動詞'=>['*'=>['*'=>['*'=>'25']]],'接続詞'=>['*'=>['*'=>['*'=>'26']]],'接頭詞'=>['形容詞接続'=>['*'=>['*'=>'27']],'数接続'=>['*'=>['*'=>'28']],'動詞接続'=>['*'=>['*'=>'29']],'名詞接続'=>['*'=>['*'=>'30']]],'動詞'=>['自立'=>['*'=>['*'=>'31']],'接尾'=>['*'=>['*'=>'32']],'非自立'=>['*'=>['*'=>'33']]],'副詞'=>['一般'=>['*'=>['*'=>'34']],'助詞類接続'=>['*'=>['*'=>'35']]],'名詞'=>['サ変接続'=>['*'=>['*'=>'36']],'ナイ形容詞語幹'=>['*'=>['*'=>'37']],'一般'=>['*'=>['*'=>'38']],'引用文 字列'=>['*'=>['*'=>'39']],'形容動詞語幹'=>['*'=>['*'=>'40']],'固有名詞'=>['一般'=>['*'=>'41'],'人名'=>['一般'=>'42','姓'=>'43','名'=>'44'],'組織'=>['*'=>'45'],'地域'=>['一般'=>'46','国'=>'47']],'数'=>['*'=>['*'=>'48']],'接続詞的'=>['*'=>['*'=>'49']],'接尾'=>['サ変接続'=>['*'=>'50'],'一般'=>['*'=>'51'],'形容動詞語幹'=>['*'=>'52'],'助数詞'=>['*'=>'53'],'助動詞語幹'=>['*'=>'54'],'人名'=>['*'=>'55'],'地域'=>['*'=>'56'],'特殊'=>['*'=>'57'],'副詞可能'=>['*'=>'58']],'代名詞'=>['一般'=>['*'=>'59'],'縮約'=>['*'=>'60']],'動詞非自立的'=>['*'=>['*'=>'61']],'特殊'=>['助動詞語幹'=>['*'=>'62']],'非自立'=>['一般'=>['*'=>'63'],'形容動詞語幹'=>['*'=>'64'],'助動詞語幹'=>['*'=>'65'],'副詞可能'=>['*'=>'66']],'副詞可能'=>['*'=>['*'=>'67']]],'連体詞'=>['*'=>['*'=>['*'=>'68']]]];
この形式だと良くわかりませんが、次のような各キーワードをキーとしたツリー構造になっています。
,'助詞'=>
['格助詞'=>
['一般'=>
['*'=>'13']
,'引用'=>
['*'=>'14']
,'連語'=>
['*'=>'15']
]
,'係助詞'=>
['*'=>
['*'=>'16']
]
あとは、順番にキーが一致するかどうかを見ているだけです。
MeCab品詞IDキー配列作成スクリプト
今回、品詞IDの配列作成用にスクリプトを作成しました。
しかし、需要が全くなさそうなのでソースは記載しません。
デフォルトと異なる辞書を使用している方は、次のファイルをダウンロードして配列を生成してみてください。
ダウンロード
使用規約:本スクリプトを使用する場合、次の事項に同意されたものとみなします。
* 1) 本スクリプトは無償で提供されるものであり、完全な動作を保証するものではありません。
* 2) 本スクリプト使用によるいかなる損害について、本プログラムの作成者は一切の責任を負いません。
最終テスト
最後にテスト用のスクリプトを作成して、思った通りの結果を得られるか確認してみます。
テスト用スクリプト:mecab-test.php
require_once './mecab-lib.php';
use Mylib\MeCab as MeCab;
$options = array('-d', '/usr/lib64/mecab/dic/mecab-ipadic-neologd');
$mecab = new MeCab\Tagger($options);
$m_str = 'もももすももももものうち';
$nodes = $mecab->parseToNode($m_str);
foreach ($nodes as $node){
echo 'length:' . $node->getLength()
. ' posId:' . $node->getPosId()
. ' Surface:' . $node->getSurface()
. '';
}
ポイントは作成したPHPファイルをrequireで取り込んでいる点と、useでMylib\MeCabのエイリアスをMeCabとして定義している点です。
これにより、既存ソースの修正が最低限ですむことになります。
実行結果
length:6 posId:38 Surface:もも
length:3 posId:16 Surface:も
length:18 posId:41 Surface:すももももも
length:3 posId:24 Surface:の
length:6 posId:66 Surface:うち
すももももも?
どうやら私の使っている辞書は、「すももももも」が固有名詞で登録されているようです。
いいのか悪いのか、判断に悩みます…
更新日:2019/12/23
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。