Эллиотт Расти Хэролд
PHP 5 представил XMLReader
, новый класс для чтения расширяемого языка разметки (XML). В отличие от простого XML или объектной модели документов (DOM) XMLReader
работает в потоковом режиме. То есть он считывает документ от начала до конца. Можно начать работать с содержимым документа в его начале, перед тем как вы увидите его окончание. Это делает работу очень быстрой, эффективной и очень экономной с точки зрения затрат памяти. Чем больше размер документов, которые необходимо обрабатывать, тем это важнее.
|
libxml
Описываемый XMLReader API основывается на libxml-библиотеке Gnome Project для C и C++. На самом деле XMLReader - это всего лишь тонкий PHP-слой на поверхности API libxml XmlTextReader . XmlTextReader также смоделирован на основе (хотя и не имеет общего кода) .NET классов XmlTextReader и 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-скрипт:
- Проверить название метода и сгенерировать сигнал о сбое (fault response), если это не
sqrt
(единственный метод, который может быть обработан этим сценарием).
- Найти аргумент и, если он отсутствует или имеет неправильный тип, сгенерировать сигнал о сбое.
- В противном случае вычислить квадратный корень.
- Вернуть результат в форме, показанной в листинге 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()) {
// обрабатывающий код...
} |
По окончании закройте парсер, чтобы освободить ресурсы, которые он занимает, и перенастройте его для следующего документа:
Внутри цикла парсер помещается в определенном узле: в начале элемента, в конце элемента, в текстовом узле, в комментарии и так далее. Следующие свойства позволяют узнать, что парсер просматривает в данный момент:
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>6<double>1.2</double></double> могут нарушить этот алгоритм. Это неправильный XML-RPC, и вскоре вы увидите, как использовать валидацию RELAX NG, чтобы отклонить все такие документы. В таких типах документов, как, например, расширяемый язык гипертекстовой разметки (XHTML), которые допускают вложение одинаковых элементов друг в друга (например, table внутри table ), также нужно отслеживать глубину элементов, чтобы убедиться, что конечный тег правильно соотнесен с начальным тегом. | |
Устойчивое решение проблемы должно обеспечивать получение всех потомков текстового узла 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
отключена, что явно указано в 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. Парсер заполняет структуру данных, в соответствии с которой выполняются действия, когда документ заканчивается. Обычно структура данных проще, чем сам документ. Здесь структура данных особенно простая: единственная строка.
|
Версия libxml
В ранних версиях libxml , библиотеки, от которой зависит XMLReader , присутствовали серьезные недочеты RELAX NG. Убедитесь, что вы используете хотя бы версию 2.06.26. Многие системы, в том числе Mac OS X Tiger, содержат более ранний выпуск с недочетами. | |
До сих пор я не придавал большого значения проверке того, действительно ли данные находятся там, где я думаю. Самый простой способ осуществить эту проверку - сравнить документ со схемой. 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. Данная реализация усложняет возможность различать атрибуты, значение которых - пустая строка, и те, которые вообще отсутствуют.)
|
Порядок атрибутов
В XML-документах порядок атрибутов не имеет значения и не сохраняется парсером. Он использует номера для индексирования атрибутов просто ради удобства. Нет гарантии, что первый атрибут в открывающем теге будет атрибутом 1, второй будет атрибутом 2 и т.д. Не создавайте код, зависящий от порядка атрибутов. | |
Если нужно знать все атрибуты элемента, а их имена заранее неизвестны, то вызовите 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
.