Последние статьи
Дефицит идей.На данный момент в сайтостроении, да и как везде, в общем то, наблюдается некий застой в решениях. Для сайтов...
Yandex с блекждеком и шлюхами.Как то размышляя о способах монетизации различных веб-проектов, я увидел некоторую нестыковку на...
Flash: построение графика с динамическим обновлением данных.До того, как я открыл для себя такие веши как Munin и прочие утилиты для отслеживания состояния серверов - я...
Защита комментариев от спама.Все люди, которые вели, ведут или собираются вести блоги, форумы или гостевые книги сталкиваются с...
Nginx и Bitrix. Использование их без использования апача. Встала тут задача поднять сервер, исключительно под сайты, написанные на базе CMS Bitrix (в силу...
Eval - наше все!Казалось бы, весь интернет напичкан статьями и рекомендациями к программистам, в которых говориться, что eval...
Кризис. Пожуем эту тему еще разок ? Бредоглава 0 (мы же программисты, у нас обязана быть нулевая глава :). Введение . О мировом экономическом...
Дизайн ЯндексаЯндекс в своем  желании заработать как можно больше демонстрирует просто потрясяющую способность...

PHP: парсер XML или еще один велосипед.

Во время работы над одним сайтом я столкнулся с задачей, что данные для него надо было получать по SOAP, и, соответственно надо было разбирать XML.
Вроде бы - а в чем вопрос ? Библиотек для разбора XML - в PHP хоть завались: и DOM XML  в PHP4 и DOM с SimpleXML и XML Parser в PHP5, и, уж если на то пошло, есть даже библиотеки для работы конкретно с SOAP... Кажется - бери да пользуйся, вон сколько счастья имеется, но встал резко вопрос - а как со всем этим зоопарком работать ?
Ладно, если ты точно знаешь какая версия PHP стоит на сервере и имеется вменяемый хостер, который готов поставить какую нибудь из вышеперечисленных библиотек, но что делать если все это нужно очень срочно и решать вопросы с хостером просто некогда ? Да и вообще, хочется иметь гарантию, что в случае смены хостинга к тебе никаких притензий не было.

Понятно, что системные требования к нормальной работе сайта можно прописать в договоре и все решать административным путем, но в ряде случаев, когда делаются простые сайты для себя или знакомых, этот путь не работает :)

Поэтому у меня было 2 варианта решения данной проблемы:

  1.  Написать класс-оболочку для вызова этих функций, то есть класс должен был определять, что из этого зоопарка для работы с XML стоит и вызывать соответствующие методы. Все бы хорошо, но что делать, если не стоит ничего ?
  2.  Написать свой класс для разбора XML. В принципе, никто не мешает объединить оба эти варианта и дергать свой класс только когда нет ничего родного. Но для начала его надо написать, не правда ли ?

Итак, цели поставлены - вперед !

Создаем наш класс:

<?php
class VPA_xmldom {
}

?>

Пока он маловат, но мы только в начале пути, так что у нас все впереди.

Пишем функцию get, которая будет регулярными выражениями разбирать наш любимый XML. Делаем это просто:
Сначала получаем строку с деревом XML тегов в виде строки, и потом (мы же помним, что родительский элемент у XML-я только один ?) выделяем этот первый тег. Но для начала мы должны учесть что теги могут быть в виде <tag />, то есть не иметь закрывающего тега, поэтому для нормального разбора приведем весь XML к нормальному виду, заменив все такие теги на пару:открывающий тег, закрывающий тег.

После выделения первого тега мы приходим к тому, что у нас есть название этого тега, его аттрибуты в виде строки и содержимое тега. Чтобы перевести строку с аттрибутами в ассоциативный массив: имя аттрибута => значение аттрибута, сделаем функцию _str2attrs, которая берет на входе строку и разбивает ее по пробелам сначала на построки, содеращие в себе только по одному определению аттрибута, а потом, по знаку "=", на имя аттрибута и его значение. Стандарт нам говорит о том, что если мы объявили аттрибут, то мы обязаны присвоить ему значение, аттрибута без значения быть не может. Проверяем это. Также не забываем о том, что по стандарту все значения аттрибутов должны быть в кавычках, поэтому - убиваем эти кавычки, и теперь мы можем смело писать полученные значения в ассоциативный  массив, который и возвращаем.

<?php
class VPA_xmldom {
    function 
get($xml)
    {
        
// выделяем декларацию XML и все остальное в массив
        
preg_match_all("/<\?xml(.*?)\?>(.*)/is",$xml,$out);
        
// теперь у нас в $xml_decl строка с XML тегами
        
$xml_decl=trim($out[1][0]);
        
// отдельным методом мы выделяем все аттрибуты, которые имеет XML декларация
        
$result->xml=VPA_xmldom::_str2attrs($xml_decl);
        
// берем наш драгоценный XML ...
        
$xml=$out[2][0];
        
// опа... а ведь XML может и не иметь закрывающего тега, и закрываться сразу же в открывающем 
        // <tag /> - такое то ведь тоже есть...
        // поэтому мы заменяем такие теги на нормальную пару тегов: открывающий и закрывающий
        
$xml=preg_replace("|<([\w:]+?)(\s*)/>|is","<\$1></\$1>",$xml);
        
// ... и выделяем из получившегося XML-я первый тег.
        
preg_match_all("|<(.+?)(\s.*?)?>(.*?)</\\1>|is",$xml,$out);
        
// теперь мы имеем имя тега
        
$tag=$out[1][0];
        
// его аттрибуты в виде одной строки
        
$attr=$out[2][0];
        
// и содержимое тега
        
$body=$out[3][0];
        
// пишем данные о первом элементе в коневой элемент нашего объекта.
        
$result->xml=new VPA_xml_element($tag,VPA_xmldom::_str2attrs($attr));
        
// скармливаем содержимое нашего корневого тега рекурсивному парсеру детишек
        
$result->xml->childs=VPA_xmldom::_get_level($xml,$result->data);
        return 
$result;
    }
    
