[PHP]文章を解析して単語ごとに分解する(形態素解析)

日本語は英語と違い、単語同士が明確に区切られていないので、一つ一つの単語の品詞を調べる場合は、いわゆる「分かち書き」をする必要があります。
分かち書きとは次のように単語を分けて書くことです。

これは日本語です → これ | は | 日本語 | です

プログラミングで分かち書きを行うには大変な労力がかかるので、ゼロから開発するのは現実的ではありません。

簡単に行う方法の一つは Yahoo! API を利用することです。
クエリを送るだけで簡単に解析結果を受け取ることができるので便利ですが、リクエスト回数に上限があったり、クレジット表記が義務付けられたりするので、状況によっては使いにくくなります。

もう一つはサーバーに MeCab をインストールする方法です。
Mecab はオープンソースの形態素解析エンジンで、様々な分野で活用されている実績あるプログラムです。
可能ならこちらを導入するのがベストですが、サーバーにインストールする必要が有るため、レンタルサーバーなどでは運用できない場合があります。

以上のメリット・デメリットを踏まえた上で最も簡便な方法は igo-php を使うことです。
igo-php は Java で作られた形態素解析器を PHP で実装したものです。
これは PHP からクラスファイルをインクルードするだけで使うことができるため、多くの環境で運用することができます。

igo-php を利用するには次の3つを用意します

Igo 本体
Mecab用辞書ファイル(IPA)
igo-php

igo-php 自体には辞書ファイルが付属されていないので、使用するには MeCab の辞書を利用する必要があります。こちらから辞書ファイルをダウンロードします。

辞書ファイルはそのままでは使えません。圧縮されている辞書ファイルを展開したら、Igo を用いてビルドします。
Igo 本体(執筆時点では igo-0.4.5.jar )をダウンロードし、コンソールに次のように入力します

java -Xmx1024m -cp (igo のファイルパス) net.reduls.igo.bin.BuildDic ipadic (展開した辞書のディレクトリ) EUC-JP

ファイルを展開した場所によってパスは変わりますが、私の環境では次のようなコマンドでした。
(Windowsで C:\igo\ に全て入れた場合)

cd C:\igo
java -Xmx1024m -cp igo-0.4.5.jar net.reduls.igo.bin.BuildDic ipadic mecab-ipadic-2.7.0-20070801 EUC-JP

「ipadic」というディレクトリができるので、これを辞書として使います。
文字コードを EUC-jP と指定してありますが、PHP から変換できるので、UTF-8 の PHP で使う場合も辞書は EUC-JP のままで問題ありません。

辞書の準備ができたら PHP プログラムで使う igo-php をダウンロードします。展開すると「lib」フォルダがあるのでそちらを使用します。

実際のPHPソースは次のようになります

<?php
require_once 'lib/Igo.php';
$igo = new Igo("./ipadic", "UTF-8");
$text = "これはテストです";
$result = $igo->wakati($text);
print_r($result);

[出力結果]

Array
(
    [0] => これ
    [1] => は
    [2] => テスト
    [3] => です
)

lib フォルダの Igo.php をインクルードし、new Igo(“辞書フォルダへのパス”, “文字コード(省略可)”) のようにインスタンス化します。

関数は分かち書きをする wakati() と、単語の品詞などを詳細に解析する parse() が利用できます。
wakati() は単語ごとの配列、parse() は単語の名前、品詞、活用形、読み方などが細かく分けられた多次元配列を返します。

もしプログラムがメモリ不足で正常に動作しない場合はプログラムの先頭に次のように記述して下さい。

ini_set('memory_limit', '128M');

[PHP]「もしかして: ~」スペルミス時の修正候補を表示する

Google などでお馴染みの修正候補を表示する機能は、PHP の標準関数 levenshtein() を活用すれば実現できます。

<?php
$word = "brue";
 
//候補一覧
$wordlist = array(
  "apple", "orange", "grape", "banana",
  "book", "car", "house", "water",
  "red", "green", "blue", "black"
);
 
$cost = array();
foreach($wordlist as $value){
    $cost[] = levenshtein($word, $value);
}
array_multisort($cost, SORT_NUMERIC, SORT_ASC, $wordlist);
 
if( $cost[0] > 0 && $cost[0] <= 2){
  echo "もしかして: " . $wordlist[0];
}

出力結果:

もしかして: blue

