BEARで始めるWebアプリケーション開発 その12「Ajaxアクセスを試してみる」

現在、今まで作ったWebサービス達をBEARベースに移行しようとしていまして、どうせならAjaxも使いたいなと思って、BEARでのAjax周りの学習をしていました。そんな訳で、今回はBEARでのAjaxアクセスに関するお話です。

今回は例として、Googleの検索APIを叩いて検索結果を返す処理をAjaxで実現するアプリケーションを作ってみたいと思います。*1

事前準備

BEARではJavaScript周りはjQueryベースのパッケージスクリプトが用意されていて、Ajaxでサーバ側のBEARアプリケーションとデータをやり取りする事ができるようになっています。

init-appした直後はその辺りのJavaScriptファイルは用意されないので、PEARインストールディレクトリからコピペして用意します。

$ mkdir $BEAR_DIR/htdocs/js
$ cp -ir $PEAR_DIR/BEAR/data/htdocs/bear $BEAR_DIR/htdocs/js/

$PEAR_DIRはPEARのインストールディレクトリ、$BEAR_DIRはinit-appした時のディレクト*2です。コピー先はhtdocsの下ならどこでもいいんですが、今回は、"htdocs/js/bear"というディレクトリの下に置くようにしました。

テンプレートを作成する

テンプレートから作成すると見通しが良いと思うので、まずはテンプレートから作成します。"App/views/pages/ajaxindex.tpl"というファイルを以下のような内容で作成します。

<html>
<head>
<script type="text/javascript" src="/js/bear/jquery.bear.min.js?{appinfo version}"></script>
<script type="text/javascript" src="/js/app.js?{appinfo version}"></script>
</head>

<body>
<h2>Ajaxテスト</h2>
<form id="ajaxform">
    <input type="text" name="keyword" size="20">
</form>
{a rel="ajax" href="./ajax.php" click="post"}検索{/a}
<dl>
    <dt>結果</dt>
    <dd id="response"></dd>
    <dd id="result"></dd>
</dl>
</body>
</html>

ポイントの一つ目は、"jquery.bear.min.js"というファイルを読み込んでいる点ですね。このファイルには、jQuery本体とBEARのJavaScript機能がパッケージされているので、Ajax処理をするには、何はなくともこのファイルが必須です。

次に、"app.js"というファイルを読み込んでいますが、これについては後述します。

続いてのポイントは、"{a}"というBEARの独自タグです。これは、HTMLのaタグの上位互換性を持っていて、いくつか拡張機能が使えるようになっています。特にclick属性は、JavaScriptの"onClick"イベントのような感覚でサーバ側アプリの処理を呼び出せる感じで、なかなか面白い機能だと思います。*3

ここでは、「Ajaxアクセスで、"ajax.php"の"post"クリックハンドラを呼び出す」というリンクを定義しています。

後は、Ajaxアクセスの結果を格納するボックスをIDを付与して置いているだけですね。

このテンプレートに対するページファイルは、単純に表示するだけのものなので、以下のように中身が何も無いクラスファイルを作っておけばオッケーですね。

<?php
require_once 'App.php';

class Page_Ajaxindex extends App_Page
{
}
App_Main::run('Page_Ajaxindex');
?>

これを、"App/htdocs/ajaxindex.php"として作成しておきます。

Ajaxで呼び出されるページファイルを作成する

次に、Ajaxアクセスで呼び出されるページファイルを作成します。"htdocs/ajax.php"というファイルを以下の内容で作成します。

<?php
require_once 'App.php';

class Page_Ajax extends App_Page
{
    public function onInject()
        $this->_ajax = BEAR::dependency('BEAR_Page_Ajax');
        $this->injectAjaxRequest();
    }

    public function onClickPost ( $args )
    {
        $keyword = $args['form']['ajaxform']['keyword'];
        $result = $this->_resource->read($params)->getBody();
        if ( is_null($responses = json_decode($result[0], true)) === false ) {
            if ( $responses['responseStatus'] === 200 ) {
                $response = 'OK';
            } else {
                $response = $responses['responseDetails'];
            }
        } else {
            $response = 'API取得エラー';
        }
        $this->_ajax->addAjax('html', array('response' => $response));
        $this->_ajax->addAjax('js', array('display' => $result[0]));
    }

    public function onOutput()
    {
        $this->output('ajax');
    }
}
App_Main::run('Page_Ajax');
?>

