Xpoint
   [напомнить пароль]

Слежение за контентом на динамических сайтах

Оглавление

Предыстория

Эта статья была задумана и написана благодаря форуму XPoint.

Методы, описанные ниже, не являются личным изобретением автора. Просто он, будучи совсем еще чайником, лично сталкивался с описываемой проблемой и не смог найти толкового материала на предмет ее решения. Решение было найдено путем прочтения десятка статей сколь-нибудь близкой тематики, мученья людей с форума XPoint и недельной ночной баталии с PHP.

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

Внимание: не факт, что Вам все удастся сделать с первого раза за полчаса. Факт, что прежде чем что-то делать, никогда не лишне почитать о том, как это делали другие.

О чем эта статья

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

  1. «правильное» кэширование;
  2. «правильные» HTTP заголовки.

Поясняю первый пункт. Кэширование — механизм, позволяющий клиенту (то есть пользователю на том конце соединения, точнее — его браузеру, для пользователя этот процесс незаметен) при просмотре одних и тех же файлов (например, картинок, составляющих элементы дизайна сайта, или файлов стилей CSS) не скачивать их каждый раз заново, а скачивать только однажды, а затем, по мере необходимости, использовать сохраненную на компьютере клиента копию. Принцип его работы примерно таков: при возникновении очередной необходимости скачать файл клиент обращается к серверу с запросом, не был ли файл изменен (не устарела ли копия на машине клиента), и если нет, не скачивает его заново, а использует сохраненный вариант. В браузерах, точно придерживающихся спецификации протокола HTTP, можно также добиться того, что запросы вообще не будут посылаться каждый раз, если есть сохраненная копия файла и точно известно, что она не успела устареть. Прелесть в том, что трафик (объем скачанных из Интернет данных) клиента уменьшается, и пользователь видит, что «сайт работает быстро». Естественно, уменьшается и трафик сервера, что позволяет снизить нагрузку на сервер и даже сэкономить, если Вы пользуетесь платным хостингом.

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

Поясню теперь пункт второй. Когда клиент запрашивает у сервера файл по протоколу HTTP, он, кроме содержимого самого файла (код в случае HTML файла, текст в случае текстового файла и т.п.), получает также HTTP headers — заголовки HTTP. Это служебные текстовые поля с информацией, которая не отображается в браузере, но интерпретируется им и, в основном, служит для сообщения клиенту данных о запрашиваемой странице.

Заголовок ETag («объектная метка»), например, служит для присвоения каждой странице уникального идентификатора, который остается неизменным, пока страница не модифицирована, и изменяется, если изменились данные на странице. Этот заголовок сохраняется на клиенте, и в случае необходимости повторного скачивания «меченой» страницы позволяет браузеру обращаться к серверу с запросом 'If-None-Match' — в таком случае сервер должен по значению ETag-метки сохраненной на клиенте копии определить, не устарела ли она, и если нет, ответить кодом '304 Not Modified' («не модифицировано»), и страница не будет скачена еще раз.

Заголовок Last-Modified («последнее изменение») предназначен для того, чтобы сообщить клиенту дату и время, когда последний раз изменилась запрашиваемая страница. Используя его, клиент, подобно случаю с ETag, может обращаться к серверу с запросом 'If-Modified-Since' — в этом случае сервер должен сравнить дату последней модификации копии, сохраненной на клиенте, с актуальной датой последней модификации. Если они совпадут, это значит, что копия в кэше клиента не устарела, и повторное скачивание не нужно (код ответа '304 Not Modified'). Last-Modified также необходим для корректной обработки Вашего сайта роботами - спайдерами (спайдер, англ. «паук» — это робот, который ходит по паутине Интернета и индексирует сайты, чтобы их можно было найти через поисковые системы, например, Google), которые используют информацию о дате модификации страниц в целях сортировки результатов поиска по дате, а также для определения частоты обновляемости Вашего сайта (см. например, что об этом пишет Яndex).

Какой из этих методов определения «свежести» интернет-страниц использует клиент (и использует ли он их вообще), зависит от его возможностей и настроек. По хорошему, надо отправлять оба этих заголовка с каждым файлом, отданным Вашим сервером.

Есть еще заголовок Expires («истечение») — он сообщает браузеру, какой временной промежуток можно считать, что копия страницы в кэше свежа, и вообще не обращаться к серверу с запросами. Это удобно для таких файлов, о которых вы точно знаете, что они не изменятся ближайший час/день/месяц: фоновая картинка страницы, например. К сожалению, поддерживается не всеми браузерами. В рассматриваемом примере Expires будет равен десяти минутам, что подходит для большинства сайтов, на которых информация обновяется не слишком часто (~ раз в час).

Чтобы реализовать отправку «правильных» HTTP заголовков страницами Вашего сайта, надо как-то определять, когда модифицируются эти страницы. Проблема состоит в том, что в отличие от ситуации со статичными HTML файлами, когда дата модификации файла и его содержимого это одно и тоже, динамические страницы могут менять свое содержимое в зависимости от предусмотренных разработчиком внешних факторов (время суток, запрос пользователя, импорт данных из БД) без изменения файла скрипта. То есть дата модификации файла и информации, которую он отсылает клиенту, запросто могут не совпадать. Другая проблема, вытекающая из предыдущей, заключается в том, что часто на динамических сайтах на каждую страницу ставят голосование, "шутку дня" или баннерокрутилку, код которых меняется при каждой следующей загрузке. То есть надо смотреть не на всю сгенерированную страницу, а только на ту ее часть, которая несет основную информацию — текст статьи, прайс-лист и т.п.

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

Теория

Для решения вышеописанных проблем необходимо:

  1. "Отловить" со страницы ту часть контента, за которой мы "следим";
  2. Сравнить то, что получилось, с тем, что получилось в прошлый раз — для этого использовать БД, где и хранить информацию о страницах;
  3. В случае модификации данных — обновить информацию о странице в БД;
  4. Отослать клиенту HTTP заголовки в зависимости от его запроса.

Сделать это можно разными способами. Мы будем рассматривать систему, написанную на языке PHP, так как этот язык сейчас популярен, довольно прост для понимания и поддерживается большинством хостингов, в том числе и бесплатными. Данные мы будем хранить в БД MySQL по тем же причинам.

