Содержание


Синтаксический анализ XML в PHP

Организация обработки потока с эффективным использованием памяти

PHP 5 представил XMLReader, новый класс для чтения расширяемого языка разметки (XML). В отличие от простого XML или объектной модели документов (DOM) XMLReader работает в потоковом режиме. То есть он считывает документ от начала до конца. Можно начать работать с содержимым документа в его начале, перед тем как вы увидите его окончание. Это делает работу очень быстрой, эффективной и очень экономной с точки зрения затрат памяти. Чем больше размер документов, которые необходимо обрабатывать, тем это важнее.

В отличие от простого API для XML (SAX), XMLReader - в большей мере принимающий парсер (pull parser), чем передающий парсер (push parser). Это означает, что программа находится под контролем. Вместо того, чтобы парсер сообщал вам, что он видит, когда он это видит; вы указываете парсеру, когда необходимо переходить к следующему фрагменту документа. Вы запрашиваете контент вместо того, чтобы реагировать на него. Другими словами, это можно представить так XMLReader - это реализация конструктивного шаблона Iterator (итератор), а не конструктивного шаблона Observer (наблюдатель).

Образец задачи

Давайте начнем с простого примера. Представьте, что вы пишете PHP-скрипт, который получает XML-RPC запросы и генерирует ответы. Точнее, представьте, что запросы выглядят, как показано в листинге 1. Корневой элемент документа methodCall, в котором содержатся элементы methodName и params. Название метода - sqrt. Элемент params содержит один элемент param, включающий в себя double - число, квадратный корень которого нужно извлечь. Области имен не используются.

Листинг 1. Запрос XML-RPC
<?xml version="1.0"?>
<methodCall>
  <methodName>sqrt</methodName>
  <params>
    <param>
      <value><double>36.0</double></value>
    </param>
  </params>
</methodCall>

Вот что должен делать PHP-скрипт:

  1. Проверить название метода и сгенерировать сигнал о сбое (fault response), если это не sqrt (единственный метод, который может быть обработан этим сценарием).
  2. Найти аргумент и, если он отсутствует или имеет неправильный тип, сгенерировать сигнал о сбое.
  3. В противном случае вычислить квадратный корень.
  4. Вернуть результат в форме, показанной в листинге 2.
Листинг 2. Ответ XML-RPC
<?xml version="1.0"?>
<methodResponse>
  <params>
    <param>
      <value><double>6.0</double></value>
    </param>
  </params>
</methodResponse>

Давайте рассмотрим это шаг за шагом.

Инициализация парсера и загрузка документа

Первым шагом является создание нового объекта парсера. Сделать это просто:

$reader = new XMLReader();

Далее нужно ввести некоторые данные для разбора. Для XML-RPC это - необработанное тело запроса по протоколу HTTP. Затем эта строка может быть передана функции XML() программы считывания:

$request = $HTTP_RAW_POST_DATA;
$reader->XML($request);

Можно проанализировать любую строку, откуда бы вы ее ни взяли. Например, это может быть строковая литеральная константа в программе или содержимое файла. Также можно загрузить данные с внешнего URL при помощи функции open(). К примеру, следующая инструкция готовит один из Atom-каналов для разбора:

$reader->XML('http://www.cafeaulait.org/today.atom');

Откуда бы вы ни взяли исходные данные, программа чтения теперь установлена и готова выполнять анализ.

Чтение документа

Функция read() перемещает парсер к следующему маркеру. Самый простой подход заключается в выполнении итераций цикла while по всему документу:

while ($reader->read()) {
  // обрабатывающий  код...
}

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

$reader->close();

Внутри цикла парсер помещается в определенном узле: в начале элемента, в конце элемента, в текстовом узле, в комментарии и так далее. Следующие свойства позволяют узнать, что парсер просматривает в данный момент:

  • localName - это локальное, предварительно не заданное имя узла.
  • name - возможное предварительно заданное имя узла. Для таких узлов, которые не имеют имен, например, комментариев, это #comment, #text, #document, и т. д., как в DOM (объектная модель документов).
  • namespaceURI - это унифицированный идентификатор ресурса (URI) для пространства имен узла.
  • nodeType - это целое число, представляющее тип узла - к примеру, 2 для узла атрибута и 7 - для оператора обработки.
  • prefix - это префикс пространства имен узла.
  • value - это текстовое содержание узла.
  • hasValue - верно, если узел имеет текстовое значение и неверно в противном случае.

