UNIX系OSでは比較的古くからtsortコマンドとしてお馴染みのトポロジカルソートですが、これは他のソート法と比べて異質なものでとても興味深いものです。



どういったソート法かというと
tsortコマンドに以下の内容のファイル(ブランク区切りでペアの行)を食わせると、論理的整合性のとれた順番を返してくれます。



[data.txtファイル内容]

3    2
3    1
1    2



[tsortコマンド実行]

> tsort data.txt
3
1
2



「3」「1」「2」の順番という答えを導き出してくれました。
これは繁雑な作業などを矛盾なく進める順番を把握するのに役立ちそうです。
まだこれくらいの規模なら頭で考えれば分かることですが、説明のために単純化してみました。



別のアプローチで説明してみたいと思います。
上記のファイル内容の関係を図で表すと


そろそろホンキ出す-tsort-312


この図でまず注目すべきポイントは、どこからも矢印を指されていないものが存在することです。
答えは「3」です。
この「3」とこれに付随する「矢印」を消してみましょう。


そろそろホンキ出す-tsort-12


前回と同様にどこからも矢印を指されていないものが存在します。
答えは「1」です。
この「1」とこれに付随する「矢印」を消してみましょう。


そろそろホンキ出す-tsort-2


最後に「2」が残りました。
今消して行った順番がまさにtsortコマンドが導き出した答えと一致します。


同様の処理を繰り返すのでこれは再帰という手法を使えば自然に求められそうですね。



昨日気づいたことなんですが、以下のような警告文がずらりと出てしまう事態が発生しました。



■警告文現る
<?php
error_reporting(E_ALL);
ini_set("display_errors", 1);

$file= "data.txt";
$fp= fopen($file, "rb");
while (!feof($fp)){


    list($a,$b,$c,$d,$e,$f)= explode('<>', rtrim(fgets($fp)), 6);
}
fclose($fp);


?>



[出力結果]
Notice: Undefined offset: 5 in C:\xampplite\htdocs\index.php on line 22

Notice: Undefined offset: 4 in C:\xampplite\htdocs\index.php on line 22


Notice: Undefined offset: 3 in C:\xampplite\htdocs\index.php on line 22


Notice: Undefined offset: 2 in C:\xampplite\htdocs\index.php on line 22


Notice: Undefined offset: 1 in C:\xampplite\htdocs\index.php on line 22



error_reporting()関数を使ってエラー警告レベルを上げたところ上記のような警告文がずらりと表示されるようになりました。




■警告源の特定

<?php
error_reporting(E_ALL);
ini_set("display_errors", 1);

$file= "data.txt";
$fp= fopen($file, "rb");
while (!feof($fp)){


    $tmp= fgets($fp);
    echo strlen($tmp),"<br />\n";
// list($a,$b,$c,$d,$e,$f)= explode('<>', rtrim($tmp), 6);
}
fclose($fp);


?>



[出力結果]

211
222
212
200
151
229
168
171
181
118
159
158
136
0


警告が出た辺りをいろいろとコメントにしてやっと特定できたのがexplode()で6分割してる行でした。


「Notice: Undefined offset: 5」~「Notice: Undefined offset: 1」までの警告文は、6分割したうちの5番目~1番目までは未定義値、つまり値が最後の6番目だけにしかセットできなかったという内容のようですね。


fgets()で読み出す部分をくくり出して、取得したデータ長を順に表示させてみると必ず最後に 0長データを取得してることがわかりました。
別のファイルでやってみてもfopen("~", "r")とかfgets($fp, 1024)とかいろいろ変えても常に最後に0長データを読み出してるということがわかりました。
つまり(!feof($fp))のループ条件を満たしてるのに最後に0長データを読み出してることになります。
これは環境特有の現象なのかな?



■対策と結論

<?php

error_reporting(E_ALL);
ini_set("display_errors", 1);


$file= "data.txt";
$fp= fopen($file, "rb");
while (!feof($fp)){


    if (($tmp= fgets($fp)) === false) break;
    echo strlen($tmp),"<br />\n";
}
fclose($fp);


?>



[出力結果]

211
222
212
200
151
229
168
171
181
118
159
158
136


というわけでfgets()で読み取るデータは一旦は一時変数に読み込んでfalseが返ってきてるかどうかチェックするようにしたほうが手堅いゾと



実質機能していないfeof()を使わずにエラーハンドリングを丁寧に書くと

<?php

$file= "data.txt";
if (file_exists($file) and $fp= fopen($file, "rb")){
    while (($tmp=fgets($fp)) !== false){
        echo strlen($tmp),"<br />\n";
    }

    fclose($fp);
}

?>



