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());

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

Читайте также

PHP4 без XML-парсера, плюс XPath
Я недавно написал простенький парсер XML для PHP 4.3+, для случаев, когда никакого встроенного парсера XML на хостинге не оказалось. А вчера мне подумалось, что парсить XML в дерево это, конечно, замечательно, но пользоваться этим всё равно неудобно.
2010

Комментарии 11

Евгений Степанищев (bolknote.ru) автор № 3
Комментарий для 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) автор № 4
Комментарий для fantaseour.ya.ru:

Кстати, вообще-то можно запретить даже PCRE (до версии 5.3), но я не видел ни разу хостинга без него и, хочется верить, таких не будет. :)
Евгений Степанищев (bolknote.ru) автор № 5
Я тут для интересно посмотрел 27 хостеров и выписал какие XML-модули там установлены. Это, конечно, не показатель (тем более, что у большинства PHP5), но раз уж собрал статистику, пусть будет:

DOM/XML — 100%
libxml — 96%
SimpleXML — 92%
xml — 96%
xmlreader — 89%
Дмитрий Фантазеров (Смирнов) (fantaseour.ya.ru) № 9
Комментарий для Евгения Степанищева:

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

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

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

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

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

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

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

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