PHPで自分のはてなブックマークを整形して見やすく表示する方法

先日、こんなおたよりを頂きました。

初めまして、RSSに登録していつも勉強させてもらってます。

先日の記事「管理人の新着ブックマークを見れるページを作りました | Stocker.jp / diary」を見てとても衝撃を受けました。
今まで見たどの個人ニュースサイトよりも見やすくブックマークがまとまっていて
はてブ数や、なによりコメントまで引用できているのは素晴らしいの一言です。
これを作った過程の記事なんかが見たいなー(チラッ

というわけで、PHP ではてなブックマークを整形して見やすく表示する方法について書きます。
具体的に言うと、これ↓の作り方ですね。
Stocker.jp 管理人のブックマーク

概要

具体的に言うと、はてなブックマークはユーザーごとに RSS が発行されていますので、PHP でそれを読み込んで simplexml_load_file() という関数で RSSをパース(解析) して表示しているだけです。
MySQL 等のデータベースすら使っていない、超シンプルで簡単なスクリプトです。

実は、このPHPスクリプトを書いた時、ちょうど CakePHP という有名なPHPフレームワークの勉強中だったので、「CakePHP で作るのには向いていない」と分かっていながらも試しに CakePHP で作ってみました。

CakePHP のような MVCフレームワーク を使うのは初めてだったので、かなり四苦八苦しながらもなんとか作ることができたのですが、私が使用している さくらのVPS 512 ではメモリ不足の警告がでてしまいました。

仕方ないので、CakePHP で作ったものを一旦普通の PHP で書きなおしたため、構造が MVC っぽく(?)3つのファイルに分かれています。

はてなブックマークのRSSの仕様

はてなブックマークの自分の RSS を見るには、
http://b.hatena.ne.jp/はてなID/rss
という URL にアクセスするだけです。

…が、この RSS、実は1度に20件ずつしか表示されません。
2ページ目以降を表示したければ、URLの最後に of=20 のようなパラメータを追加して

2ページ目を表示するURL
http://b.hatena.ne.jp/はてなID/rss?of=20
3ページ目を表示するURL
http://b.hatena.ne.jp/はてなID/rss?of=40

のようにする必要があります。

今回は、URLに of パラメータをつけると次のページを読み込めるようにしました。
つまり、最新20件を表示する場合のURL
https://stocker.jp/bookmark/
次の20件を表示するURL
https://stocker.jp/bookmark/?of=20
という感じになります。

作り方

私は PHP を書き始めたばかりの頃、以下のようなソースを書いていました。

1
2
3
4
<?php
/* ここに処理を書く */
?>
<!-- ここにHTMLを書く -->

しかし、ソースの量が増えてきたり関数をよく使うようになるとこの方法では見づらく感じて、関数だけ別ファイルに分けたりしていました。

今回は一度 CakePHP で作っていたこともあり、

  • 他のファイルをインクルードするためのファイル(index.php)
  • 関数をまとめたファイル(functions.php)
  • テーマファイル(theme.php)

の3つのファイルに分けて作っています。

他のファイルをインクルードするためのファイル(index.php)

1
2
3
4
5
6
7
8
9
10
11
<?php
/* はてなブックマークの表示 */
 
// WordPressの読み込み(これでWP関数使えるようになる)
include ('../diary/wp-load.php');
 
// 関数ファイルの読み込み
include ('functions.php');
 
// HTMLの読み込み
include ('theme.php');

5行目でどうしてWordPressを読み込んでいるかというと、サイドバーにブログと共通のサイドバーを表示したいからです。
WordPressフォルダ内にある wp-load.php を include すると、WordPress の独自関数が使えるようになるので、それを使ってブログと共通のサイドバーを表示させます。

関数をまとめたファイル(functions.php)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<?php
 
function rss2html() {
 
	// of は何件目から取得するか
	$of = isset($_GET['of']) ? $_GET['of'] : 0;
 
	if (! is_numeric($of)) {
		$of = 0;
	}
 
	// RSSの読み込み
	$xml = simplexml_load_file('http://b.hatena.ne.jp/hatena_id/rss?of=' . $of);	// of=20 で次ページ
 
	// $output に null を代入(Notice防止)
	$output = null;
 
	foreach ($xml->item as $item) {
		$dc = $item->children('http://purl.org/dc/elements/1.1/');
		$date = date('n月j日', strtotime($dc->date));
		if ($date !== $date_log){
			$output .= '<h3>' . $date . 'のブックマーク</h3>';
			$date_log = $date;
		}
		$output .= '<article><a class="bm_cap_a" href="' . $item->link . '" target="_blank"><div class="bm_cap" style="background:url(http://s.wordpress.com/mshots/v1/' . urlencode($item->link) . '?w=200);"></div></a>';
		$output .= '<h4 class="bm_content_h4"><a href="' . $item->link . '" target="_blank">' . $item->title . '</a></h4><img class="bm_count" src="http://b.hatena.ne.jp/entry/image/' . $item->link . '"><br />' . PHP_EOL;
		$output .= '<p>' . $item->description . '</p></article>' . PHP_EOL;
	}
 
	return $output;
}
 
function bm_next() {
 
	// of は何件目から取得するか
	$of = isset($_GET['of']) ? $_GET['of'] : 0;
 
	if (is_numeric($of)) {
 
		// of に20を足す
		$of = $of + 20;
 
		$output = '						<div id="bm_next">
						<a href="https://stocker.jp/bookmark/?of=' . $of . '">次の20件</a>
					</div>';
 
	} else if (! $of) {
 
		$output = '						<div id="bm_next">
						<a href="https://stocker.jp/bookmark/?of=20">次の20件</a>
					</div>';			
	}
 
	return $output;
 
}
 
function bm_prev() {
 
	// $output 初期化
	$output = null;
 
	// of は何件目から取得するか
	$of = isset($_GET['of']) ? $_GET['of'] : 0;
 
	if (is_numeric($of) and $of >= 20) {
 
		// of から20を引く
		$of = $of - 20;
 
		if ($of <= 0) {
 
			$output = '	<div id="bm_prev">
						<a href="https://stocker.jp/bookmark/">前の20件</a>
					</div>';
 
		} else {
 
			$output = '	<div id="bm_prev">
						<a href="https://stocker.jp/bookmark/?of=' . $of . '">前の20件</a>
					</div>';
 
		}
 
	}
 
	return $output;
 
}

functions.php は

  • RSS を読み込んで HTML を表示するための rss2html() 関数
  • 「次の20件」を表示するための bm_next() 関数
  • 「前の20件」を表示するための bm_prev() 関数

の3つの関数が書かれています。

5
6
	// of は何件目から取得するか
	$of = isset($_GET['of']) ? $_GET['of'] : 0;

6行目は 三項演算子 で ‘of’ というパラメータがセットされていればそれを変数 $of に代入し、セットされていなければ 0 を変数 $of に代入されるようにしています。

8
9
10
	if (! is_numeric($of)) {
		$of = 0;
	}

8〜10行目はいたずら防止で、もし ‘of’ というパラメータの値が数字でなければ 0 を代入してしまうようにしています。

12
13
	// RSSの読み込み
	$xml = simplexml_load_file('http://b.hatena.ne.jp/hatena_id/rss?of=' . $of);	// of=20 で次ページ

13行目は、 hatena_id 部分に自分のはてなIDを入れます。
simplexml_load_file() 関数を使うと、簡単に XML をパースして RSS リーダーのようなものを作れる のでかなり便利です。

ただ、細かい情報が取得できなかったりするのですがこれについては後述します。
パースした XML を、変数 $xml に代入します。

15
16
	// $output に null を代入(Notice防止)
	$output = null;

16行目は、変数 $output に何も代入されていないと PHP の Notice が出てしまうので、null を代入しています。

18
19
20
21
22
23
24
25
26
27
28
	foreach ($xml->item as $item) {
		$dc = $item->children('http://purl.org/dc/elements/1.1/');
		$date = date('n月j日', strtotime($dc->date));
		if ($date !== $date_log){
			$output .= '<h3>' . $date . 'のブックマーク</h3>';
			$date_log = $date;
		}
		$output .= '<article><a class="bm_cap_a" href="' . $item->link . '" target="_blank"><div class="bm_cap" style="background:url(http://s.wordpress.com/mshots/v1/' . urlencode($item->link) . '?w=200);"></div></a>';
		$output .= '<h4 class="bm_content_h4"><a href="' . $item->link . '" target="_blank">' . $item->title . '</a></h4><img class="bm_count" src="http://b.hatena.ne.jp/entry/image/' . $item->link . '"><br />' . PHP_EOL;
		$output .= '<p>' . $item->description . '</p></article>' . PHP_EOL;
	}

18〜28行目は、foreach を使って変数 $xml の内容をバラして、出力用の HTML を変数 $output に代入しています。
自分のブックマークコメントを表示させるには $item->description を入れるだけです。

19〜20行目は、RSS に含まれる日付の部分を解析しています。
日付の部分を解析するには children メソッドの引数に名前空間を指定する必要があるそうで、こちらのページを参考にさせて頂きました。
PHPのSimpleXML関数でRSS1.0の「dc:date」を取得する方法 | 自由が丘で働くWeb屋のブログ

21〜23行目で、日付が異なれば日付を見出しとして追加し、日付が同じであれば何もしないようにしています。

30
	return $output;

30行目で変数 $output を return します。

次は bm_next() 関数です。
‘of’ というパラメータの値が数字であれば、次ページへのリンク(現在の ‘of’ というパラメータの値+20)を表示し、数字でなければ(通常はパラメータの値が存在しなければ)2ページ目のリンクとして /bookmark/?of=20 を表示するだけです。

最後は bm_prev() 関数です。
‘of’ というパラメータの値が数字で、かつ20以上であれば、20を引いてみて0になったら ‘of’ パラメータのないリンク(/bookmark/)を表示し、そうでなければ前ページへのリンク(現在の ‘of’ というパラメータの値-20)を表示します。

テーマファイル(theme.php)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE HTML>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>管理人のブックマーク</title>
	<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
	<h1>管理人のブックマーク</h1>
	<div id="main">
		<div class="bm_content">
			<?php echo rss2html(); ?>
			<?php echo bm_next(); ?>
			<?php echo bm_prev(); ?>
		</div>
	</div>
	<?php get_sidebar(); ?>
</body>
</html>

実際はもっと色々書かれているのですが、ここでは説明のためシンプルにしています。
テーマファイルでは、先ほどの3つの関数から return されたものを表示し、ついでに WordPress の get_sidebar() 関数で WordPress のサイドバーを表示させています。
(index.php で WordPress を読み込んでいるため、WordPress の関数が使えます)

関数を別ファイルに分けておくと、テーマファイルがすっきり見やすくて良いですね。

見やすさのための工夫

わざわざはてなブックマークを読み込んで表示するページを作るなら、ページのスクリーンショットは絶対に付けたいと思っていました。
私は以前、はてなブックマークではなく livedoor クリップ というソーシャルブックマークサービスを利用していました。
なぜかというと、各ページごとにスクリーンショットが表示されていて見やすいと思ったからです。

ただ、とある会社で働いた時に livedoor へのログインが禁止されて利用できなくなったので、はてなブックマークへ移行しました。

スクリーンショットは WordPress.com の API を利用し、隣に表示するタイトルが1〜2行、コメントが1〜2行ということを考えると width: 120px; height: 75px; が良いかなと思いました。

ただ、普通に 120×75 で表示してみると、サイトのロゴがほとんど判別できないレベルで見づらいなと感じました。
もちろん、表示サイズを大きくすれば見やすくはなりますが、どうしてもスカスカな印象になってしまいます。

そこで、枠は 120×75 のまま、中身だけ拡大することにしました。
大抵のサイトは左上にロゴがあるので、これでだいぶ見やすくなったと思います。

中身を拡大して表示

注意点

ユーザー(閲覧者)がページにアクセスするたびにはてなブックマークのRSSを読みに行っていたのではページの表示に時間がかかってしまいますし、はてなのサーバーにも余計な負荷をかけてしまいます。

そこで、私のブックマーク一覧ページでは2時間に1度だけ最新の RSS を取得し、それをキャッシュするようになっているのですが、PHP ではそういうコードは書いていません。
なぜかというと、私のサーバは Apache の代わりに Nginx を利用し、PHP の出力結果を2時間キャッシュするようになっているからです。

ですので、一般のレンタルサーバ等でこういうページを作られるのであれば、出力結果をそのまま表示するのではなく一旦ファイルや MySQL データベース等に保存し、2時間毎くらいに更新するようにしたほうが良いと思います。

反省点

はてなブックマークの自分のブックマークの RSS にはブックマークした日時の情報も含まれているので、1日分ごとに <h3>2月15日のブックマーク</h3> のような小見出しを付けたかったのですが、simplexml_load_file() 関数では日時の情報の部分がうまくパースできないようです

追記

パーフェクトPHP の著者である @sotarok さんより、以下のようなリプを頂きました。

というわけで、名前空間について調査し、日付部分を取得できるよう改良しました。
情報提供いただき、ありがとうございました。

…というわけで、はじめておたよりに応えて記事を書いてみたのですが、こんな感じでよかったでしょうか。