Джон прав, что существует любое количество выражений XPath, которые приведут к одному и тому же узлу в экземпляре документа. Самым простым способом построения выражения, которое однозначно приводит к определенному узлу, является цепочка тестов узлов, которые используют положение узла в предикате, например ::
.
/node()[0]/node()[2]/node()[6]/node()[1]/node()[2]
Очевидно, что это выражение не использует имена элементов, но если все, что вы пытаетесь сделать, это найти узел в документе, вам не нужно его имя. Его также нельзя использовать для поиска атрибутов (поскольку атрибуты не являются узлами и не имеют позиции; вы можете найти их только по имени), но он найдет все другие типы узлов.
Чтобы построить это выражение, вам нужно написать метод, который возвращает позицию узла в дочерних узлах его родителя, потому что XmlNode
не предоставляет это как свойство:
static int GetNodePosition(XmlNode child)
{
for (int i=0; i<child.ParentNode.ChildNodes.Count; i++)
{
if (child.ParentNode.ChildNodes[i] == child)
{
// tricksy XPath, not starting its positions at 0 like a normal language
return i + 1;
}
}
throw new InvalidOperationException("Child node somehow not found in its parent's ChildNodes property.");
}
(Вероятно, есть более элегантный способ сделать это с помощью LINQ, поскольку XmlNodeList
реализует IEnumerable
, но я пойду с тем, что знаю здесь.)
Тогда вы можете написать рекурсивный метод, подобный этому:
static string GetXPathToNode(XmlNode node)
{
if (node.NodeType == XmlNodeType.Attribute)
{
// attributes have an OwnerElement, not a ParentNode; also they have
// to be matched by name, not found by position
return String.Format(
"{0}/@{1}",
GetXPathToNode(((XmlAttribute)node).OwnerElement),
node.Name
);
}
if (node.ParentNode == null)
{
// the only node with no parent is the root node, which has no path
return "";
}
// the path to a node is the path to its parent, plus "/node()[n]", where
// n is its position among its siblings.
return String.Format(
"{0}/node()[{1}]",
GetXPathToNode(node.ParentNode),
GetNodePosition(node)
);
}
Как видите, я взломал его, чтобы он тоже нашел атрибуты.
Джон вошел в его версию, когда я писал свою. Что-то в его коде заставит меня немного разглагольствовать, и я заранее извиняюсь, если это звучит так, как будто я обижаю Джона. (Я не уверен. Я почти уверен, что список того, чему Джон должен научиться у меня, чрезвычайно короток.) Но я думаю, что мысль, которую я собираюсь сделать, является довольно важной для тех, кто работает с XML для думать о.
Я подозреваю, что решение Джона возникло из того, что, как я вижу, делают многие разработчики: думать о XML-документах как о деревьях элементов и атрибутов. Я думаю, что это в значительной степени исходит от разработчиков, чье основное использование XML заключается в качестве формата сериализации, потому что весь используемый ими XML структурирован таким образом. Вы можете заметить этих разработчиков, потому что они используют термины «узел» и «элемент» взаимозаменяемо. Это заставляет их предлагать решения, которые рассматривают все другие типы узлов как особые случаи. (Я был одним из этих парней сам в течение очень долгого времени.)
Такое ощущение, что это упрощающее предположение, пока вы делаете это. Но это не так. Это усложняет задачи и усложняет код. Это побуждает вас обходить части технологии XML (например, функцию node()
в XPath), которые специально разработаны для общей обработки всех типов узлов.
В коде Джона есть красный флаг, который заставил бы меня запросить его в обзоре кода, даже если я не знал, каковы требования, и это GetElementsByTagName
. Всякий раз, когда я вижу, что этот метод используется, возникает вопрос: «Почему он должен быть элементом?». И ответ очень часто «о, этот код должен обрабатывать текстовые узлы тоже?»