[PHP]PubSubHubbub(PuSH)を使ってサイトの更新を瞬時に通知

通常、RSS リーダーなどで最新の記事があるかをチェックするには、定期的にサイトをチェックする必要がありました。そこで、データの変更をリアルタイムに通知するためのプロトコル「PubSubHubbub(パブサブハバブ)」が作られ、更新があった場合瞬時に RSS リーダーにプッシュ通知を送ることができるようになりました。これにより、定期的にサイトをチェックして更新がないか確認する必要がなくなり、更新通知を受けてから読み込みに行くこと可能となります。
WordPress ではプラグインの「pubsubhubbub」や「PuSHPress」などを使って実装することができます。

PHP でも curl を使った POST でサーバーと簡単にやりとりすることが可能です。
パラメーターは検索エンジンによって異なりますが、Google の場合 hub.mode と hub.url をサーバー (http://pubsubhubbub.appspot.com/)に送信することで要求できます。

hub.url とはフィードの URL で、更新したことを伝える場合 hub.mode は「publish」としておきます。

PHP の関数にしたものがこちらです。

<?php
// フィードのURL
$feed = 'http://example.com/feed/rss/';

pubSubHubbub($feed);

function pubSubHubbub($feed){
	$url = 'http://pubsubhubbub.appspot.com/';
	$post = array(
		'hub.mode' => 'publish',
		'hub.url' => $feed
	);

	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_POST, true);
	curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));
	curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
	curl_setopt($ch, CURLINFO_HEADER_OUT, true);
	$response = curl_exec($ch);
	$info = curl_getinfo($ch);
	//print_r($info);
	curl_close($ch);
	return $response;
}

フィードの自動探査に対応させる

RSS や Atom フィードを PubSubHubbub に対応させるには、記述を追加する必要があります。
必要とする要素や形式、記述の詳細は送信先サーバーの仕様によって異なりますが、一例として「http://pubsubhubbub.appspot.com」のケースを説明します。

【RSS 1.0 / RSS 2.0 の場合】

<?xml version="1.0"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <atom:link rel="hub" href="http://pubsubhubbub.appspot.com" />
    <atom:link rel="self" type="application/rss+xml" href="http://example.com/feed/rss/" />
    ...
  </channel>
</rss>

rss 要素(rdf:RDFの場合も同様)に「xmlns:atom="http://www.w3.org/2005/Atom"」を追加し、channel 要素内に link 要素として PubSubHubbub サーバー(hub)と自サイトのフィード URL (self)を追加します。

【Atom の場合】

属性 hub と self の link 要素を追加し、hub に Pubsubhubbub サーバー、self に自サイトのフィード URL を記述します。

<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>...</title>
  <link rel="hub" href="http://pubsubhubbub.appspot.com/" />
  <link rel="self" type="application/atom+xml" href="http://example.com/feed/atom/" />
  ...
  <entry>
  ...
  </entry>
</feed>

リアルタイムに更新通知を受けるには、フィードリーダー側も PubsubHubbub に対応している必要があります。

参考:
http://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html

[PHP]自然言語処理(形態素解析)を利用した簡易全文検索

ある文字列が文章内に存在するかを確認するだけなら mb_strpos() で調べることができますが、完全一致なので少しでも言い回しを変えると一致しなくなります。
例えば「東京は日本の首都です」という文章と「日本の首都は東京です」は人間の感覚ではほとんど同じですがコンピュータにとってイコールではありません。
検索に曖昧さを持たせるには、文章を小さな単位に分解し、それがある程度含まれていれば一致したとみなすという手法が一般的で、今回は形態素解析を利用した全文検索を行ってみます。

このサンプルの動作には igo-php が必要です。
過去の記事を参考に環境を準備して下さい。

<?php
require_once 'lib/Igo.php';

$keyword = '東京は日本の首都です';

$text = array(
	'これは日本語で書かれた文章です',
	'日本の首都は東京です',
	'日本語には漢字が使われます。',
	'かつては京都が日本の首都でしたが現在は東京です'
);

$fulltext = new FullTextSearch();

// インデックスを作成
foreach($text as $value){
	$fulltext->index($value);
}

$results = $fulltext->search($keyword);
print_r($results);

class FullTextSearch
{
	private $igo;
	private $indexes = array();
	private $threshold = 0.8; // 許容するしきい値

	function __construct(){
		$this->igo = new Igo("./ipadic", "UTF-8");
	}
	
	// 分かち書き
	function wakati($str){
		$arr = $this->igo->wakati($str);
		return array_map(array('FullTextSearch', 'remove_space'), $arr);
	}
	