ここはポイントがたくさんですね。順を追って見ていきたいと思います。

まず、onInject()でBEARのAjaxコンポーネントの準備と、Ajaxアクセスのリクエストパラメータ受け取りの処理をしています。

次に、onClickPost()で、テンプレートで書いた{a}タグがクリックされた時の処理を書いています。Ajaxアクセスした時のパラメータは$argsに自動的に格納されて、フォームの内容は$args['form']で受け取る事ができます。フォームに付けたID毎に連想配列として格納されるので、複数のフォームを作ってもちゃんと値を区別して受け取る事ができます。

受け取ったキーワードを使ってGoogle検索APIにアクセスした結果の内、処理結果を$response、検索結果を$resultに入れています。

この結果を、addAjax()というメソッドで返していますが、"html"と"js"という2種類の方法で返すようにしています。*4

"html"で返した場合は、HTMLの中の該当するIDの場所にBEARが自動的に挿入してくれます。また、"js"の場合は、引数として値を渡しつつ、自分で定義したJavaScriptメソッドを呼び出す事ができます。今回の場合は、Google検索APIの処理結果のJSONを引数にして、"display"というメソッドを呼び出すようにしています。

アプリケーション独自のJavaScriptを定義する

続いて、Ajax呼び出し後に結果を格納したりする処理を作成します。"htdocs/js/app.js"というファイルを以下の内容で作成します。

(function($) {
    $.app = {
        display : function(result) {
            eval('var results=' + result);
            var html = '<ul>';
            $.each(results.responseData.results, function(i, item){
                html += '<li>' + item.titleNoFormatting + '</li>';
            });
            html += '</ul>';
            $("#result").html(html).show('slow');
        }
    }
})($);

$(document).ready( function() {
    // rel=ajaxとなっているAタグをAjaxLink化する。
    $("a[rel^='ajax']").bearAjaxLink({form:true});
});

一つ目のブロックがクリックハンドラ側から呼び出されるメソッドの定義で、JSONをevalして、HTML内に埋め込む処理をしています。

二つ目のブロックはHTMLの読み込み後の初期化処理で、BEARのパッケージの機能を使って、rel属性が"ajax"となっているaタグに対してAjax処理を行うイベントを設定しています。引数で"{form:true}"としておくと、クリックハンドラを呼び出す時に、自動的にHTML内のフォームの内容をパラメータとして渡すようになります。

Ajaxアプリケーションの完成!

ここまでで、HTMLからのAjaxでの値の送信、サーバでの処理結果の受け取りと表示、という基本的な部分について作成できました。

キャプチャした時には、init-appした時に作成されるデフォルトのCSSを適用したものを使ったので、今回の記事のテンプレートファイルだとこの画像の通りにはなりませんが、まぁイメージ映像という事で…。

ちなみに、画像だと伝わりませんが、結果を表示する時にjQueryのエフェクトを入れているので、ボワッという感じで表示されます。jQuery使うと、ちょっとしたエフェクトが簡単に入れられてなかなか良いですな。

雑感

という訳で、AjaxなアプリケーションをBEARで作ってみましたが、「参考までに」と思ってCakePHPAjaxヘルパーの使い方をちょっとだけググって見てみたら、基本的なやり方は同じような感じっぽい印象でした。

ただBEARの場合、「aタグにイベントを定義して、そのクリックハンドラを"サーバ側の"ページに定義する」というメタファーになっているところが個人的に気にいっていたりします。

次回は?

次回は、テンプレート周りの話にしようかなという予定です。

*1:実際には、Google検索APIは直接JavaScriptから呼び出して使った方が断然便利なので、わざわざAjaxアクセスしてサーバ側でAPI叩くなんてのは全くスマートとは言えませんが、まぁ、サンプルとしてこんなもんで…。

*2:"bear show-app"で確認する事ができます。

*3:{a}タグの詳細は、本家Wikiクリックハンドラのページ参照。

*4:もう一つ"val"というのもありますが、今回は省略。

BEARで始めるWebアプリケーション開発 その11「リソースをWebAPIとして外部に公開する」

今回は、リソースをWebAPI*1として外部に公開する為の方法について学びます。

リソースをWebAPIとして提供する場合、外部からはGETのみで、かつ返す表現がXMLJSONの場合は非常に簡単で、PageのonOutputハンドラでdisplayメソッドを使っていたところを、outputメソッドに変えるだけです。