    function 
_str2attrs($str)
    {
        
$attrs=array();
        
$list=explode(" ",$str);
        foreach (
$list as $attr)
        {
            
$s=explode("=",$attr);
            if (
count($s)==2)
            {
                
$key=trim($s[0]);
                
$value=trim($s[1],'"');
                
$attrs[$key]=$value;
            }
        }
        return 
$attrs;
    }
    
    function 
_get_level($str,&$parent)
    {
        
preg_match_all("|<(.+?)(\s.*?)?>(.*?)</\\1>|is",$str,$out);
        
$data=$out[0];
        
$i=0;
        
$ret=array();
        foreach (
$data as $key => $record)
        {
            
$tag=$out[1][$key];
            
$attr=$out[2][$key];
            
$body=$out[3][$key];
            
$ret[$i]=new VPA_xml_element($tag,VPA_xmldom::_str2attrs($attr));
            
$ret[$i]->parent=$parent;
            
$ret[$i]->childs=VPA_xmldom::_get_level($body,$ret[$i]);
            if (empty(
$ret[$i]->childs))
            {
                
$ret[$i]->text=$body;
            }
            
$i++;
        }
        return 
$ret;
    }
}
?>

Теперь, когда уже пошел анализ XML-я, нам пора определиться с видом хранения полученной информации. Хранить всю информацию в виде вложенных друг в друга ассоциативных массивов можно, но не очень удобно, поэтому для работы с элементами DOM дерева было решено сделать объект VPA_xml_element.

Он содержит в себе следующие поля:
  • name - имя элемента
  • childs - массив элементов, для которых данный элемент является родительским
  • attrs - массив аттрибутов элемента
  • parent - ссылка на родителя, для верхнего элемента равна null
  • text - если внутри данного  элемента нет других элементов, а только текстовое содержимое, то это содержимое пишется сюда

Инициализация объекта элемента проиходит вызвом конструктора VPA_xml_element() с двумя праметрами: названием элемента и массивом аттрибутов.

Чем удобна такая организация данных в отличие от простого хранения в ассоциативном массиве ? Да тем, что вся логика по поиску элементов ложится на сами элементы, и мы можем организовывать
набор методов, очень похожий на методы для работы с DOM в JavaScript. К примеру, я реализовал метод getElementsByTagName(), который ищет заданный элемент по имени среди всех детей, внуков,
правнуков и так далее и так далее. Никто не мешает по аналогии написать и все остальные методы, такие как nextSibling, firstNode() и прочее.

<?php
class VPA_xml_element {
    var 
$name;
    var 
$childs;
    var 
$attrs;
    var 
$parent=null;
    var 
$text;
    
    function 
VPA_xml_element($name,$attrs)
    {
        
$this->name=$name;
        
$this->attrs=$attrs;
    }
    
    function 
getElementsByTagName($name)
    {
        if (!isset(
$results)) $results=array();
        if (!isset(
$this->childs) || empty($this->childs))
        {
            return 
null;
        }
        foreach (
$this->childs as $i => $element)
        {
            if (
$element->name==$name)
            {
                
$results[]=$element;
            }
            else
            {
                
$ret=$element->getElementsByTagName($name);
                if (!empty(
$ret)) $results=array_merge($results,$ret);
            }
        }
        return 
$results;
    }
}
?>

Теперь, когда у нас есть в чем хранить данные, давайте продолжим разбор текста XML. После того, как мы сохранили корневой элемент документа, мы видим, что содержимое
этого XML-я мы отдаем на съедение другому методу _get_level:

<?php
function _get_level($str,&$parent)
    {
        
preg_match_all("|<(.+?)(\s.*?)?>(.*?)</\\1>|is",$str,$out);
        
$data=$out[0];
        
$i=0;
        
$ret=array();
        foreach (
$data as $key => $record)
        {
            
$tag=$out[1][$key];
            
$attr=$out[2][$key];
            
$body=$out[3][$key];
            
$ret[$i]=new VPA_xml_element($tag,VPA_xmldom::_str2attrs($attr));
            
$ret[$i]->parent=$parent;
            
$ret[$i]->childs=VPA_xmldom::_get_level($body,$ret[$i]);
            if (empty(
$ret[$i]->childs))
            {
                
$ret[$i]->text=$body;
            }
            
$i++;
        }
        return 
$ret;
    }
?>
    
Этот метод делает все то же самое что и get, только с учетом того, что в отличие от корневого элемента, тут может быть много тегов, поэтому мы используя нежадное регулярное выражение получаем список всех детей, и прходя в цикле, заносим их в список детей родительского тега. Поскольку уровень вложенности тегов в друг друга не ограничен, мы должны разобрать содержимое и этих тегов, поэтому после того как мы прописали полученный элемент к родителю, мы рексурсивно вызваем для его содержимого этот же метод _get_level. Таким образом, после скармливания нашей строки с XML парсеру на выходе мы имеем объект с набором элементов.
вернуться в список статей