30 July 2008

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

PHP

Предыстория


Решив запустить небольшой сервис на подаренном мне хостинге, оказалось, что там нету ни одного 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 ArrayAccess, IteratorAggregate, Countable {

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($parser, XML_OPTION_CASE_FOLDING, false);

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:phpxmlsimplexmllibxmlexpatвелосипед
Hubs: PHP
+1
66.7k 205
Comments 42
Popular right now
Middle PHP developer (удалённо)
from 120,000 ₽BoxberryRemote job
PHP developer
to 250,000 ₽РНКБ Банк (ПАО)МоскваRemote job
PHP-разработчик
from 40,000 to 60,000 ₽Dota2.ruRemote job
Web-разработчик ( Middle)
from 100,000 to 150,000 ₽Финансовые Информационные СистемыНовосибирскRemote job
PHP разработчик
to 110,000 ₽Sportmaster LabМоскваRemote job