Не так страшен XPATH как его незнание

XPath является одним из ключевых моментов на дороге к пониманию XSLT. И на первых порах хочется получить какой-нибудь простой способ поэкспериментировать с ним, чтобы в деталях разобраться, как он работает. Способа такого, впрочем, не наблюдается. Приходится или качать совсем не бесплатные XML/XSLT редакторы, или довольствоваться статичными картинками с zvon.org . Может быть, я плохо искал. Но с моей колокольни все обстоит именно так. И когда передо мной в очередной раз встала задача «Объяснить XSLT», в голове и родилась идея крохотного сервиса. После того, как все заработало, было решено выкатить его для всеобщего пользования: наверняка не я один сталкивался с похожими проблемами.

Как это работает

Все просто до безобразия: вводим XML для экспериментов, пишем запрос и по ходу набора наблюдаем результат. Выглядит это примерно вот так:

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

Сервис, впрочем, может пригодиться не только в образовательных целях. Заковыристые запросы очень часто бывает удобно наблюдать визуально. Со своей задачей сервис справился. Буду рад, если поможет кому-нибудь еще.

Само собой разумеется и во всех книгах проговаривают, что для нахождения элемента лучше всего и быстрее использовать локаторы id и name и, что характерно, основные примеры по использованию локаторов и по работе Selenium показывают именно с ними. Но в реальной жизни часто бывает так, что id элементов формируется динамически, а потому все привязки к нему бесполезны, class может иметь десятки представителей на странице, а name может отсутствовать. Как вы уже догадываетесь в этом случае нужно применять локаторы xpath и css. В данной статье я не собираюсь говорить о каком то превосходстве над css или сравнивать быстродействие, я лишь расскажу почему я использую именно xpath и как это нужно делать. Букв будет много, так как в свое время мне пришлось достаточно порыться в интернет, чтобы получить нужную мне информацию, я выкладываю все самое полезное, в надежде, что кому это поможет в использовании xpath-локаторов. Важно, что у тебя, мой читатель должно быть хоть небольшой представление о xpath, если его нет, то можешь .

Сначала о том, почему новички (и не только) не любят xpath:

  1. Со времен далекой, далекой Галактики, существует миф о том, что xpath во много раз медленнее css, что на данный момент времени не является правдой. Не знаю как обстояло дело раньше, но в наши дни я лично написал несколько тестов с использованием xpath и css и сравнивая их могу сказать, что никакого значительного преимущества нет, а порой даже xpath работает быстрее. Не собираюсь вступать в длительные баталии по поводу скорости, просто разница в несколько миллисекунд нельзя считать значительной, особенно при общей длительности УИ-тестов.
  2. Xpath неверно используют, во многом из-за того, что стандартные панели разработчика и плагины выдергивают xpath из страницы в совершенно непотребном виде, который неудобен и нечитаем. Потому у многих сложилось мнение, что xpath это тяжеловесная и непонятная ерунда.
  3. Нет или по меньшей мере мне не попался какой-нибудь вменяемый мануал по xpath, в основном предлагают ссылки на pdf файл где локаторы приведены всей кучей вместе с css, этакая выжимка, в которой я уверен мало кто разбирается просто из-за обилия информации.

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

— он не уступает (или незначительно уступает) в скорости css

— он понятен и легко читаем, по нему можно понять о каком элементе идет речь

— он похож на язык программирования и его удобно использовать

— можно добраться до самых запрятанных элементов страницы, благодаря выстроенным цепочкам отношений

Итак, несколько правил использования xpath :

  1. Никогда не используй плагины или копирование xpath из кода страницы средствами браузера или веб-разработчика. Вот например как показывает одну ссылку плагин к Файрфокс: //header/div/ul/li/a . Разве из этой ссылки понятно, о каком элементе речь, что мы ищем? Ведь порой бывает, что взглянув на локатор в коде или в тексте исключения мы должны понять о каком элементе речь. Как это можно понять из такой строки? Я уже не говорю о том, что любой код, основанный на таких локаторах упадет при любом дуновении ветерка. Каждый раз, когда ты пишешь локатор подобный // div / div / ul / li (продолжать можно долго) в мире умирает что-то хорошее!!! Это, если хотите, говнокод тестировщика, который нужно выжигать каленым железом.
  2. Старайся написать xpath как можно короче и понятнее, используй его возможности и схожесть с языком программирования, чтобы и через месяц ты сам мог понять о каком элементе речь и что нужно поправить в случае изменения верстки
  3. Xpath’у время и место! Если есть возможность использовать id, name или внести в код id то сделай это!
  4. Вместо длинной цепочки слешей, как указано выше, используй отношения элементов: предок, потомок, сестринский элемент
  5. Можно и нужно использовать логические операции and, not , or
  6. Нормальный xpath всегда начинается с // и не использует фильтры с номером элемента в стиле (например // div )

