Публикация не ставит целью заменить или продублировать руководства по XPath, коих полным-полно, в т.ч. на русском. Публикация лишь описывает основные понятия, необходимые для быстрого освоения этого механизма именно в 1С.
Заранее извиняюсь за огрехи форматирования, но я ухлопал два часа, так и не победив полностью окно публикации ИС... Ну вот что-то криво у меня в манипуляторах... Если где потеряны скобки или ещё что, извиняйте. По смыслу, думаю, всё будет ясно.
Что это такое?
Суть механизма XPath - быстрое получение нужных элементов из документа, построенного согласно DOM на основании текста, являющегося правильным XML. То есть, имеем xml-строку или файл, содержимое которых хотим быстро рассмотреть на предмет наличия неких тегов. В терминах DOM - хотим найти элементы и/или атрибуты, или другие составляющие DOM-модели, в этом документе. Чаще всего речь именно об элементах, атрибутах и их свойствах.
XPath - механизм, на входе которого строковый запрос, описывающий нужное нам, а на выходе коллекция найденных объектов, которую можно обойти циклом.
Пространство имён
Прежде, чем рассматривать собственно XPath, замечу, что критически важное значение для успешной работы с ним имеет понятие "Пространство имён". Известное по работе с другими механизмами, оно, тем не менее, не всегда очевидно и понятно.
Пространство имён - это просто строка, уникальное имя, которое определяет "среду обитания" тегов. Исторически есть 2 варианта написания, URL и URN, и в 1С используются URL, т.е. внешний вид этой строки должен выглядеть правильным интернет-адресом, пусть и реально не существующим. Например, "http://www.w3.org/2001/XMLSchema" или "http://www.404_1C.ru", лишь бы он был уникален в пределах данных, с которыми вы работаете. Эти адреса и называют URI.
Для краткости, вместо URI, в тексте используют префиксы (по сути, алиасы, псевдонимы) пространств имён - это тоже строки, но покороче. Поэтому объявление пространства имён, например, для "среды обитания" тегов/элементов DOM, понятий, которыми пользуются айтишники, будет таким:
<НачалоДокумента xmlns:IT="http://v8.it_cool.ru">.
А для понятий отдела кадров, в свою очередь, можно объявить:
<НачалоДокумента xmlns:Kadri="http://NashiKadri"/>.
В общем случае, объявление пространства имён размещается внутри теговых рамок и имеет вид xmlns:="". Причём, объявление само по себе не считается ни отдельным тегом, ни атрибутом, и в коллекции элементов DOM-документа тоже не входит.
Зачем это нужно? Чтобы различать понятия. Предположим, в xml-файле есть тег или атрибут "". Понять его содержимое можно по-разному. И вот тут используются префиксы пространств имён, поэтому IT:Мать это сведения о материнской плате ПК, а Kadri:Мать - семейные данные человека. Обратите внимание, что, если речь о теге, и закрываться он тоже должен с указанием префикса.
На один xml-текст можно объявить сколько угодно пространств имён с разными префиксами и применять рядом, вперемешку и как угодно, для тегов и атрибутов. Объявлений внутри тега может быть несколько, они разделяются пробелами. Более того, для конкретного случая можно "по месту" переопределить или доопределить пространство имён - объявлять их вовсе не обязательно в начале документа, это можно сделать где угодно:
<Klients:Klient xmlns:Klients ="http://www.ohrenevshie.com">
<Summ>99999<Summ>
<Klients:Person xmlns:Klients="http://www.VIP_bazar.com">Господин директор<Klients:Person>
<Klients:Chances>No chance (((<Klients:Chances>
<Klients:Klient>
Так, тут мы используем пространство имён для неплательщиков, но элемент Person переопределён, и находится в пространстве имён для ВИПов. И действует это переобъявление только в рамках этого тега. Также заметим, что тег/элемент Summ вообще не входит явным образом в указанное пространство имён.
Итак, внутри одного xml обычно находятся теги и атрибуты с префиксами, принадлежащими разным, объявленным уникально, пространствам имён. Для работы с этим никаких особых средств нет, да это и не требуется. В 1С есть объект, который, теоретически, должен предоставлять возможность работы с разными пространствами имён, и он называется РазыменовательПространствИменDOM, но пользы от него мало. Разыменователь может создаваться на основании одной или нескольких связок "префикс-URI", или на основании документа DOM или его узла, но на практике - как ни создавай, разницы не заметно. Несмотря на уверения СП, прямое влияние контекста разыменователя, по крайней мере на работу XPath, проследить не удалось. Обойти коллекцию имеющихся пространств имён с его помощью тоже нет возможности. И единственная, пожалуй, польза, это что на некоем уровне, для документа в целом или для любого его элемента, где используется указание URI пространства имён, можно его получить по его префиксу:
Разыменователь=Новый РазыменовательПространствИменDOM(ДокументИлиЭлемент);
URI=Разыменователь.НайтиURIПространстваИмен("IT");
// для конструкции xmlns:d3p1=http://v8.1c.ru/8.1/data/core метод с аргументом "d3p1" вернёт строку "http://v8.1c.ru/8.1/data/core"
Т.е. чисто теоретически, можно пробежаться по DOM-документу и поискать по префиксам, но несколько полезнее в этом плане, на мой взгляд, поиск самим XPath по оси "namespace" (см.ниже).
Замечу ещё, что понятие "имя" для элемента DOM (в т.ч. атрибута) и понятие "локальное имя" в рамках 1С также выглядят иначе, нежели должны бы по смыслу. Локальное имя пусто и бесполезно, хотя, по идее, полное имя должно включать префикс пространства имён, а локальное - не включать, т.е. в целом Klients:Person это должно быть имя, а Person - локальное имя. Однако локальные имена хоть сколько-то логично ведут себя лишь для самих пространств имён, т.е. для элемента типа "ПространствоИменXPath", и для стандартного пространства там локальное имя пусто. Также, мне не удалось добиться проку ни от одного из свойств ДокументDOM, связанных с URI, они все пусты, как ни инициализируй документ.
А вот теперь - внимание! В реализации XPath 1С есть одна решающая тонкость. Дело в том, что, вообще-то, возможно и пространство имён БЕЗ префикса, напрямую xmlns="", оно называется "стандартное" пространство имён. Придумали это ради работы "по умолчанию", но для 1С это не срабатывает. Если у вас в xml-тексте есть хоть одно объявление стандартного пространства, то внутри области его действия (а иногда и вообще по всему тексту) для любых тегов, не имеющих префиксов другого пространства имён, поиск XPath работать не будет.
Проще говоря, логика работы XPath с xml в зависимости от наличия там пространств имён такова:
1. Если нет никаких пространств имён, отлично всё находит при любых запросах.
2. Если есть некие префиксованные пространства имён, отлично всё находит, но не забывайте указывать префиксы как часть имени. Если у вас одинаковые имена (но разные префиксы), пишите их в запросе вместе с префиксом. Если вам нужны имена, у которых префиксов нет (т.е. относящиеся к стандартному пространству имён), то тоже смело пишите их, прямо без префиксов, всё найдёт. Учтите, что в ряде случаев указание в запросе префикса, который не объявлен, приводит к ошибке "неверный запрос".
3. Если, наряду с префиксованными пространствами имён, или как единственное, есть стандартное пространство имён, то поиск по именам элементов (тегов) возможен только для тех, у кого указан напрямую префикс. Если стандартное пространство единственное, или если префиксов у элементов нет, поимённый поиск по элементам не работает. Всегда работать будет только поиск по именам атрибутов. То есть, при наличии стандартного пространства имён:
"//*[@Номер]" - вернёт все те элементы, где есть атрибут с именем "Номер";
"//Документ[@Номер]" - не вернёт вам ничего.
4. Если есть не только стандартное пространство имён, но и префиксованное, вообще начинается красота. Так, если в целом заявлено только лишь стандартное пространство имён, а в тексте для конкретного тега есть переопределение:
<Документ Номер="000000017"/>
<Тыц:Документ xmlns:Тыц="http://NowhereNamespace" Номер="000000018"/>
<Документ Нумер="000000019"/>
то при попытке найти "//Документ" вы не найдёте ничего; а при попытке "//Тыц:Документ" вы получите что? Не угадали, все три элемента, хотя остальные два без префиксов. А вот если у вас то же пространство имён "Тыц" объявлено в старшем теге, наряду со стандартным, тогда "//Документ" тоже ничего не вернёт, но "//Тыц:Документ" вернёт ровно те элементы, в чьём имени префикс чётко указан. Словом, внутренняя логика происходящего несколько своеобразна.
Повторюсь, аргумент конструктора разыменователя вообще никак ни на что не влияет, даже на локально заданные пространства имён. Либо XPath успешно ищет везде, либо не может найти нигде.
Советую просто искать по тексту xml вхождение подстроки "xmlns=" и заменять на любое вменяемое с префиксом, например, на "xmlns:Пыщь=", и поиск по именам элементов сразу заработает.
Запрос XPath
Теперь о том, что такое вообще этот "запрос".
Строковый запрос XPath чуть более процедурный, чем SQL, т.е. больше значимость указания того, как именно получить результат, по сравнению с обычными запросами, где важно "что нужно", но не очень важно "как это получить". Запрос XPath составляется с учётом и пониманием порядка получения нужной выборки. Делается это, в первую очередь, исходя из естественного рассуждения, "как бы мы получали это вручную", т.е. работа механизма легко эмулируется и отлаживается, и легко разбивается на этапы.
Запрос XPath - это строка, разделённая символами "/" на отдельные "шаги" выполнения.
На каждом шаге мы находимся в некоем контексте (на некоей позиции в тексте xml, на ветке в дереве DOM), т.е. знаем, откуда ищем - это называется "контекст" и этим в 1С можно управлять по ходу поиска. Это та ветка дерева, куда нас привёл предыдущий шаг, если он был, и откуда начнётся новый шаг поиска.
На каждом шаге можно указать, среди чего ищем - это называется "ось". Осей по смыслу немного и они отлично понятны, если представить обычный поиск в дереве значений 1С. Оси бывают такие:
ancestor:: Возвращает множество всех предков (родительских веток до корня).
ancestor-or-self:: - Возвращает множество предков и текущий элемент (то же, плюс текущий).
attribute:: - Возвращает множество атрибутов текущего элемента (т.е. это уже когда известна ветка дерева, получить значения колонок дерева для неё). Можно заменять на "@".
child:: - Возвращает множество потомков на один уровень ниже (все ветки, у которых родитель - текущая). Есть тонкость: обозначение этой оси часто пропускают и можно запутаться, будьте внимательны. Кроме того, начиная "от корня", учтите, что запрос "/child::*" даст вам первый дочерний элемент от корня, т.е. то, что называется "корневой элемент" и получается как ДокументDOM.ЭлементДокумента. А запрос "child::*" даст вам уже элементы, подчинённые этому корневому, т.е. на 1 уровень глубже.
descendant:: - Возвращает полное множество потомков (вообще все ветки по поддереву текущей). Можно заменять на "//".
descendant-or-self:: - Возвращает полное множество потомков и текущий элемент.
following:: - Возвращает множество, ниже текущего элемента (вглубь и далее по дереву).
following-sibling:: - Возвращает множество элементов на том же уровне, следующих за текущим (все следующие ветки того же уровня, и необязательно от того же родителя, что текущая).
parent:: - Возвращает предка на один уровень назад (родительскую ветку или пустую выборку, если ветка корневая). Можно заменять на "..".
preceding:: - Возвращает множество элементов, исключая множество предков (т.е. только предыдущие ветки, попавшие в выборку).
preceding-sibling:: - Возвращает множество элементов на том же уровне, предшествующих текущему.
self:: - Возвращает текущий элемент. Можно заменять на ".". Это полезно, если надо уже поработать с атрибутами текущего элемента, и быть уверенным, что контекст при этом "не уедет" на другой элемент.
namespace:: - Возвращает множество, имеющее пространство имён (то есть присутствует атрибут xmlns). Только эта ось, пожалуй, хоть как-то позволяет рассматривать пространства имён, объявленные где-либо в рассматриваемых данных. Запрос "namespace::*" даёт выборку из ПространствоИменXPath, причём один элемент есть всегда, даже если никаких пространств имён не объявлено. Правда, дообъявления в конкретных тегах он, похоже, не видит, и от контекста тоже, насколько я понял, не зависит.
И наконец, на каждом шаге можно указать, что ищем. Собственно, что нам нужно. Это может быть общий шаблон (всем знакомая звёздочка *), имя конкретного тега (элемента DOM), имя атрибута, и некое уточнение в довесок к этому. Например, "//Документ" найдёт все элементы, объявленные тегом что-то, и сделает это за один раз, т.к. использована ось "//" (искать сразу по всей глубине). А запрос "/Платёжки/*" найдёт все элементы, дочерние для . В простом виде, запрос похож на путь к файлу в файловой системе, но вот написать //МойМузон.mp3 и найти это сразу на диске файл-менеджеры обычно не могли, а XPath позволяет. Можно искать элементы, у которых есть некий атрибут: запрос "//Документ[@Номер]" найдёт все элементы с таким атрибутом, а если у кого-то атрибут называется "Нумер", то, конечно, такое не будет найдено. Можно получить сразу выборку атрибутов, а не элементов, например, запрос "//@*" выдаст вообще все атрибуты всех элементов.
Можно уточнить условие с помощью предиката - это нечто в квадратных скобках, указанное для конкретного шага после основного условия. Можно указать индекс (порядковый №, начиная с 1). Так, "/Платёжки/Документы[2]" вернёт второй из документов в ветке платёжек. Интересно, что это всегда не номер в итоговой общей выборке (это вам не "Первые 10" в SQL), а индекс в своей подветке, т.е. мы получаем запросом "//@*[3]" все атрибуты, идущие третьими в своих элементах. На самостоятельную работу оставляю вам понимание запроса "(х/у)[2]", дерзайте :)
Можно указать в предикате условие, например, "/Платёжки/Документы[@Номер="025"] - и тут полный простор творчества и воображения, т.к. поддерживаются все логические операции, условия можно организовывать по принципу "и", "или", "не". Синтаксис, правда, имеет особенности: "не равно" выглядит как "!=", а знаки "больше" и "меньше" следует писать, как в нотации html. Допустимы также функции, числовые и строковые операции, т.е. "//Документ[starts-with(@Номер,"003")]" вернёт все элементы, где значение атрибута "Номер" начинается с подстроки "003". Функций много. Мне показалась полезной функция "normalize-space", заменяющая все повторные пробелы в строке одиночным и обрезающая крайние левый и правый, а также убирающая всякие спецсимволы (а такие прелести в xml бывают).
Выборку можно скомпоновать из нескольких запросов, разделённых символом "|", при этом условия каждого учитываются по "или", но результаты выборки идут без повторов (т.е. как "различные" в SQL).
Помните, запрос регистрочувствителен! Т.е. не только к написанию условий, но и в процессе компиляции. "//документ" и "//Документ" - разные вещи и разные результаты, "not(условие)" - правильно, а "Not(условие)" - ошибка компиляции. Строковые функции также различают регистр. При этом, функции, аналогичной ВРег/НРег, в XPath нет, можно лишь делать замену символов функцией Translate.
Оператор равенства, когда по обе стороны от него элементы/реквизиты, а не литералы, проверяет, имеют ли эти узлы одно и то же значение, а не то, являются ли они одним и тем же узлом. Т.е., говоря о дереве значений, мы сравниваем не две ветки, а два их наполнения значениями. Это может быть использовано при сравнении значений атрибутов. Например, запрос "Сотрудница[@ДевичьяФамилия = @ФамилияПоМужу]" выберет элементы тех сотрудниц, у кого атрибуты имеют одинаковое значение.
Итак, запрос XPath - строка вида Шаг1/Шаг2/..../ШагN, где каждый "Шаг" - это указание оси и указание условия (возможно, уточнённое предикатом).
Ещё раз обращаю внимание на важность того, с чего начинается поиск, и что "/Документ" не эквивалентно "Документ". И тут играет роль, помимо запроса, ещё и программное указание 1С, откуда "начинать искать" - речь об узле DOM, который указывается соответствующим методам поиска XPath. Если вы не уверены, где примерно находится искомое, лучше задавать "точкой отсчёта" весь документ DOM, а если хотите ускорить процесс и точно знаете элемент, можно и конкретизировать. Учтите, что переопределение узла поиска в рамках одного сеанса существования выражения XPath в 1С недопустимо, независимо ни от каких факторов. Поэтому, если хотите менять контекст поиска в процессе работы, или создавайте разные объекты выражений, или играйте с навигацией осями и шагами поиска.
Поскольку ось не одна на весь поиск, а своя на каждый шаг, мы можем на каждом шаге менять "направление" поиска - сначала поискать среди всех уровней вложенности, где-то глубоко найти нужную ветку; на втором шаге, исходя из этой ветки, получить предыдущую на том же уровне; на третьем шаге от этой предыдущей получим нужный элемент-родитель гораздо "выше" по уровням, потом два следующих за ним, и так далее. Важно лишь, чтобы каждый не-последний шаг позволял однозначно понять, где мы в данный момент, т.е. результаты промежуточных шагов должны быть счётны и конечны - но необязательно единичны! Это не просто "брожение" по дереву во все стороны, это возможность пойти сразу по нескольким маршрутам сразу и прийти в несколько конечных веток. Эти ветки, либо значения их колонок (атрибуты) и будут результатной выборкой XPath. Например, небольшое "путешествие":
"/descendant::Документ[@Тип="ПКО"]/*[last()-1]/parent::*/following-sibling::*[5]"
- нашли среди документов имеющие тип "ПКО", среди их дочерних получили предпоследние по индексу, от них перешли к их родительским и получили от этих родительских пятые по счёту следующие по уровню ветки.
Как это работает?
Теперь о том, как со всем этим работать в 1С.
Начинается всё с xml-строки, на основе которой надо создать ДокументDOM. Напрямую с xml механизм XPath не работает, потому что коллекция результатов должна быть чем-то более внятным, чем куски текста - и результат является коллекцией элементов DOM (обычно это значения типа ЭлементDOM или АтрибутDOM). Это делается так:
хмл=Новый ЧтениеXML;
хмл.УстановитьСтроку(СтрокаXML);
// или хмл.ОткрытьФайл(ИмяФайлаXML итд)
постр=Новый ПостроительDOM;
докDOM=постр.Прочитать(хмл);
Создаём разыменователь пространств имён. Несмотря на всю вышеизложенную теорию, всё равно каким именно вариантом конструктора и с какими аргументами. Самый общий вариант, например:
Разыменователь=Новый РазыменовательПространствИменDOM(док);
Далее можно либо быстро искать "по ходу дела", либо создавать отдельный объект для поиска XPath. Быстрый поиск делается с помощью метода документа ВычислитьВыражениеXPath, куда сразу передают все нужные параметры и который сразу возвращает выборку. Поиск с помощью отдельного объекта предназначен, по идее, для получения разных выборок результатов по итогам одного и того же текста запроса и разыменователя, но исходя из разного контекста и типа результата. Возможно, этот способ ещё и планировался 1С как более оптимальный при групповом вызове, т.к. инициализация делается единожды.
Это делается так:
Выражение=док.СоздатьВыражениеXPath(СокрЛП(ТекстЗапроса),Разыменователь);
Результат=Выражение.Вычислить(ИсходныйУзелПоиска,ТипРезультата);
// заметим, что именно при создании выражения (а не при вычислении) делается
//проверка правильности написания запроса!
По факту, метод Вычислить() вызывается для каждой инициализации единожды, т.к. изменить исходный узел мне не удалось ни при каком типе результата. Опять-таки, теоретически, понятно, что тип результата должен влиять на поведение объекта "Выражение", но на практике ни один тип результата мне не позволил переопределить исходный узел поиска так, чтобы использовать тот же объект повторно.
Тип результата не связан с тем, что результат всегда может быть представлен как выборка. Наряду с выборкой, для нужд XSLT и других смежных механизмов технологии xml, можно представить результат как число, строку или булево значение. По опыту, число - это обычно количество элементов в выборке, строка - это конкатенация значений, которые можно представить строкой (например, для атрибутов их значения выдаёт, но текстовое содержимое для элементов не выдаёт), булево - есть непустой результат или нет (т.е. пуста ли выборка). Кроме того, результат можно представить как одиночный (первый или случайный) элемент выборки - это обычно имеет смысл, если выборка чем-то по сути однородна, или вообще состоит из одного элемента. Также, выборка может представлять собой "снимок" - т.е. данные этой результатной коллекции уже не изменятся, что ни делай с их первоисточником (т.е. хранятся абсолютные значения), или "итератор" - т.е. данные результатной коллекции привязаны к первоисточнику (т.е. хранятся ссылки на значения). Оба варианта выборки бывают упорядоченными и нет, но практической пользы в этом особой не нашёл, т.к. обойти выборку методом ПолучитьСледующий() можно всегда:
Пока Истина Цикл
ОбработкаПрерыванияПользователя();
Узел=рРезультат.ПолучитьСледующий();
Если Узел=Неопределено Тогда Прервать КонецЕсли;
КонецЦикла;
И уже с этим объектом Узел можно делать что угодно, работать с ним как с элементом DOM, считывать и обрабатывать его данные, использовать его свойства и методы. Эти объекты в выборке всегда запоминаются "по значению", их можно хранить во временных коллекциях и данных форм (на сервере), и их существование не связано, по сути, ни с исходным документом DOM, ни с выражением XPath.
В заключение отмечу, что для больших текстов xml (более 100 мб) лучше не использовать XPath 1С, т.к. построитель загружает весь документ DOM в оперативную память без кэширования. Можно сперва попробовать искать нужное с помощью MSXML-XPath, а уж потом кусками обрабатывать в 1С.
Успеха, коллеги! И каждый, рассказавший больше, да будет достоин уважения!
p.s. Прилагаю обработку для обычных форм, позволяющую удобно тестировать XPath-запросы (сам на ней тренировался).