Счетчики посещаемости веб-страниц

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

Решение
Реализуйте счетчик нажатий (hit counter) для интересующей вас страницы.

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

Есть несколько способов создания страницы, отображающей счетчик ее просмотров. Самый простой – это хранение счетчика в файле.


При запросе страницы вы открываете файл, считываете счетчик, увеличиваете его, записываете новое значение в файл и выводите на странице. Преимущество данного способа в простоте реализации, а недостаток в том, что для каждой страницы со счетчиком нужен соответствующий файл. Кроме того, он некорректно работает, когда два клиента одновременно запрашивают страницу, если только вы не используете какую-либо блокировку в процедуре доступа к файлу. Можно несколько уменьшить количество файлов, храня несколько счетчиков в одном файле, но тогда будет сложно обращаться к отдельным
значениям внутри файла, а проблема одновременного доступа все равно не будет решена. На самом деле, будет только хуже, так как одновременное обращение нескольких клиентов к файлу, содержащему несколько счетчиков, вероятнее, чем к файлу с одним счетчиком. Так что вам придется реализовывать методы хранения и извлечения для обработки содержимого файла и протоколы установки блокировок, чтобы не допустить конфликта нескольких процессов. Но ведь обо всем этом может позаботиться MySQL! Хранениесчетчиков в базе данных позволяет сосредоточить их в одной таблице, SQL обеспечивает интерфейс для хранения и извлечения, а вопрос с блокировками отпадает сам собой, так как MySQL предоставляет последовательный доступ к таблице, поэтому клиенты не могут мешать друг другу.


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

Для этого создайте таблицу, содержащую по строке для каждой обрабатываемой страницы. То есть каждая страница должна иметь уникальный идентификатор, чтобы не перепутать соответствующие счетчики. Идентификаторы можно присваивать любым способом, но проще всего использовать путь страницы в вашем дереве документов. Языки веб-программирования обычно обеспечивают простой доступ к этому пути. Создаем таблицу hitcount:

CREATE TABLE hitcount
(
path VARCHAR(255) BINARY NOT NULL,
hits BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (path)
);

При определении таблицы сделано несколько допущений:

• Ключевое слово BINARY в определении столбца path делает значения столбца чувствительными к регистру. Это целесообразно для веб-платформы, путевые имена которой чувствительны к регистру, как многие версии UNIX. Для Windows или файловой системы HFS+ под управлением Mac OS X файловые имена не чувствительны к регистру, поэтому можно убрать BINARY из определения.

• Столбец path имеет максимальную длину в 255 символов, то есть вы не можете задавать более длинные пути страниц.


Если вы полагаете, что понадобятся более длинные значения, используйте тип BLOB или TEXT вместо VARCHAR. Но в этом случае индексирование все равно будет возможно только по первым 255 символам, поэтому лучше применить неуникальный индекс, а не PRIMARY KEY.

• Механизм работает с одним деревом документов, то есть для случаев, когда ваш веб-сервер используется для обслуживания страниц одного домена. Если вы вводите счетчики посещения на хосте, обслуживающем несколько виртуальных доменов, то можете добавить столбец с именем домена.

Это значение доступно как значение SERVER_NAME, помещаемое Apache в окружение сценария. Тогда индекс таблицы счетчиков посещаемости будет включать и имя хоста, и путь к странице.

Общая идея, лежащая в основе ведения счетчиков посещаемости, заключается в приращении поля hits в записи страницы и извлечении обновленного значения. Это можно сделать при помощи двух таких запросов:UPDATE hitcount SET hits = hits + 1 WHERE path = 'путь к странице';

SELECT hits FROM hitcount WHERE path = 'путь к странице';

К сожалению, значение, полученное таким способом, может быть некорректным. Если несколько клиентов запрашивают одну страницу одновременно, то несколько предложений UPDATE может быть создано в очень близкие моменты времени. Поэтому предложения SELECT совсем не обязательно извлекут соответствующее значение hits. Чтобы избежать ошибки, можно использовать транзакцию или заблокировать таблицу hitcount, но это замедлит работу. MySQL предлагает решение, позволяющее каждому клиенту извлекать собственный счетчик вне зависимости от того, сколько обновлений производится одновременно:

UPDATE hitcount SET hits = LAST_INSERT_ID(hits+1) WHERE path = 'путь к странице';
SELECT LAST_INSERT_ID();

Для обновления счетчика применяется функция LAST_INSERT_ID(выражение), о которой мы говорили в рецепте 11.6. Предложение UPDATE находит нужную запись и увеличивает для нее значение счетчика. Использование LAST_INSERT_ ID(hits+1) вместо просто hits+1 указывает MySQL на то, что необходимо интерпретировать значение так, как если бы оно относилось к типу AUTO_INCREMENT.

Поэтому в следующем запросе его можно извлечь при помощи
LAST_INSERT_ID(). Функция LAST_INSERT_ID() возвращает значение, специфичное для соединения, поэтому вы всегда получаете значение, соответствующее обновлению, произведенному в рамках того же соединения. Кроме того, предложению SELECT не нужно обращаться к таблице, поэтому оно работает очень быстро. Можно еще повысить производительность, вообще избавившись от запроса SELECT, что возможно, если ваш API предлагает средство прямого доступа к последнему номеру последовательности. Например, в Perl можно обновить счетчик и получить новое значение в одном запросе:

$dbh->do (
"UPDATE hitcount SET hits = LAST_INSERT_ID(hits+1) WHERE path = ?",
undef, $page_path);
$hits = $dbh->{mysql_insertid};

