Пишем свой XML-парсер

    Предыстория


    Решив запустить небольшой сервис на подаренном мне хостинге, оказалось, что там нету ни одного xml-парсера: ни SimpleXML, ни DOMXML, а только libxml и xml-rpc. Недолго думая, я решил написать свой. Мне требовался разбор не сложных rss-лент, поэтому хватило достаточно просто класса xml => array.[1]

    Но для интересной статьи этого было явно не достаточно, поэтому сейчас мы напишем свою замену для SimpleXML. А заодно пробежимся по многим интересным возможностям PHP 5.

    Постановка задачи


    Доступ к элементам у нас будет осуществляться как доступ к свойствам класса, например $xml->element, а доступ к атрибутам элемента, как к массиву, те $xml->element['attr'], также реализуем проверку на существование атрибута при помощи isset() и итерацию по элементам при помощи foreach. И так, начнем.


    Немного магии?


    В PHP 5 для классов определены некоторые ‘магические’ методы, они начинаются с двойного подчеркивания ‘__’ и вызываются при происхождении определенного действия.[2] Нам понадобятся следующие:
    • void __construct ([ mixed $args [, $... ]] ) — самый известный магический метод, вызывается после создания класса оператором new.
    • mixed __get ($name) – вызывается при обращении к свойствам класса, если соответствующее поле не было найдено, например $obj->element вызовет __get('element'), если element не был объявлен как поле класса.
    • void __set ($name, $value) – соответственно вызывается при изменении свойства класса, например $obj->element = $some_var вызовет __set('element', $some_var).
    • string __toString() — вызывается при любых операциях над классом, как над строкой, допустим echo $obj или strval($obj). Этот метод нам потребуется для получения содержимого элемента. К сожалению, методов возвращающих не строку нету, поэтому чтобы преобразовать элемент в число придется делать так: intval(strval($obj)).


    SPL


    Standard PHP Library – стандартная библиотека PHP, как и STL из мира C++, создавалась для того, чтобы дать разработчику инструменты для решения типовых задач.[3]
    Нам потребуется реализовать следующие интерфейсы:
    • ArrayAccess – для доступа к классу, как к массиву, например $obj['name'] или isset($obj['name']).
    • IteratorAggregate – для возможности итерации по классу при помощи foreach.
    • Countable — чтобы узнать количество потомков у элемента.


    XML и expat


    Это стандартные библиотеки для работы с XML и создания XML-парсеров.[4] То, что надо для решения нашей задачи. Ради интереса можете написать разбор xml-файла вручную, допустим на регулярных выражениях.
    Больше всего в expat нас интересуют следующие функции:
    • bool xml_set_element_handler (resource $parser, callback $start_element_handler, callback $end_element_handler) – устанавливает функции, вызываемые при нахождении открытого и закрытого тегов соответственно.
    • bool xml_set_character_data_handler (resource $parser, callback $handler) – вызывает функцию, передавая ей символьное содержание элемента, причем даже если там ничего не было, она все равно вызывается.

    Примечание: callback в php это либо имя функции, переданное как строка, либо массив с двумя значениями – первое это название класса, а второе название метода этого класса.

    Указатели


    Указатели в PHP работают не совсем так, как в C или в C++.[5] Фактически, конструкция $a =& $b всего лишь означает, что теперь $a указывает на ту же область с данными, что и $b, причем изменить адрес куда указывает $b через $a невозможно, те можно сказать, что изменение адреса имеет один уровень вложенности.
    Начиная с пятой версии, в PHP все переменные передаются в функцию по указателю, но как только вы изменяете ее значение – выделяется память под новую. В нашем случае указатели пригодятся для указания на родительский элемент.

    Кодинг


    С теорией закончили, теперь приступим непосредственно к написанию парсера.
    Каждый объект будет представлять один xml-элемент, поэтому ему потребуются такие свойства, как имя тега, атрибуты, данные, ссылка на родителя и массив с потомками, кроме того, потребуется переменная-указатель на текущий элемент. Из методов нам потребуется реализовать все интерфейсы, добавление потомка, установку ссылки на родителя, присвоение содержимого элемента и три функции, требуемые для парсера — открытие и закрытие тега и получение содержимого элемента.
    Сделаем набросок будущего класса:
    class XML implements ArrayAccessIteratorAggregateCountable {

        
    private $pointer;

        
    private $tagName;


        
    private $attributes = array();

        
    private $cdata;

        
    private $parent;

        
    private $childs = array();

        

        
    public function __construct($data) { }


        

        
    public function __toString() { return; }

        

        
    public function __get($name) { return; }


        

        
    public function offsetGet($offset) { return; }

        

        
    public function offsetExists($offset) { return; }


        

        
    public function offsetSet($offset$value) { return; }

        
    public function offsetUnset($offset) { return; }


        

        
    public function count() { return; }

        

        
    public function getIterator() { return; }

        


        
    public function appendChild($tag$attributes) { return; }

        

        
    public function setParent(XML $parent) {}


        

        
    public function getParent() { return; }

        

        
    public function setCData($cdata) {}

        


        
    private function parse($data) {}

       

        
    private function tag_open($parser$tag$attributes) {}



        
    private function cdata($parser$cdata) {}


        
    private function tag_close($parser$tag) {}


    }


    Теперь примемся за реализацию функций. По порядку, начнем с конструктора. В нашем случае он может принимать два типа значений – строку (xml) или массив из двух элементов (название элемента, атрибуты), так как перегрузки одного метода с разными параметрами в php нету – придется вручную проверять тип.
    public function __construct($data) {

        if (
    is_array($data)) {

            list(
    $this->tagName$this->attributes) = $data;


        } else if (
    is_string($data))

            
    $this->parse($data);

    }


    Как уже упоминалось – при помощи магического метода __toString() пользователь сможет получить данные элемента в виде строки, а затем преобразовать ее в любой требуемый ему тип, к сожалению, напрямую возвращать, что хочется, не получится, поэтому только так.
    Заодно разберем следующий магический метод __get($name), при помощи него будет осуществляться доступ к потомкам текущего элемента. Вполне логично, что если потомок всего лишь один, то его сразу и вернуть, без необходимости обращаться по 0 индексу массива. Например: $xml->rss->channel->item[5]->url, вместо $xml->rss[0]->channel[0]->item[5]->url[0], если элементы rss, channel и url существуют в единственном экземпляре на своем уровне вложенности.
    public function __toString() {

        return 
    $this->cdata;

    }

        

    public function __get($name) {


        if (isset(
    $this->childs[$name])) {

            if (
    count($this->childs[$name]) == 1)


                return 
    $this->childs[$name][0];

            else

                return 
    $this->childs[$name];


        } 

        
    throw new Exception(«UFO steals [$name]!»);

    }



    Функции offsetGet, offsetExists, offsetSet и offsetUnset реализуют интерфейс ArrayAccess, для доступа к объекту как к массиву. Мы его используем для доступа к атрибутам элемента. offsetSet и offsetUnset оставим пока заглушками.
    public function offsetGet($offset) {

        if (isset(
    $this->attributes[$offset]))


            return 
    $this->attributes[$offset];

        
    throw new Exception(«Holy cow! There is'nt [$offset] attribute!»);

    }


        

    public function offsetExists($offset) {

        return isset(
    $this->attributes[$offset]);

    }


    А теперь мы столкнулись с проблемой из-за принятого недавно решения. Если вдруг мы захотим запустить цикл foreach по единственному элементу, то он запустится по самому xml-объекту! Поэтому придется пожертвовать возможностью простым способом использовать foreach для атрибутов элемента и реализовать метод getAttributes(). А итератор и количество элементов мы будем возвращать для массива элементов, к которому принадлежит вызываемый, а если у него нету родителя, то итератор по массиву из одного текущего элемента. Таким образом, будут реализованы интерфейсы IteratorAggregate и Countable.
    public function count() {

        if (
    $this->parent != null)

            return 
    count($this->parent->childs[$this->tagName]);


        return 
    1;

    }

        

    public function getIterator() {

        if (
    $this->parent != null)


            return new 
    ArrayIterator($this->parent->childs[$this->tagName]);

        return new 
    ArrayIterator(array($this));


    }


    Добавление потомка простая функция, интересно в ней разве только то, что после добавления элемента, она возвращает ссылку на него.
    public function appendChild($tag$attributes) {

        
    $element = new XML(array($tag$attributes));


        
    $element->setParent($this);

        
    $this->childs[$tag][] = $element;

        return 
    $element;


    }

    Теперь реализуем сам парсер. Для создания древовидной структуры будем использовать указатель на текущий элемент. В начале он устанавливается непосредственно на текущий элемент, при открытии тега – на открытый элемент, для того, чтобы все содержащиеся в нем элементы добавились ему к потомкам, а при закрытии тега – на его родительский элемент.
    private function parse($data) {

        
    $this->pointer =& $this;

        
    $parser xml_parser_create();


        
    xml_set_object($parser$this);

        
    xml_parser_set_option($parserXML_OPTION_CASE_FOLDINGfalse);

        
    xml_set_element_handler($parser«tag_open»«tag_close»);


        
    xml_set_character_data_handler($parser«cdata»);

        
    xml_parse($parser$data);

    }

       

    private function tag_open($parser$tag$attributes) {


        
    $this->pointer =& $this->pointer->appendChild($tag$attributes);

    }


    private function cdata($parser$cdata) {


        
    $this->pointer->setCData($cdata);

    }


    private function tag_close($parser$tag) {


        
    $this->pointer =& $this->pointer->getParent();

    }



    Все. Парсер готов к работе. Дабы не раздувать статью еще больше, полностью исходный код с комментариями я загрузил на Google Docs и пример использования тоже.[6]

    Что дальше?


    Это все еще не полная замена для SimpleXML, наш парсер до сих пор не умеет создавать xml-документ из данных, находящихся в нем. Добавление нужных функций не сложная задача, поэтому я ее оставлю, для тех, кому это интересно, как домашнее задание :)

    Ссылки


    1) Первая версия xml=>array парсера.
    2) Документация по магическим методам (eng) (рус).
    3) Документация по SPL.
    4) Описание функций xml-парсера.
    5) Документация по указателям (eng) (рус).
    6) Окончательная версия парсера и простой пример использования.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 42

      0
      спасибо, интересно )
        +2
        1)
        $this->pointer = &$this;
        public function &appendChild($tag, $attributes)
        в php5 обьекты передаются по ссылке, spl требует php5, зачем ставить ссылку? (сами же об этом пишите)
        2)
        стоит учитывать что __get не сработает при обращении к
        private $pointer;
        private $tagName;
        private $attributes = array();
        private $cdata;
        private $parent;
        private $childs = array();

        а значит некоторые теги придется "потерять"
        3)
        не плохо бы ввести методы __isset() и __unset()

        ps интересующимся могу предложить вариант написанный с использованием строковых функций парсер xml 2 array() http://weblancer.net/download/portfolio/…
          0
          1) передается, но если специально не указать, то объект будет склонирован, при попытке изменения, о чем я тоже написал
          2) да, глупость, можно поставить перед именами переменных '_'
          3) это просто и самому добавить - там просто :) статья писалась во-первых, чтобы пробежаться по всем возможностям пхп
            +1
            1) http://www.php.net/manual/ru/language.oo… пример 6.
            2) извиняюсь, тут arrayAccess вмешивается, но в общем случае именно так.
            class xxx
            {
            private $test = 123;
            function ___get($Name)
            {
            echo $Name;
            }
            }
            $q = new xxx();
            $q->test;


            про xml 2 array в моем посте я упомянул, потому что обертку к libxml нельзя называть парсером, парсер - прямая работа с xml документом. см код.
              0
              1) Ну да, и этот пример как раз показывает, что иногда смысл ставить & всё же бывает. Например, когда вы передаёте какой-то объект и хотите, чтобы в случае замены изначального объекта был заменён и переданный объект. Хотя подобное бывает нужно крайне редко.
            0
            2) только-что проверил - срабатывает, на 5.2.1

            а про xml=>array в начале поста я написал, см 1 ссылку
            +8
            Статья интересная и познавательная, но начало "недолго думая, я решил написать свой" меня от души повеселило!
            Интересно, что бы вы делали, если бы на хостинге не оказалось PHP? А веб-сервера?
            Не стоит изобретать велосипед, проще найти нормальный хостинг где есть всё, что нужно для вашего проекта.
            Такие усилия оправданы только в том случае, если изначально перед вами стояла задача написать собственный XML-парсер на PHP, а не какой-то коммерческий веб-сайт.
              0
              Велосипед этот изобретался во-первых ради интереса :)
              К тому же проект этот делался "для себя"
              Кстати тех-поддержка этого, на предложение поставить нужные модули ответила достаточно интересно:

              I understand that you want to enable xml file, I am very sorry but we were advise by our seniors that we dont support xml file as of now because it gives an error on our php but our seniors are working on it to enable it.
                0
                Бежать без оглядки от таких хостеров.
                  +5
                  Смените хостинг, от души советую.
                  У меня просто слёзы на глаза наворачиваются, когда я читаю такие вот "отписки" от саппорта :)
                  Если эти люди не в стостоянии собрать PHP с поддержкой DOM (которая, к слову, по умолчанию включена), я бы им даже крохотный "длясебяшный" проект не доверил...

                  Кстати, про статью. Вы лукавите :) Парсер вы не написали, вы написали только удобный интерфейс к нему. А libxml вы перепутали с XML-парсером expat - именно он предоставляет используемые вами функции xml_set_element_handler() и xml_set_character_data_handler().

                  Но ценность статьи для разминки мозгов это не уменьшает :)
                    +1
                    Сеньоры работают. Не судите их строго :)
                      0
                      seniors - это "старшаки" по-нашему? :)
                      Они бы еще пхп убрали, потому что небезопасен, апач - потому что 80-й порт открыт и вообще могут заддосить, ну и мускул может нагрузку на сервер создать - тоже снести.
                  • UFO just landed and posted this here
                      0
                      Вот не понимаю - зачем изобретать велосипед?
                        +1
                        Прочитайте хотя-бы до хабраката.
                        Это полезно и интересно - если не сумеешь изобрести велосипед, то какая речь может идти о машинах :)
                      • UFO just landed and posted this here
                          0
                          Никогда не пишите, что бы вы могли сделать, если б могли =)
                          +1
                          Спасибо интересено. Вот только по всей статье слово "указатель" лучше заменить на "ссылка".
                          Указателей в php нет :)
                            0
                            Мне понравилось как MS реализовала LINQ to XML. Не то чтобы что-то новое, но довольно так аккуратно.
                              0
                              public function offsetGet($offset) {
                              if (isset($this->attributes[$offset]))
                              return $this->attributes[$offset];
                              throw new Exception("Атрибута [$offset] не существует");
                              }

                              public function offsetExists($offset) {
                              return isset($this->attributes[$offset]);
                              }
                              --------------------------------------------------------
                              Метод offsetGet необходимо переписать с учетом существования метода offsetExists
                                0
                                На всякий случай:
                                SimpleXML, DOM, DOMXML в PHP5 это три разных модуля действительно работающие поверх упомянутой библиотеки libxml, причем модуль DOMXML это атавизм,
                                указанные вами функции
                                xml_set_character_data_handler
                                xml_set_element_handler
                                ни к указанным модулям PHP5, ни к libxml не имеют никакого отношения.

                                Не засоряйте людям мозги, если сами не разобрались
                                  +1
                                  правильно подправили, не libxml, а expat,
                                  +1
                                  Спасибо. Как раз мне нужно для одного документа создать дерево объектов наподобие DOM с простым доступом X->Y->Z. Теперь знаю как делать.
                                    –1
                                    единственное примечание:
                                    "поэтому" - слитно
                                    "по порядку" - без дефиса

                                    не поймите неправильно, сбивает просто при чтении
                                    а статья классная, спасибо
                                      +2
                                      Статья сбивает с толку. Полнейшая путаница с XML. Возможно стоило писать статью с упором на описание работы интерфейсов, "магических" методов и стандартной библиотеке, а не "я тут решил сделать свой XML парсер". К тому же, как правильно заметили выше, это не парсер, это обертка к нему. Ошибка по поводу передачи по ссылке объектов очень грубая. Исключения с сообщениями на русском не самое лучшее решение.
                                      Из хорошего: статья очень хорошо оформлена ;)
                                        0
                                        исправил :)
                                        +1
                                        А можно маленький вопрос?
                                        А зачем так сложно?
                                        Уж если делать простенький XML парсер - достаточно реализовать обыкновенный SAX parser и ВСЕ!
                                        Безо всякой магии :)
                                        Я не в коем случае не хочу сказать что сделано чтото плохо - конечно нет. Но просто существует гораздо более простой спосою. Под PHP я парсер не написал - не надо было, а вот под JS пожалуйста пример:

                                        proto.saxParser = function() {
                                        }

                                        proto.saxParser.prototype.parse = function( dom, ignoreParent ) {
                                        if ( ignoreParent ) {
                                        if ( dom.documentElement ) {
                                        this.parseRecursive( dom.documentElement );
                                        } else if ( dom.tagName ) {
                                        for ( var i = dom.firstChild; i; i = i.nextSibling ) this.parseRecursive( i );
                                        }
                                        }
                                        else this.parseRecursive( dom, ignoreParent );
                                        }

                                        proto.saxParser.prototype.parseRecursive = function( dom ) {
                                        if ( dom.documentElement ) {
                                        this.startDocument( dom );
                                        this.parseRecursive( dom.documentElement );
                                        this.endDocument();
                                        } else if ( dom.tagName ) {
                                        this.startElement( dom );

                                        for ( var i = dom.firstChild; i; i = i.nextSibling ) this.parseRecursive( i );

                                        this.endElement( dom );
                                        }
                                        else this.characters( dom );
                                        }

                                        proto.saxParser.prototype.startDocument = function( doc ) {
                                        }

                                        proto.saxParser.prototype.startElement = function( el ) {
                                        }

                                        proto.saxParser.prototype.endElement = function( el ) {
                                        }

                                        proto.saxParser.prototype.characters = function( el ) {
                                        }

                                        proto.saxParser.prototype.endDocument = function() {
                                        }


                                        Все :) Весь парсер. Смысл я думаю объяснять не надо ) С задачей XML => Array справляется на ура и очень быстро )
                                          0
                                          По тому, что это замена SimpleXML - у него интерфейс другой, а про простой xml=>array я написал в первом абзаце и на него есть ссылка
                                          +2
                                          бардак в названиях.

                                          function offsetGet
                                          function getIterator
                                          function tag_close
                                            0
                                            во всем php кстати тоже :)
                                            в spl они использую camel, да, а в остальных местах старую нотацию с '_'
                                            0
                                            А насколько быстро это работает? Я имею в виду насколько быстрее(или медленнее) по сравнению с SimpleXML и пр.
                                              0
                                              Интересно. Можно, в принципе написать и проще...
                                              Вот, например простенький, но справляющийся со своей задачей XML парсер на JavaScript
                                              http://www.codelibrary.ru/index.php?page=192
                                                0
                                                >> Мне требовался разбор не сложных rss-лент
                                                http://simplepie.org/

                                                При всем моем уважении к чужому труду, я так и не понял, зачем же все-таки изобретать велосипеды. Понятно, что люди это делают скорее для себя, но ведь можно воспользоваться готовыми решениями, особенно если задача стандартна (которые, уж поверьте, реализуют гораздо больше возможностей написанного вами чего-либо).
                                                  –2
                                                  payalnik
                                                  Пишем свой XML-парсер
                                                  Решив запустить небольшой сервис на подаренном мне хостинге, оказалось, что там нету ни одного xml-парсера: ни SimpleXML, ни DOMXML, а только libxml и xml-rpc. Недолго думая, я решил написать свой

                                                  pc.speaker
                                                  Не обнаружив в доме ни одной женщины, недолго думая...
                                                  Я решил...
                                                    0
                                                    Статья хорошая.
                                                    А вот магические вещи PHP5 в ООП - плохие.
                                                    Методы __get() и __set() работают медленно! Рискуте так же меленно работать как JSP (Java Servlet Pages).

                                                    Так что аккуратнее с этими методами в высоконагрузочных проектах.
                                                      0
                                                      А еще print заменять на echo, да? :)
                                                        0
                                                        без разницы :) там разница не существенная
                                                        а здесь раз в 10 вызов метода __get() и __set() медленнее.
                                                          0
                                                          Хабр не хочет постить длинный комментарий - вот исходный код теста. Вот результаты:
                                                          Direct method: 10.228451728821
                                                          Magic methods: 46.129437685013


                                                          Ок, разница есть в 5 раз и это при 5 000 000 повторений. Нету никакого желания жертвовать удобством написания, ради такой мизерной разницы. Опкод-кешер даст прирост производительности в сотни раз больше.
                                                      0
                                                      Не понятно вот что, если вы используете в своем коде SPL, то почему вы не увидели в нем http://ru2.php.net/manual/ru/class.simpl…
                                                        0
                                                        Если не стоит сам SimpleXML, то и итератора этого нету
                                                        0
                                                        У меня есть вопрос , можете подсказать .
                                                        столкнулся с задачей - вывода из xml файла.
                                                        стоит использовать свой класс который с помошью строковых функции находит и заносит в массив данные из xml или использовать какой-то готовое решение?

                                                        вот класс

                                                        abstract class xml
                                                        {

                                                        protected $num_args;
                                                        protected $what_to_select_var;
                                                        protected $number_of_echo_news;
                                                        protected $variable;
                                                        protected $buffer;
                                                        protected $xml_path;
                                                        protected $counter;
                                                        protected $counter_s;
                                                        protected $possition_open;
                                                        protected $possition_close;


                                                        //конструктор вводи путь xml и количество выводов блоков
                                                        function __construct($xml_path,$number_of_print_blocks)
                                                        {
                                                        $this->xml_path = $xml_path;
                                                        $this->number_of_echo_news = $number_of_echo_news;
                                                        }

                                                        //заносим в буфер контент xml файла
                                                        function get_xml_file()
                                                        {
                                                        $this->buffer = file_get_contents($this->xml_path);
                                                        }


                                                        //достаём аргументы и количество аргументов вводимые в данную функцию (то что хотим вывести)
                                                        function what_to_select()
                                                        {
                                                        $this->num_args = func_num_args();
                                                        $this->what_to_select_var = func_get_args();
                                                        }

                                                        //заносим в матрицу $this->variable уже данные внутри тех тегов которые желаем вывести
                                                        function solve_variables()
                                                        {
                                                        for ($this->counter = 1 ; $this->counter number_of_echo_news ; $this->counter++)
                                                        {
                                                        for ($this->counter_s = 0 ; $this->counter_s < $this->num_args ; $this->counter_s++)
                                                        {
                                                        $this->possition_open = stripos($this->buffer,'') + 2 + strlen($this->what_to_select_var[$this->counter_s]);
                                                        $this->possition_close = stripos($this->buffer,' what_to_select_var[$this->counter_s].'>');
                                                        $this->variable[$this->counter][$this->counter_s] = substr($this->buffer , $this->possition_open , $this->possition_close - $this->possition_open);

                                                        $this->buffer = substr($this->buffer , $this->possition_close + strlen($this->what_to_select_var[$this->counter_s]) + 3);
                                                        }

                                                        }
                                                        }
                                                        abstract function printing();
                                                        }





                                                        и дочерний класс для вывода


                                                        class print_news extends xml
                                                        {
                                                        function printing()
                                                        {
                                                        for ($this->counter = 1 ; $this->counter number_of_echo_news ; $this->counter++)
                                                        {//цифры - поля которые желаем вывести
                                                        echo $this->variable[$this->counter][0]."\n";
                                                        echo $this->variable[$this->counter][1]."\n";
                                                        echo $this->variable[$this->counter][2]."\n";
                                                        echo $this->variable[$this->counter][3]."\n";


                                                        }
                                                        }
                                                        }


                                                        //тело скрипта
                                                        $obj = new print_news('./xml/news.xml',"5");
                                                        $obj->get_xml_file();
                                                        $obj->what_to_select('id','title','date','message');
                                                        $obj->solve_variables();
                                                        $obj->printing();
                                                          0
                                                          подскажите, а почему игнорируются такие тегиг как, напр., <name:exp></name:exp> (имею ввиду из-за налилчия ":" ) — и возможно переопределить этот момент при использовании SimpleXML?

                                                          Only users with full accounts can post comments. Log in, please.