Сергей Келер
Журнал "Мир Internet"
#11 (38) ноябрь 1999
В практической работе web-мастера сегодня все чаще встает задача организации процесса голосования по какому-либо актуальному вопросу, и это постепенно становится таким же стандартом и признаком хорошего тона, как наличие на сайте гостевой книги, чата или web-форума. Конечно, если вы новичок, или у вас нет времени, или для вас не очень критичны детальная статистика опроса или эксклюзивность дизайна, теперь можно воспользоваться стандартным сервисом www.voting.ru (так же как и www.guestbook.ru для организации гостевой книги), но в некоторых случаях (особенно для организации достаточно валидных политических анкетирований) необходимо бывает более детально углубиться в теорию и практику организации процесса голосования именно на своем сервере.
Формулировка задачи и возможные инструменты для ее решения
Сегодня на многих сайтах можно встретить формы голосования. Тематика у них, как правило, самая разнообразная: от секса до политики, от устойчивости операционных систем персональных компьютеров до рейтинга музыкальных альбомов. Книжные магазины спрашивают мнение о книгах, стоматологические сайты - об отношении к "суперновой" зубной пасте... Все эти задачи в простейшем случае сводятся к одной:
Есть ВОПРОС.
Есть несколько вариантов ОТВЕТОВ.
Посетитель должен выбрать правильный с его точки зрения ответ (при этом он имеет право узнать, какова полная картина голосования в данный момент).
На самом деле, если бы все было так просто, существовал бы один или несколько простых скриптов (вроде раздаваемых на www.script.ru), которые можно было бы брать за основу и переделывать под конкретные нужды. Но при внимательном рассмотрении оказывается, что здесь существует еще масса тонких моментов и подводных камней. Поэтому я полагаю, что универсального скрипта подобного рода просто не существует, и лучше всего углубляться в процесс написания системы голосования, начиная с простых примеров, шаг за шагом отслеживая возможные возникающие проблемы и при этом четко понимая, как можно научиться их обходить.
Итак, если с исходной формулировкой задачи все более или менее понятно, то с методом ее решения нужно определиться. В принципе, вариантов использования голосования на сайте не так много. Я постараюсь осветить варианты реализации некоторых из них.
Разделим для начала задачу на два класса: "простой счетчик" и "счетчик со статистикой".
В качестве инструментов я предлагаю выбрать PHP3 (www.php3.net) в качестве on-line-препроцессора и Perl5 (www.perl.com) для off-line-обработки.
Язык PHP3 уже достаточно распространен среди хостинг-провайдеров (webmaster.comset.net) как в России, так и за рубежом. Он был создан специально для написания скриптов, исполняемых на стороне сервера. Для сервера Apache он работает в виде дополнительного модуля, обеспечивая высокую скорость работы и удобство программирования.
Программа на PHP представляет собой файл на языке HTML с включенными в него тегами PHP, которые выглядят следующим образом:
команды ?>
Таким образом, вы можете легко подготовить весь дизайн страницы в любимом редакторе HTML, а затем вставить в него необходимые команды PHP.
Простой пример программы голосования
С чего начинается система опроса? Правильно, с формы. Итак:
Поместим эти строки в наш файл vote.html. Обратите внимание на то, что суффикс у обработчика формы .phtml, а не .html или .cgi. Так обычно обозначают скрипт для PHP. Вам не требуется делать этот файл исполняемым, так как его выполняет сам сервер. И, соответственно, вам не требуется выкладывать его в отдельный каталог cgi-bin.
Поставим задачу как "Сделать счетчик для каждого варианта ответа".
В простейшем случае файл stat.phtml должен содержать следующие строки:
$errmsg="; // Изначально ошибок нет.
$fp=fopen('vote.dat','w+'); //Откроем файл для записи,
но не очищая его.
if ($fp) { // Если не открыть, сообщим об ошибке.
// Прочитаем строку, уберем в конце ее лишний '\n' и
// разделим ее по символу "\t" (табуляция) в массив.
$votes=split("\t",chop(fgets($fp,80)));
$votes[$vote]++; // Увеличим на 1 наш голос.
rewind($fp); // Отмотаем файл в начало.
// Запишем в него массив счетчиков, склеив элементы через
знак табуляции.
fputs($fp,join("\t",$votes));
fclose($fp); // Закроем файл.
} else $errmsg.=' Не открыть файл голосования';
?>
После исполнения этого кода массив $votes будет содержать результаты голосования (счетчики), а строка $errmsg - сообщение об ошибке (или будет пустой, если ошибок не было). Так как $vote - это значение переменной vote из формы файла vote.html (в таком виде программа на PHP получает значения полей вызывающей формы) и оно может быть равным 0 или 1 (как следует из атрибута value тегов input формы), то его удобно использовать в качестве индекса в массиве счетчиков.
Файл vote.dat должен быть доступен на запись пользователю, от имени которого работает www-server. Этот вопрос следует уточнить у системного администратора вашего сервера. Обычно достаточно сделать его доступным для записи (группе) командой ftp
chmod 660
Можно вывести результаты голосований в виде таблицы (эти строки можно записать тоже в stаt.phtml):
// Запишем в виде массива ответы.
$names=array('мэр Лужков','мэр Петушков');
?>
Кандидат
Голосов
for ($i=0; $i
счетчикам ?>
echo $names[$i] ?>
echo $votes[$i] ?>
endfor // Конец цикла ?>
От простого к сложному
Вот и все? А вот и нет... У такой программы масса недостатков:
1. Ее очень неудобно администрировать. То есть чтобы сменить вопрос или ответ, придется изменить 2 файла и не забыть про файл счетчиков. Это не всегда просто и всегда не быстро...
2. Переменная $vote получает значение как параметр value поля input формы. Нехороший человек может изменить форму, скачав ее на свой компьютер и выполнив ее оттуда с новым значением поля, например 5. Тогда в статистике будет выводиться не 2, а 6 строк. Также очень интересное значение будет - 1.
3. Нехорошему человеку ничто не мешает нажать кнопку "Go!" не один, а, скажем, сто раз. Более того, совсем плохой человек даже нажимать кнопку не будет, ибо достаточно иcпользовать весьма простую программу для отправки формы сколько угодно раз... Обсуждение подобных продуктов-накрутчиков не входит в рамки данной статьи, а вот защита от этого нас весьма интересует.
Эх, усложнять так усложнять. Убьем сначала первого зайца. Давайте заведем отдельный файл "конфигурации" опроса и переименуем vote.html в vote.phtml. Вот пример такого настроечного файла (назовем его vote.cfg):
# Файл счетчиков
vote.dat
# Вопрос
Кто будет президентом?
# Ответы
мэр Лужков
мэр Петушков
Будем игнорировать пустые строки и строки, начинающиеся с символа #.
Первая строка - имя файла, вторая - вопрос, а следующие - ответы.
Теперь, если написать "умный" обработчик vote.cfg, можно легко менять анкету на странице опроса, не меняя скриптов. Доверить изменения настроечного файла можно даже неспециалисту (см. врезку).
Таким образом файл vote.phtml, содержащий форму, будет выглядеть так:
$errmsg=";
// Загрузка конфигурации
$fp=fopen('vote.cfg','r');
if ($fp) {
// Читать файл, пропуская комментарии и пустые строки.
while ($line=chop(fgetss($fp,200))) if (!ereg('^( *)|(
*#.*)$',$line)) break;
$cntFile=$line;
while ($line=chop(fgetss($fp,200))) if (!ereg('^( *)|(
*#.*)$',$line)) break;
$cntQuestion=$line;
$cntName=array();
while ($line=chop(fgetss($fp,200))) {
if (ereg('^( *)|( *#.*)$',$line)) continue;
$cntName[]=$line;
}
fclose($fp);
} else $errmsg.=' Не открыть файл конфигурации голосования.';
?>
...
...
Как видите, наша форма совсем не зависит от количества и содержания данных.
Код загрузки файла конфигурации можно вынести в отдельный файл, скажем vote.cfg.inc, и включать его командой include:
include('vote.cfg.inc') ?>
Теперь файл stat.phtml будет выглядеть так:
include('vote.cfg.inc');
$errmsg=";
if ($vote>=0 && $vote
$fp=fopen('vote.dat','w+');
if ($fp) {
$votes=split("\t",chop(fgets($fp,80)));
$votes[$vote]++;
rewind($fp);
fputs($fp,join("\t",$votes));
fclose($fp);
} else $errmsg.=' Не открыть файл голосования';
} // else это попытка хака.
?>
Выводим результаты голосований в виде таблицы:
Кандидат
Голосов
for ($i=0; $i
счетчикам ?>
echo $cntName[$i] ?>
echo $votes[$i] ?>
endfor // Конец цикла ?>
Итак, мы устранили недостатки 1 и 2 первоначального варианта. Решение же задачи 3 требует более сложных действий. Напомню, речь идет о "накрутках", когда нехороший человек пытается проголосовать не один, а много раз. К сожалению, полностью избежать накруток очень тяжело. В общем случае задача состоит в том, чтобы не допустить повторное голосование. Для этого надо как-то идентифицировать посетителя. Для поиска новых механизмов идентификации вам пригодится полезная функция phpinfo(), которая выводит всю доступную информацию о посетителе (и многое другое) на экран. Что нам может пригодиться?
1. Cookies
Можно в начале файла stat.phtml (до вывода первого символа) проверить наличие переменной, например, if (isset ($reload)). Если она определена - значит, отказать в голосовании, если нет, то использовать функцию setcookie ('reload','yes') и разрешить голосование. Недостаток такого метода очевиден: накрутчик может отключить куки. Но такой метод спасает от случайных обновлений страницы stat.phtml.
2. REMOTE_ADDR
Это ip-адрес посетителя. Можно его запомнить в хэш-файле. Вот так:
$MIN_TIME=30*60; // Минимальное время для разрешения
повтора (30 мин.)
$voteOK=0; // По умолчанию нельзя.
$db=dbmopen('vote','w');
if ($db) {
if (dbminsert($db,$REMOTE_ADDR,time())==0) { // Новый
посетитель
$voteOK=1;
} else { // Посетитель уже был
// Если он был давно, то можно...
$time=dbmfetch($db,$REMOTE_ADDR);
if (time()-$time>$MIN_TIME) {
// Разрешая, надо обновить время последнего доступа.
dbmreplace($db,$REMOTE_ADDR,time());
$voteOK=1;
}
}
dbmclose($db);
}
?>
Далее в программе можно пользоваться логической переменной $voteOK, которая показывает, можно ли посетителю менять счетчик или нет. У этого метода, конечно, есть свои достоинства и недостатки. К достоинствам можно отнести его достаточную параноидальность. Она же и главный недостаток. Из-за применения этого метода, например, лукавит счетчик "Рамблера". Все дело в том, что $REMOTE_ADDR - это и ip-адрес прокси-сервера, если посетитель идет через такой сервер, и ip-адрес моста, если посетитель заходит из локальной сети своей фирмы. Таким образом, в СПб может оказаться всего сотня-другая хостов, что несколько не соответствует действительности. Пользователи, выходящие в Интернет с помощью модема, как правило, получают ip-адрес динамически, и следующий посетитель вашей страницы может быть с тем же ip, но это будет другой компьютер. Можно использовать $MIN_TIME, как в моем примере, то есть через этот интервал времени ip считается не использованным. Получаса обычно хватает. Вы можете использовать более сложный ключ, включая такие переменные запроса, как HTTP_X_FORWARDED_FOR, REMOTE_PORT, HTTP_USER_AGENT, но, в отличие от REMOTE_ADDR, первые две из них легко подделываются злоумышленниками, а REMOTE_PORT может быть у каждого запроса новый.
Таким образом, с применением интернет-технологий в опросах (показах баннеров, счетчиках и т. п.) не избежать искажения результатов статистики. В ваших руках как разработчика опроса есть только возможность достичь максимального приближения к идеалу. Это поле деятельности для искусственного интеллекта. Экспериментируйте, делитесь со мной результатами...
Допустим вариант с off-line-обработкой статистики. Программа stat.phtml может просто записывать результат голосования и данные о пользователе в последовательный файл, в конец этого файла-журнала. Информацию о результате голосования можно брать из файла результатов. Некая же отдельная процедура, написанная на Perl, будет запускаться раз в полчаса, анализировать такой журнал и записывать результат в файл счетчиков. Если вы предполагаете, что по вашей анкете будут голосовать как минимум раз в пару минут, то лучше пойти по такому пути:
$fp=fopen('vote.log','a');
$time=time();
fputs($fp,"$vote\t$time\t$REMOTE_ADDR\t$HTTP_USER_AGENT\t$
HTTP_X_FORWARDED_FOR\n");
fclose($fp);
$fp=fopen('vote.dat','r');
//Открываем только на чтение!
$votes=split("\t",chop(fgets($fp,80)));
?>
Такая программа будет отрабатывать максимально быстро. А обработчик журнала не будет сильно нагружать компьютер, так как будет выполняться весьма редко по компьютерным понятиям.
#!/usr/local/bin/perl -w
#
# Простой анализатор журнала.
#
use strict;
# Читаем журнал
open LOG,'
my @votes=();
while () {
chomp;
my ($vote,$time,$addr,$agent,$proxy)=split/\t/;
$votes[$vote]++;
}
close LOG;
# Записываем счетчики.
open CNT,'>vote.dat' or die;
print CNT join "\t",@votes;
close CNT;
exit;
В принципе, off-line-обработчик может не просто записывать данные в файл, а генерировать сам html-файл статистики. Так можно поступать на очень загруженных системах для минимизации нагрузки на процессор сервера. Конечно, надо бы читать и файл vote.cfg, проверять поля $vote и т. д...
Примеры статистической обработки результатов голосования
Как видите, во время выполнения программа получает массу полезной информации о пользователе. Да и анкета может не ограничиваться одним вопросом. Как только условия работы программы начинают выходить за рамки поставленной в начале статьи задачи в сторону усложнения статистики, on-line-обработка результатов сразу начинает отходить на второй план.
В общей задаче анкетирования присутствуют несколько вариантов ответов на вопросы, не предусмотренные и просто текстовые ответы, статистика данных о посетителе. Еще, в принципе, можно отслеживать изменение статистики во времени в случае продолжительных интернет-тестов. И вряд ли для этого стоит заводить базу данных SQL для хранения данных анкеты - даже для обработки десятков тысяч анкет достаточно и простого текстового файла, как в расмотренном нами примере.
В этой статье нет возможности рассмотреть даже малую часть всех вариантов статистической обработки. Самое простое, что приходит на ум, - это динамика ответов (за равные промежутки времени) и распределение респондентов по географии.
Коротко рассмотрим оба случая, а файл журнала оставим от предыдущего примера.
А. Динамика ответов
#!/usr/local/bin/perl -w
#
# Простой анализатор журнала со статистикой по времени.
#
# Статистика по дням.
use strict;
# Читаем журнал
open LOG,'
my @slices=();
my $slice=-1; # Номер среза по времени.
my $last=0;
my @stamp=();
while () {
chomp;
my ($vote,$time,$addr,$agent,$proxy)=split/\t/;
my $day=floor($time/(24*60*60))*(24*60*60); # Приводим
время к началу дня.
$slice++ if $last!=$day; # Следующий день наступил.
$slices[$slice][$vote]++;
$stamp[$slice]=$day;
}
close LOG;
# Записываем счетчики.
open CNT,'>vote.dat' or die;
my $i;
for ($i=$#slices; $i; $i-) { # По всем дням в обратном порядке.
print CNT $stamp[$i],"\t"; # День
print CNT join "\t",@slices[$i]; # Счетчики
print CNT "\n";
}
close CNT;
exit;
В результате выполнения это программы файл vote.dat будет содержать строки следующего вида:
ВРЕМЯсчетчик1счетчик2...
По этим данным можно построить красивый график изменения показаний счетчиков по времени, что может характеризовать, в частности, результаты рекламных кампаний персон из нашего самого первого примера.
Ясно, что после прокрутки по телевидению удачного клипа количество голосов за данного кандидата должно увеличиться, - особенно если рекламный ролик будет каждые десять минут прерывать модный боевик. И будет особенно радостно это увидеть наглядно!
Б. Статистика по географии
О географическом положении посетителя можно узнать как по его ip-адресу, так и прямо задав вопрос в анкете. Не будучи социологом по образованию, не буду углубляться в дебри вопроса "Можно ли доверять ответу пользователя?", но и ip-адресу тоже особенно доверять не стоит. Несмотря на это некую оценку географического положения респондентов получить вполне возможно.
По ip-адресу можно получить информацию из двух источников. Во-первых, это доменное имя. Во-вторых, это данные о регистрации сети. Рассмотрим подробнее оба источника.
Доменное имя мы можем получить из переменной $REMOTE_HOST, если web-сервер успел его определить, или командой
nslookup
в противном случае.
Однако ни для кого уже не секрет, что сервер с именем qq.spb.ru может находиться в США, а www.zzz.com - в Петербурге. Так что анализ самого имени нам ничего не даст. А вот регистрирующий орган (INTERNIC, RIPN, etc) нам, в первую очередь, поведает о географическом положении владельца домена. Получить эту информацию можно как воспользовавшись командой
whois домен.ru@whois.ripn.net
так и подключившись к серверу whois, вводя запросы последовательно, что нас как раз устроит в поставленной задаче. Все бы хорошо, но не на все домены имеется информация. Пример - тот же домен spb.ru.
Тогда можно определить сеть посетителя. Запишем ip-арес в виде aaa.bbb.ccc.0 и запросим также по протоколу whois орган, регистрирующий сети:
whois 194.105.206.0@whois.ripe.net
В зависимости от сервера результат вывода может иметь иной формат, но программу можно настроить на все такие серверы. Сервер whois.ripe.net, в частности, предоставляет поля address и country в выводной информации, характеризующей географическое положение владельца ip-сети.
К сожалению, и этот метод не дает стопроцентной гарантии. Вы можете обнаружить большую сеть, подсети которой находятся в различных, весьма удаленных друг от друга, регионах. Что ж, если вам требуется совсем точный результат, то автоматизация тут будет только помощником, решающим большую долю задач, а спорные вопросы придется выяснять вручную.
Я не буду приводить пример обработки журнала с анализом географии респондентов. Это уже большая и серьезная программа, напишите ее сами - это хорошая гимнастика для ума...
Визуализация результатов обработки статистики голосования
Итак, мы получили чудесную статистику по нашей анкете. Как ее вывести?
Самый простой вариант - это таблица из строк вида
Ответ - Счетчик
Именно такой вывод осуществлен в нашем примере, рассмотренном выше.
Однако хочется иметь более красивый вид страниц. Особенно в этом вас будет убеждать дорогой дизайнер, осуществляющий разработку внешнего вида проекта. Давайте его послушаем... Итак, что мы можем выжать из таблицы? На таблицах языка HTML мы можем построить красивую столбцовую диаграмму. Даже трехмерную. Ведь в наших руках такой умный инструмент, как PHP.
Построим диаграмму по последнему примеру с изменением счетчиков во времени. Пусть по горизонтали изменяется время, а по вертикали - величина счетчика. Для каждого варианта построим отдельный график:
include('vote.cfg.inc');
$base=80; // Максимальная высота полоски.
$fp=fopen('vote.dat','r');
$votes=array(); $max=0;
$time=floor(time()/(24*60*60))*(24*60*60); // Начало сегодня.
for ($i=0; $line=chop(fgets($fp)); $i++) {
$tmp=split("\t",$line);
// Индекс там расчитывается хитро, поскольку может быть день,
когда не было голосования.
$ind=floor(($tmp[0]-$time)/(24*60*60)); // Кол-во дней от
сегодня.
$votes[$ind]=$tmp;
// Вычисляет максимальное значение счетчика. Заносит в $max.
getMaxVote($tmp);
}
// $votes содержит все голосования.
?>
...
...
В принципе, можно нарисовать графики отдельно, во время off-line-обработки - в виде изображений в формате GIF, используя такие пакеты, как ImageMagic или GD, имеющие интерфейс к Perl. PHP (с подключеными модулями графики) может тоже рисовать графики, и даже весьма изощренные, но это не задача для on-line-препроцессора. Мы, например, даже не включили в on-line-версию PHP поддержку графических библиотек. Хотя удобство PHP побудило меня собрать отдельно off-line-интерпретатор PHP, работающий из командной строки. Вот в него-то и включены все возможные функции.
Думаю, в этой статье я достаточно полно описал вопрос анкетирования на web-страницах. Как обычно бывает в таких случаях, начав с простой задачи, мы подошли к более общей ее постановке. Надеюсь, что мои рассуждения немного помогли разобраться в построении систем голосования не только авторам домашних страничек, но и достаточно профессиональным разработчикам...