<?php
    public function onOutput()
    {
        $this->output('xml');
        //$this->output('json');
    }
?>

BEARでは、リソースが値を返す時点では、表現を伴わない単なるデータ構造になっているので*2、状況に応じた表現の変更を極めて容易に行う事ができます。

この、表現形式を決めるアウトプットフィルタは自分で作る事もできて、"App/Resource/output"というディレクトリを作って、その中に"{形式名}.php"というファイル名でPHPファイルを作成すると、それを利用する事ができるようになります。

デフォルトで用意されているアウトプットフィルタは、PEARディレクトリ配下の"BEAR/Resource/output"の中にあります。デフォルトのアウトプットフィルタの動作を変えたい場合は、ここから"App/Resource/output"にコピーしてきて中身を書き換えてやれば、そちらが優先して使われるようになります。

今回は試しに、何らかの画像生成を行ってIMAGICKオブジェクトを返すリソースを作った時に、それを画像ファイルとしてHTTP出力するアウトプットフィルタを作ってみました。

<?php
/**
 * イメージ出力
 *
 * @param array $values  出力データ
 * @param array $options オプション(type:gif|jpeg|jpg|png , expire:有効期限)
 *
 * @return BEAR_Ro
 */
function outputImage($values, array $options)
{
    $ro = BEAR::factory('BEAR_Ro');
    $type = ( isset($options['type']) === true ) ? $options['type'] : 'png';
    switch ( $type ) {
        case 'gif':
            $mime = 'image/gif';
            break;
        case 'jpeg':
        case 'jpg':
            $mime = 'image/jpeg';
            break;
        case 'png':
            $mime = 'image/png';
            break;
    }
    $expire = ( isset($options['expire']) === true && $options['expire'] > 0 )
        ? $options['expire'] : 0;
    $headers = array();
    $headers['X-BEAR-Output: IMAGE'] = 'Content-Type: '.$mime;
    $headers[] = 'Expires: '.$expire;
    $headers[] = 'Last-Modified: '.gmdate('D, d M Y H:i:s ', time()).' GMT';
    $headers[] = 'Cache-Control: public';
    $headers[] = 'Pragma: ';
    $ro->setHeaders($headers);
    $ro->setBody(array_shift($values));
    return $ro;
}
?>

これを、"App/Resource/output/image.php"として作成しておいて、Pageファイルのアウトプットフィルタを以下のようにすると画像データを返すようになるので、imgタグのsrcとかで使えるようになります。

<?php
    public function onOutput()
    {
        $this->output('image', array('type' => 'png'));
    }
?>

今回は、リソース側で画像生成するような場合の想定でしたが、「リソース自体は普通に連想配列を返すけど、出力する時にその情報を利用して画像生成してHTTP出力する」*3なんていうアウトプットフィルタも簡単に作れる事が分かるかと思います。

次回は?

今回は実は元々、外部からリソースのCRUD操作を行えるような、RESTfulWebAPI用Page作成にしようと思っていたんですが、現状、PUTやDELETEも含めた外部向けWebAPIを作る予定は無かったし、むしろアウトプットフィルタの方が使いどころが多いかなと思って、アウトプットフィルタの話にしました。

次回の内容はまだ決めていないのですが、イベント駆動の辺りかAjaxか、そんな辺りの話にしようと思います。

*1:予告では"RESTfulWebサービス"という言い方をしましたが、"Webを支える技術"での使い分けに倣って、"WebAPI"としました。

*2:"リソース自体は表現を決めない"という特徴がある故に、リソース自体で考えると対人用とか対機械用とかの区別が無いモノと言えるので、"Webを支える技術"で言うところの「WebサービスとWeb APIを分けて考えない」という事を実現できているのではないかと思います。

*3:具体例で言うと、はてブAPIみたいな感じで、リソースは数値データだけを返すようにしておいて、アウトプットフィルタで画像生成するようにするイメージ。

"Webを支える技術"を読みました

山本陽平さん著の"Webを支える技術"を先ほど読み終わりました。

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

第1部から第4部までは、Webに関する技術について歴史的な経緯も含めて非常によくまとまっているので、Web技術の知識の総復習的な感覚で読みました。自分自身、Web技術に関して基本的な知識は抑えているつもりですが、細かい部分であやふやだったりするところもあったので、そういった部分を補完できて非常に助かったかなと。

