Stellaqua - TOMの技術日記 このページをアンテナに追加 RSSフィード

2010年08月08日

[]Amazon EC2上のHadoopMeCabを使えるようにしてみた…い

最近すっかり"Webサービスを作ってみた"系の記事ばっかりでしたが、今回は久々にEC2上でのHadoopのお話。

以前の記事EC2上でHadoopを動かした時は、MeCabデフォルトで入っていなかったので、事前に自宅サーバ上で分かち書きしたデータを使っていました。

ただ、日本語処理するならMeCabはほぼ必須だろうし、せっかくならMeCabが使える状態でMapReduceしたいのが人情ってもんです。

という訳で、EC2上のHadoopを使って、MeCabを利用した日本語文章の単語カウントMapReduceに挑戦してみました。

基本的な方向性としては以下の通りです。

  1. EC2上でインスタンス起動。
  2. 起動したインスタンスMeCabインストール
  3. AMIとして保存。
  4. Hadoopを起動する時にこのAMIが使われるようにする。

で、実際にやってみた訳なんですが、結論から言うとうまくいきませんでした…。

AMIを保存して、そのAMIが使われるようにするところまではできたんですが、そのインスタンスログインできない状態になってしまって、実際にMeCabを利用したMapReduceを動かすところまでできていません。

という事で途中までではありますが、やってみた手順を書き残しておきたいと思います。

以下、"[local]$"がローカルのサーバ上、"[ec2]$"がEC2上でのプロンプトを表す事とします。

EC2上でHadoopを起動してログインする

まずはEC2上にインスタンスを1つ起動させてログインします。

[local]$ hadoop-ec2 launch-master hadoop-test 1
[local]$ scp -i stellaqua.id *.pem root@ec2-XXX-XXX-XXX-XXX.compute-1.amazonaws.com:/mnt
[local]$ hadoop-ec2 login hadoop-test

タイムゾーンを変更する

今回(というか毎度)参考にさせて頂いているid:rx7さんの記事によるとタイムゾーンの設定をしておいた方がよいとの事なので、ついでにここでタイムゾーンの変更をしておきます。