Переходим к делу и практике, тот xpath, что указан выше (//header/div/ul/li/a ) на самом деле можно указать в виде //a . Согласись, что есть разница и в длине текста и в понимании его, ведь тут видно по тегу, что это ссылка и ее текст –Pricing. То есть ты можешь и сам найти этот элемент на странице визуально и в случае исключения с таким локатором сразу знаешь, что и где искать!

Теперь о тех командах, которые тебе реально пригодятся для написания грамотных и удобных локаторов:

Как видим id явно сгенерирован и привязаться к нему нельзя, класс тоже не внушает доверия, кроме того Selenium не разрешает использовать сложносоставные имена в локаторе className, но тут есть текст, который решает проблему: // a

  • contains(параметр, искомое ) –возвращает элемент если он содержит искомое, знакомая команда не так ли? Ты ее видишь постоянно в языке программирования. Очень удобно использовать в связке с text() если составляем сложный локатор и не знаем точно всего текста, например: // div[@ class=’ buttons’ and contains(text(),’ Save’)] – как видишь, это некоторый элемент, который относится к кнопкам и на нем есть текст Save. Представь, что в твоем тестируемом веб-приложении есть несколько страниц, на которых есть кнопка сохранения, но с разными текстами –сохранить файл, сохранить диаграмму, сохранить отчет и так далее. Тебе не придется писать локаторы для них всех, хватит одного для всех кнопок сохранения и он описан выше. Обрати внимание на использовании в фильтре сразу двух условий!

Кроме того, очень полезная возможность – это искать элемент по одному из слов в названии класса.Пример:

Все решается вот так: //div , то есть мы ищем элемент, у которого в классе есть какое-то уникальное сочетание слов. Данная возможность contains очень помогает в самых разных ситуациях! Обрати внимание, что параметр и искомое идут через запятую, нельзя писать contains(text()=’smth’)

  • starts- with(параметр, искомое) –все аналогично contains, только эта команда возвращает элементы начинающиеся с искомого. Если возвращаться к примеру с кнопками сохранения, то локатор можно переписать вот так // div[@ class=’ buttons’ and starts- with(text(),’ Save’)] у нас ничего не упадет, так как слово save обычно первое на кнопке и значит локатор опять же будет работать для всех кнопок сохранения. В общем это более узкая форма contains

Теперь пойдут команды отношения элементов (предок, родитель, ребенок, потомок, сестринский элемент), которые позволяют очень гибко найти практически любой элемент на странице при грамотном применении.

Формат использования //начальный элемент/отношение::тег(фильтр) конечного элемента. Обрати внимание на два двоеточия после отношения и не забывай после двоеточий указать тег, а лучше и фильтр искомого элемента, так как потомков может быть и много, а нам нужен какой-то конкретный.

Нам нужно ввести текст в input, но как видишь тут имеется ряд проблем – id динамический, классов и сгенеренных id со словом input на странице много, привязаться вроде не к чему. Но тут есть элемент с текстом, который уникален для страницы, вот к нему и прицепимся:

// div[ text()=’Тема’]/ preceding- sibling:: input — мы сначала находим уникальный элемент с текстом, а потом от него ищем предшествующий сестринский элемент, делая фильтр-уточнение, что ищем именно input. Еще пример:

Нам нужно кликнуть кнопку, на которой нет текста, только иконка, но как видишь у нее все те же проблемы с id плюс есть куча одноименных классов. Нас спасает то, что у предшествующего элемента есть уникальное название класса, вот от него и будем плясать: //div/following-sibling::div – находим элемент у которого есть уникальное слово в названии класса и от него уже ищем следующий сестринский элемент, с тегом div. Учитывай, что даже если сестринских последующих элементов с тегом div будет много вернется только самый первый!

То представим, что нам нужен непосредственно элемент с id=__vz4019, для всех на данной картинке он является родителем (parent) и поэтому его можно вытянуть через любой из них, например // div[ text()=’Тема’]/ parent:: div

Кстати, обращение к родительскому элементу, можно заменить двумя точками и без тега, вот так // div[ text()=’Тема’]/ ..

Так как все элементы в примере — дети, то можно любого из них найти от родителя вот так:

//div/child::input – находим родителя, а от него ищем ребенка с тегом input.


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

// div[@ class=’ listitem Folder’]/ descendant:: span[ text()=’ Folder name’] – сначала находим класс папки, потом среди его потомков ищем тег span и нужный нам текст. Вы можете спросить –а почему просто по тексту не искать? Дело в том, что элементов с таким текстом на странице может быть больше одного, а нам нужна именно папка.

Кстати вместо descendant можно использовать двойной слеш // это означает -любой вложенный элемент. Пример выше превращается в

// div[@ class=’ listitem Folder’]/ / span[ text()=’ Folder name’]

  • ancestor – предок, опять же отличающийся от parent тем, что может быть любой удаленности, то есть прадедушкой. Если возвращаться к предыдущему примеру, то найти элемент папки по тексту можно так // span[ text()=’ Folder name’]/ ancestor:: div[@ class=’ listitem Folder’]

Важно понимать, что можно, но крайне нежелательно использовать в одном локаторе несколько отношений, например:

// div[@ class=’ One]/ child :: div[@ class=’ Two’]/ descendant :: input[@ class=’ Three] . Такой локатор работать будет, но он уже сложно читается и скорее всего есть возможность подобрать другой, не нужно использовать такое без необходимости, помним правило номер 2. Совсем недопустимо использовать в одном локаторе обратные отношения то есть сначала искать потомка, потом его предка или наоборот.

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

Итак, применяйте указанные команды, ищите правильные элементы, соблюдайте вышеозначенные правила и у вас не будет проблем с написанием грамотных локаторов, которые легко понять, прочесть, исправить. А главное вы поймете, что xpath очень удобен для написания локаторов к любым элементам.

Стандартное решение через xsl:choose и его недостатки

Подобные задачи в XSLT традиционно решают так:

< xsl : choose > < xsl : when test = " $cond " > < xsl : copy-of select = " $one " /> < xsl : otherwise > < xsl : copy-of select = " $two " />

Однако это решение, помимо очевидной громоздкости, обладает еще одним, весьма существенным недостатком: то, что получается в результате — RTF. Т.е. результат такого выражения приемлем в выходном потоке, но никак не может быть использован (по крайней мере в рамках "чистого" XSLT 1.0, без расширений) для последующих преобразований.

Конечно, с помощью exsl:node-set (или ее аналога), мы можем превратить этот RTF в полноценное множество узлов, но останется одна проблема: это будет новое множество узлов, никак не связанное с исходными документами. Узел, полученый в результате преобразования, будет его корневым узлом — применить к нему, к примеру, ось ancestor мы уже не сможем.

Попробуем преодолеть этот недостаток.

Решение на чистом XPath 1.0

Решение

Воспользуемся тем фактом, что при задании осей выборки мы можем использовать предикаты , налагающие на них ограничивающие условия: $one[$cond] | $two .

Его недостатки

Это выражение лишено недостатков "классического" варианта, но у него есть свои, «дополняющие» их:

  1. Если xsl:choose нельзя (кросс-процессорным способом) использовать для получения node-set"ов, то этот вариант, напротив, может работать только с node-sets в качестве как параметров, так и результата.

Т.е. приведенный пример будет работать только если $one и $two — множества узлов. Запихнуть в это выражение строку или число уже не получится.

  1. Если в примере вместо переменной $cond использовать некоторое сложное выражение, результат его вычисления может зависеть от содержимого $one и $two .

Например $one | $two — значение count(.) будет разным в обоих случаях, и совсем не тем, которое, очевидно, имел бы ввиду гипотетический горе-автор такого выражения...

  1. Исходное выражение достаточно сложно для понимания. Когда я привел пример его употребления в форуме, мне приходилось слышать замечания, что «вообще-то | в xslt — это не or , а объединение нодесетов...»

Совершенно верно, но or в XPath работает отнюдь не так, как привыкли пользователи большинства современных языков. Оно возвращает результат уже преобразованным в логический тип, что делает его совершенно непригодным для данной задачи. И здесь нужно использовать именно объединение с последующей проверкой, хоть это и не очевидно.

  1. При обработке этого выражения вычисляются обе ветки и лишь затем проверяется их соответствие условию. Хотя это почти никогда не вызывает проблем (выражения XSLT не должны иметь побочных действий) — иногда это все же имеет значение.

Утверждение «никакие функции в XSLT не имеют побочных эффектов» неверно . Помимо функций расширений (а наличие у них побочных эффектов вполне возможно), есть по крайней мере одна такая функция, входящая в спецификацию XSLT 1.0. Это функция document : действительно, вряд ли кто-нибудь станет утверждать, что вызов document("http://example.com/cgi-bin/counter.pl") побочных эффектов иметь не будет...

Заметим сразу, что преодолеть эти недостатки этого выражения в рамках «чистого» XSLT 1.0 (т.е. без расширений) нельзя. В этом случае остается лишь предложить использовать этот вариант, если нам нужен на выходе node-set, и "классический" — во всех остальных случаях.

Использование расширений EXSLT

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

Для создания функций расширений EXSLT предоставляет элементы func:function и func:result , где префикс func: соответствует xmlns http://exslt.org/functions .
Первый из них практически точно соответствует элементу XSLT 2.0 xsl:function . Аналога же result там нету, для создания возвращаемых значений используется стандартный синтаксис коструктора последовательности .

Решение для общего случая

Итак, определим собственную функцию расширения (lib: — префикс нашего собственного пространства имен, допустим — urn:xslt:library):

< func : function name = " lib:if" > < xsl : choose > < xsl : when test = " $cond " > < func : result select = " $then" /> < xsl : otherwise > < func : result select = " $else" />

Наш пример с ее помощью запишется так: lib:if($cond, $one, $two) .

Если есть побочные эффекты

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

В большинстве случаев этим можно пренебречь. Но если наши аргументы имеют побочные эффекты — это недопустимо. Потому воспользуемся для решения этой задачи еще одной функцией EXSLT — dyn:evaluate , где xmlns:dyn = "http://exslt.org/dynamic" . Она, как нетрудно догадаться, получает в качестве аргумента строку и вычисляет ее как выражение XPath непосредственно в том контексте , в котором вызвана:

< func : function name = " lib:if-dyn" > < xsl : choose > < xsl : when test = " $cond " > < func : result select = " dyn:evaluate($then)" /> < xsl : otherwise > < func : result select = " dyn:evaluate($else)" />

Таким образом, любое выражение, которое мы передадим этой функции — в виде строки, не следует забывать об этом — будет вычислено лишь в том случае, если оно действительно используется. Но следует отметить, что lib:if , вообще говоря, должна выполняться быстрее (и не требовать еще одного модуля расширений) — поэтому, если нам не нужна эта дополительная функциональность, всегда следует использовать lib:if .

Сегодня мы плотно рассмотрим тему использования XPath вместе с PHP. Вы увидите на примерах, как XPath значительно сокращает количество кода. Рассмотрим использование запросов и функций в XPath.

В начале, предоставлю вам два типа документов: DTD и XML, на примере которых мы рассмотрим функционирование PHP DOM XPath. Вот как они выглядят:

A Book An Author Horror chapter one Another Book Another Author Science Fiction chapter one

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

Простой синтаксис XPath позволяет обращаться к элементам XML документа. Наиболее простым способом, можно прописать путь к желаемому элементу. Используя XML документ, поданный выше, следующий XPath запрос возвратит коллекцию текущих элементов, находящихся в элементе book:

//library/book

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

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

//library/book/author/..

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

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

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

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

domDocument->getElementsByTagName("author"); foreach ($elements as $element) { if ($element->nodeValue == $author) { $total++; } } return $number; }

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