ウェブサイトの更新情報を手軽に知ることのできるRSSというものがあります。
ブログを中心とした様々なサイトでこのRSSを取得できるようになっていて、自分が見たいサイトを集めて一覧表示させると更新の有無を効率よく知ることができて便利です。



このアメブロでも読者登録すれば「マイページ」の「チェックリスト」に更新情報を一覧表示させることができることはご存知のことと思います。
これに似たようなことをPHPでやってみようと思います。



まず押さえておくポイントは、ブログ等で取得できる更新情報が書かれたファイルにはいろいろなタイプがあるということです。
調べたところ「RSS1.0」「RSS2.0」「Atom0.3」「Atom0.9」「Atom1.0」などがあるようですが、これらはそれぞれフォーマットが違います。
とりあえずこの5種に対応したものを作ればほとんどまかなえそうなので早速作ってみます。



RSSはHTMLのようなタグ(XML系のマークアップ言語)で構造化された単なるテキストファイルなので正規表現で抜き出すのは簡単です。
PHPにもXML関係の関数がいろいろ用意されていますがどれを使ったらいいかよくわかりませんでした。試しに使ってみたら日付が取得されなかったのでpregを使って正規表現で切り出すことにします。




■ソースコード(rssreader.php)
<?php

// RSSアドレス設定他
$rss_url= array(
    "http://www.example.com/rss/rss10.rdf",
    "http://www.example.com/rss/rss20.xml",
    "http://www.example.com/atom/atom09.xml",
    "http://www.example.com/atom/atom10.xml",
);
$interval= 10; // RSSを読みにいく間隔 単位[分]
$jcode= "SJIS"; // 掲載サイトの文字コードに合わせる
$max_text= 20; // 表示させる文字数(2連続の半角文字は1文字扱い)
$tempfile= "rssdata.tmp";
$width= 300; // 単位[px]
$bgcolor1= "white";
$bgcolor2= "#fff0c0";



// RSSキャッシュ読み込み
$cache= array();
if (file_exists($tempfile) and $fp= fopen($tempfile, "rb")){
    while (!feof($fp)){

        $tmp= rtrim(fgets($fp));
        if (empty($tmp)) continue;
        list($atime, $rss, $sitename, $title, $link, $date)= explode("<>", $tmp, 6);
        if (array_search($rss, $rss_url) === false) continue;
        $cache[ 'ATIME'][$rss]= $atime;
        $cache[ 'RSS'][$rss]= $rss;
        $cache['SITENAME'][$rss]= $sitename;
        $cache[ 'TITLE'][$rss]= $title;
        $cache[ 'LINK'][$rss]= $link;
        $cache[ 'DATE'][$rss]= $date;
    }
    fclose($fp);
}



// RSSキャッシュ期限切れまたはキャッシュのないRSSを読みに行く
$interval*= 60;
$now= time();
foreach ($rss_url as $rss){


    // キャッシュ期限内ならスキップ
    if (isset($cache['ATIME'][$rss]) && $cache['ATIME'][$rss] + $interval > $now) continue;


    // 読みに行く
    $xml= request($rss);


    // 切り分け
    list($sitename, $title, $link, $date)= _xml_parse($xml);


    // セット
    $cache[ 'ATIME'][$rss]= $now;
    $cache[ 'RSS'][$rss]= $rss;
    $cache['SITENAME'][$rss]= entity($sitename);
    $cache[ 'TITLE'][$rss]= entity($title);
    $cache[ 'LINK'][$rss]= $link;
    $cache[ 'DATE'][$rss]= $date;
}



// 表作成
$view= "<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"border:1px solid black;width:{$width}px\">";
arsort($cache['DATE']);
$i= 0;
foreach ($cache['DATE'] as $rss=>$date){


    // データ取得失敗?ならスキップ
    if (empty($date)) continue;


    // 背景色を変える
    $bgcolor= ++$i % 2 ? $bgcolor2 : $bgcolor1;


    // 冗長なタイトルなら詰める
    $sitename= preg_replace("/((?:[\x81-\x9f\xe0-\xfc][\x40-\x7e\x80-\xfc]|(?:(?:&amp;|&lt;|&gt;|&nbsp;|&quot;|&#x?[a-f\d]+;|[\x09\x0a\x0d\x20-\x7e\xa1-\xdf]){1,2})){1,$max_text}).*$/i", '$1', $cache['SITENAME'][$rss]);
    if ($sitename !== $cache['SITENAME'][$rss]) $sitename.= "...";


    $view.= "<tr><td nowrap style=\"text-indent:6px;background-color:{$bgcolor};border-bottom:1px dashed black;font-size:14px;text-align:left\"><a href=\"{$cache['LINK'][$rss]}\" title=\"{$cache['TITLE'][$rss]}\" style=\"display:block;text-decoration:none\" target=\"_blank\">{$sitename}</a></td><td nowrap style=\"background-color:{$bgcolor};border-bottom:1px dashed black;font-size:12px\">(" . date("n/j G:i", $date) . ")</td></tr>";
}
$view.= "</table>";



