知らなかった

perlで、use strict; してても$aと$bってmy or ourせずに使っても、
未定義エラーを吐かないんですね。。。知らんかったー。
といっても$a,$bなんていう変数名は僕的に滅多に使わないので、
別にどうでもいいっちゃ、いいんですがね^^;

PHP5はClassを定義して、OOPなコードが書けるので、修正や機能追加しやすいように依頼されたコードをクラスで書いて渡した。もし機能の追加・既存メソッドの修正があったら、ファイルを一個追加して、修正・変更が必要なクラスを派生させて(継承して)新しいクラスを作ればソースファイル管理もラクになるだろうし・・・と思ったんだよねぇ。

でも実際は違った。どうやらその人はPHPを「まともなコードが書ける人」じゃなくて、単に「PHPの文法が分かってるだけの人」なんだというのがやっと分かった。知らんかった-。

その人は、渡したファイルのクラスのメソッドを直接修正してるし・・・何のためにクラス化したんだよ orz
渡すとき、「変更があったら該当クラスを継承させて使ってねー」とか言っておくべきだった。

誰が、どの言語を、どの程度知っているのか、全く分からん!

mod_rewriteでアクセス拒否?

会社で管理している、とあるお客さんのホームページ(サイト)の偽物らしきページが某国のトップドメインを持つサイトに上がってるらしいからそこからのアクセスを拒否したい、という依頼があったらしい。

よくよく聞いたら、どうやら、その某国の問題サイトにアクセスがある度に、お客さんのサイトへアクセスしてきて内容を取得して携帯電話で見られるように形にして表示させているらしい。

まぁ、どうやらグーグルにもあるサービスを模した感じかな?普通ならそんなの無視しておけばいいんだが、お客さんのサイトは信用第一で万が一間違いがあってはなならないような固い業態なので放っておけないらしい。

まぁ、サイトのドキュメントルートに 下記ような感じの .htaccess ファイルを置けば、問題無い。

Order Allow,Deny
Allow from all
Deny from  拒否するリモートIPアドレスもしくはリモートホストのドメイン

が!、しかし! 話はここで終わらなかった!
なんと、そのお客さんのサイトのサーバー(正確には共用サーバー)は、limit を上書きできないんだ!
要するに上記の設定を .htaccess に記述してしまうと、500 Internal Server Error を吐いてしまうんだ!

ああ、なんという、クソさ加減。

そして、その時点で、僕にヘルプを求めてきたw

