2025, Nov 01 02:16
ElementPath против XPath в lxml: когда findall(), а когда xpath()
Разбираем разницу между ElementPath и полным XPath в lxml: когда уместны методы find(), findall(), findtext(), а когда нужен xpath(). Ошибки и верные подходы.
При работе с lxml рука тянется сразу к xpath(). Это мощный и привычный инструмент. Но если вы заметили, что часть вызовов xpath() можно заменить на findall(), вы правы: по назначению методы пересекаются, а по возможностям — нет. Понимание различий помогает выбрать верный способ и избежать скрытых ошибок.
В чём реальная разница?
lxml.etree поддерживает упрощённый синтаксис путей в методах find, findall и findtext у ElementTree и Element, знакомый по оригинальной библиотеке ElementTree (ElementPath). В качестве расширения, специфичного для lxml, эти классы также предоставляют метод xpath(), который поддерживает выражения в полном синтаксисе XPath, а также пользовательские функции-расширения.
ElementPath
В библиотеке ElementTree есть простой язык путей, похожий на XPath, — ElementPath. Главное отличие в том, что в выражениях ElementPath можно использовать нотацию {namespace}tag. Однако продвинутые возможности — такие как сравнение значений и функции — недоступны.
Element.findall() находит только те элементы с указанным тегом, которые являются непосредственными дочерними узлами текущего элемента.
Element.find() возвращает первого ребёнка с заданным тегом, а Element.text даёт доступ к текстовому содержимому элемента.
Отсюда следует, что первый аргумент findall() — это не полноценное XPath‑выражение. Это простая строка ElementPath, подходящая для прямой структурной навигации по непосредственным детям. Напротив, xpath() вычисляет полное XPath‑выражение относительно узла контекста.
Пример проблемы: использование findall() как будто он понимает полный XPath
В примере ниже попытка получить текстовые узлы из дочерних td выполняется через findall(). Здесь и возникает ошибка: ElementPath не поддерживает такие функции, как text().
from lxml import etree
xml_blob = "<table><tr><td>One</td><td>Two</td></tr></table>"
root_node = etree.fromstring(xml_blob)
# Здесь используется функция (text()), предполагается полная поддержка XPath.
# ElementPath этого не умеет, и вызов завершается ошибкой.
cell_texts = root_node.findall(".//td/text()")
Это не баг lxml. Проблема в несоответствии между возможностями ElementPath и тем, что предоставляет полноценный механизм XPath. Как отмечалось выше, ElementPath не реализует продвинутые возможности вроде функций.
Почему так происходит: ElementPath против полного XPath
Методы find(), findall() и findtext() опираются на простой язык, похожий на XPath, — ElementPath. Его возможности намеренно ограничены. Практически это означает, что методы предназначены для структурной навигации по непосредственным дочерним узлам, а не для функциональных возможностей выражений — таких как функции, сравнения значений или сложные оси. В то время как xpath() выполняет полные XPath‑выражения, включая функции вроде text().
Вывод прост: там, где требуются все возможности XPath, используйте xpath(). Если нужно лишь пройтись по непосредственным детям по имени тега, подойдут методы семейства find*().
Решение: xpath() — для полноценных выражений, findall() — для непосредственных детей
Чтобы получить текстовые узлы из вложенных td, перейдите на xpath().
from lxml import etree
xml_payload = "<table><tr><td>One</td><td>Two</td></tr></table>"
doc_root = etree.fromstring(xml_payload)
# Полноценное XPath-выражение: работает с функциями и потомками
texts = doc_root.xpath(".//td/text()")
# texts == ["One", "Two"]
С другой стороны, если нужно пройти только по непосредственным детям по имени тега, оставайтесь на findall(). Это соответствует назначению ElementPath.
from lxml import etree
snippet = "<table><tr><td>One</td><td>Two</td></tr></table>"
root_el = etree.fromstring(snippet)
# Выбор непосредственных детей через ElementPath
rows = root_el.findall("tr")
first_row_cells = rows[0].findall("td")
values = [cell.text for cell in first_row_cells]
# values == ["One", "Two"]
Что на самом деле представляет собой аргумент path у findall()
Параметр path — это строка ElementPath. Это «простой синтаксис пути», описанный выше, а не полноценное XPath‑выражение. Поэтому конструкции вроде text() там не работают, тогда как в xpath() — работают.
Почему это различие важно
Выбор метода с нужным уровнем выразительности помогает избежать хрупкого кода и непонятных ошибок. Полагаетесь на полный набор возможностей XPath — берите xpath(). Если нужно выбирать непосредственных детей по тегу и сохранять запросы простыми, отлично подойдёт семейство find*(). Соблюдение этой границы делает работу с XML чище и предсказуемее.
Итог
В lxml методы find(), findall() и findtext() реализуют ElementPath — простой и ограниченный язык путей, ориентированный на работу с непосредственными детьми и базовую структурную навигацию. Метод xpath() вычисляет полноценные XPath‑выражения с расширенными возможностями, включая функции. Используйте семейство find*() для выбора непосредственных детей по тегу, а при необходимости полной мощности XPath — например, функций или более сложных запросов — переходите на xpath().