第5部に関しては、WEB+DB PRESSの連載当時から興味深く読ませて頂いていましたが、当時はまだ自分でリソース設計をした事が無くてピンと来ていなかった部分も、今読むとより深い理解ができるようになったかなと思います。

BEARを使うようになってからは、リソースを設計してから実際に動くものに落とし込む事が非常に容易にできるようになったので、そういう意味でハードルは下がったんですが、本の中でも述べられていますが、リソース設計はまだまだ確固とした手法が確立されておらず、どうしても手探りになってしまうところはあるなぁ、というのは最近の自分自身の感覚としてあります。

そういう意味では、第5部はリソース設計のヒントが散りばめられているので、それを参考にしつつリソース設計する人がもっと増えて、設計手法も洗練されていくと良いなと思います。

そんな訳で、Web技術者ならぜひ一読をお勧めします!

Amazon Elastic MapReduceでHadoop Streamingする時にライブラリをrequireする方法

ちょっと元データの件数が大量にある処理をしたいという要件があって、普通に逐次処理していくと恐ろしく時間が掛かるので、「こんな時こそHadoop!」って事で、久々にHadoopをいじくっていました。

ただ、自宅サーバでやろうとすると、いくら分散処理できるとは言っても結局処理するのは物理的には1台な訳で、メモリを使い切ってスワップしまくってウンともスンとも言わなくなってしまうという、とっても悲しい状況になってしまいました。

「そんな時はAmazon先生にお願い!」って事で、Amazon Elastic MapReduceを試していたんですが、ちょっとハマってしまったところがあったので、備忘録がてら記事にしようかと思います。

Mapper/Reducer以外のファイルが使えない!

以前にAmazon Elastic MapReduceを試した時は、MapperとReducerが1ファイルだけの簡単な処理でやってみただけでした。

ただ、今回はもうちょっと処理が複雑で、Mapperから別ファイルをrequireしたり、serializeしてファイルに書き出しておいたデータを使ったりするものでした。

CLIAmazon Elastic MapReduceを実行する場合、一番簡単な書き方で以下のような形になります。

$ elastic-mapreduce --create --stream \
--input s3n://stellaqua/mapreduce/inputs \
--output s3n://stellaqua/mapreduce/outputs \
--mapper s3n://stellaqua/mapreduce/map.php \
--reducer s3n://stellaqua/mapreduce/reduce.php

Mapper/ReducerはS3に置いたものを指定する事ができて、初期化時に勝手にS3から読み込んできてHadoopの実行ノードに転送して実行してくれます。

しかし、Mapper/Reducer以外のファイルは例えS3上に置いておいても、勝手に読み込んだりはしれくれないので、Mapperとかでrequireしようとしても、読み込む事ができません。

で、どうすればいいか途方に暮れつつ調べていたら、以下のサイトさんで解決方法を見い出す事ができました。

Soffritto::Journal

"--cache-archive"というオプションを使う事で、Hadoop初期化時に各ノードに転送する事ができるようです。

まずMapper/Reducer以外の、ライブラリなどのファイルを一つのディレクトリにまとめておいて、jarコマンドでアーカイブします。

$ jar cvf lib.jar -C lib/ .

Mapper/Reducerでrequireしたりする時は、相対パスで書いておきます。

<?php
require 'lib/Hogeclass.php';
?>

後はElastic MapReduceを実行する時に、"--cache-archive"オプションを使ってjarファイルを指定してやればオッケーです。

$ elastic-mapreduce --create --stream \
--input s3n://stellaqua/inputs/input.dat \
--output s3n://stellaqua/outputs \
--mapper s3n://stellaqua/map.php \
--reducer s3n://stellaqua/reduce.php \
--cache-archive s3n://stellaqua/lib.jar#lib \
--num-instances 4 \
--log-uri s3n://stellaqua/logs

ついでながら、上記では、起動インスタンス数とログ出力先の指定もしています。

うまく使えば、ローカルにPEARを展開したものをアーカイブして、Hadoop Streamingから利用するとかできそうですね。

という訳で実際にElastic MapReduce上で処理させてみたんですが、またしても問題が…。

処理が成功しようが失敗しようが料金が掛かる
1回分の料金が安いとは言え、何度も失敗しているとその分料金がかさんでいくのは微妙に痛いです…。
どこまで処理が進んだか分かりづらい
一応、AWS Management Console上にデバッグコンソールというのもあってsyslogが見れるんですが、ちゃんと処理が進んでいるのかどうか微妙に分かりづらいです…。

