Статьи

PHP DOM: использование XPath

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

Я буду использовать те же DTD и XML из предыдущей статьи, чтобы продемонстрировать функциональность PHP DOM XPath. Чтобы быстро освежить вашу память, вот как выглядят DTD и XML:

<!ELEMENT library (book*)> <!ELEMENT book (title, author, genre, chapter*)> <!ATTLIST book isbn ID #REQUIRED> <!ELEMENT title (#PCDATA)> <!ELEMENT author (#PCDATA)> <!ELEMENT genre (#PCDATA)> <!ELEMENT chapter (chaptitle,text)> <!ATTLIST chapter position NMTOKEN #REQUIRED> <!ELEMENT chaptitle (#PCDATA)> <!ELEMENT text (#PCDATA)> 
 <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE library SYSTEM "library.dtd"> <library> <book isbn="isbn1234"> <title>A Book</title> <author>An Author</author> <genre>Horror</genre> <chapter position="first"> <chaptitle>chapter one</chaptitle> <text><![CDATA[Lorem Ipsum...]]></text> </chapter> </book> <book isbn="isbn1235"> <title>Another Book</title> <author>Another Author</author> <genre>Science Fiction</genre> <chapter position="first"> <chaptitle>chapter one</chaptitle> <text><![CDATA[<i>Sit Dolor Amet...</i>]]></text> </chapter> </book> </library> 

Основные запросы XPath

XPath — это синтаксис, доступный для запроса XML-документа. В простейшей форме вы определяете путь к нужному элементу. Используя приведенный выше XML-документ, следующий запрос XPath вернет коллекцию всех присутствующих элементов книги:

  // Библиотека / книга 

Вот и все. Две косые черты указывают, что библиотека является корневым элементом документа, а одиночная косая черта означает, что книга является дочерней. Это довольно просто, нет?

Но что, если вы хотите указать конкретную книгу. Допустим, вы хотите вернуть любые книги, написанные «Автор». XPath для этого будет:

  // библиотека / книга / автор [text () = "Автор"] / .. 

Вы можете использовать text() здесь в квадратных скобках, чтобы выполнить сравнение со значением узла, а завершающий символ «/ ..» указывает на то, что нам нужен родительский элемент (т. Е. Переместиться назад на дерево на один узел).

Запросы XPath могут быть выполнены с использованием одной из двух функций: query() evaluate() . Оба выполняют запрос, но разница заключается в типе результата, который они возвращают. query() всегда будет возвращать DOMNodeList тогда как DOMNodeList evaluate() будет возвращать типизированный результат, если это возможно. Например, если ваш запрос XPath должен вернуть количество книг, написанных определенным автором, а не сами книги, то query() вернет пустой DOMNodeList . evaluate() просто возвращает число, поэтому вы можете использовать его немедленно, вместо того, чтобы извлекать данные из узла.

Преимущества кода и скорости с XPath

Давайте сделаем небольшую демонстрацию, которая возвращает количество книг, написанных автором. Первый метод, который мы рассмотрим, будет работать, но не использует XPath. Это должно показать вам, как это можно сделать без XPath и почему XPath такой мощный.

 <?php public function getNumberOfBooksByAuthor($author) { $total = 0; $elements = $this->domDocument->getElementsByTagName("author"); foreach ($elements as $element) { if ($element->nodeValue == $author) { $total++; } } return $number; } 

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

 <?php public function getNumberOfBooksByAuthor($author) { $query = "//library/book/author[text() = '$author']/.."; $xpath = new DOMXPath($this->domDocument); $result = $xpath->query($query); return $result->length; } 

Обратите внимание, как мы на этот раз избавили нас от необходимости проверять PHP на значение автора. Но мы можем пойти еще дальше и использовать функцию XPath count() для подсчета вхождений этого пути.

 <?php public function getNumberOfBooksByAuthor($author) { $query = "count(//library/book/author[text() = '$author']/..)"; $xpath = new DOMXPath($this->domDocument); return $xpath->evaluate($query); } 

Мы можем получить необходимую нам информацию только с помощью строки XPath, и нет необходимости выполнять кропотливую фильтрацию с помощью PHP. Действительно, это гораздо более простой и лаконичный способ написания этой функциональности!

Обратите внимание, что в последнем примере была использована метод evaluate() . Это потому, что функция count() возвращает типизированный результат. Использование query() вернет DOMNodeList но вы обнаружите, что это пустой список.

Это не только делает ваш код чище, но и дает преимущества в скорости. Я обнаружил, что версия 1 была в среднем на 30% быстрее, чем версия 2, но версия 3 была примерно на 10 процентов быстрее, чем версия 2 (примерно на 15% быстрее, чем версия 1). Хотя эти измерения будут варьироваться в зависимости от вашего сервера и запроса, использование XPath в чистом виде в целом даст значительный выигрыш в скорости, а также облегчит чтение и обслуживание вашего кода.

Функции XPath

Существует довольно много функций, которые можно использовать с XPath, и есть много отличных ресурсов, в которых подробно описываются доступные функции. Если вы обнаружите, что перебираете DOMNodeList или сравниваете nodeValue , вы, вероятно, найдете функцию XPath, которая может устранить большую часть PHP-кодирования.

