Опубликован: 16.06.2010 | Уровень: специалист | Доступ: платный
Лекция 3:

Алгоритмизация сжатия текстовых файлов

< Лекция 2 || Лекция 3: 123 || Лекция 4 >

2.4. Собственная реализация сжатия со стороны сервера

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

<?php
  class mod_compress
  {
    // настройки

    // уровень сжатия
    static public $deflatelevel = 9;
    // минимальный размер ответа (в байтах), который будет
    // сжат (0 — нет ограничений)
    static public $minsize = 0;
    // максимальный размер
    static public $maxsize = 500000;
    // внутренняя переменная — поддерживает ли браузер сжатие
    static private $supported = null;
    static public function init()
    {
      // поддерживает ли браузер gzip?
      if (!self::issupportgzip()) {
        return self::$supported = false;
      }
      // каков тип ответа, тело которого будет сжимать?
      $type = self::detecttype();
      if ($type == 'unknown') {
        return self::$supported = false;
      }
      $ua = $_SERVER['HTTP_USER_AGENT'];
      // сжатие браузер поддерживает, теперь нужно
      // исключить браузер, который поддерживает его
      // с ошибками — MSIE< 6.0SP2
      if (preg_match('/MSIE [4-6](?:.(?!Opera|SV1))+/', $ua)) {
        return self::$supported = false;
      }
      // если требуется сжимать CSS/JS, то нужно
      // отфильтровать небезопасные браузеры
      if (
        $type == 'notsafe' &&
        preg_match('@Chrome/2|Konqueror|Firefox/(?:[0-2]\.|3\.0)@', $ua)
      ) {
        return self::$supported = false;
      }

        return self::$supported = true;
      }
      // посмотрим — поддерживает ли браузер gzip
      static private function issupportgzip()
      {
        foreach (preg_split('/\s*,\s*/',
        $_SERVER['HTTP_ACCEPT_ENCODING']) as $method) {
          // некоторые браузеры указывают вес
          // (предпочтения) методов сжатия, например,
          // "bzip2;q=0.9, gzip;q=0.1"
          // говорит о том, что браузер
          // хотел бы, чтобы ему отдавали контент, сжатый
          // методом bzip2,
          // но он поддерживает и gzip
          $method = explode(';', $method, 2);
          // но так как мы поддерживаем только gzip, вес
          // мы игнорируем
          if ($method[0] == 'gzip' ||
          $method[0] == 'x-gzip') {
            return true;
          }
        }
        return false;
      }
      // отделим "безопасные" типы от "небезопасных"
          static private function detecttype()
          {
            // поддерживаются не всеми браузерами
            $notsafe = array('text/css', 'text/javascript',
              'application/javascript',
              'application/x-javascript',
              'text/x-js', 'text/ecmascript',
              'application/ecmascript', 'text/vbscript',
              'text/fluffscript');
          // поддерживаются всеми браузерами


          $safe = array('text/html', 'image/x-icon',
            'text/plain',
            'text/xml', 'application/xml',
            'application/rss+xml');
          foreach (headers_list() as $header) {
            if (stripos($header, 'content-type') === 0) {
            $header = preg_split('/\s*:\s*/', $header);
            $type = strtolower($header[1]);
            if (in_array($type, $safe)) return 'safe';
            if (in_array($type, $notsafe))
            return 'notsafe';
            return 'unknown';
          }
        }
        // в случае, если Content-type не задан, считаем,
        // что это text/html
        return 'safe';
      }
      // проверка ограничний на размер
      static private function checksize($len)
      {
        if ($minsize && $len < $minsize) return false;
        if ($maxsize && $len > $maxsize) return false;
        return true;
      }
      // проверка, прошел ли запрос через прокси
      static private function checkproxy()
      {
        // в версии HTTP 1.1 есть обязательный заголовок,
        // который выставляет прокси, — Via, в HTTP/1.0
        // такого признака нет
        return $_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.0' ||  isset($_SERVER['HTTP_VIA']);
      }


      // обработчик, который решит, будет ли сжат контент,
      // и сожмет его
      static public function handler($content, $stage)
        {
          // проверим — нужно ли сжимать (не срабатывают ли
          // ограничения на размер)
          if ($content == '' ||
          !self::checksize(strlen($content))) {
          return $content;
          }
          if (self::$supported === null) {
          self::init();
          }
          // браузер не поддерживается, высылаем оригинал
          if (self::$supported === false) {
          return $content;
          }
          // этот заголовок скажет прокси-серверу и браузеру,
          // что возвращаемое нами будет иметь разное
          // содержимое в зависимости от типа браузера
          // и заголовка типа кодирования
          header('Vary: User-agent, Content-encoding');
          // запрещаем прокси сохранять содержимое,
          // для обхода прокси, не справляющихся
          // с кэшированием сжатого контента
          if (self::checkproxy()) {
            header('Cache-control: private');
          }
          return self::compress($content);
          }
          static public function compress($content)
          {
          // сжимаем текст gzip'ом
          $content = gzencode ($content, $deflatelevel,FORCE_GZIP);

          // не забываем отдать длину сжатого потока,
          // это *очень* важно: некоторые браузеры не
          // обрабатывают сжатый поток без указания длины
          header('Content-length: ' . strlen($content));
          // …и метод его кодирования
          header('Content-encoding: gzip');
          return $content;
    }
  }
  ob_start(array('mod_compress', 'handler'));
  // здесь вывод вашего скрипта

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

