Русские документы
Ежедневные компьютерные новости RSS rusdoc.ru  Найти :
Новости
Последние поступления
Книжный магазин
  Hardware:
Видеоустройства
Системные платы
Процессоры
Мобильные устройства
Аудиосистема
Охлаждение системы
Накопители информации
КПК и ноутбуки
Телефоны и связь
Периферия
Система
Сети
Разные устройства
 
  Programming:
Web-разработка
Языки программирования
Технологии и теория
Разработка игр
Программная инженерия
 
  Software:
Операционные системы
Windows 7
Базы данных
Обзоры программ
Графика и дизайн
   
  Life:
Компьютерная жизнь
Разные материалы
   
Партнеры
Публикация
Правовая информация
Реклама на сайте
Обратная связь
Экспорт в RSS Экспорт в RSS2.0
    Читать в Яндекс.Ленте



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

Раздел: Programming / PHP @ 30.07.2008 | Ключевые слова: php xml parser парсер версия для печати

Автор: sylvio
Источник: habrahabr

Предыстория

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

Это стандартные библиотеки для работы с XML и создания XML-парсеров.[4] То, что надо для решения нашей задачи. Ради интереса можете написать разбор xml-файла вручную, допустим на регулярных выражениях.

Больше всего в libxml нас интересуют следующие функции:

  • 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("Потомка с именем [$name] не существует");
}

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

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]);
}

А теперь мы столкнулись с проблемой из-за принятого недавно решения. Если вдруг мы захотим запустить цикл 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) Окончательная версия парсера и простой пример использования.








версия для печатиРаспечатать статью


Вернуться в раздел: Programming / PHP


Реклама:
Читать наc на:

Add to Google
Читать в Яндекс.Ленте






Rambler's Top100
© Copyright 1998-2012 Александр Томов. All rights reserved.