За последние 24 часа нас посетили 18315 программистов и 1643 робота. Сейчас ищут 1763 программиста ...

ООП: автоматизированный GZip-генератор sitemap 50К+

Тема в разделе "Решения, алгоритмы", создана пользователем Pran, 23 окт 2011.

?

Оцените решение?

  1. Отлично

    0 голосов
    0,0%
  2. Хорошо

    0 голосов
    0,0%
  3. Нормально

    0 голосов
    0,0%
  4. Плохо

    0 голосов
    0,0%
  5. Ужасно

    0 голосов
    0,0%
  1. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    При разработке движка огромного сайта рано или поздно на повестке дня возникает вопрос - где взять скрипт, который самостоятельно проследит за рутинными моментами синтаксиса sitemap.xml, разделит большой файлик на части по пятьдесят тысяч адресов и запакует всё это хозяйство в GZip?..

    Предлагаю Вашему вниманию очередную разработку в духе «сделаю-ка сам».
    Пример использования (тестировалось под IIS 7.5 + PHP 5.3.2):

    PHP:
    1.  
    2. <?php
    3.  
    4.     // Инициализируем «машину»
    5.     // Вынесено в отдельные конструкции для работы с планировщиком задач Windows
    6.     // php.exe -f путь_к_сценарию
    7.  
    8.     Site_Mapper::setPhysicalPath( 'D:/www/ ');
    9.     Site_Mapper::setHost( 'http://localhost/' );
    10.  
    11.     ##
    12.     ## Создаём sitemap
    13.     ##
    14.  
    15.     // Связать новую sitemap с индексным файлом
    16.     //
    17.     $sitemap = Site_Mapper::getSitemap('mycatalog');
    18.  
    19.     // Краткая форма
    20.     //
    21.     $sitemap->addURL('http://localhost/mycatalog/page-0001.html');
    22.  
    23.     // Запись с необязательными атрибутами
    24.     //
    25.     $sitemap->addURL('http://localhost/mycatalog/page-0002.html', array(
    26.  
    27.         'lastmod' => date('c'),
    28.         'changefreq' => 'weekly',
    29.         'priority' => '1.0'
    30.     ));
    31.  
    32.     // Представим цикл... 50000+ самостоятельно разделит на несколько sitemap-mycatalog-xxx.xml.gz,
    33.     // где xxx - порядковая нумерация.
    34.     //
    35.     $sitemap->addURL('http://localhost/mycatalog/page-0003.html');
    36.  
    37.     ##
    38.     ## Завершаем сей процесс созданием индексной карты sitemap, т.н. sitemapindex:
    39.     ##
    40.  
    41.     // Индексный файл записывает ссылки на части карты, начиная их со значения setHost
    42.     Site_Mapper::createIndex();
    43.  
    44. ?>
    45.  
    Вот и всё. При наличии разрешения на запись в соответствующую директорию у Вас появятся несколько архивов:

    • sitemap.xml.gz - индексный файл
    • sitemap-mycatalog-0001.xml.gz
    • sitemap-mycatalog-0002.xml.gz (при наличии до краёв (50К) заполненного адресами mycatalog-001)

    Остаётся только отправить на индексацию файл sitemap.xml.gz

    Можно создавать несколько sitemap подряд:
    PHP:
    1.  
    2. <?php
    3.  
    4.     ##
    5.     ## При необходимости создаём ещё одну карту для другого каталога:
    6.     ##
    7.  
    8.     // Связать новую sitemap с индексным файлом
    9.     //
    10.     $sitemap = Site_Mapper::getSitemap('mycatalog');
    11.     $sitemap->addURL('/mycatalog/page-0001.html');
    12.     $sitemap->addURL('/mycatalog/page-0002.html');
    13.  
    14.     // Взять существующую или ранее созданную sitemap (если уже вызывалась за время выполнения)
    15.     //
    16.     $sitemap = Site_Mapper::getSitemap('product');
    17.     $sitemap->addURL('/product/page-0001.html');
    18.  
    19. ?>
    20.  
    Код класса:
    PHP:
    1.  
    2. <?php
    3.  
    4.     // Preconditions
    5.     //
    6.     if (!defined(DS))
    7.     {
    8.         define(DS, DIRECTORY_SEPARATOR);
    9.     }
    10.  
    11.     // Построитель множественных карт Sitemap
    12.     //
    13.     class Site_Mapper {
    14.  
    15.         const PREFIX = 'sitemap-';
    16.         const EXTENSION = '.xml.gz';
    17.  
    18.         protected static $filenames;
    19.  
    20.         protected static $modified;
    21.         protected static $path;
    22.         protected static $host;
    23.  
    24.         protected $enumerator;
    25.         protected $name;
    26.  
    27.         protected $resource;
    28.         protected $level;
    29.  
    30.         protected function __construct($name) {
    31.  
    32.             $this->name = self::PREFIX . $name . '-';
    33.         }
    34.  
    35.         public function addURL($loc, array $params = array())
    36.         {
    37.             if ($this->level >= 50000)
    38.             {
    39.                 gzwrite($this->resource, '</urlset>');
    40.                 gzclose($this->resource);
    41.                 $this->level = 0;
    42.             }
    43.  
    44.             if (!$this->level)
    45.             {
    46.                 if (self::$path)
    47.                 {
    48.                     // Инкрементирует счётчик, вносит имя файла в реестр и открывает его на запись.
    49.                     //
    50.                     $filename = $this->name . str_pad(++$this->enumerator, 3, '0', STR_PAD_LEFT) . self::EXTENSION;
    51.    
    52.                     if ($this->resource = gzopen(self::$path . $filename, 'w9'))
    53.                     {
    54.                         gzwrite($this->resource, '<?xml version="1.0" encoding="UTF-8" ?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
    55.                         self::$filenames[] = $filename;
    56.                     }
    57.                     else
    58.                     {
    59.                         throw new Eception("Ошибка при создании файла {$filename}");
    60.                     }
    61.                 }
    62.                 else
    63.                 {
    64.                     throw new Exception('Не установлено расположение файла, необходимо использовать метод '. __CLASS__ .'::setPhysicalPath');
    65.                 }
    66.             }
    67.  
    68.             $tmp = '<url><loc>' . $loc . '</loc>';
    69.  
    70.             foreach ($params as $key => $value)
    71.             {
    72.                 $tmp .= "<{$key}>{$value}</{$key}>";
    73.             }
    74.  
    75.             $tmp .= '</url>';
    76.  
    77.             gzwrite($this->resource, $tmp);
    78.  
    79.             ++$this->level;
    80.             if (!self::$modified) self::$modified = true; # Добавлена строка, необходима (пере)запись индексной карты.
    81.         }
    82.  
    83.         public function __destruct() {
    84.  
    85.             if ($this->resource)
    86.             {
    87.                 gzwrite($this->resource, '</urlset>');
    88.                 gzclose($this->resource);
    89.             }
    90.         }
    91.  
    92.         public static function setPhysicalPath($path) {
    93.  
    94.             if (self::$path !== $path)
    95.             {
    96.                 if (self::$modified)
    97.                 {
    98.                     self::createIndex(); # Завершает активную сессию.
    99.                 }
    100.  
    101.                 self::$path = $path;
    102.             }
    103.         }
    104.  
    105.         public static function setHost($host) {
    106.  
    107.             self::$host = rtrim($host, '/') . '/';
    108.         }
    109.  
    110.         public static function getSitemap($name) {
    111.  
    112.             static $sitemaps;
    113.  
    114.             // Класс использует свою динамическую часть в качестве производной.
    115.             //
    116.             return isset($sitemaps[$name]) ? $sitemaps[$name] : $sitemaps[$name] = new self($name);
    117.         }
    118.  
    119.         public static function createIndex() {
    120.  
    121.             if (self::$modified)
    122.             {
    123.                 $lastmod = date('c');
    124.  
    125.                 if (!isset(self::$host))
    126.                 {
    127.                     throw new Exception('Не установлен хост, необходимо использовать метод '. __CLASS__ .'::setHost');
    128.                 }
    129.  
    130.                 if ($resource = gzopen(self::$path . DS . 'sitemap' . self::EXTENSION, 'w9'))
    131.                 {
    132.                     gzwrite($resource, '<?xml version="1.0" encoding="UTF-8" ?><sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
    133.  
    134.                     foreach (self::$filenames as $filename)
    135.                     {
    136.                         gzwrite($resource, '<sitemap><loc>' . self::$host . $filename . '</loc><lastmod>' . $lastmod . '</lastmod></sitemap>');
    137.                     }
    138.  
    139.                     gzwrite($resource, '</sitemapindex>');
    140.                 }
    141.  
    142.                 // Вызов createIndex не будет иметь эффекта
    143.                 // до очередного срабатывания Site_Mapper::addURL
    144.                 //
    145.                 self::$modified = false;
    146.             }
    147.         }
    148.     }
    149.  
    150. ?>
    151.  
    152.  
    Данное решение находится в разработке, если у Вас есть замечания или советы по улучшению - предлагаю совместными усилиями довести генератор до идеального состояния :)

    В несжатом виде на 50К выходит 2 MiB, в сжатом - 6 KiB.
     
  2. Михаил

    Михаил Активный пользователь

    С нами с:
    12 июл 2009
    Сообщения:
    545
    Симпатии:
    0
    Адрес:
    Bielarus
    чего у вас конструктор protected?
    чего статиков так много зачем они вообще?
    я вижу также throw но не вижу try/catch почему?
     
  3. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Конструктор сделан 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:
    1.  
    2. <?php
    3.  
    4. try
    5. {
    6.      ##
    7.     ## При необходимости создаём ещё одну карту для другого каталога:
    8.     ##
    9.  
    10.      // Связать новую sitemap с индексным файлом
    11.      //
    12.      $sitemap = Site_Mapper::getSitemap('mycatalog');
    13.      $sitemap->addURL('/mycatalog/page-0001.html');
    14.      $sitemap->addURL('/mycatalog/page-0002.html');
    15.  
    16.      // Взять существующую или ранее созданную sitemap (если уже вызывалась за время выполнения)
    17.      //
    18.      $sitemap = Site_Mapper::getSitemap('product');
    19.      $sitemap->addURL('/product/page-0001.html');
    20. }
    21. catch(Exception $ex)
    22. {
    23.       // Реакция на ошибку
    24. }
    25. ?>
    26.  
     
  4. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Выявлены баги:
    • закрытие тега у необязательных тегов внутри URL
    • запись оконечного тега urlset при переполнении файла / завершении сценария

    Необходимые исправления внесены в исходный код примера. Возможно, стоит добавить register_shutdown_function для завершения файла при закрытии, тогда не потребуется вызывать Site_Mapper::createIndex.
     
  5. Михаил

    Михаил Активный пользователь

    С нами с:
    12 июл 2009
    Сообщения:
    545
    Симпатии:
    0
    Адрес:
    Bielarus
    ну относительно ясно, но тогда если исключение передаётся наверх, то лучше было бы сделать собвсвенный класс исключений, чтобы улавливать именно эти исключения.
     
  6. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Можно добавить Site_Mapper_Exception extends Exception. Этим вечерком скормил сжатые sitemap-ы основного сайта Яше, Гоше, Bing, Ask и ещё нескольким поисковикам... жду возмущений с их стороны.
     
  7. iliavlad

    iliavlad Активный пользователь

    С нами с:
    24 янв 2009
    Сообщения:
    1.689
    Симпатии:
    4
    зачем так назначать, а не использовать DIRECTORY_SEPARATOR ?
     
  8. Апельсин

    Апельсин Активный пользователь

    С нами с:
    20 мар 2010
    Сообщения:
    3.645
    Симпатии:
    2
    $_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';

    скорей всего.
     
  9. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Для краткости лишь - при желании можно и без сокращений. У Windows слеш в одну сторону, у Unix - в другую.

    Когда выполняется shell-скрипт PHP (через php.exe), переменная окружения $_SERVER['DOCUMENT_ROOT'] не существует. На этот случай использую getenv('DOCUMENT_ROOT'), который возвращает пустое значение и не ворчит про отсутствующий индекс.
     
  10. iliavlad

    iliavlad Активный пользователь

    С нами с:
    24 янв 2009
    Сообщения:
    1.689
    Симпатии:
    4
    я бы не стал так сокращать, тем более зависеть от if (!defined(DS)) . хз, кому чего захочется определить в дс.
    а памяти во время работы сколько скушал? и время.
     
  11. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Исходный код теста:

    PHP:
    1.  
    2. <?php
    3.  
    4.     Debug_Profiler::snap('Инициализация профайлера');
    5.     Debug_Profiler::snap('Уровень шума Debug_Profiler');
    6.  
    7.     for ($i = 0; $i < 140000; $i++);
    8.  
    9.     Debug_Profiler::snap('Уровень шума цикла FOR');
    10.  
    11.     Site_Mapper::setPhysicalPath(dirname(__FILE__) . DIRECTORY_SEPARATOR);
    12.     Site_Mapper::setHost('http://' . getenv('SERVER_NAME'));
    13.     Debug_Profiler::snap('Установка основных параметров');
    14.  
    15.     $majormap = Site_Mapper::getSitemap('sample-1');
    16.     Debug_Profiler::snap('Открытие первой карты');
    17.  
    18.     $minormap = Site_Mapper::getSitemap('sample-2');
    19.     Debug_Profiler::snap('Открытие второй карты');
    20.  
    21.     $majormap = Site_Mapper::getSitemap('sample-2');
    22.     Debug_Profiler::snap('Повторное открытие первой карты');
    23.  
    24.     for ($i = 0; $i < 140000; $i++)
    25.     {
    26.         $majormap->addURL('http://localhost/majorsample-1/?id=' . $i);
    27.     }
    28.  
    29.     Debug_Profiler::snap('Завершение записи для первой карты');
    30.  
    31.     for ($i = 0; $i < 140000; $i++)
    32.     {
    33.         $majormap->addURL('http://localhost/majorsample-2/?id=' . $i);
    34.         $minormap->addURL('http://localhost/minorsample-1/?id=' . $i);
    35.     }
    36.  
    37.     Debug_Profiler::snap('Завершение параллельной записи для первой и второй карты');
    38.     Site_Mapper::createIndex();
    39.     Debug_Profiler::snap('Создание sitemapindex');
    40.  
    41.     echo Debug_Profiler::getResults();
    42.  
    43. ?>
    44.  
    Результаты:

    Код (Text):
    1.  
    2.     0.000000 с - файл index.php, строка 92
    3.         Объём занимаемой памяти: 455.66 Кбайт, изменение 0 байт
    4.         Комментарий: Инициализация профайлера
    5.  
    6.     0.000026 с - файл index.php, строка 93
    7.         Объём занимаемой памяти: 456.48 Кбайт, изменение 0 байт
    8.         Комментарий: Уровень шума Debug_Profiler
    9.  
    10.     0.014191 с - файл index.php, строка 97
    11.         Объём занимаемой памяти: 457.3 Кбайт, изменение 1.64 Кбайт
    12.         Комментарий: Уровень шума цикла FOR
    13.  
    14.     0.000805 с - файл index.php, строка 101
    15.         Объём занимаемой памяти: 487.65 Кбайт, изменение 31.99 Кбайт
    16.         Комментарий: Установка основных параметров
    17.  
    18.     0.000020 с - файл index.php, строка 104
    19.         Объём занимаемой памяти: 489.04 Кбайт, изменение 33.38 Кбайт
    20.         Комментарий: Открытие первой карты
    21.  
    22.     0.000011 с - файл index.php, строка 107
    23.         Объём занимаемой памяти: 490.36 Кбайт, изменение 34.7 Кбайт
    24.         Комментарий: Открытие второй карты
    25.  
    26.     0.000010 с - файл index.php, строка 110
    27.         Объём занимаемой памяти: 491.14 Кбайт, изменение 35.48 Кбайт
    28.         Комментарий: Повторное открытие первой карты
    29.  
    30.     0.696146 с - файл index.php, строка 117
    31.         Объём занимаемой памяти: 493.01 Кбайт, изменение 37.35 Кбайт
    32.         Комментарий: Завершение записи для первой карты
    33.  
    34.     1.358294 с - файл index.php, строка 125
    35.         Объём занимаемой памяти: 494.59 Кбайт, изменение 38.94 Кбайт
    36.         Комментарий: Завершение параллельной записи для первой и второй карты
    37.  
    38.     0.001649 с - файл index.php, строка 127
    39.         Объём занимаемой памяти: 495.43 Кбайт, изменение 39.77 Кбайт
    40.         Комментарий: Создание sitemapindex
    Скрипт собирает карту по расписанию раз в день, думаю для него это нормально. Получил результаты от Гоши - он съел 100К+ адресов основного сайта из сжатой sitemap.xml.gz и остался весьма доволен собой да картой в целом.

    Тест проводился на отладочной машине:
    Intel Xeon E5606 @ 2.13GHz, 2134 МГц
    2GB ОЗУ