アクセス解析 CounterizeII から NewStatPress へマイグレーション

WordPress のアクセス解析には現在 Counterize II と Google Analytics を併用しています。一時は Google Analytics へ一本化しようか迷ったのですが、いかんせんIPアドレスなどがデフォルトでは取得できませんので、不正アクセスやスパムなどでの BAN (拒否)ができません。これでは一本化はまず無理ですし、結局、併用がベターという結論に至りました。いざという時に(生)ログが手元にあるという安心感もありますしね。

ただ、 CounterizeII のアクセス解析はデザインが旧態依然ですし、すでに4年以上更新がないため、そろそろアクセス解析プラグインの乗り換えを考えることにしました。いろいろなプラグインがある中で、見た目とテーブルの簡素さから StatPress 系の NewStatPress にすることにしました。

ただ、そのまま使い始めますとアクセスカウンターがまたゼロからのスタートになってしまいます。これ、なんとかしたいなーということで、CounterizeII のアクセスログを NewStatPress へマイグレーション(移行)することにしました。テーブル間でSQL一発のマイグレーションも可能ですが、やはりできるだけ資産(データ)を生かしてマイグレーションしたいなーと。

テーブル構成(ER図)

まずは CounterizeII と StatPress のER図を以下に示します。今回ほぼ初めてまともにテーブル構成を見たのですが、まあ CounterizeII は正規化し過ぎですね。アクセスログでここまで正規化する必要はないと思いますね。ログ系はまず素の生ログに近いベースとなるテーブルにタンキングしておき、解析・統計はそのテーブルから加工していくのがセオリーかと。

ER図

このER図は、A5:SQL Mk-2 というツールで作成したのですが、いままで使用した数々のER関連ツールの中でも、抜群ではないかと。今までのツールって帯に短し襷に長し的なところがあったんですよね。この分野では海外モノが多いツールの中で日本製ですし、とにかく使いやすく表示形式(物理、論理、データ型)のバリエーションやDDLへの落とし込み、はたまたDB接続にSSHトンネルがデフォルトで可能など、このツールの開発者はかゆいところに手が届くというか、ER図とはなんなのかをよく知ってますね。こんな優れ物ツールが無料ってある意味凄すぎです。上記ER図もリバースエンジニアリングから、ものの5分程度で作成できました。MS-Visio ならおそらく1時間はかかっていたかなーと。Donate を募ってらっしゃるので本当はしたいのですが、まずはここでの紹介からとさせてくださいね(汗)

SQLによるマイグレーション

フィールドマッピング

SQL による CounterizeII から NewStatPress へのマイグレーションですと、純粋に CounterizeII のログデータを NewStatPress テーブルにコピーするだけですね。date, time, ip, urlrequested, agent, referrer, timestamp の生ログが対象になりますかね。
テーブルのフィールドマッピングは以下の通りです。テーブル名の接頭辞($wpdb->prefix)は、省略しています。

Counterize II SQL NewStatPress
Counterize.timstamp statpress.date
Counterize.timstamp statpress.time
Counterize_Pages.IP statpress.ip
Counterize_Pages.url statpress.urlrequested
Counterize_UserAgents.name statpress.agent
Counterize_Referers.name statpress.referrer
Counterize.timstamp statpress.timestamp

SQL

SQL ですとこのような感じになりますかね。
INSERT INTO statpress (
    date,
    time,
    ip,
    urlrequested,
    agent,
    reverrer,
    timestamp)
SELECT
    DATE_FORMAT(a.timestamp, '%Y%m%d') as date,
    DATE_FORMAT(a.timestamp, '%H:%i:%s') as time,
    a.IP,
    b.url,
    c.name as agent,
    d.name as referrer,
    a.timestamp
FROM
    Counterize a 
    LEFT JOIN Counterize_Pages b ON a.pageID = b.pageID
    LEFT JOIN Counterize_UserAgents c ON a.agentID = c.agentID
    LEFT JOIN Counterize_Referers d ON a.refererID = d.refererID
ORDER BY
    id ASC ;

なお、CounterizeII のIPアドレスはデフォルトでは暗号化されています、私は CounterizeII をカスタマイズして暗号化せず生IPをロギングするよう変更していましたのでそのまま移行できますが、暗号化されたハッシュ値のままであれば、あまり使い道がなさそうなので移行不要かもしれませんね。

NewStatPress のログ解析処理を適用しマイグレーション

フィールドマッピング