Класс также можно доделать, чтобы поддержать метод bzip2 (нужно проверять вхождения "bzip2", "x-bzip" или "bzip"), если ответ не является проксируемым. Для сжатия bzip2 в PHP есть одноименный модуль. Его также можно улучшить: заменить проверку наличия в заголовке gzip и сжатие на вызов ob_gzhandler, но мы этого делать не стали намеренно чтобы продемонстрировать, как это делается, если читатель захочет портировать код на другой язык.

Алгоритм работы класса следующий:

  • проверяется длина поступивших данных, если она не укладывается в ограничения, указанные в настройках, то сжатия не происходит и браузеру отдается оригинальный контент;
  • проверяется — поддерживает ли браузер gzip, если нет, отдается оригинальный контент;
  • далее идет проверка, является ли сжимаемый контент одним из текстовых типов, если нет, то он не будет сжат;
  • также контент не сжимается, если используется Internet Explorer версии, меньшей, чем 6.0 Service Pack 2, или если файл CSS или JavaScript запрашивается браузером, который не умеет корректно обрабатывать сжатые файлы этих типов;
  • если все в порядке, выставляется заголовок, указывающий, как правильно кэшировать такое содержимое и, если запрос проксируемый, указывающий прокси, что проксировать его не нужно, это позволяет избежать проблем с некоторыми типами прокси-серверов;
  • далее происходит сжатие и выставляется заголовок, указывающий длину сжатого содержимого, которое затем отдается браузеру.

2.5. Альтернативные методы сжатия

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

Мы рассмотрим два метода реализации сжатия, которые можно использовать, если встроенное сжатие в клиентском браузере реализовано с ошибками. Первый метод — это сжатие через тег canvas, второе — сжатие JavaScript, реализованное на самом JavaScript.