サンプルでは「brue」というキーワードに対して、「blue」という修正候補が表示されます。
もし正しく「blue」と入力した場合や、「yellow」と入力した場合など、修正候補とあまりにかけ離れている時は表示されません。
if 文「reset($cost) <= 2」の「2」がその許容量を表しています。修正にかかるコストが2以下の場合のみ表示することになっています。
キーワードがリストの単語と一致するとき、コストは「0」なのでこの場合も表示しません。

原理はかなりシンプルですが、単語リストが増えれば処理時間も増えるのでコストが「0」か「1」のものがあった場合 break するなどの工夫する必要があるかもしれません。
あるいは、文字数ごとにリストを用意し、キーワードの文字数プラスマイナス1文字の単語グループの中から修正候補を表示するようにすればずっと早く終わります。

言い忘れましたが、levenshtein() はマルチバイト非対応です。 mb_levenshtein があればいいのですが標準ではないので、代わりに前回作った関数で代用して下さい。

function mb_levenshtein($str1, $str2){
  $length1 = mb_strlen($str1, 'utf-8');
  $length2 = mb_strlen($str2, 'utf-8');
 
  $str1 = mb_str_split($str1, 'utf-8');
  $str2 = mb_str_split($str2, 'utf-8');
  
  $distance = array();
  for($i=0;$i<=$length1;$i++){
    $distance[$i] = array();
    $distance[$i][0] = $i;
  }
  for($i=0;$i<=$length2;$i++){
    $distance[0][$i] = $i;
  }
  $cost = 0;
  for($i=1;$i<=$length1;$i++){
    for($j=1;$j<=$length2;$j++){
	
      $cost = ($str1[$i - 1] === $str2[$j-1]) ? 0 : 1;
      $distance[$i][$j] = min(
        $distance[$i - 1][$j] + 1,
        $distance[$i][$j - 1] + 1,
        $distance[$i - 1][$j - 1] + $cost
      );
    }
  }
  return $distance[$length1][$length2];
}

function mb_str_split($str, $encoding){
  $arr = array();
  $length = mb_strlen($str, $encoding);
  for($i=0;$i<$length;$i++){
    $arr[] = mb_substr($str, $i, 1, $encoding);
  }
  return $arr;
}

文字列を一文字ずつ配列に分解する str_split() という便利な関数があるのですが、こちらもマルチバイトはないので mb_str_split() として作っておきました。
日本語は大変・・・。

[PHP]マルチバイト文字列を比較して類似の度合いを計算する

PHP には2つの文字がどれだけ似ているかを表す関数が2つ用意されています。
ひとつは similar_text() で、もうひとつは levenshtein() です。

similar_text() は類似度をパーセントで返し、
levenshtein() は、両者を一致させるのにかかる作業量を返します。

後者のレーベンシュタイン距離を調べるアルゴリズムは Wikipedia に掲載されているので、
それを参考にマルチバイト対応版 mb_levenshtein を作ってみました。

<?php
function mb_levenshtein($str1, $str2, $encoding){
  $length1 = mb_strlen($str1, $encoding);
  $length2 = mb_strlen($str2, $encoding);
  
  $str1 = mb_str_split($str1, $encoding);
  $str2 = mb_str_split($str2, $encoding);
  
  $distance = array();
  for($i=0;$i<=$length1;$i++){
    $distance[$i] = array();
    $distance[$i][0] = $i;
  }
  for($i=0;$i<=$length2;$i++){
    $distance[0][$i] = $i;
  }
  $cost = 0;
  for($i=1;$i<=$length1;$i++){
    for($j=1;$j<=$length2;$j++){
      $cost = ($str1[$i - 1] === $str2[$j-1]) ? 0 : 1;
      $distance[$i][$j] = min(
        $distance[$i - 1][$j] + 1,
        $distance[$i][$j - 1] + 1,
        $distance[$i - 1][$j - 1] + $cost
      );
    }
  }
  return $distance[$length1][$length2];
}

function mb_str_split($str, $encoding){
  $arr = array();
  $length = mb_strlen($str, $encoding);
  for($i=0;$i<$length;$i++){
    $arr[] = mb_substr($str, $i, 1, $encoding);
  }
  return $arr;
}

$str1 = "sample";
$str2 = "example";

header("Content-type:text/html; charset=utf-8");

echo mb_levenshtein($str1, $str2, 'utf-8');