Однако одна проблема все еще не решена. Что, если страница не входит в таблицу hitcount? Тогда предложение UPDATE не находит записи для изменения, и вы получаете для счетчика значение ноль. Можно потребовать, чтобы каждая страница, содержащая счетчик посещаемости, была зарегистрирована в таблице hitcount перед ее публикацией в Интернете. Более удобным способом является автоматическое создание записи для каждой страницы, которая еще не содержится в таблице. Тогда создатели страниц смогут помещать на них счетчики без предварительной подготовки. Чтобы еще более упростить использование счетчиков, поместим код в функцию, которая принимает в качестве аргумента путь к странице, обрабатывает внутри себя отсутствие за писи о странице и возвращает счетчик. Принцип действия функции таков:

обновление счетчика
если обновление изменяет строкуто извлечь новое значение счетчика
иначе
вставить запись для страницы, установив счетчик в 1

Когда вы впервые запрашиваете счетчик для страницы, операция обновления не изменяет строки, так как информации о странице в таблице еще нет. Функция создает новый счетчик и возвращает значение 1. Для каждого последующего запроса обновление изменяет существующую запись, и функция возвращает соответствующие увеличивающиеся счетчики.

В Perl функция для счетчика посещаемости могла бы выглядеть так (аргументами являются дескриптор базы данных и путь к странице):

sub get_hit_count
{
my ($dbh, $page_path) = @_;
my $rows = $dbh->do (
"UPDATE hitcount SET hits = LAST_INSERT_ID(hits+1) WHERE path = ?",
undef, $page_path);
return ($dbh->{mysql_insertid}) if $rows > 0; # счетчик увеличивается
# Если страница не была зарегистрирована в таблице, сделать это
# и установить счетчик в 1. Использовать IGNORE, если другой клиент
# пытается сделать то же самое в то же время.
$dbh->do ("INSERT IGNORE INTO hitcount (path,hits) VALUES(?,1)",
undef, $page_path);
return (1);
}

Функция CGI.pm script_name() возвращает локальную часть URL, поэтому можно использовать get_hit_count() так:

my $hits = get_hit_count ($dbh, script_name ());
print p ("This page has been accessed $hits times.");

Счетный механизм включает в себя несколько запросов, а транзакции мы не используем, поэтому в алгоритме остается еще одно место – первый запрос страницы, в котором возможны проблемы с конкуренцией. Если несколько клиентов одновременно запрашивают страницу, которая еще не содержится в таблице hitcount, каждый из них может создать запрос UPDATE, обнаружить, что страница отсутствует и, в результате, создать запрос INSERT для регистрации страницы и инициализации счетчика. Алгоритм использует INSERT IGNORE для устранения ошибок, если одновременные вызовы сценария пытаются инициализировать счетчик для одной и той же страницы, но в результате все они получают счетчик 1. Стоит ли попробовать исправить положение при помощи транзакций или блокировки таблицы? Для счетчика посещаемости я бы ответил: «Нет». Небольшая потеря точности не является поводом для дополнительных расходов на обработку. В каком-то другом приложении, где точность важнее эффективности, следует предпочесть транзакции, чтобы избежать потерь при подсчете.PHP-версия функции для счетчика посещаемости будет такой:

function get_hit_count ($conn_id, $page_path)
{
$query = sprintf ("UPDATE hitcount SET hits = LAST_INSERT_ID(hits+1)
WHERE path = %s", sql_quote ($page_path));
if (mysql_query ($query, $conn_id) && mysql_affected_rows ($conn_id) > 0)
return (mysql_insert_id ($conn_id));
# Если путь страницы не приведен в таблице, то зарегистрировать ее
# и установить счетчик в 1. Использовать IGNORE, если другой клиент
# пытается сделать то же самое в то же время.
$query = sprintf ("INSERT IGNORE INTO hitcount (path,hits)
VALUES(%s,1)", sql_quote ($page_path));
mysql_query ($query, $conn_id);
return (1);
}

Для использования счетчика вызовите функцию get_self_path(), возвращающую путевое имя сценария:

$self_path = get_self_path ();
$hits = get_hit_count ($conn_id, $self_path);
print ("

This page has been accessed $hits times.

\n");
В Python функция счетчика будет такой:
def get_hit_count (conn, page_path):
cursor = conn.cursor ()
cursor.execute ("""
UPDATE hitcount SET hits = LAST_INSERT_ID(hits+1)
WHERE path = %s
""", (page_path,))
if cursor.rowcount > 0: # счетчик увеличивается
count = cursor.insert_id ()
cursor.close ()
return (count)
# Если путь страницы не приведен в таблице, то зарегистрировать ее
# и установить счетчик в 1. Использовать IGNORE, если другой клиент
# пытается сделать то же самое в то же время.
cursor.execute ("""
INSERT IGNORE INTO hitcount (path,hits) VALUES(%s,1)
""", (page_path,))
cursor.close ()
return (1)

И используется она так:

self_path = os.environ["SCRIPT_NAME"]
count = get_hit_count (conn, self_path)
print "

This page has been accessed %d times.

" % count

Дистрибутив recipes содержит сценарии, демонстрирующие работы счетчиков посещаемости на Perl, PHP и Python в каталоге apache. JSP-версия хранится в каталоге tomcat. Установите любые из них в вашем дереве веб-документов, вызовите несколько раз и посмотрите, как увеличивается счетчик.

Оцените статью: (0 голосов)
0 5 0

Статьи из раздела MySQL на эту тему:
Ведение журнала Apache с помощью MySQL
Выполнение поиска и получение результатов
Журнал доступа к веб-странице
Загрузка в форму записи базы данных
Использование ввода через Web для формирования запросов