[ec2]$ cp /usr/share/zoneinfo/Japan /etc/localtime
cp: `/etc/localtime' を上書きしてもよろしいですか(yes/no)? y

MeCabインストールする

続いて本命のMeCabインストールします。

[ec2]$ yum -y update
[ec2]$ yum -y install mecab mecab-devel mecab-ipadic
[ec2]$ echo 'こんにちは' | mecab
こんにちは      感動詞,*,*,*,*,*,こんにちは,コンニチハ,コンニチワ
EOS

ちゃんとMeCabが使えるようになりましたね!

EC2root権限が与えられるので、yumで必要なものをガシガシ入れてしまうとよいですね。

AMIを作成してS3にアップロードする

AMI作成の手順については、毎度お世話になっているid:rx7さんの記事を参考にさせて頂きました。

Amazon EC2/S3を使ってみた - 3.EC2起動後〜AMI作成 - 元RX-7乗りの適当な日々

[ec2]$ cd /mnt/
[ec2]$ ec2-bundle-vol -d /mnt --privatekey pk-XXXX.pem --cert cert-XXXX.pem --user XXXX-XXXX-XXXX -p hadoop-0.17.0-i386 -r i386
[ec2]$ ec2-upload-bundle --bucket stellaqua/ec2_images/hadoop_mecab --manifest hadoop-0.17.0-i386.manifest.xml --access-key XXXX --secret-key XXXX
[ec2]$ exit
[local]$ hadoop-ec2 terminate-cluster hadoop-test
[local]$ ec2-register stellaqua/ec2_images/hadoop_mecab/hadoop-0.17.0-i386.manifest.xml
IMAGE   ami-XXXX
[local]$ vi hadoop-ec2-env.sh
→"S3_BUCKET=hadoop-ec2-images"となっているところを、上記で指定したバケットに変える

ポイントは、manifestファイルをHadoopのバージョンとアーキテクチャの名前を付けて保存する事と、設定ファイルでAMIを保存したバケットを設定しておく事ですね。

これで、hadoop-ec2コマンドでHadoopを起動する時に、登録したAMIが使われるようになるはずです。

早速、クラスタを起動してログインしてみます。

[local]$ hadoop-ec2 launch-cluster hadoop-test 1
[local]$ hadoop-ec2 login hadoop-test

これでログインできるはずなんですが、なぜかログインしに行ったままウンともスンとも言わない状態になってしまいます。

"Permission denied"とも言われないし、netstatで見るとESTABLISHEDにはなっているので、接続自体はできているとは思うのですが…。その後も色々試してはみたんですが、現時点でお手上げ状態になっています…。

また折を見て試してみて、うまくいったら記事にしようかなと思います。

2010年03月26日

[]はてなキーワードからMeCabのユーザ辞書を作る

今まで作ったWebサービスのいくつかでMeCabを使っているんですが、最近の言葉がMeCabの辞書に反映されていないので、特に話し言葉に近いブログとかの単語判別が甘いという悩みがありました。

そんな訳で、以前からずっとやろうと思っていたまま後延ばしにしていた、はてなキーワードからMeCabのユーザ辞書を作って利用するようにする作業を行いました。

手順などは、以下のサイトを参考にさせてもらいました。

はてなキーワードからMecCab辞書を生成する(Ruby版)

以下、実際に行った手順。

はてなキーワードファイルをダウンロードする

以下のページからはてなキーワードがまとまって入っているCSVファイルをダウンロードします。

はてなダイアリーキーワードふりがなリストを公開しました - はてなダイアリー日記

$ wget "http://d.hatena.ne.jp/images/keyword/keywordlist_furigana.csv"

辞書作成用のCSVファイルを作成する

最初は上記Ruby版の変換スクリプトをそのまま使わせて頂いたのですが、スワップを使い切ってしまうぐらいメモリを食ってしまって処理が止まってしまうという事態になってしまった為、PHPスクリプトを書き直して変換処理を掛ける事にしました。

#!/usr/bin/php
<?php
$fd_in = fopen('keywordlist_furigana.csv', 'r');
$fd_out = fopen('hatena.csv', 'w');

$i = 0;
while ( !feof($fd_in) ) {
    // EUC-JPのファイルなので、使用しているUTF-8に変換(ついでにtrim)
    // システム辞書でEUCを使用している場合は、toutf8は削ってください
    $line = trim(mb_convert_encoding(fgets($fd_in), 'utf-8', 'euc-jp'));
    // タブ区切り(仮名\t単語)になっているので、split
    $words = explode("\t", $line);
    if ( count($words) < 2 ) { continue; }
    $kana = ( $words[0] == '' ) ? '*' : trim($words[0]);
    $word = trim($words[1]);

    // 日付が入ったワードは、不要なものが多いので外す
    $pattern = '/[0-9]{4}(\/|\-)[0-9]{2}(\/|\-)[0-9]{2}/';
    if ( preg_match($pattern, $word) === 1 ) { continue; }
    $pettern = '/[0-9]{4}年/';
    if ( preg_match($pattern, $word) === 1 ) { continue; }
    $pattern = '/[0-9]{1,2}月[0-9]{1,2}日/';
    if ( preg_match($pattern, $word) === 1 ) { continue; }

    // 制御文字、HTML特殊文字が入ったものは外す
    $pattern = '/[[:cntrl:]]/';
    if ( preg_match($pattern, $word) === 1 ) { continue; }
    $pattern = '/\&\#/';
    if ( preg_match($pattern, $word) === 1 ) { continue; }

    // はてなという言葉が入ってるものは、運用の為のワードが多いので削除
    // 一部、正しい用語も消してしまっているので、用途によっては下行をコメントアウト
    $pattern = '/はてな/';
    if ( preg_match($pattern, $word) === 1 ) { continue; }

    // MeCabでパース
    $nodes = explode("\n", `echo '{$word}' | mecab --unk-feature='未知語'`);

    $node_count = 0;
    $unk_count = 0;
    $area_count = 0;
    $name_count = 0;

    // ノードと種類をカウント
    foreach ( $nodes as $node ) {
        $result_mecab = explode("\t", $node);
        $node_word = trim($result_mecab[0]);
        $feature = explode(',', $result_mecab[1]);
        // BOS/EOSはスキップ
        if ( $node_word === 'BOS' || $node_word === 'EOS' ) { continue; }
        $area_count += ( trim($feature[2]) === '地域' ) ? 1 : 0;
        $name_count += ( trim($feature[2]) === '人名' ) ? 1 : 0;
        $unk_count += ( $node_word === '未知語' ) ? 1 : 0;
        $node_count++;
    }

    // node数が1つ(システム辞書で1語として解析可能)の場合は登録しない
    if ( $node_count <= 1 && $unk_count === 0 ) { continue; }
    // 全nodeが地域名だった場合は、登録しない(東京都北区は、東京都 | 北区で分けたい為)
    if ( $node_count === $area_count ) { continue; }
    // 全nodeが人名だった場合は、登録しない(相田翔子は、相田 | 翔子で分けたい為)
    if ( $node_count === $name_count ) { continue; }

    // コストの計算
    $cost = -400 * pow(mb_strlen($word, 'utf-8'), 1.5);
    if ( $cost < -36000 ) { $cost = -36000; }

    // 平仮名を片仮名に変換
    $kana = mb_convert_kana($kana, 'C', 'utf-8');

    // 行出力
    $output = "$word,1345,1345,$cost,名詞,一般,*,*,*,*,$word,$kana,$kana\n";
    fputs($fd_out, $output);

    // 英字の場合は、小文字統一、大文字統一も出力しておく
    if ( $word !== strtolower($word) ) {
        $output = strtolower($word).",1345,1345,$cost,名詞,一般,*,*,*,*,$word,$kana,$kana\n";
        fputs($fd_out, $output);
    }
    if ( $word !== strtoupper($word) ) {
        $output = strtoupper($word).",1345,1345,$cost,名詞,一般,*,*,*,*,$word,$kana,$kana\n";
        fputs($fd_out, $output);
    }

    $i++;
    if ( $i % 1000 === 0 ) {
        echo "{$i}件目を処理\n";
    }
}
fclose($fd_in);
fclose($fd_out);
?>

基本ベタ移植で、コメントもほぼそのまま流用させて頂きました。

(追記(2010/3/27 01:10) 平仮名を片仮名に変換するところでエンコーディングを指定し忘れていたのでコードを修正しました。あとCSVへの出力形式が間違っていたのでそちらも修正しました。)

MeCab用ユーザ辞書に変換する

できあがったCSVファイルから、以下のようにMeCab用ユーザ辞書に変換します。

$ /usr/lib/mecab/mecab-dict-index -d `awk '/^dicdir/{print $3}' /etc/mecabrc` \
-u hatena.dic -f utf-8 -t utf-8 hatena.csv

mecab-dict-indexのパスは環境によって違うと思うので適宜置き換えて下さい。また、システム辞書のパスも環境によって違うと思うので、/etc/mecabrcから取ってくるような書き方にしてみました。

ユーザ辞書が使われるように設定する

/etc/mecabrcに以下の行を加えると、MeCabを実行した時に必ずユーザ辞書が使われるようになります。

userdic = /path/to/hatena.dic

またはMeCabコマンド実行時にユーザ辞書を指定する事もできます。

$ echo "けいおん二期ktkr" |mecab
けい    名詞,一般,*,*,*,*,けい,ケイ,ケイ
おん    名詞,一般,*,*,*,*,おん,オン,オン
二      名詞,数,*,*,*,*,二,ニ,ニ
期      名詞,接尾,助数詞,*,*,*,期,キ,キ
ktkr    名詞,固有名詞,組織,*,*,*,*
EOS
$ echo "けいおん二期ktkr" |mecab -u /path/to/hatena.dic
けいおん        名詞,一般,*,*,*,*,けいおん,ケイオン,ケイオン
二      名詞,数,*,*,*,*,二,ニ,ニ
期      名詞,接尾,助数詞,*,*,*,期,キ,キ
ktkr    名詞,一般,*,*,*,*,ktkr,キタコレ,キタコレ
EOS

自宅サーバの方には反映済みなので、MeCabを使っているWebサービス(今北川柳とか、あいウェ文とか)は、若干単語の判別性能が上がってるんじゃないかなと思います。

2009年08月31日

[][]Webサイトやブログの文章を元に川柳を自動生成するサービスを作ってみた

すっかり、"作ってみた"シリーズの公開ブログになっていますが、また、どうでもいいようなアイデアを思い付いてしまったので作ってみました。

今北川柳

ネーミングセンスがアレなのは気にしない方向で…。*1

これは何?

WebサイトやブログのURLを入力すると、文章から適当に単語を拾ってきて川柳をでっちあげるサービスです。

ただ文字列として生成するだけだと見た目的に面白くないので、色紙に毛筆で書いたっぽい画像を生成するようにしました。

試しにこのブログでやってみたら、こんな感じになりました。

全くもって中身がありませんね。(笑) これだけだとなんなので、ついでにasahi.comのトップ画面でやってみたら、こんなのになりました。

総選挙直後なので、やっぱり"総選挙"という単語が入ってきてますね。まぁ、やっぱり意味はサッパリ分かりませんが。(笑)

どんな仕組みなの?

川柳生成部分

基本的な仕組みとしては、対象の文章から5文字・7文字の言葉を拾ってきて、5・7・5に並べ替えているだけです。とは言え、一応まともな単語が出現するように、色々と工夫しています。

とりあえず、元の文章から日本語部分だけを拾う為に、半角の英数記号はばっさり捨てています。なので、"PHP"とか文章に含まれていたとしても、川柳に入ってくる事はありません。本当は英単語とか略語とかも単語の候補として拾えるといいんですけど、そういった単語の読みを取得する術がないので、仕方ないところですね…。

次に文章を単語に分解する訳ですが、最初は例によってMeCabたんに活躍してもらおうかと思ったんですが、MeCabで文章を解析してから、品詞も考慮して単語の候補を作ろうと思うと、とてもじゃないけど8秒ルールとかで収まるようなレスポンスにならなかったので、他の方法を探す事にしました。

必要なのは、"文章を文節ごとに区切る事ができる"という事と、"単語の読みが取得できる"という事で、これに関しては、CaboChaというまさにうってつけなライブラリがあるので、そちらを使う事にしました。

CaboChaで文章を解析すると、次のような形で文節の区切りやら、読みやらが得られます。

吾輩は猫である。名前はまだ無い。
* 0 1D 0/1 1.378671
吾輩    名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ   O
は      助詞,係助詞,*,*,*,*,は,ハ,ワ    O
* 1 4D 0/2 0.000000
猫      名詞,一般,*,*,*,*,猫,ネコ,ネコ  O
で      助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ   O
ある    助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル       O
。      記号,句点,*,*,*,*,。,。,。      O
* 2 4D 0/1 0.000000
名前    名詞,一般,*,*,*,*,名前,ナマエ,ナマエ    O
は      助詞,係助詞,*,*,*,*,は,ハ,ワ    O
* 3 4D 0/0 0.000000
まだ    副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ  O
* 4 -1D 0/0 0.000000
無い    形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ  O
。      記号,句点,*,*,*,*,。,。,。      O
EOS

この結果から、一旦、文節の読みの長さ毎に配列に突っ込んでいって、5文字の配列から2つ、7文字の配列から1つ単語を取得して、川柳を生成しています。

ただし、5文字・7文字の単語が1つも無い場合のみ、プラスマイナス1文字の字余り・字足らずも許すようにしています。

あと、CaboChaの結果の中に係り受けの点数があるので、それを使って、いっぱい係られているほど単語として選ばれやすくなるように重み付けをしていたりもします。

イメージ生成部分

背景は自前で用意しておいて、ImageMagickを使って文字を載せています。

ただ、ImageMagickで縦書きする方法が分からなかったので、1文字ずつ分解して、座標指定で無理やり縦書きにしています。

拗音・促音だと幅が違うので、それっぽい場所に収まるように調整していたりとか、伸ばし棒(ー)の向きが縦にならないので、縦棒(|)にして誤魔化していたりとかしてるので、若干おかしな感じになる場合もありますが、まぁそれっぽい画像にできたんじゃないかなぁ、とそれなりに満足しています。

毛筆のフォントはフリーで使えるものを探してきて、利用させて頂きました。

404 Not Found

こんなクオリティのものをフリーで使えるとは素晴らしい!

雑感

元々、文章要約の一つの形として川柳とか面白いかな、と思って作ってみたんですが、選択された単語同士に意味的な繋がりが無いので、いかんせん意味を持った川柳になる可能性が低いのが難点ですね。

もっと精度の高い川柳を生成しようと思うと、人工知能的なアプローチが必要になりそうです。それはそれで面白そうではあるので、またチャレンジしてみたいところではありますが。


という訳で、ぜひとも遊んで みて下さい!(七五調、一部字余り)

*1:元ネタは言うまでもなく、今北産業。

2009年03月11日

[][]何番煎じか分からないけど集合知プログラミングをPHPでやってみた その5「グループを見つけ出す…の下準備…の下準備」

今回から、元書の第3章に入っていきます。第3章は"グループを見つけ出す"というタイトルになっていて、アイテムを複数のグループに分類するアルゴリズムがいくつか紹介されています。今回は、実際にグルーピングをする前段階として、フィード中の単語を数える部分を作ってみます。

…の前にphp_hadoop_streaming_frontendの修正について

以前の記事の中で、php_hadoop_streaming_frontendというのを作成して、ちょうどサンプルで単語を数えるというのをやった訳ですが、そのまま応用できそうなので、今回はそれを流用します。が、そのまま使おうとするとちょっと問題があって、少しばかり手を加えました。

Mapperの中で定義するmap関数の引数が($key,$value)となっているんですが、実は若干手を抜いていて、$valueにSTDINから取得された1行分のデータが入って、$keyには必ずnullが入るという実装にしていました。

と言うのも、Mapperに入ってくる情報は飽くまで1行ずつのデータな行指向の処理になるので、元々、$keyと$valueのセットとして値が入ってくる訳じゃないんですよね。例えば、ログファイルなんかを処理する時には、行の羅列に対して1行ずつ処理していけばいいだけなので、Mapperに渡すデータは特にkeyとvalueの組み合わせというのは必要ありません。

ただ今回は、最終的に"どの単語がどのエントリの中に含まれている"という情報が欲しいので、"key=>エントリID,value=>エントリ内容"というデータが必要になります。そうするとmapメソッドの引数の$keyが常にnullでは困るので、Mapperに渡すデータを、keyとvalueのタブ区切りというフォーマットに対応できるようにしました。

最終的にReducerが吐き出すデータもタブ区切りになっているので、Mapperがタブ区切りに対応していると、多段階のMapReduceをやりたい場合にも都合が良さそうですね。( ̄ー ̄)

ちなみに、GitHubに上げたソースコードは修正済みです。→http://github.com/stellaqua/php-hadoop-streaming-frontend/tree

元データを準備する

という事でやっと本題です。今回は最初から元書をガン無視して、はてブのHotEntryを元データのターゲットにしたいと思います。*1

まずは、はてブのHotEntryのフィードから、エントリのタイトルと概要を取得してきてデータを作成します。

<?php
require_once('XML/RSS.php');
$rss = new XML_RSS('http://b.hatena.ne.jp/hotentry.rss');
$rss->parse();
$items = $rss->getItems();
$titles = '';
$descriptions = '';
for ( $id=0; $id<count($items); $id++ ) {
    $title = preg_replace('/[\t\r\n]/', '', $items[$id]['title']);
    $description = preg_replace('/[\t\r\n]/', '', $items[$id]['description']);
    $titles .= $id."\t".$title."\n";
    $descriptions .= $id."\t".$description."\n";
}
file_put_contents(dirname(__FILE__).'/hotentry_titles.txt', $titles);
file_put_contents(dirname(__FILE__).'/hotentry_descriptions.txt', $descriptions);
?>

とりあえずデータが作れればいいので、こんなところで…。タイトルと概要でファイルを分けているのは、後でMapReduceする時に、タイトルがkeyやvalueの中に含まれると長ったらしくて困るので、連番を振ってIDで管理する為です。

単語の出現頻度のデータを作る

元書だと、データ形式に関して、"列方向に単語、行方向にブログ"という超巨大テーブルを用意して、どのブログにどの単語がいくつあるかを全て埋めていくという、なかなか恐ろしい事をやっているんですが(^^;、「さすがにそれは無いだろ。」という事で、我が道を行く事にします。

せっかくMapReduceができるようになったので、元書のベタ移植ではなく、アルゴリズムは借用させてもらいつつも、"いかにMapReduceに載せていけるか"という事に挑戦していこうと思っています。

という事で、まずはMapperから。

#!/usr/bin/php
<?php
require_once(dirname(dirname(__FILE__)).'/lib/HadoopStreaming/Mapper.php');

class Mapper extends HadoopStreaming_Mapper
{
    public function map ( $key, $value )
    {
        $value = escapeshellcmd($value);
        $result = shell_exec("echo ${value}|/usr/bin/mecab");
        $words = explode(PHP_EOL, $result, -1);
        foreach ( $words as $word ) {
            if ( $word === 'EOS' ) {
                break;
            }
            $wordinfo = split("\t|,", $word);
            if ( $wordinfo[1] === '名詞' || $wordinfo[1] === '動詞' ) {
                $wordinfo[7] = ( $wordinfo[7] !== '*' ) ? $wordinfo[7] : $wordinfo[0];
                $this->emit($wordinfo[7], $key);
            }
        }
    }
}

$mapper = new Mapper( $is_tab_separated = true );
$mapper->run();
?>

記事の冒頭で書いたフレームワークの修正で、Mapperのコンストラクタで入力がタブ区切りかどうか指定できるようになりました。(デフォルトはfalseです。) 今回はエントリIDをキーに持つデータが入力値なので、trueに設定しています。

あと、HadoopStreamingについて書いた記事では、元から分かち書きしたデータを使って単純にスペースで分割しましたが、今回はせっかくなので単語の品詞にも注目して、名詞と動詞のみ抜き出すようにしてみました。*2

これで、"単語=>[エントリID,エントリID,…]"という、各単語毎にその単語が含まれるエントリのIDが列挙される形でMapperから出力されます。

では続いてReducerに…といきたいところですが、だいぶ長くなってしまったので、続きは次回に…。

*1:元書は、筆者が用意したRSSフィードURLの一覧を使って、フィードから単語数をカウントするようになっていました。

*2:本当は名付けて.ね〜むでやっているように、"○○する"を一語とみなすとかもした方がいいんですが、今回は手を抜いて省略…。

2009年02月26日

[][]自然言語による検索ができるような工夫

タイトルだけ見るとスゴイ事をしているように見えてしまいますが(^^;ゞ、そんな仰々しい話では無いです。

名付けて.ね〜むのサイトで適当な文章を入力して検索すると、名詞と動詞のみを抽出して、検索対象にするようにしています。

こうする事で、登録される単語が、できるだけ"名詞or動詞一語"に限定されやすいようになる事を狙っています。

あまり自由度を高くして、「"○○を△△する関数"→"□□××"」みたいな対応関係を登録する仕組みにしてしまうと、検索する時に目的のものが得られにくくなってしまうかなと思い、今の形にしています。

どういう仕組みになっているの?

で、内部的にどうやって処理しているかと言うと、基本的には、MeCabを使って形態素に分解して、名詞と動詞のもののみを抜き出してくるようにしています。

ただ、単純に形態素分解するだけだと以下のような問題があって、ちょっとばかし工夫しています。

  • "○○する"という単語が"○○"と"する"に分割されてしまう。
  • "長さ"などの形容詞の名詞化された単語が、"長"と"さ"のように分割されてしまう。
  • "ファイルポインタ"のような複数の名詞からなる単語が、"ファイル"と"ポインタ"のように分割されてしまう。

上記3つのケースに関しては、特別ルールとして分割された形態素を繋げたものを、結果として出力するようにしています。

MeCabの結果取得で一苦労…

という訳で、日本語処理に関しては非常に優れもののMeCabたんなんですが、PHP側に形態素解析の結果を取り込むのに結構苦労してしまいました…。

というのも、開発をWindows上でやっているので、MeCab PHP extensionは使えず…。なので、mecabコマンドを直接叩いて、結果を文字列で取得してからゴニョゴニョする事にしました。

最初は、次のようなコードで実験…。

<?php
$mecab_exe = '"C:\Program Files\MeCab\bin\mecab.exe"';
$string = 'すもももももももものうち';
$result = shell_exec('echo '.$string.'|'.$mecab_exe);
echo $result;
?>

結果は…

すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
・Eぁ	名詞,一般,*,*,*,*,*
E	名詞,固有名詞,組織,*,*,*,*
・・	記号,一般,*,*,*,*,*
EOS

何だか、一部、文字化けしてしまう…。

色々試してみる内に、MeCabの問題ではなく、PHPでshell_execに日本語を引き渡した時点で文字化けしてしまっている事が分かりました。*1

困ったときのGoogle先生…という事で、以下のサイトなどを参考にさせてもらって、shell_execを使うのを諦めて、proc_openを使うようにしてみたら、うまく動くようになりました。

no title

書き換えたコードは次のような感じ…。

<?php
$mecab_exe = '"C:\Program Files\MeCab\bin\mecab.exe"';
$string = 'すもももももももものうち';
$result = '';
$descriptorspec = array(
    0 => array("pipe", "r"),
    1 => array("pipe", "w"),
);
$process = proc_open($mecab_exe, $descriptorspec, $pipes);
if (is_resource($process)) {
    fwrite($pipes[0], $string);
    fclose($pipes[0]);
    while (!feof($pipes[1])) {
        $result .= fread($pipes[1], 4096);
    }
    fclose($pipes[1]);
    proc_close($process);
}
echo $result;
?>

結果は…

すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

おぉ、うまくいきました。


という訳で、今回はちょっとした苦労話なんぞを書いてみました。

こんな感じのサイト制作の裏話みたいのは、またちょこちょこ書いていこうと思います。

*1:SJISだと大丈夫なようなので、UTF-8を使っているのがよくなさそうな感じなんですが、今時のWebサービス制作は文字コードはUTF-8にしたいですしねぇ…。