Вы уже видели, как функционирует count() . Давайте использовать функцию id() чтобы вернуть названия книг с указанными номерами ISBN. Вы должны использовать выражение XPath:

  id ("isbn1234 isbn1235") / название 

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

 <?php public function findBooksByISBNs(array $isbns) { $ids = join(" ", $isbns); $query = "id('$ids')/title"; $xpath = new DOMXPath($this->domDocument); $result = $xpath->query($query); $books = array(); foreach ($result as $node) { $book = array("title" => $booknode->nodeValue); $books[] = $book; } return $books; } 

Выполнение сложных функций в XPath относительно просто; Хитрость заключается в том, чтобы ознакомиться с доступными функциями.

Использование функций PHP с XPath

Иногда вы можете обнаружить, что вам нужны более широкие функциональные возможности, которые стандартные функции XPath не могут предоставить. К счастью, PHP DOM также позволяет вам включать собственные функции PHP в запрос XPath.

Давайте рассмотрим возвращение количества слов в названии книги. В его простейшей функции мы могли бы написать метод следующим образом:

 <?php public function getNumberOfWords($isbn) { $query = "//library/book[@isbn = '$isbn']"; $xpath = new DOMXPath($this->domDocument); $result = $xpath->query($query); $title = $result->item(0)->getElementsByTagName("title") ->item(0)->nodeValue; return str_word_count($title); } 

Но мы также можем включить функцию str_word_count() непосредственно в запрос XPath. Есть несколько шагов, которые необходимо выполнить, чтобы сделать это. Прежде всего, мы должны зарегистрировать пространство имен с объектом XPath. PHP-функциям в запросах XPath предшествует «php: functionString», а затем имя функции, которую вы хотите использовать, заключено в скобки. Кроме того, пространство имен, которое должно быть определено, является http://php.net/xpath . Пространство имен должно быть установлено на это; любые другие значения приведут к ошибкам. Затем нам нужно вызвать registerPHPFunctions() который сообщает PHP, что всякий раз, когда он сталкивается с пространством имен функции с расширением «php:», это должен обрабатывать PHP.

Фактический синтаксис для вызова функции:

  php: functionString ("nameoffunction", arg, arg ...) 

Объединение всего этого приводит к следующей переопределению getNumberOfWords() :

 <?php public function getNumberOfWords($isbn) { $xpath = new DOMXPath($this->domDocument); //register the php namespace $xpath->registerNamespace("php", "http://php.net/xpath"); //ensure php functions can be called within xpath $xpath->registerPHPFunctions(); $query = "php:functionString('str_word_count',(//library/book[@isbn = '$isbn']/title))"; return $xpath->evaluate($query); } 

Обратите внимание, что вам не нужно вызывать функцию XPath text() для предоставления текста узла. Метод registerPHPFunctions() делает это автоматически. Однако следующее также верно:

  php: functionString ('str_word_count', (// библиотека / книга [@isbn = '$ isbn'] / title [text ()])) 

Регистрация функций PHP не ограничивается функциями, которые поставляются с PHP. Вы можете определять свои собственные функции и предоставлять их в XPath. Единственное отличие состоит в том, что при определении функции вы используете «php: function», а не «php: functionString». Кроме того, можно предоставлять только функции самостоятельно или статическими методами. Вызов методов экземпляра не поддерживается.

Давайте использовать обычную функцию, которая выходит за рамки класса, чтобы продемонстрировать основные функциональные возможности. Функция, которую мы будем использовать, будет возвращать только книги Джорджа Оруэлла. Он должен возвращать true для каждого узла, который вы хотите включить в запрос.

 <?php function compare($node) { return $node[0]->nodeValue == "George Orwell"; } 

Аргумент, переданный функции, является массивом элементов DOMElement . Функция выполняет итерацию по массиву и определяет, следует ли возвращать проверяемый узел в DOMNodeList . В этом примере проверяется узел /book и мы используем /author для определения.

Теперь мы можем создать метод getGeorgeOrwellBooks() :

 <?php public function getGeorgeOrwellBooks() { $xpath = new DOMXPath($this->domDocument); $xpath->registerNamespace("php", "http://php.net/xpath"); $xpath->registerPHPFunctions(); $query = "//library/book[php:function('compare', author)]"; $result = $xpath->query($query); $books = array(); foreach($result as $node) { $books[] = $node->getElementsByTagName("title") ->item(0)->nodeValue; } return $books; } 

Если compare() был статический метод, вам нужно было бы изменить запрос XPath так, чтобы он читался следующим образом:

  // библиотека / книга [php: function ('Библиотека :: сравнить', автор)] 

По правде говоря, все эти функции можно легко кодировать только с помощью XPath, но пример показывает, как можно расширить запросы XPath, чтобы сделать их более сложными.

Вызов метода объекта невозможен в XPath. Если вы обнаружите, что вам нужен доступ к некоторым свойствам объекта или методам для выполнения запроса XPath, лучшим решением будет сделать то, что вы можете с XPath, а затем по мере необходимости работать с результирующим DOMNodeList с любыми методами или свойствами объекта.

Резюме

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

Изображение через Fotolia