Итак, что там у нас там, на сайте? Динамические страницы, то есть PHP скрипты. Как функционирует эта «динамика»? Если сайт у Вас небольшой, то, скорее всего, у Вас под каждую страницу существует свой скрипт: index.php – для главной страницы, news.php для страницы новостей и т.п. Если же сайт у Вас выходит за рамки «домашней странички» и имеет сложную структуру, свой форум или пользовательскую зону, построен с использованием баз данных, то, вероятно, одним скриптом типа showpage.php генерируются сотни концептуально различных страниц (например, страницы форума генерирует один скрипт, но страницы-то разные, и надо следить за каждой отдельно). Первый случай проще для рассмотрения, хотя, если Вы поймете суть предлагаемой системы, Вы сможете без особых проблем интегрировать ее и на сайте, описанном во втором случае. А мы рассмотрим случай первый.

Чего надо «избежать» при «отлове» контента? Баннеров, счетчиков и всего, что написано в меню. Это не принципиально — Вы сами решаете, какой объем информации, отдаваемой Вашими скриптами, будет использоваться для определения их модификации.

Если «нужная» информация и «ненужные» примочки выводятся разными функциями, то все «нужное» можно просто забить в одну переменную. Что-нибудь вроде этого:

<?php

// Это – часть гипотетического PHP файла.
// Он не имеет никакого смысла и используется только как пример.

include 'settings.inc.php'; // Подключаем какие-нибудь настройки, модули, классы и т.п.
print page_header(); // Выводим шапку – нам она не нужна;
print page_ads(); // Выводим рекламу – аналогично;

print $contentmonitoring_var = main_info(); // Показываем "основную" информацию;
// это как раз то, что нам нужно - кладем данные в переменную $contentmonitoring_var

print $temp = info2(); // Еще какая-нибудь нужная информация;
$contentmonitoring_var .= $temp; // добавляем данные в ту же переменную

print something_other(); // Опять что-то не нужное;
print page_footer(); // Выводим футер - он нам тоже не понадобится.

?>

Если Ваш сайт слишком громоздкий и сложный для такой модификации, есть более простой выход: буферизация вывода, стандартная функция PHP (когда буферизация вывода активна, все, что генерирует скрипт, не высылается клиенту, а сохраняется во внутреннем буфере). То, что попало в буфер, можно положить в переменную и работать с этими данными, как с обычной строковой переменной, а потом отослать клиенту. Тогда Вам необходимо просто найти в коде начало и конец того куска выводимой информации, которую надо проверять, и маркировать этот кусок HTML-комментариями. Для этого перед началом вывода интересующей нас информации надо вставить строчку

<!--content-->

а после — строчку

<!--/content-->

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

Теперь мы отделили «мух от котлет» и информация, которую будем «контент-мониторить», заключена у нас между HTML-комментариями. Остается включить буферизацию, в конце скрипта взять данные из буфера, выловить оттуда часть кода между комментариями, и обработать его на предмет модификации. По результатам этой обработки, а также в зависимости от запроса клиента, отослать HTTP заголовки. Все это будет делать скрипт, который надо подключить ко всем Вашим скриптам. Делается это так: в начале каждого файла, сразу после строчки <?php вставляем код

ob_start(); // Слежение за контентом – запускаем буферизацию.

а в конце, перед строчкой ?> код

include_once('content_monitoring.inc.php'); // Слежение за контентом – подключаем исполняемый скрипт.

Первая строчка запускает буферизацию, чтобы можно было работать с тем, что генерирует скрипт, а вторая подключает скрипт, который работает с тем, что нагенерировала страница, откуда его подключили. Он проверяет, не модифицировалась ли она, и отсылает HTTP заголовки.

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

Практика

Код исполняемого скрипта:

<?php

// Файл content_monitoring.inc.php, система контент-мониторинга.
// Должен лежать в той же папке, что и Ваши скрипты.

if (!isset($page_id)) $page_id = $_SERVER['PHP_SELF'];
// $page_id это идентификатор страницы в базе данных.
// Если Вы используете схему "1 скрипт = 1 страница", оставьте эту строчку как есть.
// Если у Вас один скрипт генерирует много концептуально разных страниц,
// придется генерировать $page_id этим скриптом динамически.
// Переменная не должна содержать ничего кроме латиницы, цифр и подчеркивания.

ConnectToDatabase(); // Соединяемся с базой данных.
// Функция ConnectToDatabase() не определена в этом примере, так как
// подключение к БД MySQL и обработка возникших ошибок выходят
// за рамки данной статьи. Вам придется самому написать эту функцию.

$page_all_contents = ob_get_contents(); // Достаем вывод скрипта из буфера
$page_main_content = preg_replace("#.*?(<!--content-->(.*?)<!--/content-->|$)#is", "$2", $page_all_contents);
$page_hash = md5($page_main_content); // Вычисляем хэш "нужной" части страницы

// Проверим, создана ли таблица статистики...
if (!mysql_num_rows(mysql_query("SHOW TABLES LIKE 'content_monitoring'"))) {
    // Таблица статистики не найдена.
    mysql_query("CREATE TABLE content_monitoring (cm_id VARCHAR(255) NOT NULL, cm_md5 CHAR(32), cm_modified TIMESTAMP(14), PRIMARY KEY(cm_id(255)))"); // Создаем таблицу.
    mysql_query("INSERT INTO content_monitoring VALUES ('$page_id', '$page_hash', NULL)"); // Пишем в таблицу первую запись.
}

// Достаем из базы данных информацию о текущей странице.
$FILE_INFO = mysql_fetch_row(mysql_query("SELECT cm_id, cm_md5, UNIX_TIMESTAMP(cm_modified) as modified FROM content_monitoring WHERE cm_id = '$page_id'"));

if (empty($FILE_INFO)) {
    // Запись для файла не найдена.
    mysql_query("INSERT INTO content_monitoring VALUES ('$page_id', '$page_hash', NULL);"); // Создаем запись о текущем файле.
    $FILE_INFO[2] = time(); // Модификация - сейчас.
    $last_modified = gmdate("D, d M Y H:i:s", $FILE_INFO[2]);
}
else {
    // Запись для файла найдена.
    if ($page_hash != $FILE_INFO[1]) {
    mysql_query("UPDATE content_monitoring SET cm_md5='$page_hash' WHERE cm_id='$page_id';"); // Обновляем запись о текущем файле, если он изменился.
    $FILE_INFO[2] = time(); // Модификация - сейчас.
    }

    $last_modified = gmdate("D, d M Y H:i:s", $FILE_INFO[2]);

    // Дальше делаем обработку Conditional GET'а:
    if (!isset($_SERVER['HTTP_IF_NONE_MATCH']) && !isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
        // Conditional Get не задан - просто отдаем файл.
        header('ETag: "'.$page_hash.'" '); // присваеваем метку
        header("Last-Modified: $last_modified GMT"); // последнее изменение - сейчас
        header('Expires: '.gmdate("D, d M Y H:i:s", time()+60*10).' GMT'); // страница остается неизменной 10 минут
    }

    elseif (!isset($_SERVER['HTTP_IF_NONE_MATCH']) && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
    // Случай первый - Conditional GET задан, проверка только по If-Modified-Since:
    $unix_ims = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); // значение If-Modified-Since в UNIX формате
    if ($unix_ims > time() || !is_int($unix_ims)) {
        // Ошибка Conditional GET - просто отдаем файл.
        header('ETag: "'.$page_hash.'" '); // присваеваем метку
        header("Last-Modified: $last_modified GMT"); // последнее изменение - сейчас
        header('Expires: '.gmdate("D, d M Y H:i:s", time()+60*10).' GMT'); // страница остается неизменной 10 минут
    }
    else {
        // Conditional GET корректен.
        if ($unix_ims >= $FILE_INFO[2]) {
            // Копия файла в кеше клиента не устарела - сообщаем ему об этом...
            header("HTTP/1.1 304 Not Modified"); // не модифицировано
            header('ETag: "'.$page_hash.'" '); // присваеваем метку
            // ...и заканчиваем выполнение скрипта, не отсылая сам файл.
            while(ob_get_level()) ob_end_clean();
            exit;
        }
        else {
            // Похоже, что копия клиента устарела.
            header('ETag: "'.$page_hash.'" '); // присваеваем метку
            header("Last-Modified: $last_modified GMT"); // последнее изменение - сейчас
            header('Expires: '.gmdate("D, d M Y H:i:s", time()+60*10).' GMT'); // страница остается неизменной 10 минут
            }
        }
    }

    elseif (isset($_SERVER['HTTP_IF_NONE_MATCH']) && !isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
    // Случай второй - Conditional GET задан, проверка только по If-None-Match:
    $INM = split('[,][ ]?', $_SERVER['HTTP_IF_NONE_MATCH']); // массив значений If-None-Match
    foreach($INM as $enity) {
        if ($enity == "\"$page_hash\"") {
            // Копия файла в кеше клиента не устарела - сообщаем ему об этом...
            header("HTTP/1.1 304 Not Modified"); // не модифицировано
            header('ETag: "'.$page_hash.'" '); // присваеваем метку
            // ...и заканчиваем выполнение скрипта, не отсылая сам файл.
            while(ob_get_level()) ob_end_clean();
            exit;
            }
        // Если дошло до этой линии, копия клиента устарела. Отдаем файл.
        header('ETag: "'.$page_hash.'" '); // присваеваем метку
        header("Last-Modified: $last_modified GMT"); // последнее изменение - сейчас
        header('Expires: '.gmdate("D, d M Y H:i:s", time()+60*10).' GMT'); // страница остается неизменной 10 минут
        }
    }

    else {
    // Случай третий - проверка и по If-Modified-Since, и по If-None-Match:
    $unix_ims = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); // значение If-Modified-Since в UNIX формате
    $INM = split('[,][ ]?', $_SERVER['HTTP_IF_NONE_MATCH']); // массив значений If-None-Match
    if ($unix_ims > time() || !is_int($unix_ims)) {
        // Ошибка Conditional Get - просто отдаем файл.
        header('ETag: "'.$page_hash.'" '); // присваеваем метку
        header("Last-Modified: $last_modified GMT"); // последнее изменение - сейчас
        header('Expires: '.gmdate("D, d M Y H:i:s", time()+60*10).' GMT'); // страница остается неизменной 10 минут
        }
    else {
        // Conditional GET корректен.
        foreach($INM as $enity) {
            if ($enity == "\"$page_hash\"" && $unix_ims >= $FILE_INFO[2]) {
            // Копия файла в кеше клиента не устарела - сообщаем ему об этом...
            header("HTTP/1.1 304 Not Modified"); // не модифицировано
            header('ETag: "'.$page_hash.'" '); // присваеваем метку
            // ...и заканчиваем выполнение скрипта, не отсылая сам файл.
            while(ob_get_level()) ob_end_clean();
            exit;
            }
        // Если дошло до этой линии, копия клиента устарела. Отдаем файл.
        header('ETag: "'.$page_hash.'" '); // присваеваем метку
        header("Last-Modified: $last_modified GMT"); // последнее изменение - сейчас
        header('Expires: '.gmdate("D, d M Y H:i:s", time()+60*10).' GMT'); // страница остается неизменной 10 минут
        }
    }
}
}

?>

Несколько слов напоследок

Данная статья, мнение ее автора по отношению к поднятой проблеме, а также предложенные методы решения этой проблемы не претендуют на универсальность — вполне возможно (а, скорее всего, так и есть), что существует более простое и лучшее решение для Вашего случая. Автор не ставил перед собой цель создать нечто гениальное, что будет способно существенно изменить Интернет — это невозможно. Он лишь хотел показать один из возможных путей небольшого облегчения жизни веб-разработчикам, большей частью начинающим.

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

В любом случае буду рад, если кому-нибудь помог.

Антон Клесс[досье]

Ссылки

  1. GET-запрос с условием (Conditional GET)
  2. Hypertext Transfer Protocol - HTTP/1.1
  3. Header Field Definitions - ETag
  4. Header Field Definitions - Expires
  5. Header Field Definitions - Last-Modified

Комментарии

2005-03-07 09:45:27 [обр] Антон Клесс[досье]
Первоначальное обсуждение статьи в теме http://xpoint.ru/forums/about/thread/29591.xhtml.
спустя 17 часов [обр] Владимир Палант[досье]
В наиболее продвинутых браузерах можно также добиться того, что запросы вообще не будут посылаться каждый раз

Не уверен, что это можно считать признаком "продвинутости" браузера. Скорее уж признаком того, читали ли его разработчики RFC2616(ietf) или предпочли изобретать собственные стандарты.

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

При такой формулировке напрашивается вопрос: а есть такие, что не могут?

Один из таких заголовков под названием last-modified

Заголовок называется Last-Modified. Несмотря на то, что стандарт предписывает игнорировать регистр букв в заголовках, «Applications ought to follow "common form", where one is known or indicated, when generating HTTP constructs, since there might exist some implementations that fail to accept anything» (http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).

"Отловить" со страницы ту часть контента, за которой мы "следим" – для этого придется написать скрипт

А для всего остального скрипт писать не придётся? Мне кажется, что если сказать нечего, то лучше ничего не говорить. Но это вопрос вкуса, конечно.

обратите внимание на синтаксис!

А надо ли обращать на него внимание? Вы ведь написали весьма тривиальную вещь, ничего особенного в ней нет. И вряд ли кто-нибудь станет этот код перенимать, статья лишь демонстрирует принцип.

никакой вывод скрипта не высылается клиенту, а сохраняется во внутреннем буфере

Здесь что-то не по-русски...

Первая строчка подключает скрипт, который лезет в БД, ищет там информацию о текущей странице и отсылает HTTP заголовки, а вторая – скрипт, который проверяет, не изменилось ли наполнение страницы на этот раз, и в положительном случае делает соответствующую запись в таблице.

Как это понимать? Мы будем выводить дату изменения от прошлого запроса? А если с тех пор что-то изменилось, то дату мы обновим только к следующему запросу? Что-то с вашей идеей не так...

Мне казалось, вы хотели буферизовать весь ответ, в конце вырезать из него важную часть, сверять её с тем, что хранится в базе, обновлять (при надобности) дату и только тогда выводить заголовки. Не слишком красиво, но хотя бы заголовки будут правильные. Я, видимо, ошибся...

Добавлю еще, что данную систему можно расширить — например, сюда же можно присовокупить счетчик посещений. Но это уже выходит за рамки тематики данной статьи.

Да уж, каким боком счётчик посещений к этой теме — понять сложно.

$query = mysql_query("SHOW TABLES FROM $dbase_db");

Вы уверены, что вы хотите перебирать все таблицы? Я никогда не работал с MySQL, но один взгляд в документация моментально подсказала мне такое решение:

mysql_query("SHOW TABLES LIKE 'content_control'");

Вам нужно лишь проверить, было ли что-то найдено.

mysql_query("CREATE TABLE content_control (stat_id MEDIUMINT NOT NULL AUTO_INCREMENT, stat_file TINYTEXT NOT NULL, stat_md5 CHAR(32), file_modified TIMESTAMP(14), PRIMARY KEY(stat_id))");

Что-то я не понял, зачем нужно поле stat_id и почему оно должно быть первичным ключем. Вы всегда ищете запись по названию файла, так что это поле и должно быть PRIMARY KEY. Кроме того, искать по полю типа TEXT весьма неэффективно (про MySQL ничего не могу сказать с уверенностью, но думаю, что и там это так). Решите, какая максимальная длина имеет смысл, и поставьте её. Я бы поставил VARCHAR(256).

mysql_query("INSERT INTO content_control VALUES ('', '$page_id', NULL, NULL)"); // Пишем в таблицу первую запись
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); // Модификация – сейчас.

Зачем это нужно? Чуть дальше вы всё равно сделаете запрос и обнаружите, что записи для текущего файла нет. И для этого случая у вас стоит в точности такой же код. Хотя, нет, с одним исключением. Там вы вместо текущей даты выводите $last_modified — не определённую переменную.

if (isset($http_headers['If-None-Match']) && $FILE_INFO[3] == $http_headers['If-None-Match']) {
// Если ETag запроса совпадает с тем, что получилось у нас, сообщаем клиенту, что файл не изменился:
header("HTTP/1.1 304 Not Modified ");
// ...и заканчиваем выполнение скрипта, не отсылая сам файл.
exit;

Это уже становится интересно. То есть, вы берёте из базы хеш, который вы рассчитали при прошлом запросе. Вместо того, чтобы проверить, какой хеш у вас получится в этот раз (что-то ведь могло с тех пор измениться), вы просто сравниваете его с тем, что у вас запросил клиент, и в случае совпадения отвечаете ему: "я не знаю, что у меня есть сейчас, но в прошлый раз я дал тебе как раз то, что у тебя и есть сейчас, так что ты, наверно, можешь обойтись без актуальной информации".

Дальше читать нет смысла. Пересматривайте концепцию, меняйте решение, я потом посмотрю ещё раз.

спустя 9 часов [обр] Владимир Палант[досье]
На что я ещё обратил внимание, но забыл написать:
  1. If-None-Match — не одно значение, а список. Причём список значений в кавычках, так что ваше сравнение никогда не даст результата.
  2. Вы нигде в вашей статье даже не упомянули заголовки If-None-Match, If-Modified-Since, ETag и Expires — а как раз они для этой тематики важнее многого, о чём вы написали. Если ваша целевая аудитория — начинающие, не имеющие никакого понятия о протоколе HTTP, то они будут только пялиться на ваш код, пытаясь понять, что он такое странное делает и почему об этом ничего не написано в статье. Но недолго, терпение у них быстро кончится, и они уйдут читать статьи получше.
спустя 4 часа 39 минут [обр] Антон Клесс[досье]

Владимир, знаете, большое Вам спасибо — было очень приятно читать конструктивную критику в большом количестве, причем он-топик. Такое не часто увидишь... Спасибо!

По порядку:

Не уверен, что это можно считать признаком "продвинутости" браузера. Скорее уж признаком того, читали ли его разработчики RFC2616(ietf)(ietf) или предпочли изобретать собственные стандарты.

Почему же нельзя?.. Если разработчики не следуют стандарту, то они либо делают второсортный продукт (пусть очень красивый/престижный/дорогой/хорошо продающийся, но все-таки второсортный — такого мнения я придерживаюсь о IE), либо революционно-новаторский (про IE этого не скажешь, а именно на него был намек, если кто не понял).
Если Вас очень коробит слово "продвинутый", можно написать "правильный", но сее не будет правдой, ибо найти Правду и указать на нее другим –- великое и почти неосуществимое дело, и я не собираюсь его на себя возлагать. Демократичный вариант "браузеры, поддерживающие все навороты HTTP 1.1" думаю, всех устроит?

При такой формулировке напрашивается вопрос: а есть такие, что не могут?

Лично я не встречал, но утверждать, что их нет, не в моей компетенции. Что, если убрать слово "некоторые"?.. То есть могут, но не факт, что будут (ведь могут они все –- можно поставить манипулятор, так и тапочки подносить будут).

Заголовок называется Last-Modified....

Спасибо, изменено.

никакой вывод скрипта не высылается клиенту, а сохраняется во внутреннем буфере

Это фраза из русской документации PHP =)

спустя 16 минут [обр] Антон Клесс[досье]
Немного почистил статью. В процессе редактирования заметил баг, который описал в Багзилле под номером 156.
Насчет остального, сказанного Владимиром, буду думать.
спустя 55 минут [обр] Владимир Палант[досье]

Почему бы не сказать: "в браузерах, придерживающихся спецификации протокола HTTP"? Заодно будет вам хорошая возможность объяснить, в чём именно состоят эти спецификации и какую роль при этом играют различные заголовки, которые вы выдаёте.

Лично я не встречал, но утверждать, что их нет, не в моей компетенции.

И не в моей тоже. Но показать, что это скорее правило, чем исключение, всё-таки не мешало бы. К примеру, заменить "некоторые серверы могут" на "серверы обычно могут".

Это фраза из русской документации PHP

... которую кто-то дословно перевёл с английского.

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

Вы уверены, что других вариантов быть не может?

спустя 31 минуту [обр] Антон Клесс[досье]
Вы уверены, что других вариантов быть не может?
Хороший вопрос ;)
спустя 25 минут [обр] Антон Клесс[досье]

Поправил то, что было, на предмет работоспособности кода, плюс небольшая оптимизация и упрощение. "Концепцию" не менял.

Владимир Палант[досье]

Мне казалось, вы хотели буферизовать весь ответ, в конце вырезать из него важную часть......

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

спустя 5 часов [обр] Владимир Палант[досье]

Да, отмечать основное содержимое — один из вариантов. Можно, к примеру, вставить в начале <!--MainContent--> и в конце <!--/MainContent-->. Ведь даже с вашим теперешним вариантом человеку всё равно придётся искать начало вывода основного содержимого, чтобы включить там буферизацию — так вместо этого он напечатает комментарий. А буферизацию вы включите в файле, который нужно вставлять в начало документа. Мне кажется, что это предпочтительней вложенной буферизации, перехватывать одни и те же данные два раза уж точно не стоит.

Другое дело, что следует упомянуть о недостатках такого подхода (расход памяти, быстродействие, невозможность отсылать страницу постепенно). Он годится исключительно для уже существующего сайта, на серьёзное изменение которого потребуется слишком много усилий. При проектировании нового сайта следует следить за тем, чтобы дату изменения можно было получить до вывода содержания.

спустя 15 часов [обр] Антон Клесс[досье]

Владимир Палант[досье], если подумать, то получается полная фигня :(

Получен HTTP запрос, запущен скрипт. Смотрим в базу данных на предмет существования записи о данной странице. Если записи нет, создаем ее, вычисляем хеш требуемой части страницы, отсылаем Last-Modified равный time() и ETag равный хешу. Если запись есть, вычисляем хеш требуемой части страницы и сравниваем с хешом из БД, при несовпадении пишем в базу новый хеш и новую дату. Далее при If-None-Match запросе сравниваем хеш запроса с текущим, при If-Modified-Since — дату в запросе с текущей датой модификации, в конце концов опять же отсылаем Last-Modified и ETag (или, при случае, 304 Not Modified).

Expires тут ни при чем, он указывается/не указывается независимо ото всего, его указание/неуказание и значени зависит от того, чего хочет добиться сайтостроитель (об этом я допишу).

То есть: я узнаю, какие HTTP заголовки отсылать, только когда получу в свое распоряжение всю страницу (или ее часть). Тогда выход — это буферизовывать все, маркируя HTML комментариями "нужное" (в этом случае "нужное" может состоять из нескольких кусков), а в конце скрипта вызывать скрипт контент-контроля, который выудит из буфера маркированные куски, обработает их и пошлет заголовки.

Я все правильно понимаю?..

спустя 4 минуты [обр] Владимир Палант[досье]
Да, правильно. Именно поэтому я и говорю, что такой подход не рекомендуем. Лучше проектировать систему так, чтобы новая дата записывалась в базу при изменении содержания, а не при его отображении (да и не только я, в старой теме вам на это уже указывали).
спустя 23 минуты [обр] Антон Клесс[досье]

Ага, система доложна знать, когда данные изменятся.
IMHO реализовать это /на должном уровне/ слишком сложно...

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

Чтобы обеспечить движку сайта такую "телепатию", надо... Что надо? Уметь проектировать сложные сайты. А новичку это не под силу, а даже если под силу, его не убедишь перестроить весь сайт. Как следствие — "левые" сайты, не отсылающие Last-Modefied (А я без него жить не могу, просто плачу каждый раз, когда такой сайт увижу. Шутка).

В моем же варианте все немного проще и понятнее (мне так кажется, возможно, я ошибаюсь).

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

Следующую статью буду писать про "подключение к БД MySQL и обработку возникших ошибок" ;))

спустя 3 дня [обр] Антон Клесс[досье]
Ну что же: версия 1.0 release candidate 1.
Критика приветствуется!
спустя 1 день 5 часов [обр] Антон Клесс[досье]
Замечен баг: когда я отсылаю 304 Not Modified и делаю exit;, PHP автоматом выводит содержимое буфера, а этого надо избежать.
спустя 21 час [обр] Андрей Анатольич+[досье]
Антон Клесс[досье] Кстати, зря вы используете getallheaders(). Лучше берите нужные заголовки из $_SERVER
спустя 52 минуты [обр] Александр Галкин[досье]

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

$page_main_content = preg_replace("#.*?(<!--content-->(.*?)<!--/content-->|$)#is", "$2", $page_all_contents);

Регулярное выражение, которое используется в статье сейчас, не справится с двумя блоками content. Да и с одним не справится, если там будут переносы строк (а они там будут обязательно).

mysql_query("CREATE TABLE content_monitoring (cm_id VARCHAR(256) NOT NULL, cm_md5 CHAR(32), cm_modified TIMESTAMP(14), PRIMARY KEY(cm_id(256)))"); // Создаем таблицу.

#1074 - Too big column length for column 'cm_id' (max = 255). Use BLOB instead

