人気ランキング ブロック

 

サイドバーに「人気ランキング (TOP 20)」ブロックを設置しました。


図1 人気ランキング ブロックの例

本稿では、このブロックをどのように実装したかを紹介します。

基本方針

ブロックの実現方法について

  • このブロックは、Drupal のモジュールとして開発するのではなく、 ブロックに PHP コードを記述する方法により実現します。
  • このブロックをモジュール化する予定はありません。

作成するプログラムについて

  • 集計に使用するアクセスログは、Drupal の accesslog テーブルとします。 これは Apache のアクセスログを使用するよりも遥かに楽です。
  • ページ生成の負荷を軽くするために、プログラムを2つに分けて作成します。
  • 1つめは、アクセスログを集計するための「アクセスログ集計プログラム」です。 これは Ruby で作成します。 このプログラムは cron により定期的に実行されることを想定しているため、 多少の負荷がかかってもページの生成速度に影響しません。
  • 2つめは、上で作成した集計結果を表示するための「ランキング表示プログラム」です。 これはブロックに記述する PHP コードなので、ページの生成速度に影響します。 特に Drupal のキャッシュを無効にしている場合は、 ページがリクエストされるたびにこの PHP コードが実行されます。 そのため、高速に処理されることを期待する部分です。

ランキングテーブルの作成

  • 集計結果を保存するために、2つのランキングテーブル(node_ranking_new と node_ranking_old) を作成します。
  • それぞれの用途は、node_ranking_new には最新の集計結果を、 そして node_ranking_old には前回の集計結果(node_ranking_new のコピー)を保存します。 こうすることにより、順位の変動を調べることが可能になります。
  • トランザクション処理するので、ENGINE に InnoDB を指定します。 InnoDB を指定しないと、「アクセスログ集計プログラム」が実行されているのと同じタイミングで 「ランキング表示プログラム」が実行された場合に、ランキングの表示が真っ白になります。 デフォルトの ENGINE が InnoDB であれば、わざわざ指定する必要はありません。
create table node_ranking_new (
nid integer primary key,
path varchar(50),
title text,
count integer,
rank integer
) ENGINE=InnoDB;

create table node_ranking_old (
nid integer primary key,
path varchar(50),
title text,
count integer,
rank integer
) ENGINE=InnoDB;

アクセスログ集計プログラム

  • Drupal の accesslog テーブルを集計して、結果をランキングテーブルに保存します。
  • あるページについて何人の訪問者が “興味を持ったか” を集計します。 集計期間内において、1人の訪問者が1つのページを複数回リクエストした場合でも、 そのページに付与されるのは1ポイントだけです。
  • データベースへの接続パラメータは ../config.xml に設定します。
  • カスタマイズ可能な項目は次の通りです。
    • 何位まで取得するか(limit)
    • 過去何日分のデータを集計するか(interval)
    • 集計から除外するIPアドレス 例:管理者のIPアドレス (filter)
../config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<config>
    <drupal>
        <database>drupaldb</database>
        <username>fooqux</username>
        <password>abracadabra</password>
    </drupal>
</config>

node_ranking.rb
#!/usr/bin/ruby -Ku

require 'mysql'
require 'rexml/document'

#-------------------------------------------------------------------------------

limit    = 20                       # TOP 20
interval = 7                        # 過去何日分を集計する?
node     = '^node/[[:digit:]]+$'    # 集計するノードのパターン(正規表現)

filter = [                          # 集計から除外するIPアドレス("'と'"で囲む)
    "'192.168.100.200'",
    "'192.168.100.201'",
    "'192.168.100.202'"
]

#-------------------------------------------------------------------------------

summary = [
    "set character set utf8",

    "start transaction",

    "delete from node_ranking_old",

    "insert into node_ranking_old select * from node_ranking_new",

    "delete from node_ranking_new",

    "set @rank=0",

    "insert into node_ranking_new (nid, path, title, count, rank)
     select tmp2.nid, tmp2.path, node.title, tmp2.count, tmp2.rank from node,
     (select replace(path, 'node/', '') nid, path, count(*) count, (@rank:=@rank+1) rank from
     (select path from accesslog
     where hostname not in (#{filter.join(",")})
     and path regexp '#{node}'
     and timestamp between unix_timestamp(subdate(curdate(), #{interval})) and  unix_timestamp(curdate())
     group by path,hostname) as tmp1
     group by path
     order by count desc
     limit 0, #{limit}) as tmp2
     where node.nid = tmp2.nid",

    "commit"
]

#-------------------------------------------------------------------------------

sio = open(File.dirname(File.expand_path(__FILE__)) + '/../config.xml')
doc = REXML::Document.new(sio.readlines.to_s)
cfg = doc.elements['config/drupal']

database = cfg.elements['database'].text
username = cfg.elements['username'].text
password = cfg.elements['password'].text

mysql = Mysql::new('localhost', username, password, database)
summary.each { |sql|
    mysql.query(sql)
}

ランキング表示プログラム

  • ランキングテーブルの内容を加工して表示します。
  • ポイントの高い記事から順に20件表示します。 表示件数を変更する場合は、アクセスログ集計プログラムを修正します。
  • 2つのランキングテーブルから順位の変動(↑↓→)を調べて表示します。
  • 記事のタイトルをリンクで表示します。
CSS
#node_ranking td.rank {                 /* 順位 */
    text-align: right;
    white-space: nowrap;
}

#node_ranking td.fluct {                /* 変動 */
    padding: 0px 5px;
    text-align: center;
}

#node_ranking td.entry {                /* 記事 */
    padding-bottom: 3px;
    text-align: left;
}

#node_ranking .notdown {
    color: darkorange;
}

#node_ranking .down {
    color: cornflowerblue;
}

PHP
<?php

global $base_url;
global $active_db;

$sql =<<< SQL
    select new.*,
        case when new.rank = old.rank then "→"
             when new.rank < old.rank then "↑"
             when new.rank > old.rank then "↓"
             when old.rank is null then "↑"
    end fluctuate
    from node_ranking_new new left outer join node_ranking_old old
    on new.path = old.path
    order by new.rank;
SQL;

$result = mysql_query($sql, $active_db);

echo '<table id="node_ranking">';
while ($row = mysql_fetch_assoc($result)) {
    $color = $row['fluctuate'] == "↓" ? "down": "notdown";
    $title = htmlentities($row['title'], ENT_QUOTES, 'UTF-8');
echo <<< RANKING
    <tr>
        <td class="rank">${row['rank']}.</td>
        <td class="fluct ${color}">${row['fluctuate']}</td>
        <td class="entry"><a href="${base_url}/${row['path']}">${title}</a></td>
    </tr>
RANKING;
}
echo '</table>';

?>

改版履歴

日付 内容
2007-05-28 [修正] ランキング表示プログラムで、記事のタイトルをHTMLエスケープするようにした。
2007-01-18 [修正]  ランキングテーブルの作成で InnoDB を明示的に指定した。 / データベースへの接続パラメータを config.xml で設定するようにした。 / 集計期間を設定可能にした。
2006-08-26 [初版]