Первый метод предложил Джейкоб Седелин в своем блоге "Nihilogic" (http://blog.nihilogic.dk/2008/05/compression-using-canvas-and-png.html); о реализации второго метода, возможно (тут трудно установить истину), впервые задумался один из авторов этой книги, реализовав в 2001 году в рамках проекта JUnix (http://junix.kzn.ru) простенькое сжатие, которое называлось jzip.

2.5.1. Сжатие тегом canvas

canvas — предложенный фирмой Apple тег, который на данный момент входит в HTML5. Он позволяет при помощи JavaScript создавать на ограниченном тегом участке растровые изображения.

Идея сжатия, реализуемого при помощи этого тега, проста — каждый байт сжимаемого содержимого представляется как точка изображения любого формата сжатия без потерь, поддерживаемого браузером (GIF, PNG). Это изображение запрашивается с сервера и подгружается в тег canvas методом drawImage (поддерживается браузерами Firefox 1.5 и выше, Safari 2.0 и выше, Opera 9.0 и выше, а также Google Chrome).

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

Уровень сжатия таким способом кода колеблется в условных пределах от 20 до 50%. Например, библиотека jQuery версии 1.2.3 сжимается с 53 Кб до 17, что экономит 32% (для сравнения: gzip сжимает ее до 15,5 Кб, bzip2 — до 14,6 Кб).

Особенно привлекательно в этом методе то, что часть JavaScript, ответственная за получение кода на стороне клиента, очень небольшая:

var codeimg = new Image()
// когда картинка загрузится, вызовется эта функция
codeimg.onload = function () {
  var code = '' // переменная, куда будет собираться код
  var size = 119 // размер изображения (оно квадратное)
  var canvas = document.createElement("canvas")
  canvas.width = canvas.height =
  canvas.style.width = canvas.style.height = size
  var inner = canvas.getContext("2d")
  // загружаем изображение в созданный нами CANVAS
  inner.drawImage(codeimg)
  // забираем содержимое и переводим его в символы
  var data = inner.getImageData(0, 0, size, size)
  for (var i = 0, len = data.length; i<len; i+="4"
    if="" (data=""[i=""] >
  0) code += String.fromCharCode(data[i])
  }
  eval(code)
}
// указываем URL изображения, где у нас хранится код
codeimg.src = 'image-with-our-code.png'

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

Таким же методом можно паковать и CSS — в конце вместо eval нужно лишь создать в DOM тег<style> с соответствующим содержимым.

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

Библиотека Prototype (версия 1.6.0.2), сжатая до 30 Кб PNG (экономия 24%)

Рис. 2.1. Библиотека Prototype (версия 1.6.0.2), сжатая до 30 Кб PNG (экономия 24%)

У метода есть и недостатки. Во-первых, автор не использует цветовую составляющую для передачи каких-либо данных, и тут есть простор для экспериментов. Во-вторых, на текущий момент распаковка средних по размеру (200-500 Кб) данных занимает несколько секунд. В-третьих, метод поддерживается не всеми браузерами (например, не поддерживается IE). И в-четвертых, автор не позаботился о поддержке UTF-8 (что, впрочем, можно исправить).

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

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

2.5.2. Распаковка, реализованная на JavaScript

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

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

Впрочем, на этом фронте есть улучшения. Новые интерпретаторы языков JavaScript браузеров Firefox, Safari и Google Chrome показывают впечатляющие результаты. Скажем, реализация на JavaScript сжатия LZW (Lempel-Ziv-Welch, используется в GIF и PDF) (http://zapper.hodgers.com/labs/?p=90) распаковывает в этих браузерах библиотеку Prototype 1.6.0.2 (это 123 Кб) на среднем ноутбуке менее чем за одну десятую секунды.

Впрочем, десятая "Опера", последняя на данный момент, показывает куда менее интересное время — полсекунды, а Internet Explorer 8.0 — 0,3 секунды.

В будущем, если там еще будут встречаться проблемы с реализацией gzip у браузеров, можно будет использовать реализации достаточно сложных методов сжатия на JavaScript, а в настоящем же приходится довольствоваться чем-то менее ресурсоемким.

Например, программа "Packer" Дина Эдвардса

(http://dean.edwards.name/packer/), использует алгоритм, который автор назвал "Base62", потому что в кодировании используется 62 символа — большие и маленькие латинские буквы плюс цифры.

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

Этот алгоритм работает достаточно быстро для того, чтобы накладные расходы на распаковку скрадывались улучшенным временем загрузки.

< Лекция 2 || Лекция 3: 123 || Лекция 4 >
Ольга Артёмова
Ольга Артёмова

Доброго времени суток!

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

Сертификация: оптимизация и продвижение web-сайтов.

Ярославй Грива
Ярославй Грива
Россия, г. Санкт-Петербург
Ёдгор Латипов
Ёдгор Латипов
Таджикистан, Кургантепа