…という事で、何回もの失敗を乗り越えてやっとまともに動くようになったので、1時間ほど動かしてみたんですが、ちゃんと処理が進んでいるのかどうかよく分からなかったので止めてしまいました。Elastic MapReduceを使いこなすには、もうちょっと研究が必要そうですね…。

ツイッターを掲示板的に利用できるTweetboardをはてなダイアリーに設置してみた

今までツイッターは使うのにどうしても抵抗があって利用してこなかったんですが、Tweetboardという素敵なサービスがあるのを知って当ブログに設置してみたので、これを利用してツイッターを活用していこうかと思います。

Tweetboardというのは、Webサイトにツイッターを利用した掲示板を設置できるサービスで、プロフィール欄の右側にある"tweets"というボタンがそれです。

使い方は詳しく紹介しているサイトがあったので、そちらのリンクを貼っておきます。

Free Dynamic DNS(DDNS) by POP3,IMAP4,FTP,HTTP-BASIC for Home Server, VPS | MyDNS.JP

ちょっと苦労話

Tweetboardは現在アルファ版との事で、利用する場合はテスター扱いという事のようで、管理者に招待リクエストを送って承認してもらって初めて利用できるようになります。

招待リクエストを送る事自体は簡単だったんですが、何せ1件1件人手で承認をしているようで、私の場合は結局最初のリクエストから1ヶ月ほど掛かって、なおかつ2回ほど催促をしてやっと承認してもらえました。(^^;

あと、もう一つ苦労したのが、はてなダイアリーで利用できるJavaScriptが特定のものだけに制限されている事で、これに関してはGoogleガジェットを間に挟む事で解決する事ができました。

ただどうしても解決できなかった問題があって、本来は左端に設置されるものを右端に持って来ざるを得なくて、展開表示すると画面からはみ出てしまうので、いちいち画面スクロールしないといけなくなっています…。

というのは、Googleガジェットがiframeに展開される関係で、どうしても展開表示された状態の幅での表示領域を確保しておく必要があって、しかも確保されたiframe領域の内側はいくら背景を透過させても、その後ろにあるリンクとかをクリックする事ができないので、iframeの表示領域部分にリンクが掛からないように、右側に持ってくるしかありませんでした。

という事でちょっと使いにくさは残ってしまったんですが、Tweetboardの機能自体は非常に面白いと思うので、どんどん使っていこうと思います。

今後のツイッターの使い方

実はちょっと前から既に始めていたんですが、FriendFeed経由で当ブログの更新状況をツイッターにつぶやくようにしていました。

一応、ツイッターでの発言はそのままTweetboardにも反映されるようなので、Tweetboard上にはブログの更新履歴が並ぶような感じになるかなと思います。そうすると、記事毎にスレッドが作られる形になるので、各記事に関連する話はそのスレッド上で展開していくようにしようかなと思っています。

そんな訳で、記事に対してツッコミとか、それ以外でも単なるテスト投稿でも構わないので、どうぞお気軽に使ってみて下さいね〜。

第51回PHP勉強会参加&BEARについてプレゼンしました

「ブログに書くまでが(ry」…ではなく、「トラックバックするまでが勉強会」との事なので、早速。

第51回PHP勉強会に行って参りました。今回は発表者として、BEARに関するプレゼンをさせて頂きました。

今まで何度もPHP勉強会には参加させてもらいましたが、発表するのは初めてでした。

とりあえず、プレゼンする上で気を付けようと思っていた事はちゃんと気を付けてプレゼンできたかなとは思ってますが、後でUSTREAMの録画を見直して一人反省会をしようと思います。

今回の発表の一番の目的はBEARの事を一人でも多く知ってもらう事と思っていて、新米芸人のごとく、「今日は名前だけでも覚えて帰って下さいね。」という感じと思っていたので、そういう意味では目的は果たせたかなと。*1

会場提供頂いたトライコーン株式会社さん、ありがとうございました。幹事のgusagiさん始め、参加者の皆様、どうもお疲れ様&ありがとうございました。

*1:もう一つの重要なミッションである、BEAR作者のkoriymさんに直接お会いして話をさせて頂くという目的も無事果たす事ができました。(笑)

はてなキーワードから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サービス(今北川柳とか、あいウェ文とか)は、若干単語の判別性能が上がってるんじゃないかなと思います。