CounterizeII のログデータに本来 NewStatPress が行うログ解析処理を適用後、statpress テーブルにマイグレーションするという意味になります。具体的には、SQLで取得した date, time, ip, urlrequested, agent, referrer, timestamp の生ログに NewStatPress とほぼ同様のログ解析ロジックを適用し、search, nation, os,browser, searchengine, spider, feed を取得後、statpress テーブルにマイグレーションします。

Counterize II SQL NewStatPress
PHP
Counterize.timstamp statpress.date
Counterize.timstamp statpress.time
Counterize_Pages.IP statpress.ip statpress.nation
Counterize_Pages.url statpress.urlrequested statpress.feed
Counterize_UserAgents.name statpress.agent statpress.os
statpress.browser
statpress.spider
Counterize_Referers.name statpress.referrer statpress.search
statpress.searchengine
Counterize.timstamp statpress.timestamp

PHPコード

PHPコードは以下のような感じですかね。ファイル名はさし当たって counterizeii2statpress.php とでもしておきましょうか。なお5行目の require_once() の <WordPress Dir> には WordPress 設置ルートディレクトリをフルパスで指定してください。言わずと知れた外部から WordPress の関数をロードしたい場合のおまじないですね。

#!/usr/bin/php
<?php

//require the WP bootstrap
require_once('<WordPress Dir>/wp-load.php');

// Execute migration
mig_StatAppend();

/**
 * CounterizeII to NewStatPress migration
 *
 */
function mig_StatAppend() {
    global $wpdb;
    $table_name = nsp_TABLENAME;

    $offset = $wpdb->get_row("SELECT count(*) as count FROM " . $table_name);
    $count = $offset->count;
    $start = $wpdb->get_row("SELECT id FROM " . counterize_logTable() . " ORDER BY id ASC LIMIT 1 OFFSET " . $count);

    $sql = 
        "SELECT
            a.id,
            DATE_FORMAT(a.timestamp, '%Y%m%d') as date,
            DATE_FORMAT(a.timestamp, '%H:%i:%s') as time,
            ip,
            url,
            c.name as agent,
            d.name as referrer,
            a.timestamp
        FROM
            " . counterize_logTable() . " a 
            LEFT JOIN " . counterize_pageTable() . " b ON a.pageID = b.pageID
            LEFT JOIN " . counterize_agentsTable() . " c ON a.agentID = c.agentID
            LEFT JOIN " . counterize_refererTable() . " d ON a.refererID = d.refererID
        WHERE
            id >= " . $start->id . "
        ORDER BY
            id ASC;";

    $res = mysql_query($sql);

    while ($row = mysql_fetch_assoc($res)) {
        $id = $row['id'];
        $vdate = $row['date'];
        $vtime = $row['time'];
        $ipAddress = $row['ip'];
        $urlRequested = $row['url'];
        $userAgent = $row['agent'];
        $referrer = $row['referrer'];
        $timestamp = $row['timestamp'];
        $login = null;

        $urlRequested = mig_URL($urlRequested);
        $urlRequested = esc_sql($urlRequested);
        
        $referrer = esc_sql($referrer);
        $referrer = esc_html($referrer);

        $userAgent = esc_sql($userAgent);
        $userAgent = esc_html($userAgent);

        $os = nsp_GetOs($userAgent);
        $browser = nsp_GetBrowser($userAgent);
        $spider = nsp_GetSpider($userAgent);

        $searchengine = '';
        $search_phrase = '';

        if ($spider != '') {
            $os = '';
            $browser = '';
        } else {
            // Trap feeds
            $feed = nsp_IsFeed($urlRequested);
            // Get OS and browser
            $os = nsp_GetOs($userAgent);
            $browser = nsp_GetBrowser($userAgent);

            $exp_referrer = mig_GetSE($referrer);
            if ( isset($exp_referrer) ) {
                list($searchengine,$search_phrase) = explode("|",$exp_referrer);
            }
        }

        // Country (ip2nation table) or language
        $countrylang = "";
        if ( $wpdb->get_var("SHOW TABLES LIKE 'ip2nation'") == 'ip2nation' ) {
            $sql2 = 'SELECT *
                    FROM ip2nation
                    WHERE ip < INET_ATON("'.$ipAddress.'")
                    ORDER BY ip DESC
                    LIMIT 0,1';
            $qry = $wpdb->get_row($sql2);
            if ( $qry != null ) {
                $countrylang = $qry->country;
            }
        }

        $insert =
            "INSERT INTO " . $table_name . "(
                date,
                time,
                ip,
                urlrequested,
                agent,
                referrer,
                search,
                nation,
                os,
                browser,
                searchengine,
                spider,
                feed,
                user,
                timestamp
             ) VALUES (
                '$vdate',
                '$vtime',
                '$ipAddress',
                '$urlRequested',
                '".addslashes(strip_tags($userAgent))."',
                '$referrer','" .
                addslashes(strip_tags($search_phrase))."',
                '".$countrylang."',
                '$os',
                '$browser',
                '$searchengine',
                '$spider',
                '$feed',
                '$login',
                '$timestamp'
            )";

        $results = mysql_query( $insert );

        if ($results < 1) {
            print "insert error: line $id\n";
        }

        if ( ++$count % 1000 == 0 ) {
            print "$count\n";
        }
    }
}

