MODх — Учет посетителей сайта и график посещений

  • Tutorial
Как и многие программисты, я страдаю некоторой степенью подозрительности к чужим сервисам, и предпочитаю делать все сам.
К чужим сервисам, в частности относится liveinternet и другие счетчики посещений. Я им как то не доверяю, знаете ли.

Сейчас я вам расскажу (и покажу) как нетрудно сделать учет посетителей сайта с помощью modx.


Этап 1. Пишем лог.


Для начала нам нужно создать таблицу для хранения посещений. Делаем sql запрос как вам удобно. Например, в phpmyadmin.
CREATE TABLE IF NOT EXISTS `modx_visitors_log` (
  `index` int(10) NOT NULL AUTO_INCREMENT,
  `ip` varchar(15) COLLATE utf8_unicode_ci NOT NULL,
  `host` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `url` text COLLATE utf8_unicode_ci NOT NULL,
  `datetime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `referer` text COLLATE utf8_unicode_ci NOT NULL,
  `browser` varchar(255) NOT NULL,
  PRIMARY KEY (`index`),
  KEY `ip` (`ip`,`host`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1 ;

Таблица готова, теперь нам нужен плагин, для фиксирования посещений.

Большинство пользователей знают про сниппеты и чанки в modx, а вот про плагины обычно не в курсе. Рассказываю: плагин в modx, если по простому — это сниппет, который работает без вызова из документа, сам по себе, реагируя на системные события, коих в движке предусмотрено довольно много. Нас интересует событие OnLogPageHit.

Идем в
Элементы->Управление элементами->Плагины->Создать плагин

Называем новый плагин dbLog и вставляем в него следующий код:
/* Основные настройки*/
// Таблица для лога посетителей по умолчанию это modx_visitors_log
$db = '`modx_visitors_log`';
// Не логировать этих юзеров и IP. Если логировать всех - должны быть пустые массивы
$not_log_ip = array('192.168.100.1', '192.168.100.2');
$not_log_user = array('bezumkin');
/* Конец настроек */

// Выставляем переменные
$login = $_SESSION['webShortname']; // Если у вас юзеры авторизуются на сайте. Если нет - будет пустое поле.
$ip = $_SERVER['REMOTE_ADDR']; // IP запрашивающего адреса. Проверки на прокси нет.
$host = ''; // Здесь будет имя хоста, принадлежащего IP юзера. Пока не трогаем.
$url = $_SERVER['REQUEST_URI']; // Запрашиваемая страница.
$referer = urlencode($_SERVER['HTTP_REFERER']); // Откуда пришел юзер, по какой ссылке.
$browser = mysql_real_escape_string($_SERVER['HTTP_USER_AGENT']); // Браузер юзера. Я использую класс browscap, а вы как хотите. Для примера сойдет и так.

// Пишем в лог
if (!in_array($ip, $not_log_ip) and !in_array($login, $not_log_login)) {
    $modx->db->query("INSERT INTO $db (`login`, `ip`, `host`, `url`, `referer`,`browser`)
             VALUES ('$login','$ip','$host', '$url','$referer','$browser')");
    }

Переключаем вкладку Системные события и выставляем OnLogPageHit, затем жмакаем сохранить. Обратите внимание: при оформлении плагина в редакторе админки не должно быть <?php вначале и ?> в конце. У сниппетов должно, а у плагинов — нет. Не знаю почему так сделано, но при написании своего первого пагина я много матерился.
Все, плагин уже должен фиксировать всех ваших посетителей. Если вы, конечно, нигде не ошиблись!

Кстати, есть еще вариант повешать срабатывание плагина на событие OnWebPageComplete. Тогда нужно будет в настройках админки включить «Регистрировать посещения» и заработает аналогичный тырчик в свойствах документов. Тоже не плохой вариант, но я его еще не обкатывал. Подробнее о событиях можно глянуть тут.

Если вы хотите узнавать имена хостов, которые зарегистрированы на ip ваших посетителей, можно сделать еще один сниппет (Ip2Host) и запускать его по расписанию.
Почему так? Это сэкономит силы серверу, ибо, если узнавать хост юзера при каждом запросе страницы — нагрузка будет нешуточная. Лучше запускать раз в сутки, ночью.
<?php
$db = '`modx_visitors_log`';

// Выбираем все записи, с пустым полем host
$sql = $modx->db->query("SELECT `ip` FROM $db WHERE `host` = '' GROUP BY `ip`");
$arr = $modx->db->makeArray($sql);

// Перебираем их все
foreach ($arr as $v) {
    $ip = $v['ip'];
    // Делаем обычный nslookup. Если сайт хостится на Windows, во-первых, мне вас жаль, а во-вторых - эту строчку придется поменять на что-то другое.
    $host = `nslookup "$ip" | grep 'name =' | awk '{print $4}'`;
    // Если имени мы не получили - пишем, что хост не известен, чтобы повторно его не выбирать в следующий раз.
    if (empty($host)) {$host = 'unknown';}
    //Сохраняем результат в базу.
    $modx->db->query("UPDATE $db SET `host` = '$host' WHERE `ip` = '$ip' AND `host` = '';");
}
?>

Нужно оформить вызов на какой-нибудь скрытой от посторонних странице:
[!Ip2Host!]

и дергать ее cron`ом:
10 2 * * * * user wget localhost/secret_page.html


Этап второй. Вывод лога на экран.


В принципе, ваши посещения уже в базе, и выводить их оттуда можно как удобно. Лично мне приятно наблюдать это в виде симпатичного графика и отдельной табличке с циферками.

Так и сделаем.

Пишем сниппет для забора данных из БД и вывода их в понятную таблицу, график будем делать позже. Комментарии как обычно, внутри кода.
<?php
/* Основные настройки */
if (empty($db)) {$db = '`modx_visitors_log`';} // База данных с логом
if (empty($days)) {$days = 14;} // Кол-во дней для вывода таблицы
if (empty($daysText)) {$daysText = 'дней';} // Текст после кол-ва дней в чанке, не обязательно
if (empty($tpl)) {$tpl = 'visitStat.tpl';} // Шаблон для таблицы
if (empty($act)) {$act = 'graph';} // Режим вывода таблицы, всего их два, graph & table
if (empty($dateFormat)) {$dateFormat = '%d.%m';} // вывод даты, формат strftime()
if (empty($int)) {$int = 2;} // Интервал вывода даты в режиме graph. по умолчанию - каждая вторая.
/* Конец настроек */

// Выясняем, за какое время выбирать данные
$cur = time();
$end = date("Y-m-d");
$start = strftime("%Y-%m-%d", ($cur - ($days * 86400)));

// Выбираем
$sql = $modx->db->query("SELECT DATE(`datetime`) as `date`,
      COUNT(distinct `ip`) as `host`, 
      COUNT(`ip`) as `hit` 
      FROM $db WHERE DATE(`datetime`) BETWEEN '$start' AND '$end' 
      GROUP BY DATE(`datetime`) ORDER BY `datetime` ASC");
$result = $modx->db->makeArray($sql);

// Разбираем результат на 3 разных массива, дату, хосты и хиты    
    foreach($result as $v) {
        $date[] = $v['date'];
        $host[] = $v['host'];
        $hit[] = $v['hit'];
    }

// В режиме graph выводится каждая $int дата, в режиме таблицы - все даты
        $i = 1;
    foreach ($date as $v) {  
        if ($act == 'graph') {
            if ($i == $int) {
                $i = 0;
                $date2 .= '<th></th>';
            } 
            else {
                $date2 .= '<th>'.strftime($dateFormat, strtotime($v)).'</th>';
            }
            $i++;
        }
        else if ($act == 'table') {
            $date2 .= '<th>'.strftime($dateFormat, strtotime($v)).'</th>';
        }
    }
    foreach ($host as $v) {
        $host2 .= '<td>'.$v.'</td>';
    }
    foreach ($hit as $v) {
        $hit2 .= '<td>'.$v.'</td>';
    }

$placeholders = array('[+stat.days+]','[+stat.days.text+]','[+stat.date+]','[+stat.host+]','[+stat.hit+]');
$values = array($days, $daysText, $date2, $host2, $hit2);

$html = $modx->getChunk($tpl);

echo str_replace($placeholders, $values, $html);
?>

Создаем сниппет, называем его, например, generateStatGraph, копируем туда код и сохраняем.

Теперь нам нужно как то вывести на экран результат нашей бурной деятельности.
Нам понадобится два чанка-шаблона. первый, просто для таблицы, второй посложнее — для построения графика. Их можно будет использовать независимо друг от друга.

Первый шаблон, для вывода таблицы (tpl.StatTable).
<div style='margin: auto;text-align: center;'>
    <table id='chart_table'>
    <caption>Статистика посещений сайта за последние [+stat.days+] [+stat.days.text+]</caption>
        <thead>
            <tr><td></td>[+stat.date+]</tr>
        </thead>
        <tbody>
            <tr><th>Посетители</th>[+stat.host+]</tr>
            <tr><th>Просмотры</th>[+stat.hit+]</tr>
        </tbody>
    </table>
</div>


Второй шаблон, для построения графика (tpl.StatGraph).
<link type='text/css' rel='stylesheet' href='[(site_url)]inc/css/visualize.css' /> 
<script type='text/javascript' src='[(site_url)]inc/js/visualize.jquery.js'></script>
<script type='text/javascript'>
$(document).ready(function(){
    $('#chart').visualize({
        type: 'area',
        width: '570',
        height: '300'
    });   
});
</script>
<div style='margin: auto;'>
<table id='chart' style='display: none;'>
    <caption>Статистика посещений сайта за последние [+stat.days+] [+stat.days.text+]</caption>
        <thead>
            <tr><td></td>[+stat.date+]</tr>
        </thead>
        <tbody>
            <tr><th>Посетители</th>[+stat.host+]</tr>
            <tr><th>Просмотры</th>[+stat.hit+]</tr>
        </tbody>
    </table>
</div>


Как вы можете заметить, эти два шаблона отличаются наличием во втором подключения замечательного плагина jquery jQuery Visualize, который позволяет нам строить графики из таблиц. А также оформление этого графика.
Отрисовка графика запускается следующим кодом (подробнее про параметры jquery.visualize, а также примеры оформления — по ссылке выше):
$(document).ready(function(){
    $('#chart').visualize({
        type: 'area',
        width: '570',
        height: '300'
    });   
});

Уффф… Уже почти все.

Этап 3. Запуск!


Создаем новый документ modx и запускаем в нем сниппет (отдельно таблицу и график).
[!generateStatGraph?
&days=`20`
&tpl=`tpl.StatTable`
&act=`table`
&dateFormat=`%d <i>%b</i>`
!]

[!generateStatGraph?
&days=`20`
&daysText=`дней`
&tpl=`tpl.StatGraph`
&act=`graph`
&dateFormat=`%d`
!]


Хотелось бы отметить, что здесь мы рассмотрели только показ анонимной статистики посещений, а в БД у нас хранится информация об ip, времени посещений, браузерах юзеров и тд. Эту информацию вы можете просматривать или напрямую из БД, или написать простенький сниппет + чанк оформления для вывода на сайт, в защищенный раздел.

Приложение.


Кому любопытно — можно посмотреть результат в работе, заодно воочию зацените хабраэффект.
Кстати, видно некислое различие статистики, которую собираю я, и которую показывает liveinternet на счетчике внизу каждой страницы.

Параметры сниппета generateStatGraph:
&db
по умолчанию: `modx_visitors_log`
значение: [string]
описание: Имя существующей таблицы, можно вместе с БД (`modx`.`modx_log`).

&dateFormat
по умолчанию: '%d %b %Y %H:%M'
значение: переменные strftime()
описание: Формат даты.

&days
по умолчанию: 14
значение: [int]
описание: Количество дней выборки посещений от сейчас.

&$daysText
по умолчанию: 'дней'
значение: [string]
описание: Текст подписи для кол-ва дней выборки, не обязательно, используется только в шаблонах.

&tpl
по умолчанию: 'tpl.StatGraph'
значение: Имя существующего шаблона modx
описание: Шаблон для вывода таблицы.

&act
по умолчанию: 'graph'
значение: graph, table
описание: Режим обработки таблицы при выводе. graph пропускает даты.

&int
по умолчанию: 2
значение: [int]
описание: Если &act=`graph`, &int указывает, каждую какую дату показывать. по умолчанию - каждую вторую.


Плэйсхолдеры сниппета eventsCalendar:
[+stat.days+]
Количество дней выборки
[+stat.days.text+]
Подпись к дням.
[+stat.date+]
Дата в заданном формате.
[+stat.host+]
Количество уникальных посещений за день (хосты).
[+stat.hit+]
Количество не уникальных посещений за день (хиты).


Ссылки на нужные сайты.


Багрепорты засылать на bezumkin@yandex.ru, или оставлять в этой теме.
Поделиться публикацией

Похожие публикации

Комментарии 40

    +5
    Для построения графиков советую еще посмотреть на — highcharts.com гибкая и удобная штука, без флеша
      0
      Красивая штука, очень. Но сложнее в использовании, чем jquery.visualize.

      Лично мне очень нравится, что оно работает прямо из таблицы, данные специально готовить не нужно.

      В общем то, пост скорее про логирование посещений в modx, а построение графика — это пример вывода. Тут каждый может себе еще что-то придумать.
      0
      А почему вы используете datetime вместо timestamp?
      $modx->db->query(«INSERT INTO $db (`login`, `ip`, `host`, `url`, `referer`,`browser`)
      VALUES ('$login','$ip','$host', '$url','$referer','$browser')»);
      попахивает sql injection
        0
        Это столбец называется `datetime`, а внутри — timestamp, смотрите запрос sql
        `datetime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,

        Вот такой я загадочный, да.

        Про инъекцию не понял, уточните. Все данные берутся из массива $_SERVER, как туда что-то инъектировать?
        Я не мега эксперт по безопасности, поэтому, если есть возможность такой атаки — проясните, плиз, я доработаю функцию.
          +2
          _SERVER['HTTP_USER_AGENT'], по-вашему, откуда берется? Аккурат из HTTP-запроса, который посылал пользователь. И он там в принципе может прислать что угодно — произвольную строку байтов, всё, кроме CR/LF.
            0
            Я его не использую. В комментарии же написано:
            $browser = $_SERVER['HTTP_USER_AGENT']; // Браузер юзера. Я использую класс browscap, а вы как хотите. Для примера сойдет и так.

            У меня информация о браузере берется вот таким образом:
                $br = new Browscap('/tmp');
                $browserInfo = @$browser->getBrowser();
                $browser = $browserInfo->Parent.' / '.$browserInfo->Platform;
            

              0
              Ну а реферер?
                0
                А реферер да, согласен. Возможность имеется.

                Нужно, пожалуй, использовать urlencode().
              0
              Зато _SERVER['HTTP_REFERER'] используете в прямом виде. А его пользователь точно так же может послать совершенно произвольный. В частности, никаких проблем отослать там ' (0x27) нет.
        –2
        Добавлю, что очень похожим образом подключается к ModX скрипт статистики slimstat.net/

        В обоих случаях хорошо бы придумать систему архивирования логов с разбивкой по месяцам хотя бы, потому как при средней посещаемости, скажем, 300 хостов\сутки и ~10 просмотров на хост, через год работы таблица раздувается до неудобных размеров и начинает тормозить, во всяком случае на обычном хостинге.
          0
          Я просто чищу кроном старые записи. Когда делал складывание squid`ом логов — делал по месяцам. Но там реально, очень много записей было.
          0
          Зачем используете varchar для хранения адреса? С ним работать неудобно — если будете подключать другие таблицы (геолокейшн, например) — всплывет проблема конвертирования адреса
            0
            Эм… не думал об этом.

            Если потребуется, можно разбить ip при выборке: $arr = explode('.', $ip);

            Или и вовсе прогнать изменить структуру БД и забить туда старые ip заново простым скриптом за несколько минут.

            Правда, пока ни разу не требовалось.
              0
              ip2long() и дело в шляпе.
            +3
            зачем всё это? google analitics не устраивает?
              +2
              Может пригодиться для интранет сервисов
                +1
                поддерживаю, как раз нужно было для интранет проекта. Теперь и писать не нужно.
                  0
                  А как же piwik.org/?
                  0
                  Ну, я же в самом начале сказал, что хочтелось сделать свою статистику.

                  Гугл — это прекрасно, но надо же иногда и без него обходиться?
                  +2
                  «Посетителями» вы, насколько я понимаю, называете уникальные IP?
                    0
                    Да. Все оформление задается в двух чанках — можете переименовать, как вам больше нравится.
                      +1
                      Просто есть некие соглашения и определения, с которыми большинство согласилось раз и навсегда — когда вы начинаете использовать слова не по назначению (примерно сродни тому, как девочки-из-бухгалтерии с важным видом называют системный блок «процессором») — вас в худшем случае могут понять неправильно, в лучшем — догадаются и поймут, но впечатление уже будет подпорчено.

                      В случае веб-аналитики определенным «законодателем» определений и понятия является WAA. Термин «посетители» они определяют хоть и не очень четко, но всё-таки не как «только IP».
                        0
                        Окей, не спорю.

                        Вы как часто работодателю читаете лекции и объясняете, чем отличается «уникальный ip» от «посетителя», а также «запрос страницы» от «просмотра»?

                        Просто любопытно.
                          0
                          Не работодателю, скорее, а клиентам. Я лично крайне редко, а вот поддержка — по несколько десятков раз в день отвечает на подобные вопросы.
                            0
                            Ну вот. Небольшая сделка с собственной грамотностью может избавить от таких заморочек.
                    +5
                    Вступление напомнило: «Я мать-одиночка и поэтому банкам не доверяю...»
                      0
                      эм, почему не допилить уже существующую систему регистрации посещений?

                      ЗЫ ее, кстати, всегда советуют отключать, сразу… ибо нагрузка на бд
                        0
                        А ее нету.
                        Она была в Etomite, а в ModX Evolution 1.0.4 есть только тырчик в настройках, который Предоставляет данные для плагина аналитики, например, флажок, определяющий, учитывать ли просмотры конкретного ресурса.

                        Видимо, это и есть работа события OnLogPageHit, которое использует плагин.
                        А самого плагина я не нашел.
                        Скажу честно, не сильно-то и искал, но все же.
                          +2
                          Подход, при котором программист все нужные шишки хочет набить самостоятельно, достоин уважения (лишь бы заказчикам такой код не отправлял).
                          Основные проблемы вам уже указали:
                          1. Разрастание таблицы с логом
                          2. Нагрузка на сервер при росте посещаемости будет расти нелинейно
                          3. Излишнее доверие к пользовательским данным может выйти боком
                          4. Вы считаете хосты, а не посетителей

                          Направление движения пока что могут быть таковы:
                          1. Партишенинг таблицы по дате (почитайте, например, здесь)
                          2. Буфер для инсертов (лучше сразу в мемкеше, но на первое время сгодится и файл)
                          3. Плейсхолдеры или mysql_real_escape_string должны стать привычкой
                          4. Куки с уникальными айди

                          … ну а там и решение использовать зарекомендовавшие себя сервисы типа Google Analytics не за горами.
                            0
                            Спасибо за советы, почитаю на досуге.

                            Пока могу только сказать, что моим требованиям данный плагин соответствует полностью.
                            В начале топика вроде не было заяв, что мой способ — лучший, так? Ну и на нем хорошо видно, как можно использовать плагины в modx, что тоже плюс.

                            Не зря же топик в блоге modx.

                              0
                              Согласен. Если график сверху — реальный, то нет смысла серьезно усложнять систему. Сделайте экранирование запросов и обдумайте, стоит ли сделать партиционирование (чтобы не приходилось этот лог чистить, как вы описывали в комментарии выше). Остальное — так, на будущее.
                                0
                                График реальный. Сайт переживает хаброэффект )

                                С ноября в базе ~20000 записей, весят 3,5мб. Нагрузка совсем небольшая, при моих посещениях.
                                Сайт служит в основном полигоном для обработки навыков + домашняя страничка себе и друзьям.

                                Но на будущее я сегодня многое узнал, особенно от вас, спасибо!

                            +1
                            wiki.modxcms.com/index.php/API:getUserData

                            Вместо вашего $_SERVER[] можно кой-что взять оттуда, так «правильнее»
                            Очень радует MODx на главной
                              0
                              Парсер съел ссылку, извините http://wiki.modxcms.com/index.php/API:getUserData
                                0
                                Кстати да, забыл про эту полезную функцию. Спасибо за подсказку!

                                Ip, браузер и ОС можно брать оттуда, надо только посмотреть что будет выдавать.

                                Потому что, например, при авторизации через weblogin юзеру в сессию пишется браузер и ip, а без авторизации — нет. Надо посмотреть, всегда ли getUserData корректно данные выдает, может, тоже от чего зависит.

                                Ну и опять же, если брать из $_SERVER — будет полюбому быстрее.
                                –2
                                Да, видимо под новый год модераторы все отдыхают, раз такой быдлокод пропускают в мой RSS ридер.

                                > $referer = urlencode($_SERVER['HTTP_REFERER']);
                                > $host = `nslookup "$ip" | grep 'name =' | awk '{print $4}'`;
                                > 10 2 * * * * user wget localhost/secret_page.html (крон завалит вас почтой)
                                > $login = $_SESSION['webShortname']; (лог ошибок растет со скоростью 1 Мб в день)
                                > $date2 .= ''.strftime($dateFormat, strtotime($v)).''; (ага, шаблоны не нужны)

                                Еще заметил, что в ModX видимо до сих пор принято вставлять переменные прямиком в SQL запросы. Ребята, этот код — пример, как делать *не надо*. Даже не думайте им пользоваться. Мне даже сказать больше нечего. Ну и INSERT делать на каждого визитора, это конечно, сильно.
                                  0
                                  Простите что вклиниваюсь, читал тут перед сном размеренно комментарии к статьям но споткнулся об это:
                                  > Ну и INSERT делать на каждого визитора, это конечно, сильно
                                  Ммм, я не понял что вы имеете в виду. Предлагаете сначала писать посещения в файл например а потом кумулятивно по крону делать обновление в БД? Или как, мне правда интересно, я не хитрю =)
                                    0
                                    1) В примере топикстартера проще всего по крону парсить логи веб-сервера — он сам туда сложит все, что надо, правда имени залогиненного юзера там не будет.
                                    2) Да, можно в какой-нибудь кеш сваливать (что диск-то напрягать зря) данные, а потом по крону пачками их вынимать, группировать и вставлять 1 транзакцией.
                                    3) Можно поставить внешнюю статистику.

                                    Конечно, при 30000 запросов в день инсерт БД не сломает, но все равно, это решение плохое, для применения на более-менее средних серверах.
                                      0
                                      1. Можно. Но мне приятно видеть все в реальном времени, а не обновления раз в час.
                                      2. Ну я даже не знаю, зачем жалеть HDD виртуального хостинга. Да и вообще, при генерации страницы делается куча разных запросов, одним больше, одним меньше — кому какая разница?
                                      3. Можно. Liveinternet хотя бы есть. Цифры от моих отличаются. Опять же, имя юзера оно не соберет.

                                      Вы думаете, у всех пользователей modx больше 30к посещений в день? К сожалению — нифига.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое