При разработке движка огромного сайта рано или поздно на повестке дня возникает вопрос - где взять скрипт, который самостоятельно проследит за рутинными моментами синтаксиса sitemap.xml, разделит большой файлик на части по пятьдесят тысяч адресов и запакует всё это хозяйство в GZip?.. Предлагаю Вашему вниманию очередную разработку в духе «сделаю-ка сам». Пример использования (тестировалось под IIS 7.5 + PHP 5.3.2): PHP: <?php // Инициализируем «машину» // Вынесено в отдельные конструкции для работы с планировщиком задач Windows // php.exe -f путь_к_сценарию Site_Mapper::setPhysicalPath( 'D:/www/ '); Site_Mapper::setHost( 'http://localhost/' ); ## ## Создаём sitemap ## // Связать новую sitemap с индексным файлом // $sitemap = Site_Mapper::getSitemap('mycatalog'); // Краткая форма // $sitemap->addURL('http://localhost/mycatalog/page-0001.html'); // Запись с необязательными атрибутами // $sitemap->addURL('http://localhost/mycatalog/page-0002.html', array( 'lastmod' => date('c'), 'changefreq' => 'weekly', 'priority' => '1.0' )); // Представим цикл... 50000+ самостоятельно разделит на несколько sitemap-mycatalog-xxx.xml.gz, // где xxx - порядковая нумерация. // $sitemap->addURL('http://localhost/mycatalog/page-0003.html'); ## ## Завершаем сей процесс созданием индексной карты sitemap, т.н. sitemapindex: ## // Индексный файл записывает ссылки на части карты, начиная их со значения setHost Site_Mapper::createIndex(); ?> Вот и всё. При наличии разрешения на запись в соответствующую директорию у Вас появятся несколько архивов: sitemap.xml.gz - индексный файл sitemap-mycatalog-0001.xml.gz sitemap-mycatalog-0002.xml.gz (при наличии до краёв (50К) заполненного адресами mycatalog-001) Остаётся только отправить на индексацию файл sitemap.xml.gz Можно создавать несколько sitemap подряд: PHP: <?php ## ## При необходимости создаём ещё одну карту для другого каталога: ## // Связать новую sitemap с индексным файлом // $sitemap = Site_Mapper::getSitemap('mycatalog'); $sitemap->addURL('/mycatalog/page-0001.html'); $sitemap->addURL('/mycatalog/page-0002.html'); // Взять существующую или ранее созданную sitemap (если уже вызывалась за время выполнения) // $sitemap = Site_Mapper::getSitemap('product'); $sitemap->addURL('/product/page-0001.html'); ?> Код класса: PHP: <?php // Preconditions // if (!defined(DS)) { define(DS, DIRECTORY_SEPARATOR); } // Построитель множественных карт Sitemap // class Site_Mapper { const PREFIX = 'sitemap-'; const EXTENSION = '.xml.gz'; protected static $filenames; protected static $modified; protected static $path; protected static $host; protected $enumerator; protected $name; protected $resource; protected $level; protected function __construct($name) { $this->name = self::PREFIX . $name . '-'; } public function addURL($loc, array $params = array()) { if ($this->level >= 50000) { gzwrite($this->resource, '</urlset>'); gzclose($this->resource); $this->level = 0; } if (!$this->level) { if (self::$path) { // Инкрементирует счётчик, вносит имя файла в реестр и открывает его на запись. // $filename = $this->name . str_pad(++$this->enumerator, 3, '0', STR_PAD_LEFT) . self::EXTENSION; if ($this->resource = gzopen(self::$path . $filename, 'w9')) { gzwrite($this->resource, '<?xml version="1.0" encoding="UTF-8" ?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'); self::$filenames[] = $filename; } else { throw new Eception("Ошибка при создании файла {$filename}"); } } else { throw new Exception('Не установлено расположение файла, необходимо использовать метод '. __CLASS__ .'::setPhysicalPath'); } } $tmp = '<url><loc>' . $loc . '</loc>'; foreach ($params as $key => $value) { $tmp .= "<{$key}>{$value}</{$key}>"; } $tmp .= '</url>'; gzwrite($this->resource, $tmp); ++$this->level; if (!self::$modified) self::$modified = true; # Добавлена строка, необходима (пере)запись индексной карты. } public function __destruct() { if ($this->resource) { gzwrite($this->resource, '</urlset>'); gzclose($this->resource); } } public static function setPhysicalPath($path) { if (self::$path !== $path) { if (self::$modified) { self::createIndex(); # Завершает активную сессию. } self::$path = $path; } } public static function setHost($host) { self::$host = rtrim($host, '/') . '/'; } public static function getSitemap($name) { static $sitemaps; // Класс использует свою динамическую часть в качестве производной. // return isset($sitemaps[$name]) ? $sitemaps[$name] : $sitemaps[$name] = new self($name); } public static function createIndex() { if (self::$modified) { $lastmod = date('c'); if (!isset(self::$host)) { throw new Exception('Не установлен хост, необходимо использовать метод '. __CLASS__ .'::setHost'); } if ($resource = gzopen(self::$path . DS . 'sitemap' . self::EXTENSION, 'w9')) { gzwrite($resource, '<?xml version="1.0" encoding="UTF-8" ?><sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'); foreach (self::$filenames as $filename) { gzwrite($resource, '<sitemap><loc>' . self::$host . $filename . '</loc><lastmod>' . $lastmod . '</lastmod></sitemap>'); } gzwrite($resource, '</sitemapindex>'); } // Вызов createIndex не будет иметь эффекта // до очередного срабатывания Site_Mapper::addURL // self::$modified = false; } } } ?> Данное решение находится в разработке, если у Вас есть замечания или советы по улучшению - предлагаю совместными усилиями довести генератор до идеального состояния В несжатом виде на 50К выходит 2 MiB, в сжатом - 6 KiB.
чего у вас конструктор protected? чего статиков так много зачем они вообще? я вижу также throw но не вижу try/catch почему?
Конструктор сделан protected для того, чтобы использовать фабрику классов getSitemap. Иными словами, статическая часть - это коллекция, а динамическая - отдельный экземпляр этой коллекции. Своего рода класс в классе. Сами по себе файлы sitemap существовать не могут, поэтому отдельно создать их нельзя - только в составе индекса. Индексный файл sitemap нам нужен всего один для корня сайта, а всё что в него будет добавлено - это коллекция. Возможна одновременная запись нескольких файлов sitemap: $a = Site_Mapper::getSitemap('a'); $b = Site_Mapper::getSitemap('b'); $a->addURL(...); $b->addURL(...); $c = Site_Mapper::getSitemap('a'); # Вернёт начатую карту $a Здесь мы не делаем различия между созданным файлом и существующим. Логика проста - взять и начать запись. Каждый экземпляр держит свой дескриптор файла открытым, чтобы в любой момент продолжить запись. Например, мы вызвали sitemap с именем A в каком-то модуле далеко в ядре... а затем снова пожелали дописать чего-нибудь. В конце сценария все дескрипторы закрываются. Относительно блоков try/catch - машина выбрасывает исключения наружу и останавливает ваши сценарии, т.е. если что-то пошло не так, то Site_Mapper передаёт управление Вам: PHP: <?php try { ## ## При необходимости создаём ещё одну карту для другого каталога: ## // Связать новую sitemap с индексным файлом // $sitemap = Site_Mapper::getSitemap('mycatalog'); $sitemap->addURL('/mycatalog/page-0001.html'); $sitemap->addURL('/mycatalog/page-0002.html'); // Взять существующую или ранее созданную sitemap (если уже вызывалась за время выполнения) // $sitemap = Site_Mapper::getSitemap('product'); $sitemap->addURL('/product/page-0001.html'); } catch(Exception $ex) { // Реакция на ошибку } ?>
Выявлены баги: закрытие тега у необязательных тегов внутри URL запись оконечного тега urlset при переполнении файла / завершении сценария Необходимые исправления внесены в исходный код примера. Возможно, стоит добавить register_shutdown_function для завершения файла при закрытии, тогда не потребуется вызывать Site_Mapper::createIndex.
ну относительно ясно, но тогда если исключение передаётся наверх, то лучше было бы сделать собвсвенный класс исключений, чтобы улавливать именно эти исключения.
Можно добавить Site_Mapper_Exception extends Exception. Этим вечерком скормил сжатые sitemap-ы основного сайта Яше, Гоше, Bing, Ask и ещё нескольким поисковикам... жду возмущений с их стороны.
$_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.'system'.DIRECTORY_SEPARATOR.'files'.DIRECTORY_SEPARATOR.'any_folder'.DIRECTORY_SEPARATOR.'file.php'; VS $_SERVER['DOCUMENT_ROOT'].DS.'system'.DS.'files'.DS.'any_folder'.DS.'file.php'; скорей всего.
Для краткости лишь - при желании можно и без сокращений. У Windows слеш в одну сторону, у Unix - в другую. Когда выполняется shell-скрипт PHP (через php.exe), переменная окружения $_SERVER['DOCUMENT_ROOT'] не существует. На этот случай использую getenv('DOCUMENT_ROOT'), который возвращает пустое значение и не ворчит про отсутствующий индекс.
я бы не стал так сокращать, тем более зависеть от if (!defined(DS)) . хз, кому чего захочется определить в дс. а памяти во время работы сколько скушал? и время.
Исходный код теста: PHP: <?php Debug_Profiler::snap('Инициализация профайлера'); Debug_Profiler::snap('Уровень шума Debug_Profiler'); for ($i = 0; $i < 140000; $i++); Debug_Profiler::snap('Уровень шума цикла FOR'); Site_Mapper::setPhysicalPath(dirname(__FILE__) . DIRECTORY_SEPARATOR); Site_Mapper::setHost('http://' . getenv('SERVER_NAME')); Debug_Profiler::snap('Установка основных параметров'); $majormap = Site_Mapper::getSitemap('sample-1'); Debug_Profiler::snap('Открытие первой карты'); $minormap = Site_Mapper::getSitemap('sample-2'); Debug_Profiler::snap('Открытие второй карты'); $majormap = Site_Mapper::getSitemap('sample-2'); Debug_Profiler::snap('Повторное открытие первой карты'); for ($i = 0; $i < 140000; $i++) { $majormap->addURL('http://localhost/majorsample-1/?id=' . $i); } Debug_Profiler::snap('Завершение записи для первой карты'); for ($i = 0; $i < 140000; $i++) { $majormap->addURL('http://localhost/majorsample-2/?id=' . $i); $minormap->addURL('http://localhost/minorsample-1/?id=' . $i); } Debug_Profiler::snap('Завершение параллельной записи для первой и второй карты'); Site_Mapper::createIndex(); Debug_Profiler::snap('Создание sitemapindex'); echo Debug_Profiler::getResults(); ?> Результаты: Код (Text): 0.000000 с - файл index.php, строка 92 Объём занимаемой памяти: 455.66 Кбайт, изменение 0 байт Комментарий: Инициализация профайлера 0.000026 с - файл index.php, строка 93 Объём занимаемой памяти: 456.48 Кбайт, изменение 0 байт Комментарий: Уровень шума Debug_Profiler 0.014191 с - файл index.php, строка 97 Объём занимаемой памяти: 457.3 Кбайт, изменение 1.64 Кбайт Комментарий: Уровень шума цикла FOR 0.000805 с - файл index.php, строка 101 Объём занимаемой памяти: 487.65 Кбайт, изменение 31.99 Кбайт Комментарий: Установка основных параметров 0.000020 с - файл index.php, строка 104 Объём занимаемой памяти: 489.04 Кбайт, изменение 33.38 Кбайт Комментарий: Открытие первой карты 0.000011 с - файл index.php, строка 107 Объём занимаемой памяти: 490.36 Кбайт, изменение 34.7 Кбайт Комментарий: Открытие второй карты 0.000010 с - файл index.php, строка 110 Объём занимаемой памяти: 491.14 Кбайт, изменение 35.48 Кбайт Комментарий: Повторное открытие первой карты 0.696146 с - файл index.php, строка 117 Объём занимаемой памяти: 493.01 Кбайт, изменение 37.35 Кбайт Комментарий: Завершение записи для первой карты 1.358294 с - файл index.php, строка 125 Объём занимаемой памяти: 494.59 Кбайт, изменение 38.94 Кбайт Комментарий: Завершение параллельной записи для первой и второй карты 0.001649 с - файл index.php, строка 127 Объём занимаемой памяти: 495.43 Кбайт, изменение 39.77 Кбайт Комментарий: Создание sitemapindex Скрипт собирает карту по расписанию раз в день, думаю для него это нормально. Получил результаты от Гоши - он съел 100К+ адресов основного сайта из сжатой sitemap.xml.gz и остался весьма доволен собой да картой в целом. Тест проводился на отладочной машине: Intel Xeon E5606 @ 2.13GHz, 2134 МГц 2GB ОЗУ