はじめは、そんなクソサーバーを使っている時点で、手の打ちようがねーよ的に返したわけだが・・・
・・・が、しかし、少しでもウェブに関わる仕事をしている者として、プロフェッショナルとして、それではダメだ。
(少々大袈裟です(^_^;;;)

何か代替案を・・・・というわけで、サーバーの仕様書を調べてみると、mod_rewrite の設定は可能のようだ。
っちゅーわけで、Rewriteモジュールを使用して、「特定のリモートIPアドレスにマッチしたらURLを書き換えてしまえ作戦」を発動することを提案。

まぁ、以下代替案。

RewriteEngine On
RewriteCond %{REMOTE_ADDR} ^000\.111.\222\.333$
RewriteRule ^(.*)$  /forbidden/ [R=403,L]
# ちなみに、/forbidden/ は実在しないディレクトリを指定した。

R=403 のところは本来は ステータスコード、301とか302とかにしなければならないんだけど・・・試しに403(アクセス禁止)にしてみたら、アクセス禁止ページを表示してくれた。これってHTTP的にイケないこと?違反なのかな??? まぁ・・・いいや。どうせオイラに責任はないんだしー。

Apacheに精通している人なら、もっとスマートな解決方法をお持ちだと思うんですが、それにしても大手のレンタルサーバーが提供する機能があまりにも貧弱すぎて、まぁ、もろもろ理由があると思うので、しょうが無いんですが・・・。

っていうか、会社にはWebサーバー管理・構築(主にApacheね)に精通する人が一人もいないので、素人に毛の生えたような僕でも適当にやっていけるんだなー(^^;;;

【追記 2013/4/15】
リダイレクトさせるのではなく、アクセスを制限するには、下記のようにするべきでした。
【追記 2015/09/07】
間違い修正

RewriteEngine On
RewriteCond %{REMOTE_ADDR} ^000\.111.\222\.333$
RewriteRule ^(.*)$ - [F,L]
#すべてのアクセスを403(アクセス禁止)に。

PHPで形態素解析とMySQLで全文検索

備忘録メモです。長ったらしいタイトルっす。

ブログの簡易版みたいなスクリプト(管理者だけが書き込める掲示板みたいなやつ)の改造をちょっと前に依頼されたんですが、その中で検索機能(全文検索)を付けるというのがありました。全文検索っていっても、入力された単語にマッチしたレコードを全部表示する、要はSQLクエリーのselect文でlike演算子でマッチさせるだけでいい、ということだったんですが、ただでさえ、面白くないPHPの仕事だし(^^;;;、それだけでは僕にとっても得るものが少ないので(^^;、もうちょっと勉強になるものを作ってみよう、ということで調べました。

仕事しながら勉強って・・・ま、いいか。

日本語の文章をMySQLで全文検索させるには(FULLTEXTインデックスってことね。)、まず日本語の文章を形態素解析にかけて、名詞・動詞・助詞・・・といった風に分解することから始めなければいけません。英文などでは単語間は必ずスペースもしくはカンマで区切られますから、特に意識しなくても済むのですが、日本語の文章や主にアジア圏の言語では、そう簡単にはいきません。

幸いにもフリーで使用できる形態素解析エンジンは結構豊富にあります。有名なものとして、KAKASIやMeCab、Igoなどがあります。が、一般人が実際に使用するには、結構ハードルが高いものです。

まず、何よりレンタルサーバーなどの一般的なサーバーではほぼこのようなライブラリはインストールされていませんし、新たに追加できることは不可能でしょう。でも、最近では非常に低コストのVPSサーバーがあるので、それなりに知識がある人は導入できるでしょうけど、サーバー管理の知識がない方にとっては難しいでしょう。

ただ、PHP、しかも、レンタルサーバーでも利用できる・・・という条件だと、選択肢は非常に限られると思います。その中でも小規模なサイトに必要十分なものとして、Igo-PHPが手軽に利用できて、サイトへの組み込みも少ない工数で行えると思います。

Igo-PHPは、Javaの形態素解析エンジンIgoのPHP移植版で、Igo同様、MITライセンスで自由に利用できるというありがたいライブラリです。作者に感謝です。

流れとしては・・・

  1. Igo-PHPのダウンロード
  2. Igo本体のダウンロード(辞書生成に必要。別途Java実行環境が必要)
  3. 辞書の元となるファイルをダウンロード(MeCabサイト→ダウンロード→Mecab用の辞書(IPA辞書)
    (2017-10-22 リンク先修正)
  4. 2)でダウンロードしたIgo(Javaプログラム)を使用して辞書を生成。
    3)でダウンロードしたファイルを展開し、以下のコマンドをうつ。Windowsだと java.exeがあるディレクトリが%WINDIR%や%PROGRAMFILES%にあったりと環境によって違うと思います。

    >> java -cp igo-0.4.3.jar net.reduls.igo.bin.BuildDic ipadic mecab-ipadic-2.7.0-20070801 EUC-JP

といった感じになります。これらは全部Windows上で行えます。あとは・・・PHPスクリプトからIgo-PHP、生成した辞書を使って形態素解析ができます。

生成されたipadicディレクトリに辞書がビルドされていますので、以後、このipadicディレクトリとIgo-PHPだけを使用します。

WindowsにPHPをインストールされている方は、下記のようなスクリプトを作成して実行してみてください。

<?php
// test.php

// Igo-PHPとipadicディレクトリを 'lib'というディレクトリにまとめて置いとく。
require_once 'lib/Igo.php';
 
$igo = new Igo("./lib/ipadic");
$text = "私には夢がある。";

// 詳細な結果が欲しい場合は、parseメソッド
//$result = $igo->parse($text);

//単に区切ればいいだけなら、wakatiメソッド
$result = $igo->wakati($text);

//それぞれ、単語の配列が返ります。

echo mb_convert_encoding(implode('/',$result),'SJIS');

実行すると、こんな結果がでました。ちゃんと分解されてますね。

>>php test.php
私/に/は/夢/が/ある/。

さて、検索される文章・記事は、MySQLのデータベースに入れることが多いのでMySQL + PHPでの組み込みを中心に。

MySQLでテーブルを作成する際に、記事などを格納させるカラムとは別に、検索用のカラムを一個追加して、そのカラムにFULLTEXTインデックスを張ります。以下のような感じですかね?

CREATE TABLE  posts (id INT,title TEXT,content TEXT,content_s TEXT,FULLTEXT(content_s));

というようなテーブルを用意しておいて、INSERT,UPDATEする際に、contentの内容を形態素解析にかけ、名詞のみ抜き出し、content_sに抜き出した単語を半角スペース区切りでつなげた文字列を格納しておく。要は検索インデックスをcontent_sに貯めておくという方法です。

英語ならば、語と語は必ずスペースで区切られるから、こんなめんどっちーことをしなくてもいいんですが・・・。
データをINSERTするときは、こんな感じでしょうか。

<?php
$igo = new Igo("./lib/ipadic");
$pdo = new PDO('mysql:dbname=testdb;host=localhost','dbuser','password');

//テーブル作成
$pdo->exec('CREATE TABLE posts (id INT,title TEXT,content TEXT,content_s TEXT,FULLTEXT(content_s))');

//テストデータを用意
$id = 1;
$title = '私には夢がある。';
$content = '私には夢がある。私の四人の幼い子ども達が、いつの日か肌の色ではなく人格そのものによって評価される国に住めるようになるという夢です。';

//形態素解析
$result = $igo->wakati($content);
$content_s = implode(' ',$result);

//INSERT文組み立て
$sql = sprintf("INSERT INTO posts(id,title,content,content_s) VALUES(%d,'%s','%s','%s')",
               $id,
               $pdo->quote($title),
               $pdo->quote($content),
               $pdo->quote($content_s));

//クエリー実行
$pdo->exec($sql);

$pdo = null;
$igo = null;

こんな感じでデータをどんどん追加して、

で、全文検索させたいときは、contentカラムを検索するのではなく、content_sカラムを全文検索させるように、

SELECT * FROM posts WHERE MATCH(content_s) AGAINST('検索単語',IN BOOLEAN MODE);

と、するだけ。

ただ、これだけだと、ちょっと不便なことがあります。形態素解析にかけると、辞書にある単語を元に解析するので、二つ以上の名詞がくっついて一つの名詞になるような語がバラバラになってしまいます。

たとえば、「神戸市」の結果は、「神戸」と「市」に分かれてしまいます。「神戸市」と一つの単語で登録したい場合などは、ひと手間かける必要があります。

全文検索以外の他の用途でも使えそう。漢字混じりの文章をすべて平仮名や片仮名に変換したりも可能なので、使い道は結構あると思います。

またデスクトップアプリの機能として形態素解析エンジンを使いたい場合は、.NETアプリで使用できるNMeCabというMeCabの.NET移植版がありますし、形態素解析というと、ものすごく難しい、というイメージですが結構簡単に自分のアプリにも組み込めたりできますので、もっと活用の場があってもいいと思います。

ID値がグローバル変数に自動追加される

超初歩的なことに、はまってしまった。自分自身の備忘録として残しておこう。

自分で組んだjavascriptコードが chrome で動いたり動かなかったりしたことがきっかけだった。
そのスクリプトは、ブラウザ判定をしてて、chrome、いえ、webkit の検出に以下のようなコードを使用していた。
これ自体は、グーグル検索で辿り着いた、W3G(world wide web guide)というサイトに掲載されていたものを丸々拝借させてもらいました。

// chrome/safari等のオブジェクトを使った判別。
(function(undefined)
{
  var isWebkit = !document.uniqueID &amp;&amp; !window.opera &amp;&amp; !window.sidebar &amp;&amp; window.localStorage &amp;&amp; window.orientation === undefined;
})();

さて、ここで、chromeでアクセスしているにもかかわらず、HTMLファイルによってはtrueになったりfalseになったりして ????という状態で、falseと判定されるHTMLを開いてブラウザのコンソールで、調べていったら、 !window.sidebarfalseになることが判明。

これでピンときた(思い出した)。

あ!、HTMLファイルのある要素(DIVタグ)に id=”sidebar” って属性をつけてるわ・・・。

とほほ。ご存じのように、タグのid属性に指定した値は、グローバル変数に(厳密に言うと、windowオブジェクト)に追加されるんだった・・・orz

いや、確かに昔、Internet Explorer が中心だったころ、よくこれでラクしてたんだよな・・・最近は、document.getElementByIdというDOMメソッドを使用することがほとんどだったので忘れてた。でもIEならともかく・・・webkitでこれに直面するとわおもわなんだ。

結局、元になるHTMLファイルから、sidebar という名のid属性値を、違う値に全部変更することで一件落着。

これから、タグにid属性を含めるときは、必ずハイフンを使用した値にするようにしよう・・・と固く誓うのであった・・・。
というより、こんなIEの独自仕様は端っからwebkitから消し去ってくれればいいのに・・・。

_bstr_t は何気に便利だな、とか。

昔書いてたメモ帳から転記。今更感がマックスの、備忘録メモ。

ちょこっとCOMクライアントとかのテストコード書く際に、ワイド文字列(主にWindowsで言うところのUNICODE)と、マルチバイト文字列の相互変換が煩わしいときが多々ある。

まぁ、変換用に関数一個作っとけばいい話なんですが・・・。正直、Win32 APIのMultiByteToWideChar/WideCharToMultibyte は使いたくない。理由はただ一つ。コイツらの使い勝手の悪さときたらどうしようもない。ってとこ。引数が5個も6個もいるし、何より、最低限2回のAPIコールが必要。↓のような感じ。

/*
  マルチバイト文字列 ==> ワイド文字列
*/

#include <windows.h>

//ヒープを使用。
PWSTR AllocString(PCSTR& pStr)
{
  PWSTR pWStr = NULL;
  SIZE_T cchWStr = 0;

  if(pStr == NULL)
    goto cleanup;
  
  cchWStr = MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,pStr,-1,NULL,0);

  if(cchWStr <= 0)
    goto cleanup;

  if((pWStr = (PWSTR)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,cchWStr*sizeof(WCHAR))) == NULL)
    goto cleanup;

  if(!MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,pStr,-1,pWStr,cchWStr))
    FreeString(pWStr);

cleanup:
  return pWStr;
}