Конечно, не все типы узлов обладают всеми этими свойствами. Например, текстовые узлы, CDATA-разделы, комментарии, операторы обработки, атрибуты, символ пробела, типы документов и описания XML имеют значения. Другие типы узлов (в особенности – элементы и документы) – не имеют. Обычно программа использует свойство nodeType для определения того, что просматривается, и выдачи соответствующего ответа. В листинге 3 показан простой цикл while, который использует эти функции для вывода того, что он просматривает. В листинге 4 показан результат работы этой программы, когда ей на вход подается листинг 1.

Листинг 3. Что видит парсер
     while ($reader->read()) {
      echo $reader->name;
      if ($reader->hasValue) {
        echo ": " . $reader->value;
      }
      echo "\n";
    }
Листинг 4. Вывод из листинга 3
methodCall
#text: 
  
methodName
#text: sqrt
methodName
#text: 
  
params
#text: 
    
param
#text: 
      
value
double
#text: 10
double
value
#text: 
    
param
#text: 
  
params
#text: 

methodCall

Большая часть программ не так универсальна. Они принимают входные данные в особой форме и обрабатывают их определенным образом. В примере XML-RPC нужно считать только один параметр из входных данных: элемент double, который должен быть только один. Чтобы это сделать, найдите начало элемента с именем double:

if ($reader->name == "double" 
  && $reader->nodeelementType == XMLReader::element) {
    // ...
}

У этого элемента также есть единственный текстовый дочерний узел, который можно считывать, перемещая парсер к следующему узлу:

if ($reader->name == "double" && $reader->nodeType == XMLReader::ELEMENT) {
    $reader->read();
    respond($reader->value);
}

Здесь функция respond() создает ответ XML-RPC и отправляет его клиенту. Однако, прежде чем я покажу это, необходимо рассказать еще кое-что. Нет никакой гарантии того, что элементdouble в документе запроса содержит только один текстовый узел. Он может содержать несколько узлов, а также комментарии и операторы. Например, это может выглядеть следующим образом:

 <value><double>
  <!--value follows-->6.<!--fractional part next-->0
</double></value>

Устойчивое решение проблемы должно обеспечивать получение всех потомков текстового узла double, объединять их в цепочку и только затем конвертировать результат в double. Необходимо избегать любых комментариев или других возможных нетекстовых узлов. Это немного сложнее, но, как показано в листинге 5, не слишком.

Листинг 5. Суммируйте весь текстовый контент элемента
  while ($reader->read()) {
    if ($reader->nodeType == XMLReader::TEXT
      || $reader->nodeType == XMLReader::CDATA
      || $reader->nodeType == XMLReader::WHITESPACE
      || $reader->nodeType == XMLReader::SIGNIFICANT_WHITESPACE) {
       $input .= $reader->value;
    }
    else if ($reader->nodeType == XMLReader::END_ELEMENT
      && $reader->name == "double") {
        break;
    }
  }

Пока весь остальной контент документа можно игнорировать. (Позже я продолжу описание обработки ошибок).

Создание ответа

Как следует из имени, XMLReader предназначен только для чтения. Соответствующий класс XMLWriter сейчас находится в разработке, но еще не готов. К счастью, писать XML гораздо легче, чем его считывать. Во-первых, следует задать тип носителя ответа, используя функцию header(). Для XML-RPC это application/xml. Например:

header('Content-type: application/xml');

Содержание обычно легко отображается прямо на странице, как показано в функции respond() листинга 6.

Листинг 6. Отображение XML
    function respond($input) {
  echo "<?xml version='1.0'?>
<methodResponse>
  <params>
    <param>
      <value><double>" .
       sqrt($input)
  . "</double></value>
    </param>
  </params>
</methodResponse>";
  
}

Можно даже вставить буквенные части ответа прямо в страницу PHP, так же, как это было бы реализовано в HTML. Данная технология показана в листинге 7.

Листинг 7. Буквенный XML
                function respond($input) {

 ?><?xml version='1.0'?>
<methodResponse>
  <params>
    <param>
      <value><double>"<?php 
 echo      sqrt($input);
?>
  </double></value>
    </param>
  </params>
</methodResponse>
  <?php
}