/**
 * Processing the url
 *
 * @param urlRequested the source url
 * @return processing the url
 */
function mig_URL($urlRequested = '') {
    if(substr($urlRequested,0,2) == '/?') { $urlRequested = substr($urlRequested,2); }
    if($urlRequested == '/') { $urlRequested = ''; }
    return $urlRequested;
}

/**
 * Get the search engines
 *
 * @param referrer the url to test
 * @return the search engine present in the url
 */
function mig_GetSE($referrer = null){
    global $newstatpress_dir;

    $key = null;
    $lines = file($newstatpress_dir.'/def/searchengines.dat');
    foreach($lines as $line_num => $se) {
        list($nome,$url,$key) = explode("|",$se);
        if(strpos($referrer,$url) === FALSE) continue;

        # find if
        $variables = nsp_GetQueryPairs(html_entity_decode($referrer));
        $i = count($variables);
        while($i--){
            $tab = explode("=",$variables[$i]);
            if($tab[0] == $key && count($tab) >= 2){return ($nome."|".urldecode($tab[1]));}
        }
    }
    return null;
}
?>

マイグレーションの実行

今回は、ダウンタイムを出来る限り最小限に収めるという観点からマイグレーションを考えてみます。そのため多少手順が煩雑になるかもしれません。

NewStatPress プラグインのインストール

NewStatPress を稼働させたままマイグレーションしますとアクセスがあった場合、ログが新旧で混ざってしまいます。まず事前準備として、 WP Maintenance Mode プラグインなどでサイトをメンテナンスモードにして閉塞します。
閉塞後 NewStatPress プラグインをインストールします。有効化したらダッシュボードの NewStatPress メニューの Tools > Database Tools から ip2nation もインストールしておきます。

ロギングの停止

次にロギングの停止ですが、NewStatPress メニューの Options > Filters > Parameters to ignore > IP addresses 欄に以下のようにCIDRで記載しますとロギングが停止します。本来ならIPアドレスを ANY で拒否すればよいので、 0.0.0.0/0 でよいはずなのですが、どうもサブネットマスクを /0 にするとempty($mask) 判定でひっかかり強制的 32 に変更されてしまい、正常に全IPアドレスのアクセス拒否ができません。そのためサブネットマスクを 1 にしアドレスを2つに分けました。

[cc] 0.0.0.0/1,128.0.0.0/1
[/cc]

上記を追加すればロギングは停止しますので、メンテナンスモードを解除しサイトを開放します。なおログが新旧混ざってもよいのであればこの手順は不要です。

解析定義データの追加

次に昨日 2015/09/19 リリースの NewStatPress v1.0.8 は、解析定義データがやや不足している箇所がありますので、プラグインフォルダ配下の def ディレクトリの dat ファイルに、以下の定義を追加しておくとログ解析の精度が上がるかと思います。
ただし追加する際は、次の点にご注意ください。dat ファイルは上から順にマッチ検索しますので、数字が大きいほうを先に記述してください。FireFox40 と FireFox4 の場合は、FireFox40 を先に(上に)記述してください。例えばブラウザのバージョンが FireFox40 であった場合、FireFox4 が先に記述されていると FireFox4 にヒットしてしまい、バージョンは 40 であるのにも関わらず、4と判定されてしまいます。またブラウザのバージョンなどは未来のバージョンを記述しておいても特に問題ありません。

また spider.dat は、定義に該当しないアクセスは、すべて Visitor と判定され bot にも関わらず、アクセス数扱いにされてしまいますのでご注意ください。このファイルは、適宜メンテしないと正しいアクセス数が得られないのではないかと思います。ここは少々面倒ですね。

