LEARN X · ЗА 12 МИН

XPath

XPath за 12 минут: пути, оси, предикаты, атрибуты, функции и практические выражения для парсинга HTML и Selenium на одной странице.

XPath (XML Path Language) — язык запросов для навигации по дереву XML и HTML. Вы пишете выражение-путь, а движок возвращает набор узлов (элементов, атрибутов, текста), которые ему соответствуют. XPath повсюду: в Selenium и Playwright для поиска элементов на странице, в библиотеках парсинга (lxml, Scrapy), в XSLT-преобразованиях и в инструментах разработчика браузера. Ниже — весь язык на одной странице: каждое выражение сопровождается пояснением. Поскольку в XPath нет синтаксиса комментариев, пояснения идут после выражения в той же строке или в отдельном абзаце.

Что такое XPath и где он применяется

Документ XML/HTML — это дерево узлов. XPath адресует узлы по их положению в дереве, имени тега, атрибутам или тексту. Результат выражения — упорядоченный набор узлов в документном порядке.

Возьмём для примеров такой фрагмент HTML (теги экранированы, чтобы показать разметку):

<html>
  <body>
    <div class="content">
      <h1>Заголовок</h1>
      <ul id="menu">
        <li><a href="/home">Главная</a></li>
        <li><a href="/docs" class="active">Документация</a></li>
      </ul>
    </div>
  </body>
</html>

Типичное применение — Selenium: driver.find_element("xpath", "//a[@class='active']") вернёт ссылку «Документация».

Абсолютные и относительные пути

Путь — это шаги, разделённые слешами. Ведущий слеш задаёт точку отсчёта.

/html/body/div     — абсолютный путь: от корня документа строго по тегам
//div              — относительный (где угодно в дереве): любой div на любой глубине
.                  — текущий (контекстный) узел
..                 — родительский узел
.//a               — все потомки-ссылки относительно текущего узла
//ul/li            — все li, являющиеся прямыми детьми любого ul

