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-выражении — это оператор «меньше». В коде на других языках его иногда приходится экранировать как < (например, внутри 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 сразу вернёт массив узлов по вашему выражению.