参考までに、今回追加した定義ファイルを以下に置いておきます。
  • browser.dat
    Chrome 46|Chrome/46|
    Chrome 47|Chrome/47|
    Chrome 48|Chrome/48|
    Chrome 49|Chrome/49|
    Chrome 50|Chrome/50|
    Chrome 5|Chrome/5|
    
    
    Firefox 50|Firefox/50|
    Firefox 51|Firefox/51|
    Firefox 52|Firefox/52|
    Firefox 53|Firefox/53|
    Firefox 54|Firefox/54|
    Firefox 55|Firefox/55|
    Firefox 56|Firefox/56|
    Firefox 57|Firefox/57|
    Firefox 58|Firefox/58|
    Firefox 59|Firefox/59|
    Firefox 60|Firefox/60|
    Firefox 4|Firefox/4|
    Firefox 5|Firefox/5|
    Firefox 6|Firefox/6|
    
    Microsoft Edge|Edge/12|
    Microsoft Edge|Edge/13|
    Microsoft Edge|Edge/14|
    Microsoft Edge|Edge/15|
    
  • os.dat
    Windows 10|WindowsNT10.0|
    
  • searchengine.dat
    Yahoo は search.yahoo.com になっていますので、必ず search.yahoo. に変更してください。
    Yahoo|search.yahoo.|p|
    
    Bing|www.bing.com|q|
    ezweb|ezsch.ezweb.ne.jp|query|
    docomo|search.smt.docomo.ne.jp|MT|
    au|sp-search.auone.jp|q|
    Rakuten|websearch.rakuten.co.jp|qt|
    OCN|wsearch.ocn.ne.jp|MT|
    J:COM|search.myjcom.jp|q|
    So-net|www.so-net.ne.jp|query|
    JWord|search.jword.jp|name|
    Sleipnir|search.fenrir-inc.com|q|
    Lunascape|s.luna.tv|q|
    FMWORLD|search.azby.fmworld.net|q|
    MyVAIO|search.start.sony.jp|MT|
    
  • spider.dat
    360Spider|360Spider|
    Linkdex|linkdexbot|
    SimilarTech|SMTBot|
    

コンソール実行

コンソールから couterizeii2statpress.php を実行します。処理が把握できるように 1000 件処理毎に件数を出力するようにしてあります。
また Web からでも実行は可能ですが、WordPress ディレクトリ配下ではなく外部の php ファイルが動作するディレクトリに配置する必要があります。HTML 表示になりますので改行の<br>を適宜入れておくとよいと思います。ただ Web からですとログ量にもよりますが、おそらくHTTP コネクションのタイムアウトになってしまう気がします。
[cc lang=”bash” theme=”blackboard”] $ php couterrizeii2statpress.php
1000
2000
3000
….
[/cc]
27万件程度でも問題なく処理が終了しました。ただ私のサーバは実行中プロセスが60分経過すると強制的に kill (中断)される仕様になっているようで、17万件あたりで停止してしまい再実行を余儀なくされました。

再実行の処理は中断した場所から再開し継続するよう処理を組んでいます。途中から再開し継続するカラクリは以下のコードですね。
まず statpress テーブルに登録され件数を取得します。Counterize テーブルからこの件数に該当する位置の行を OFFSET 句で取得し、その行の id を取得します。この id が継続開始位置ですね。本当は副問い合わせを使って SQL 一発で id を取得したかったのですが、MySQL は LIMIT/OFFSET 句の副問い合わせはエラーになり、結局それぞれでSQLを投げて取得する方法になりました。イマイチ。PostgreSQL なら可能だと思いますね。
    $offset = $wpdb->get_row("SELECT count(*) as count FROM " . $table_name);
    $count = $offset->count;
    $start = $wpdb->get_row("SELECT id FROM " . counterize_logTable() . " ORDER BY id ASC LIMIT 1 OFFSET " . $count);
        WHERE
            id >= " . $start->id . "
また、再度最初から行う場合は、statpress テーブルの初期化(全件削除 & AUTO_INCREMENT=1)を行っておく必要があるかと思います。
[cc lang=”sql” theme=”blackboard”] mysql> TRUNCATE statpress;
[/cc]

差分の同期

最後にもう一度サイトをメンテナンスモードにしてサイト閉塞します。コンソールから couterizeii2statpress.php を再実行します。これは先ほどのコンソール実行以後にアクセスされたログの差分を同期するためです。今度は直ちに終了するかと思います。

終了したら、ロギングを再開するため先ほど NewStatPress メニューの Options > Filters > Parameters to ignore > IP addresses 欄の「0.0.0.0/1,128.0.0.0/1」を消去し保存します。最後にメンテナンスモードを解除し、マイグレーションは完了です。

最後に