Один слеш (/) — прямой потомок, ровно один уровень вниз. Двойной слеш (//) — любой потомок на любой глубине. Абсолютный путь хрупок при изменении вёрстки, поэтому в парсинге чаще берут относительные пути от устойчивого якоря.

Выбор узлов по имени

//a            — все элементы a (ссылки)
//div/p        — все p, являющиеся прямыми детьми div
//*            — любой элемент (звёздочка = любое имя тега)
//div/*        — все прямые дети любого div, независимо от тега
//ul/li[1]/*   — все дети первого li внутри ul

Звёздочка (*) подбирает узел с любым именем. Это удобно, когда тег заранее неизвестен, но важна позиция в дереве.

Оси: направления обхода дерева

Ось задаёт направление поиска относительно контекстного узла. Синтаксис: ось::узел. Привычные / и // — это сокращения осей child и descendant-or-self.

//ul/child::li                  — дети-li (то же, что //ul/li)
//a/parent::li                  — родитель ссылки, если это li
//a/ancestor::div               — все предки-div выше ссылки
//div/descendant::a             — все потомки-ссылки внутри div
//li/following-sibling::li       — следующие соседи-li на том же уровне
//li/preceding-sibling::li       — предыдущие соседи-li
//h1/following::a                — все ссылки, идущие в документе после h1
//a/ancestor-or-self::*          — сам узел и все его предки

Ось following-sibling особенно полезна для «возьми значение, которое стоит рядом с подписью»: например, ячейку справа от заголовка.

Предикаты: фильтр в квадратных скобках

Предикат [...] оставляет из набора только узлы, удовлетворяющие условию. Внутри можно писать позиции, проверки атрибутов и функции.

//li[1]              — первый li (нумерация с 1, не с 0!)
//li[last()]         — последний li в наборе
//li[last()-1]       — предпоследний li
//li[position()<=2]  — первые два li (position() меньше или равно 2)
//ul/li[2]/a         — ссылка внутри второго li
//div[@id]           — div, у которого есть атрибут id (любое значение)
//a[@href][@class]   — ссылка, у которой есть и href, и class (два предиката подряд = И)

Несколько предикатов подряд работают как логическое И и применяются слева направо.

Атрибуты

К атрибуту обращаются через символ @. Атрибут можно проверять в предикате или выбирать как самостоятельный узел.

//a[@href='/docs']     — ссылка с конкретным href
//*[@id='menu']        — любой элемент с id, равным menu
//a/@href              — выбрать сами значения атрибута href (узлы-атрибуты)
//input[@type='text']  — текстовые поля ввода
//li[@*]               — li, у которого есть хотя бы один атрибут

Выражение //a/@href вернёт не элементы, а значения href — это удобно для извлечения списка URL при скрапинге.

Фильтрация по тексту

Функция text() даёт текстовый узел элемента. Для частичного совпадения берут contains() и starts-with().

//a[text()='Главная']          — ссылка с точным текстом «Главная»
//h1[contains(text(),'Заго')]  — h1, чей текст содержит подстроку «Заго»
//a[starts-with(@href,'/doc')] — ссылки, href которых начинается с /doc
//li[contains(.,'Документ')]    — li, где где-либо внутри встречается текст
//button[normalize-space()='Отправить'] — текст с обрезанными пробелами по краям

Точка внутри contains(.,'...') означает «весь текстовый контент узла со всеми потомками», тогда как text() — только прямой текстовый узел. normalize-space() убирает лишние пробелы и переводы строк — незаменимо для верстки с отступами.

Логические операторы

Внутри предикатов условия комбинируются операторами and, or и функцией not().

//a[@class='active' and @href='/docs']  — оба условия истинны
//li[@id or @class]                       — есть id ИЛИ class
//a[not(@class)]                          — ссылки без атрибута class
//div[@class='content' and .//a]          — div с классом, внутри которого есть ссылка
//input[not(@disabled)]                   — активные поля (без атрибута disabled)

Функция not() инвертирует условие и часто комбинируется с contains() для исключения: not(contains(@class,'hidden')).

Сравнения и числовые функции

XPath поддерживает сравнения =, !=, а также <, >, <=, >= (в выражениях их часто экранируют). Полезны функции подсчёта и позиции.

//li[position()=1]       — первый по позиции (эквивалент //li[1])
//li[position()!=1]      — все, кроме первого
//ul[count(li)>3]        — список, в котором больше трёх li
//tr[count(td)=4]        — строки таблицы ровно с четырьмя ячейками
//li[position() mod 2=0] — чётные элементы (mod — остаток от деления)
//a[string-length(@href)>5] — ссылки с длинным href

Помните: < в XPath-выражении — это оператор «меньше». В коде на других языках его иногда приходится экранировать как &lt; (например, внутри XML-атрибута XSLT).

Объединение путей оператором |

Вертикальная черта объединяет результаты двух независимых выражений в один набор узлов.

//h1 | //h2 | //h3        — все заголовки трёх уровней сразу
//a[@class='active'] | //a[@href='/home']  — две группы ссылок в одном наборе
//div//a | //nav//a       — ссылки и из div, и из nav

Оператор | (union) собирает узлы в документном порядке и убирает дубликаты, если узел попал в оба набора.

Работа с HTML-классами

Атрибут class в HTML почти всегда содержит несколько имён через пробел («btn btn-primary active»). Поэтому сравнение @class='active' ломается, и нужен contains() с пробелами-границами.

//div[@class='content']                       — точное совпадение всего class (хрупко!)
//div[contains(@class,'content')]             — class содержит подстроку content
//div[contains(concat(' ',normalize-space(@class),' '),' active ')]  — надёжная проверка одного класса
//button[contains(@class,'btn') and contains(@class,'primary')]      — сразу два класса

Приём с concat и пробелами — золотой стандарт: он находит класс active как отдельное слово и не срабатывает ложно на inactive или active-tab.

Практические примеры

Собранные вместе типовые задачи парсинга и автотестов.

//a/@href
Извлечь все ссылки страницы (список URL).

//table//tr[position()>1]
Все строки таблицы, кроме строки заголовка.

//table//tr[td]/td[2]
Вторая ячейка каждой строки с данными (у которой есть td).

//label[contains(text(),'Email')]/following-sibling::input
Поле ввода, стоящее сразу после подписи «Email».

//button[normalize-space()='Купить']
Кнопка по видимому тексту, устойчивая к лишним пробелам.

//div[contains(@class,'price')]//text()[normalize-space()]
Весь непустой текст внутри блоков с ценой.

(//div[@class='card'])[last()]
Последняя карточка (скобки группируют набор перед применением индекса).

Важная тонкость: //div[1] — это «первый div среди детей каждого родителя», а (//div)[1] — «самый первый div во всём документе». Скобки меняют смысл индекса, и в реальном парсинге это частый источник ошибок.

Что дальше

Вы видели ядро XPath 1.0 — версии, которую понимают браузеры, Selenium и lxml. В XPath 2.0/3.1 добавлены регулярные выражения (matches()), последовательности, циклы for и кванторы some/every, но в вебе и автотестах их поддержка ограничена. Для тренировки откройте консоль браузера и пробуйте: команда $x("//a") в DevTools сразу вернёт массив узлов по вашему выражению.

Поддержать проект