domDocument); $result = $xpath->query($query); return $result->length; }

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

domDocument); return $xpath->evaluate($query); }

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

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

XPath стоит использовать, не только потому что это делает ваш PHP код проще, это также дает преимущество в скорости выполнения кода. Я заметил, что первая версия была на 30% быстрее в среднем, по сравнению со второй. Но третья на 10% быстрее первой. Конечно же, это зависит от вашего сервера и запросов, которые используете. Использование XPath в его чистом виде, дает величайшие результаты в скорости и простоте написания кода.

XPath Функции

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

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

id("isbn1234 isbn1235")/title

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

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 запросами.

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

domDocument); $result = $xpath->query($query); $title = $result->item(0)->getElementsByTagName("title") ->item(0)->nodeValue; return str_word_count($title); }

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

Примерный синтаксис вызова функций будет следующим:

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

Давайте совместим все это вместе в следующем примере функции getNumberOfWords():

domDocument); //регистрируем php namespace $xpath->registerNamespace("php", "http://php.net/xpath"); //теперь php функции могут вызываться в 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",(//library/book[@isbn = "$isbn"]/title))

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

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

nodeValue == "George Orwell"; }

Аргумент, который передается в функцию, является массивом DOM элементов. Эта функция проходит по массиву и определяет нужные элементы, после чего включает их в DOMNodeList. В этом примере, испытываемый узел был /book, также мы использовали /author для определения нужных элементов.

Теперь мы можем создать функцию getGeorgeOrwellBooks():

domDocument); $xpath->registerNamespace("php", "http://php.net/xpath"); $xpath->registerPHPFunctions(); $query = "//library/book1"; $result = $xpath->query($query); $books = array(); foreach($result as $node) { $books = $node->getElementsByTagName("title") ->item(0)->nodeValue; } return $books; }

Если функция compare() статическая, тогда вам нужно внести поправку в XPath запрос:

//library/book

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

В завершение

XPath - это отличный способ сократить количество кода и повысить его обработку, при работе с XML. Дополнительная функциональность PHP DOM позволяет вам расширить XPath функции. Это реально полезная штука, если вы будете ее использовать и углубляться в специфику, вам придется меньше и меньше писать кода.