NewStatPress 高機能?と言われているようですが、私が欲しいと思う集計がないというか集計が基本的なものしかありませんね。例えば 不正アクセスがないかなどの Visitor 別日(週)集計や、ランキング確認の記事別月(週)集計などがありません。またカウンタ用の前日ページビュー数などもありません。この辺はカスタマイズしていかないと無理っぽいですね。
ただCounterizeII ではスパイダー数が全く解らなかったのですが、NewStatPress にしてページビューより多くこんなに多いのかと少々驚きでした。 bot アクセスが多過ぎですね。

また定義ファイルですが、現在はブラウザ名とアイコンファイルを同一名で共有していますが、ここは分けたほうがよいのではないかと思いますね。定義ファイルに追加する度にアイコンファイルを追加しなければなりませんので、これではファイル名が違うだけの同一画像ファイルが増え過ぎてしまいます。
[cc] ブラウザ名兼アイコンファイル名|ユーザーエージェント|
Firefox 40|Firefox/40|

ブラウザ名|ユーザーエージェント|アイコンファイル名|
Firefox 40|Firefox/40|firefox|
[/cc]
また普段は PHP 使いではないため、今回 MySQL の大量データを PHP で扱う場合、どのようにすればよいか少々戸惑いました。これはまた別記事にしたいと思います。

ラグビーW杯 南アフリカ v 日本 世紀のジャイキリという衝撃

ラグビーW杯 南アフリカ v 日本、深夜世紀のジャイアントキリングに感涙。リアルタイムで観れたことに感謝(レスター岡崎に感謝(汗))。

これほど魂が震えたのは久しぶりです。個人的にはジョホールバルかWBC決勝か五輪女子ソフトか南アW杯デンマーク戦(現地)か、でも世界的には今日の勝利はそれ以上のインパクトです。RWC史上最大の衝撃ですから。どこに魂が震えたかというと試合展開も劇的でしたが、あの日本代表のメンタルの強さですね。TV画面からその気迫がひしひしと伝わってきました。そのメンタルに触れた観客もJAPANコールに。最後エンジンを組み、勝ちにこだわる選択にも感動しました。
やはり日本人は泥臭く粘り強くコツコツひたむきにやらないと世界では勝てないということですね。スモールベースボールにしろ、練習は嘘をつかない、まさしくそれでした。以前、エディー・ジョーンズ HCについて触れましたが、あの時の練習量も半端なかったですしね。

先日、プロフェッショナル・仕事の流儀というエディー・ジョーンズ ラグビー日本代表監督のNHK番組がありました。エディー監督いわく以下の点について説いてました。

  • 強みを知り、強みを伸ばす
    日本人の強み。それは、どんな過酷な練習にも耐え、向上心を持ち続ける”勤勉(タフ)さ”。真面目で忍耐力があること。それは間違いなく世界一です。他の国の選手なら、とっくに逃げ出しているでしょう。
    (AM6~PM7までの過酷な練習って高校野球じゃないんだから。でも映像からは相当な過酷さが垣間見えました。)

  • ハッピーにしない
    選手を成長させるために必要なのは、選手を正しく理解し、それに合った方法で“少しの不安”や“緊張感”を与えること。居心地がいいと能力は発揮できないからです。ときには少し突き放すことで、選手が100%安心しないようにしています。

  • どんどんミスさせる
    日本の練習で一番間違っているのはミスをしないように練習すること。ノーミス、ノーミスと言うが、ミスするから上達する。失敗から学ぶということが重要。

  • 責任感を持たせる
    各ポジションのリーダー(サブリーダー)が責任感を持つ必要がある。責任とは各ポジションのメンバーによいパフォーマンスをさせること。

  • 感情の波を小さく
    指導者として、誰にでも感情に起伏があることを理解した上で、そうした波を最小限にとどめさせることが大切。

引用元:SKYBLUES.ORG
アジアカップ2015 日本代表QF敗退 と アギーレ監督解任

南アフリカは白人:黒人の比率が1:8なんですよね(南アというといつもこの比率で見てしまう(汗))。5年前にサッカーW杯で現地に行きましたが、当時でもいまだ格差は凄まじいものがありました。南アにしてもオージーにしてもイギリス連邦加盟国ですからイギリスの影響を色濃く受けてます。同組にスコットランドがいるとはいえイギリス国内中、ジャイキリした日本をリスペクトしていて、その喜びようといったら日本人以上です。スポーツの力って凄いですね!ラグビー日本代表に「勇気」を貰いました。

最近涙腺が弱いのか(汗)、先ほど録画での再放送でもまた涙してしまいました。