Обработка ошибок

До настоящего момента подразумевалось, что входной документ оформлен корректно. Однако этого никто не может гарантировать. Как любой парсер XML, XMLReader должен прекратить обработку, как только обнаружит ошибку оформления. Если это происходит, то функция read() возвращает false (ложь).

Теоретически, парсер может обрабатывать данные до первой обнаруженной им ошибки. В моих экспериментах с маленькими документами, однако, он сталкивается с ошибкой почти сразу. Лежащий в основе парсер предварительно анализирует большой участок документа, кэширует его, а затем выдает его по частям. Таким образом, он обычно определяет ошибки на предварительном этапе. В целях безопасности лучше не берите на себя ответственность за то, что сможете выполнить анализ контента до первой ошибки оформления. Более того, не предполагайте, что не увидите никакого контента до ошибки парсера. Если нужно принять только полные, корректно оформленные документы, то убедитесь, что скрипт не делает ничего необратимого до самого конца документа.

Если парсер обнаруживает ошибку в оформлении, то функция read() отображает сообщение об ошибке, аналогичное представленному (если настроен подробный отчет об ошибке, как и должно быть на сервере разработки):

<br />
<b>Warning</b>:  XMLReader::read() [<a href='function.read'>function.read</a>]:       
< value><double>10</double></value> in <b>/var/www/root.php</b> 
on line <b>35</b><br />

Вы, возможно, не захотите копировать отчет на страницу HTML, представляемую пользователю. Лучше фиксировать сообщение об ошибке в переменной среды $php_errormsg. Для этого нужно включить опцию конфигурации track_errors в файле php.ini:

track_errors = On

По умолчанию опция track_errors отключена, что явно указано в php.ini, поэтому не забудьте изменить эту строку. Если вы добавите строку, показанную выше, в начало php.ini, то строка track_errors = Off, которая написана ниже, заменит ее.

Эта программа должна посылать ответы только на полные, правильно оформленные входные данные. (Также достоверные, но об этом позже.) Таким образом, нужно подождать завершения анализа документа (выход из цикла while). Теперь проверьте, изменилось ли значение $php_errormsg. Если нет, то документ оформлен корректно, и будет отправлено ответное сообщение XML-RPC. Если переменная задана, то это означает, что документ оформлен некорректно, и будет отправлен сигнал о сбое XML-RPC. Также сигнал о сбое отправляется, если запрашивается квадратный корень отрицательного числа. Смотрите листинг 8.