// JavaScriptコードを返す
header("Content-Type: text/javascript");
$view= str_replace('"', '\"', mb_convert_encoding($view, jcode($jcode), "SJIS-win"));
echo <<<EOT
document.open();
document.write("{$view}");
document.close();
EOT;



// RSSキャッシュ保存
if ($fp= fopen($tempfile . ".tmp", "wb")){
    foreach ($cache['ATIME'] as $rss=>$atime)
        fputs($fp, implode('<>',
            array($atime, $rss,
            $cache['SITENAME'][$rss],
            $cache[ 'TITLE'][$rss],
            $cache[ 'LINK'][$rss],
        $cache[ 'DATE'][$rss],
        )) . "\n");
    fclose($fp) and rename($tempfile . ".tmp", $tempfile);
}



function entity($s)
{
    return strtr($s, array("<"=>"&lt;", ">"=>"&gt;", '"'=>"&quot;"));
}
function jcode($s)
{
    if (preg_match('/^utf-?8/i', $s)) return "UTF-8";
    else if (preg_match('/^(?:x-)?euc[_\-]?(?:jp)?/i', $s)) return "CP51932";
    else if (preg_match('/^(?:jis|iso-2022)/i', $s)) return "JIS";
    else return "SJIS-win";
}
function request($url)
{
    $_url= parse_url($url); empty($_url['port']) and $_url['port']= 80;
    $nl= "\x0d\x0a";
    $res= '';
    empty($_url['query']) or $_url['path'].= '?' . $_url['query'];


    if (!$fp=fsockopen($_url['host'], $_url['port'], $errno, $errstr, 10)) return '';
    $senddata= "GET {$_url['path']} HTTP/1.1{$nl}";
    $senddata.="User-Agent: RSS-READER/" . phpversion() . $nl;
    $senddata.="Host: {$_url['host']}{$nl}";
    $senddata.="Referer: http://" . $_url['host'] . $nl;
    $senddata.="Accept: */*{$nl}";
    $senddata.="Accept-Language: ja,en-us{$nl}";
    $senddata.="Content-Type: application/x-www-form-urlencoded{$nl}";
    $senddata.="Connection: close{$nl}";
    $senddata.="{$nl}";


    fputs($fp, $senddata);
    while (!feof($fp)) $res.= fgets($fp, 4096);
    fclose($fp);


    $res= mb_convert_encoding($res, "SJIS-win", "SJIS-win,UTF-8,CP51932,JIS");
    list(,$res)= explode($nl . $nl, $res, 2);


    return $res;
}
function _xml_parse($xml)
{
    if (preg_match('/<title>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/title>.*?(<item\b.*?<\/item>)/is', $xml, $match)){
        $sitename= $match[1];
        preg_match('/<title>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/title>/is', $match[2], $submatch);
        $title= $submatch[1];
        preg_match('/<link>.*?(http[^\]<"\s]+)/is', $match[2], $submatch);
        $link= $submatch[1];
        preg_match('/(?:<dc:date>|<pubDate>)(.*?)</is', $match[2], $submatch);
        $date= strtotime($submatch[1]);
    } else if (preg_match('/<title>(.*?)<\/title>.*?(<entry\b.*?<\/entry>)/is', $xml, $match)){
        $sitename= $match[1];
        preg_match('/<title>(.*?)<\/title>/is', $match[2], $submatch);
        $title= $submatch[1];
        preg_match('/<link\b[^>]*?href="?(http[^"\s>]+)/is', $match[2], $submatch);
        $link= $submatch[1];
        preg_match('/(?:<modified>|<updated>)(.*?)</is', $match[2], $submatch);
        $date= strtotime($submatch[1]);
    } else return array();


    return array($sitename, $title, $link, $date);
}


?>



■PHPスクリプトの呼び出し(index.html)
<body>
<script type="text/javascript" src="rssreader.php"></script>
</body>


試しに「アメブロ 」「FC2 」「ココログ 」「Yahoo 」「goo 」「ヤプログ 」「Livodoor 」「JUGEM 」「エキサイト 」「SeeSaa 」の目ぼしい有名人ブログのRSS情報を読みに行くテストをおこなってみました。


ヤプログのRSSだけ補正が必要だったので正規表現部分を書き換える必要がありました。
一応これでできあがりです。



[表示結果]
そろそろホンキ出す-rssreader.php出力


デザインは苦手なのでこれが限界です。

見栄えは悪いですが、一応動作したということで・・