Pull to refresh

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

Reading time 9 min
Views 71K

Предыстория


Решив запустить небольшой сервис на подаренном мне хостинге, оказалось, что там нету ни одного 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) Окончательная версия парсера и простой пример использования.
Tags:
Hubs:
+1
Comments 42
Comments Comments 42

Articles