	// 検索対象を登録
	function index($str){
		$arr = array_unique($this->igo->wakati($str));
	
		$this->indexes[] = array(
			$str, implode(' ', $arr)
		);
	}

	// インデックスから検索
	function search($str){
		$words = array_unique($this->wakati($str));

		$results = array();
		$wordCount = count($words);
		if($wordCount == 0) return $results;
		
		foreach($this->indexes as $index){
			$match = 0;
			foreach($words as $word){
				if(mb_strpos($index[1], $word) != false){
					$match++;
				}
			}
			
			if($match / $wordCount >= $this->threshold){
				$results[] = $index[0];
			}
		}
		
		return $results;
	}
	
	// スペースを取り除く
	function remove_space($value){
		return str_replace(array(' ', ' '), '', $value);
	}
}

結果:

Array
(
    [0] => 日本の首都は東京です
    [1] => かつては京都が日本の首都でしたが現在は東京です
)

まずはキーワードを分かち書きし、「東京 | は | 日本 | の | 首都 | です」のような配列にします。あとは複数の文章中から一定数の語句を含むものを探して表示します。

本来は検索対象の文章をあらかじめ分かち書きしてデータベースに登録しておきますが、この例はあくまでコンセプトなのでその都度変換しています。
登録されるインデックスは次のような形で格納されています。

Array
(
    [0] => Array
        (
            [0] => これは日本語で書かれた文章です
            [1] => これ は 日本語 で 書か れ た 文章 です
        )

    [1] => Array
        (
            [0] => 日本の首都は東京です
            [1] => 日本 の 首都 は 東京 です
        )

    [2] => Array
        (
            [0] => 日本語には漢字が使われます。
            [1] => 日本語 に は 漢字 が 使わ れ ます 。
        )

    [3] => Array
        (
            [0] => かつては京都が日本の首都でしたが現在は東京です
            [1] => かつて は 京都 が 日本 の 首都 でし た 現在 東京 です
        )

)

配列 [0] にはオリジナルの文章が、[1] にはスペースで区切った語句が格納されています。
適合率が変数 threshold(しきい値)以上であった場合検索結果に加えるという流れです。
しきい値には0.0~1.0までの小数が利用でき、0.8 のときは単語の 80% 以上が一致すれば検索結果に加えるという意味になります。

MySQL を使って全文検索を行う場合は「MATCH(カラム) AGAINST(語句)」を用います。
分かち書きをしたスペース区切りの語句をデータベースに保存し、そのカラムに対して FULLTEXT インデックスを張ります。InnoDB での FULLTEXT は MySQL 5.6.4 以上でなければ使えません。バージョンが古い場合、エンジンには MyISAM を使って下さい。

ALTER TABLE テーブル名 ADD fulltext(カラム名)

あとは「SELECT * FROM pages WHERE MATCH(content) AGAINST('+東京 +首都' in boolean mode)」のようにして検索します。「+」は語句が含まれるという意味で、「-」の場合語句を含まないものという意味になります。

通常 MySQL には検索に使われるキーワードの最低文字数が設定されています。(ft_min_word_len、innodb_ft_min_token_size)
この文字数に満たない語句は無視されてしまいます。設定を確認するには次のようなクエリを発行します。

show variables like '%ft%'

4文字に設定されていることが多く、そのままでは日本語のほとんどのキーワードが無視されてしまいます。そこで MySQL の設定ファイル my.cnf に「ft_min_word_len=2」あるいは「innodb_ft_min_token_size=2」のように書き加えてサーバーを再起動します。

[PHP]コンストラクタ内で例外を投げるのは危険?

PHP に限らず他の言語でも言われていることですが、クラスのコンストラクタで例外を投げるような構造を取らざるを得ない場合、気をつけて置かなければならないことがあります。

例外の発生によってコンストラクタでの処理が中断され、インスタンスが生成されなかった場合、そのクラスのデストラクタは実行されません。

<?php
class Sample
{
	function __construct(){
		throw new Exception("error");
	}
	
	function __destruct(){
		echo "destruct";
	}
}

try {
	$sample = new Sample();
} catch (Exception $e){
	echo $e->getMessage();
}

生成に失敗したインスタンス変数は NULL のままになってしまうので、コンストラクタで利用した通信の切断やロックの解除をデストラクタやクラスメソッドから行うことはできなくなります。

コンストラクタ内で例外を投げる場合は、エラー発生時の終了処理を完結させた後でスローしたほうが良さそうです。