Пишу, по большей части, про историю, свою жизнь и немного про программирование.

PHP4 без XML-парсера

Мне тут по работе пришлось решить странную, по нынешним временам задачу — принимать XML в программе на PHP4.3+, где никакого парсера XML может не оказаться. Таковы реалии продукта и среды, где он будет использоваться (это код внешнего проекта). Поэтому мне пришлось написать небольшой парсер, который, правда, расcчитывает, что XML будет well-formed (я его получаю из доверенного источника).

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

<?
// Very Simple XML SAX Parser by Evgeny Stepanischev
// /
class VerySimple_XMLParser
{
    var $saxCallback;

    function VerySimple_XMLParser($saxCallback)
    {
        $this->saxCallback = $saxCallback;
    }

    function parse($xml)
    {
        if (preg_match_all('#<![CDATA[.*?]]>|</?[^>]*>|[^<]+#us', $xml, $matches)) {
            foreach ($matches[0] as $match) {
                $this->parseThis($match);
            }
        }
    }

    function parseThis($match)
    {
        $calls = array(
            '<!--'      => 'COMMENT',
            '<?'        => 'PI',
            '<![CDATA[' => 'CDATA',
            '<!'        => 'DTD',
            '</'        => 'TAGCLOSE',
            '<'         => 'TAGOPEN',
        );

        foreach ($calls as $need => $type) {
            if (strpos($match, $need) === 0) {
                return $this->parseByType($type, $match);
            }
        }

        return $this->parseByType('OTHER', $match);
    }

    function parseByType($type, $text)
    {
        if ($type == 'COMMENT') {
            return $this->addChunk($type, substr($text, 4, -3));
        }

        if ($type == 'CDATA') {
            return $this->addChunk('TEXT', substr($text, 9, -3));
        }

        if ($type == 'OTHER') {
            return $this->addChunk('TEXT', $text);
        }

        if ($type == 'TAGCLOSE') {
            $end = strcspn($text, ' >');

            return $this->addChunk('TAGCLOSE', substr($text, 2, $end - 2));
        }

        if ($type == 'TAGOPEN') {
            $this->addChunk('TAGOPEN', $this->parseTag($text));

            if (substr($text, -2) == '/>') {
                $end = strcspn($text, ' >');

                return $this->addChunk('TAGCLOSE', substr($text, 1, $end - 2));
            }
        }

        return '';
    }

    function parseTag($text)
    {
        $end = strcspn($text, ' />');
        $name = substr($text, 1, $end - 1);

        return array($name, $this->parseTagAttributes(substr($text, $end)));
    }

    function parseTagAttributes($text)
    {
        $nameStart = '(?:[A-Za-z_:]|[^\x00-\x7F])';
        $nameChar  = '(?:[A-Za-z0-9_:\.-]|[^\x00-\x7F])';
        $attrValue = '(?:\'[^\']*\'|"[^"]*")';

        if (preg_match_all("/{$nameStart}{$nameChar}*=$attrValue/us", $text, $matches)) {
            return $this->parseAttributes($matches[0]);
        }

        return array();
    }

    function parseAttributes($rawAttributes)
    {
        $attributes = array();

        foreach ($rawAttributes as $rawAttr) {
            list($name, $value) = explode('=', $rawAttr, 2);
            if ($value[0] == '"' || $value[0] == "'") {
                $value = substr($value, 1, -1);
            }

            $attributes[$name] = $value;
        }

        return $attributes;
    }

    function addChunk($type, $arg)
    {
        call_user_func($this->saxCallback, $type, $arg);
    }
}

// Пример использования

$xml =<<<XML
<!-- комментарий -->
<?xml version="1.0"?>
<root attribute="a.a" v="a">
    <v><![CDATA[data]]></v>
    <v>data</v>
    <v/>
</root>
XML;

// Понятно, что можно написать класс и передать в качестве
// callback в парсер метод объекта, но я для простоты, как пример,
// написал очень простую функцию
function VerySimple_XMLParser_Collector()
{
    static $tree = array();

    if (func_num_args() == 2) {
        $tree[] = func_get_args();
    } else {
        return $tree;
    }
}

$sax = &new VerySimple_XMLParser('VerySimple_XMLParser_Collector');
$sax->parse($xml);

echo '<plaintext>';
var_dump(VerySimple_XMLParser_Collector());

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

11 комментариев
Дмитрий Фантазеров (Смирнов) (fantaseour.ya.ru) 2010

expat был вроде с древнейших времен?

Дмитрий Фантазеров (Смирнов) (fantaseour.ya.ru) 2010

вот этот:
http://ru.php.net/manual/en/book.xml.php

мне его на юбых шаредах добавляли по просьбе

Евгений Степанищев (bolknote.ru) 2010

Комментарий для fantaseour.ya.ru:

«This extension is enabled by default. It may be disabled by using the following option at compile time: -​-​disable-xml»

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

Евгений Степанищев (bolknote.ru) 2010

Комментарий для fantaseour.ya.ru:

Кстати, вообще-то можно запретить даже PCRE (до версии 5.3), но я не видел ни разу хостинга без него и, хочется верить, таких не будет. :)

Евгений Степанищев (bolknote.ru) 2010

Я тут для интересно посмотрел 27 хостеров и выписал какие XML-модули там установлены. Это, конечно, не показатель (тем более, что у большинства PHP5), но раз уж собрал статистику, пусть будет:

DOM/XML — 100%
libxml — 96%
SimpleXML — 92%
xml — 96%
xmlreader — 89%

hshhhhh.name 2010

Комментарий для Евгения Степанищева:

http://spectator.ru/technology/php/simple_XML
:D

Евгений Степанищев (bolknote.ru) 2010

Комментарий для hshhhhh.name:

Это псевдоXML, а у меня не псевдо.

Евгений Степанищев (bolknote.ru) 2010

Комментарий для fantaseour.ya.ru:

Коля Мациевский ( http://sunnybear.habrahabr.ru/ ) подтвердил (а он сталкивается по работе с самыми странными хостингами), что на наличие XML-парсера лучше не рассчитывать.

Дмитрий Фантазеров (Смирнов) (fantaseour.ya.ru) 2010

Комментарий для Евгения Степанищева:

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

кстати в 4-ке был XSLT, в виде модуля sablotron, который был очень капризный и ставился не легко. А я как раз тогда млел от XML-XSLT и написал какой-то усеченный процессинг, которого мне хватало, как раз пользуясь expat. И так потом оно довольно долго за мной ездило и требовало expat на любом шареде. И если бы этот expat не ставили по первой же просьбе, я бы с этого костыля слез значительно раньше.

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

Дмитрий Фантазеров (Смирнов) (fantaseour.ya.ru) 2010

sax парсер кмк незаменим при защите от XSS, как у Кукуца когда-то было в SafeHTML

Евгений Степанищев (bolknote.ru) 2010

Комментарий для fantaseour.ya.ru:

Может Коля просто не просил?

Не просят Колины клиенты, а не Коля.

sax парсер кмк незаменим при защите от XSS, как у Кукуца когда-то было в SafeHTML

Я тоже вхожу в ряд авторов SafeHTML :) Там использовался HTMLSax3 из PEAR, он намного сложнее моего парсера и может разбирать даже очень покорёженный HTML.