Листинг 8. Проверка корректного оформления
      // 	отправка запроса (request)
    $request = $HTTP_RAW_POST_DATA;
    error_reporting(E_ERROR | E_WARNING | E_PARSE);
    if (isset($php_errormsg)) unset(($php_errormsg);
    // создание программы считывания (reader)
    $reader = new XMLReader();
    // $reader->setRelaxNGсхемой("request.rng");
    $reader->XML($request);

    $input = "";
    while ($reader->read()) {
      if ($reader->name == "double" && $reader->nodeType == XMLReader::ELEMENT) {

          while ($reader->read()) {
            if ($reader->nodeType == XMLReader::TEXT
              || $reader->nodeType == XMLReader::CDATA
              || $reader->nodeType == XMLReader::WHITESPACE
              || $reader->nodeType == XMLReader::SIGNIFICANT_WHITESPACE) {
               $input .= $reader->value;
            }
            else if ($reader->nodeType == XMLReader::END_ELEMENT
              && $reader->name == "double") {
                break;
            }
          } 
          break;
      }
    } 

    // проверка корректного оформления входной информации
    if (isset($php_errormsg) ) fault(21, $php_errormsg);
    else if ($input < 0) fault(20, "Cannot take square root of negative number");
    else respond($input);

Здесь приведена упрощенная версия общего шаблона обработки потоков XML. Парсер заполняет структуру данных, в соответствии с которой выполняются действия, когда документ заканчивается. Обычно структура данных проще, чем сам документ. Здесь структура данных особенно простая: единственная строка.

Валидация

До сих пор я не придавал большого значения проверке того, действительно ли данные находятся там, где я думаю. Самый простой способ осуществить эту проверку – сравнить документ со схемой. XMLReader поддерживает язык описания схемы RELAX NG; в листинге 9 показана простая схема RELAX NG для данной конкретной формы запроса XML-RPC.

Листинг 9. Запрос XML-RPC
<element name="methodCall" xmlns="http://relaxng.org/ns/structure/1.0" 
 datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
  <element name="methodName">
    <value>sqrt</value>
  </element>
  <element name="params">
    <element name="param">
      <element name="value">
        <element name="double">
          <data type="double"/>
        </element>
      </element>
    </element>
  </element>
</element>

Схему можно добавить непосредственно в PHP-скрипт в виде строкового литерала при помощи setRelaxNGSchemaSource() или считать ее из внешнего файла или URL с помощью setRelaxNGSchema(). Например, при условии, что содержимое листинга 9 записано в файле sqrt.rng, схема будет загружаться следующим образом:

reader->setRelaxNGSchema("sqrt.rng")

Выполните это прежде, чем начнете анализировать документ. Парсер сравнивает документ со схемой во время чтения. Чтобы проверить, является ли документ достоверным, вызовите функцию isValid(), которая возвращает значение true, если документ валиден (на данном этапе) и false в противном случае. В листинге 10 показана полная логически завершенная программа, содержащая обработку всех ошибок. Программа должна принимать любые достоверные входные данные и возвращать правильные значения и отклонять все неправильные запросы. Я также добавил метод fault(), который отправляет сигнал о сбое XML-RPC, если что-то идет не так.

Листинг 10. Полная серверная часть извлечения квадратного корня XML-RPC
<?php
header('Content-type: application/xml');

// проверка грамматики
$schema = "<element name='methodCall' 
                   xmlns='http://relaxng.org/ns/structure/1.0' 
                   datatypeLibrary='http://www.w3.org/2001/XMLSchema-datatypes'>
  <element name='methodName'>
    <value>sqrt</value>
  </element>
  <element name='params'>
    <element name='param'>
      <element name='value'>
        <element name='double'>
          <data type='double'/>
        </element>
      </element>
    </element>
  </element>
</element>";


if (!isset($HTTP_RAW_POST_DATA)) {
   fault(22, "Please make sure always_populate_raw_post_data = On in php.ini");
}
else {

     // отправка запроса
    $request = $HTTP_RAW_POST_DATA;
    error_reporting(E_ERROR | E_WARNING | E_PARSE);
    // создание программы считывания
    $reader = new XMLReader();
    $reader->setRelaxNGSchema("request.rng");
    $reader->XML($request);

    $input = "";
    while ($reader->read()) {
      if ($reader->name == "double" && $reader->nodeType == XMLReader::ELEMENT) {

          while ($reader->read()) {
            if ($reader->nodeType == XMLReader::TEXT
              || $reader->nodeType == XMLReader::CDATA
              || $reader->nodeType == XMLReader::WHITESPACE
              || $reader->nodeType == XMLReader::SIGNIFICANT_WHITESPACE) {
               $input .= $reader->value;
            }
            else if ($reader->nodeType == XMLReader::END_ELEMENT
              && $reader->name == "double") {
                break;
            }
          } 
          break;
      }
    } 

    if (isset($php_errormsg) ) fault(21, $php_errormsg);
    else if (! $reader->isValid()) fault(19, "Invalid request");
    else if ($input < 0) fault(20, "Cannot take square root of negative number");
    else respond($input);

    $reader->close();
}


function respond($input)
{
?>
<methodResponse>
  <params>
    <param>
      <value><double><?php 
 echo      sqrt($input);
?></double></value>
    </param>
  </params>
</methodResponse>
  <?php
}


function fault($code, $message)
{

  echo "<?xml version='1.0'?>
<methodResponse>
  <fault>
    <value>
      <struct>
        <member>
          <name>faultCode</name>
          <value><int>" . $code . "</int></value>
        </member>
        <member>
          <name>faultString</name>
          <value>
             <string>" . $message . "</string>
          </value>
        </member>
      </struct>
    </value>
  </fault>
</methodResponse>";
  
}

Атрибуты

Атрибуты не видны при нормальном выполнении анализа. Чтобы считать атрибуты, необходимо остановиться в начале элемента и запросить конкретный атрибут либо по имени, либо по номеру.

Передайте имя атрибута, значение которого необходимо найти в текущем элементе, функции getAttribute(). К примеру, следующая конструкция запрашивает атрибут id текущего элемента:

$id = $reader->getAttribute("id");

Если атрибут - в пространстве имен, например, xlink:href, то вызовите getAttributeNS () и передайте локальное имя и URI пространства имен в качестве первого и второго аргументов соответственно (префикс не имеет значения). Например, данная инструкция запрашивает значение атрибута xlink:href в пространстве имен http://www.w3.org/1999/xlink/:

$href = $reader->getAttributeNS ("href", "http://www.w3.org/1999/xlink/");

Если атрибут не существует, то оба метода возвратят пустую строку. (Это неправильно, так как они должны вернуть null. Данная реализация усложняет возможность различать атрибуты, значение которых - пустая строка, и те, которые вообще отсутствуют.)

Если нужно знать все атрибуты элемента, а их имена заранее неизвестны, то вызовите moveToNextAttribute(), когда считывающая часть установлена на элементе. Если парсер находится на узле атрибута, то можно считать его имя, пространство имен и значение при помощи тех же свойств, которые использовались для элементов. Например, следующий фрагмент кода распечатывает все атрибуты текущего элемента:

  if ($reader->hasAttributes and $reader->nodeType == XMLReader::ELEMENT) {
    while ($reader->moveToNextAttribute()) {
      echo $reader->name . "='" . $reader->value . "'\n";
    }
    echo "\n";
  }

Очень необычно для XML API то, что XMLReader позволяет считывать атрибуты либо с начала, либо с конца элемента. Чтобы избежать двойного отсчета, важно убедиться, что типом узла является XMLReader::ELEMENT, а не XMLReader::END_ELEMENT, у которого тоже могут быть атрибуты.

Заключение

XMLReader - полезное дополнение к инструментарию программиста PHP. В отличие от SimpleXML это полный парсер XML, который обрабатывает все документы, а не только некоторые из них. В отличие от DOM он может обрабатывать документы большие, чем доступная память. В отличие от SAX он устанавливает контроль над программой. Если PHP-программам нужно принимать входные данные XML, то стоит всерьез задуматься об использовании XMLReader.


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: Pull parsing XML in PHP . — оригинал этой статьи на developerWorks.(EN)
  • Официальная документация по XMLReader: прочитайте руководство по PHP 5 .(EN)
  • PHP класс XMLReader: узнайте больше об этой надстройке над libxml C XMLReader API.(EN)
  • .NET Ajax System.Xml.XmlTextReader: для чего можно использовать данный API.(EN)
  • Коротко об XML (Эллиотт Расти Хэролд и В. Скот Минз, O'Reilly, 2005 г.): узнайте обо всех деталях XML при помощи этой маленькой книги с исчерпывающим содержанием, которая хороша и как подробное введение в тему, и как справочник.(EN)
  • XML-RPC в Java-программировании (Рой Миллер, developerWorks, январь 2004 г.): напишите XML-RPC-клиенты и серверы для организации простейшего способа обмена информацией между приложениями.(EN)
  • Обработка SimpleXML при помощи PHP (Эллиотт Расти Хэролд, developerWorks, октябрь 2006 г.): с помощью расширения SimpleXML напишите специальный считыватель RSS разметки в PHP, использующий SimpleXML.(EN)
  • Отдача с помощью RELAX NG, часть 1 (Дэвид Мерц, developerWorks, февраль 2003 г.): создайте действенные, лаконичные и семантически простые классы, чтобы описать валидные экземпляры XML с помощью RELAX NG.(EN)
  • Шаблоны проектирования (Эддисон-Уэсли, 1995 г.): вникните в описание шаблонов проектирования Observer и Iterator в основополагающей работе группы GOF (Банды четырех).(EN)
  • Сертификация IBM XML: узнайте, как можно стать сертифицированным разработчиком IBM в области XML и смежных технологий.(EN)
  • Техническая библиотека XML: смотрите в разделе developerWorks XML различные технические статьи и советы, учебные пособия, стандарты и Redbooks IBM.
static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=XML, Open source
ArticleID=261229
ArticleTitle=Синтаксический анализ XML в PHP
publish-date=10112007