//ヒープ解放
BOOL FreeString(PWSTR& pWStr)
{
  BOOL bRet = FALSE;

  if(pWStr && (bRet = HeapFree(GetProcessHeap(),0,reinterpret_cast<PVOID>(pWStr))))
    pWStr = NULL;

  return bRet;
}

でも、こんな感じだと、明らかにテストしたいコードより遙かに長いコード行・・・何か腹立つ。
お金の掛からないVC++ Express Edition を使っていて贅沢は言えませんが・・・なんかで代用できないものか・・・。

で、最近MSDNのドキュメント見てて気付いた。
_bstr_t クラスって便利やないかい!、と。

もともとは、COMでの文字列表現、BSTR型のラップするスマートポインタみたいなクラス。BSTR型はちょっと特殊で簡単に言えば文字列の長さとバッファを合わせたような型。BSTR型をPWSTR型と混同しているケースを極たまに見ます。まぁ、そんなことはどうでもよく。

_bstr_t クラスは、PCWSTR,PCSTRの両方をコンストラクタで受け付けてくれます。しかも、キャスト演算子をオーバーロードしてくれてるので、引数にPCWSTRもしくはPCSTRが必要なAPIや関数にそのまま放り込めます。

※注意点
これらのキャスト演算子は内部バッファへのナマのポインタを返すので、やたらめったら使用すると、おそらくどこかでクラッシュするバグを抱えるでせう。

#pragma comment(lib,"user32.lib")
#pragma comment(lib,"comsuppw.lib")

#include <windows.h>
#include <comutil.h>
#include <tchar.h>

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
  _bstr_t bStr("テストでげす");
  
  //ANSI版
  MessageBoxA(NULL,bStr,"ANSI版",MB_OK);

  //UNICODE版
  MessageBoxW(NULL,bStr,L"UNICODE版",MB_OK);
  
  return 0;
}

/*
ANSIバージョンのAPIは、内部でマルチバイト文字<==>ワイド文字の変換を行って
UNICODE版APIをコールしているだけのようなので、このコード自体無意味。
*/

スコープを抜けると自動的にSysFreeStringをコールしてくれるみたいなので、解放処理も必要なし。

万歳!