if (!mysql_fetch_row(mysql_query("SHOW TABLES LIKE 'content_monitoring'"))) {
if (count($FILE_INFO) <= 1) {

Ну почему же mysql_fetch_row, когда нужно mysql_num_rows? Зачем count, когда по смыслу — empty? Зачем в блоке создания таблицы сразу делать новую запись? Обычный блок обработки новой страницы, который находится на десяток строк ниже, разве с этим не справится? Если мы всю страницу заключаем в буфер — почему не избавиться от лишнего вмешательства в код, сделав handler (в смысле, ob_start('content_monitoring') и соответствующая функция)? Поверьте, неопрятный код действительно сложно читать.

header("HTTP/1.1 304 Not Modified");
// ...и заканчиваем выполнение скрипта, не отсылая сам файл.
exit;

А Вы добавьте туда после комментария while(ob_get_level()) ob_end_clean();, и содержимое буфера не выведется.

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

Совершенно непонятно, почему вообще нужно выделять контент из страницы. Если у нас, допустим, баннеры на странице генерируются серверным скриптом, то почему эти изменения нужно игнорировать? Пользователь закеширует один баннер и будет на него месяц смотреть, пока контент страницы не обновится. Или, допустим, у нас в шапке меню изменилось — а в кеше старое: получаем веселую игру "угадай, на какой странице последняя версия". Получается так: если не включать в контент хоть какое-нибудь изменение на странице — в кеше не будет изменений, и вместе с уменьшенным трафиком мы получим раздраженного посетителя. А если все изменяемые куски заключать в <!--content--> — то можно просто хешировать весь выдаваемый хтмл без разбора.

спустя 1 час 20 минут [обр] Антон Клесс[досье]
код кричит о том, что его даже и не пытались никогда запускать

Вы слишком уверены в этом. Код тестировался.

#1074 - Too big column length for column 'cm_id' (max = 255). Use BLOB instead

Ничего подобного :(

спустя 1 час 15 минут [обр] Александр Галкин[досье]
Не то чтобы я был уверен — просто, перечитывая статью, я зацепился глазами за рег, а потом для верности попробовал вставить запрос CREATE TABLE в phpMyAdmin... Там все-таки надо длину поставить 255, а не 256 — иначе получится процитированная выше ошибка MySQL.
спустя 1 день 4 часа [обр] Антон Клесс[досье]
Сделано.
спустя 1 день 20 часов [обр] Антон Клесс[досье]
Версия 1.0 release candidate 2.
Код оттестирован и работает так, как это от него ожидается.
спустя 10 часов [обр] Алексей Пешков[досье]

header('Expires: '.gmdate("D, d M Y H:i:s", time()+24*60*60).' GMT');

Это строка говорит о том, что контент будет браться из кеша в течении 24 часов со времени первого обращения. И никаких запросов к серверу не будет. Сомневаюсь, что это правильное "слежение за контентом на динамических сайтах".

Кроме того заголовок HTTP выдается в форме
Expires: Sat, 19 Mar 2005 02:33:37 GMT GMT
(по-крайней мере это так, на моем сайте при моих настройках сервера).

Остальное не смотрел, времени жалко...

спустя 3 часа 35 минут [обр] Антон Клесс[досье]
Expires: Sat, 19 Mar 2005 02:33:37 GMT GMT
Странно, у меня ничего подобного не наблюдается... Какой запрос Вы давали?
спустя 11 часов [обр] Алексей Пешков[досье]

У меня в коде www.orlis.ru, на котором, к слову, "правильное кеширование" :)
header('Expires: '.gmdate("D, d M Y H:i:s", time())); //без .'GMT'
Решил, что это мой баг и попробовал исправить на Ваш вариант - получил бяку.

Если кому интересно, то в реальном интернет-магазине '304 Not Modified' отправляется в примерно 30% от запросов страниц.

спустя 11 часов [обр] Антон Клесс[досье]
Хм... А какой смысл в header('Expires: '.gmdate("D, d M Y H:i:s", time())); ? Насколько я понимаю, отправление такого заголовка отрубает любое клиентское кеширование на хрен.
спустя 8 часов [обр] Алексей Пешков[досье]
Почитайте RFC2616(ietf). Глава 13.
спустя 2 часа 7 минут [обр] Антон Клесс[досье]

Спасибо за напутствие, обязательно прочту(бегло я это все просматривал, но так, чтобы вчитаться во все эти английские словоформы, не пришлось пока).

И все-таки: Вы таким заголовком что хотите сообщить пользователю? Что страница "устаревает прямо сейчас"?.. Зачем? Статья о "правильном кешировании", а не о том, как его избежать.

Или я опять туплю.

спустя 2 часа 32 минуты [обр] Алексей Пешков[досье]
Ну, попробуйте найти на русском этот RFC. Загнать в translate.ru наконец...
Извините, Вы просто не владеете темой, о которой написали статью.
спустя 19 часов [обр] Антон Клесс[досье]
Все может быть.
спустя 12 дней [обр] Дмитрий Кучкин[досье]

При проверке заголовков If-None-Match и If-Modified-Since если If-None-Match существует, но не совпадает с хэшем, нужно отдавать содержимое, а не проверять еще и If-Modified-Since - это противоречит RFC2616(ietf).
Вот так гораздо лучше:

if ((!@$_SERVER['HTTP_IF_NONE_MATCH'] && !@$_SERVER['HTTP_IF_MODIFIED_SINCE']) ||
    (@$_SERVER['HTTP_IF_NONE_MATCH'] && $_SERVER['HTTP_IF_NONE_MATCH'] != $ETag
                                     && $_SERVER['HTTP_IF_NONE_MATCH'] != '*') ||
    (@$_SERVER['HTTP_IF_MODIFIED_SINCE'] && $_SERVER['HTTP_IF_MODIFIED_SINCE'] != $LastModified)) {
    // 200
    ...
} else {
    // 304
    // !!! обязательно отдать заголовок ETag: - требование стандарта !!!
    ...
}

Но в добавок к этому лучше еще

  1. разбирать If-None-Match на предмет нескольких значений entity tag, каждое из которых сравнить с хэшем (об этом здесь уже писал Владимир Палант[досье])
  2. сравнивать If-Modified-Since < Last-Modified. Для этого нужно преобразовать время If-Modified-Since в unix time, например, функцией strtotime, предварительно проверив на соответствие RFC2616(ietf), глава 3.3 ( и, если вдруг время в формате ANSI C, добавив ' GMT' в конец строки перед вызовом strtotime).

И измените все-же логику вычисления Expires. Этот заголовок сообщает клиенту дату, до которой можно брать содержимое кэша без обращения к серверу. Что современные браузеры и делают - они вообще не посылают запросов на сервер для этой страницы, пока не наступит время из Expires. Об этом, собствено, уже писал Алексей Пешков[досье].

P.S. Я с php впервые познакомился неделю назад, поэтому за синтаксис не ручаюсь.

спустя 12 часов [обр] Антон Клесс[досье]

Дмитрий Кучкин[досье] Спасибо за уделенное внимание. Код я подправил.

"Логика вычисления Expires" вообще-то не существует, значение Expires должен установить вебмастер зная особенности своего сайта (на Яндксе, насколько я помню, стоит пять минут, а на большинстве хостингов по-умолчанию — сутки). Короче, поставил я 10 минут, по-моему, для большинства случаев нормально. И вообще, IE с умолчанными настройками (а таких большинство) плюет на Expires, или я не прав? Но, конечно, раз взялся делать статью, надо все делать в стандарту. Ну я к нему и стремлюсь :)

Обработка нескольких значений entity tag — в следующей серии.

спустя 3 часа 15 минут [обр] Дмитрий Кучкин[досье]

Антон Клесс[досье] Все равно у Вас то же самое. Если If-None-Match существует и не совпал с хэшем, но If-Modified-Since равен Last-Modified, выдается 304 Not Modified. Так неправильно.
Несовпадение любого из двух присутствующих полей должно вызывать отправку содержимого, так же, как и отсутствие обоих полей одновременно. Рассмотрите внимательнее мой пример и сравните со своим, проверьте, какие ветки будет выполняться при всех сочетаниях заголовков.

P.S. В принципе, можно отдавать 304, если If-None-Match совпал, а If-Modified-Since нет, если я правильно понял этот кусок RFC2616(ietf). Но при несовпадении If-None-Match нужно отдавать содержимое обязательно. Иное поведение не соответствует стандарту.

спустя 2 дня 1 час [обр] Антон Клесс[досье]

Пока что пусть срабатывает "If-Modified-Since равен Last-Modified" только в случае отсутствия If-None-Match, потом сделаю так, как Вы говорите — сейчас нет возможности протестировать всё.

Все значения времени надо привести к UNIX time и сравнивать по числам, так оно очивиднее будет...

спустя 51 минуту [обр] Дмитрий Кучкин[досье]
Все значения времени надо привести к UNIX time и сравнивать по числам, так оно очивиднее будет...

Так и я о том же в первом комментарии писал: Слежение за контентом на динамических сайтах. Добавка номер 2 :)

Кстати, в статье GET-запрос с условием (Conditional GET) и комментариях к ней тема правильных заголовков обсуждается довольно подробно.

спустя 1 день 5 часов [обр] Антон Клесс[досье]

Код обновлен.
Я очень сомневаюсь, а точнее, почти уверен, что он нерабочий :( , и за публикацию кривого кода меня надо бить. Но концептуально он правильный (если я сейчас хоть что-то соображаю, а в этом есть сомнения).

Ближайшую неделю я на Точке не появлюсь, так что у всех желающих есть потрясающая возможность оттестировать и поправить код. Даёшь!!

спустя 2 дня 13 часов [обр] Антон Клесс[досье]
Код работает, ура.
Жду новых замечаний.
спустя 27 дней [обр] L&L[досье]

Антон Клесс[досье] ИМХО структура кода должна быть иной: анализ заголовков $_SERVER['...'] определяет переменную-признак, а затем в одном месте посылаем нужные header'ы. Впрочем для концепта это не существено.

Вопрос: 1. Как Вы определили что "Код работает, ура." ?
Для проги с If-Modified-Since я проверял используя страницу http://seolab.ru/add/header.htm
Как сделать проверку для If-None-Match - я не знаю.

  1. Мне известно, что запросы с If-Modified-Since реально существуют. А существуют ли запросы с ETag / If-None-Match ?

Мне понятно, что пока я не ставлю ETag, их не будет. Но появятся ли они если добавлять ETag ?

спустя 1 час 3 минуты [обр] Владимир Палант[досье]
сообщение промодерировано
  1. Думаю (надеюсь), что Антон составлял запросы вручную — либо по старинке, с telnet, либо каким-нибудь из веб-сервисов, которые это умеют. В принципе и скрипт для этого написать несложно.
  2. Кроме IE есть ещё и другие браузеры — и они очень даже используют If-None-Match.
спустя 2 часа 33 минуты [обр] Антон Клесс[досье]

Я использовал WebBug 5.3 - прога такая, потом для очищения совести проверил в трех-четырех онлайновых сервисах просмотра заголовков. "Все работает" значит, что при стандартных запросах (какие посылает ненастроенный IE) страница отдается в сопровождении Last-Modified и ETag, а при Conditional GET с верным значением етага или даты - отдается 304 + ETag и больше ничего.

Буду крайне рад, если кто-нибудь поставит приведенный код себе на сайт и оттестирует хорошенько.

спустя 22 минуты [обр] L&L[досье]

Антон Клесс[досье]
В программе:
    // Запись для файла найдена.
    if ($page_hash != $FILE_INFO[1]) mysql_query("UPDATE content_monitoring SET cm_md5='$page_hash' WHERE cm_id='$page_id';"); // Обновляем запись о текущем файле, если он изменился.
    $last_modified = gmdate("D, d M Y H:i:s", $FILE_INFO[2]);

При выполнении условия ($page_hash != $FILE_INFO[1]) запись обновляется, а $last_modified берётся от старой записи.
Сие не есть верно.

спустя 1 час 6 минут [обр] L&L[досье]

Антон Клесс[досье] Пишу свой скрипт на основе Вашего и "чем дальше в лес, тем больше дров".
В программе
elseif (!isset($_SERVER['HTTP_IF_NONE_MATCH']) && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
    // Случай первый - Conditional GET задан, проверка только по If-Modified-Since:
    $unix_ims = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); // значение If-Modified-Since в UNIX формате
    if ($unix_ims > time()) {
    // Ошибка Conditional GET - просто отдаем файл.

Нельзя сравнивать локальное время time() с гринвичевским $unix_ims. ИМХО эту проверку можно убрать, т.к. при неверном времени файл всегда окажется слишком старым, что и требуется.

Далее в программе
    else {
    // Conditional GET корректен.
        if ($unix_ims <= $FILE_INFO[2]) {

 $FILE_INFO[2] содержит правильное время только если не выполнялся UPDATE. Вместо него следует использовать strtotime($last_modified)

спустя 1 час 53 минуты [обр] Антон Клесс[досье]
L&L[досье] (первый пост) точно, это баг. Спасибо.
спустя 43 минуты [обр] Антон Клесс[досье]

L&L[досье] (второй пост)

Нельзя сравнивать локальное время time() с гринвичевским $unix_ims. ИМХО эту проверку можно убрать, т.к. при неверном времени файл всегда окажется слишком старым, что и требуется.

Похоже, Вы правы — в данном случае это некорректно. Но по спецификации, если IF_MODIFIED_SINCE больше текущего времени, это ошибка и надо отдавать 200 ok. Исправлю позже, сейчас просто убрал проверку.

$FILE_INFO[2] содержит правильное время только если не выполнялся UPDATE. Вместо него следует использовать strtotime($last_modified)

При апдейте таблицы просто присваеваем $FILE_INFO[2] значение time() и все =)

спустя 2 дня 16 часов [обр] L&L[досье]

Антон Клесс[досье]

     L&L[досье] Нельзя сравнивать локальное время time() с гринвичевским $unix_ims

Приношу свои извинения, погорячился: при наличии в тексте букв GMT функция strtotime возвращает правильное локальное время. Оба времени локальных, их можно (и наверно должно) сравнивать.

     Антон Клесс[досье] При апдейте таблицы просто присваеваем $FILE_INFO[2] значение time()

Мне кажется, Вы забыли заключить эту и предыдущую строку в { }

спустя 2 часа 9 минут [обр] Антон Клесс[досье]
Испралено. Теперь при IF_MODIFIED_SINCE меньшем даты модификации отдается 200, равным или большем даты модификации — 304, но при большем текущего time() — 200. Насколько я понимаю, именно этого требует стандарт.
спустя 14 дней [обр] L&L[досье]
Я поставил описанную выше систему на один из наших сайтов. О сайте: 1000 хостов в день, кроме индексной страницы всё содержимое динамическое (mod_rewrite), число страниц - не считал, но более 30000. Кроме того, я веду лог всех условных get запросов с момента включения (11.05.2005).
Обнаружив, что каждый четвёртый запрос имеет стандарт HTTP/1.0, изменил
      header("HTTP/1.1 304 Not Modified"); на
      header($_SERVER['SERVER_PROTOCOL']." 304 Not Modified"); ,
но оставил header('ETag: "'.$page_hash.'" '); . В результате примерно каждый третий запрос по стандарту HTTP/1.0 содержит IF_NONE_MATCH, которое этим протоколом не предусмотрено.
Примерно половина запросов содержит оба поля (IF_NONE_MATCH и IF_MODIFIED_SINCE), половина только IF_MODIFIED_SINCE. Очень редко встречается IF_NONE_MATCH без IF_MODIFIED_SINCE. Иногда встречаются запросы с неправильно сформированной датой (дата 2 раза).
Поэтому я нуждаюсь в совете: Правильно ли по стандарту HTTP/1.0 посылать ETag ?
Мне кажется, вреда не должно быть, а если кто-то это использует, то есть польза.
спустя 3 часа 48 минут [обр] Владимир Палант[досье]
Клиент должен проигнорировать любые заголовки, которые он не поддерживает. Поэтому не будет никакого вреда от того, чтобы посылать ETag с HTTP/1.0, да и стандарт этого ИМХО не запрещает. Думаю, что запросы с HTTP/1.0 и If-Modified-Since просто прошли через прокси, который не поддерживает HTTP/1.1, так что можно отдавать обычный ответ.
спустя 6 месяцев [обр] Степан[досье]

Сам использую похожий метод, и от того вопросы-предложения:

вместо

if (!isset($page_id)) $page_id = $_SERVER['PHP_SELF'];

использовать

if (!isset($page_id)) $page_id = $_SERVER['REQUEST_URI'];

может быть это обеспечит большинство потребностей, без дополнительной генерации $page_id?

вместо

cm_id VARCHAR(255) NOT NULL

использовать

cm_id CHAR(32) NOT NULL

и класть туда

md5($page_id)

кажется такой индекс будет лучше?
(если вдруг кто решит использовать только первую часть, напомню про mysql_escape_string)

И на счет того, что отметить комментариями нужное. Как вариант вырезать отмеченные ненужные блоки.

$page_main_content = preg_replace('/<!--nohash-->.*<!--\\/nohash-->/isU', '', $page_all_contents);

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

спустя 12 дней [обр] Антон Клесс[досье]

Насчет if (!isset($page_id)) $page_id = $_SERVER['REQUEST_URI']; несогласен — при таком подходе БД легко будет заспамить, генерируя запросы к одной и той же странице с бессмысленным концевым параметром (index.html?param=12, index.html?param=123, index.html?param=1234 и т.п.) — а такие запросы будут генерировать различный ID для страницы, хотя контент один и тот же. Я все-таки предпочитаю генерить $page_id исполняемым скриптом, меньше потерь при изменении структуры сайта (страницы с теми же ID по другим адресам).

Насчет md5() — ничего не могу сказать, возможно, смысл есть для дейтсвительно больших сайтов с целью "разогнать" таблицу контентмониторинга.

Про то, чтобы вырезать ненужное из нужного — ценная идея. Только вот в регэкспах я не силён, кто-нибудь, подтвердите, что выражение Степана правильное :) Можно еще <noindex> вырезать, кстати.

спустя 1 год 12 месяцев [обр] Руслан Дворковой[досье]
А что же на текущий момент? Какие рекомендации? где читать?
спустя 5 лет [обр] Василий[досье]
У меня этот код не работает(((
спустя 37 минут [обр] Василий[досье]
заменил
$page_main_content = preg_replace('/<!--content-->.*<!--\\/content-->/isU', '', $page_all_contents);
уже работает нормально вставка в базу $page_hash, только заголовка Last-Modified не отправляет((((((((((((
может кто-то подскажет как доделать????
Powered by POEM™ Engine Copyright © 2002-2005