Javascript получить XPath узла
Есть ли способ вернуть строку XPath элемента DOM в Javascript?
9 ответов
Для узла не существует уникального XPath, поэтому вам придется решить, какой способ построения пути является наиболее подходящим. Использовать идентификаторы, где это возможно? Цифровая позиция в документе? Положение относительно других элементов?
Увидеть getPathTo()
в этом ответе за один из возможных подходов.
Я рефакторинг это из другого примера. Он будет пытаться проверить, или есть наверняка уникальный идентификатор, и если это так, используйте этот регистр для сокращения выражения.
function createXPathFromElement(elm) {
var allNodes = document.getElementsByTagName('*');
for (var segs = []; elm && elm.nodeType == 1; elm = elm.parentNode)
{
if (elm.hasAttribute('id')) {
var uniqueIdCount = 0;
for (var n=0;n < allNodes.length;n++) {
if (allNodes[n].hasAttribute('id') && allNodes[n].id == elm.id) uniqueIdCount++;
if (uniqueIdCount > 1) break;
};
if ( uniqueIdCount == 1) {
segs.unshift('id("' + elm.getAttribute('id') + '")');
return segs.join('/');
} else {
segs.unshift(elm.localName.toLowerCase() + '[@id="' + elm.getAttribute('id') + '"]');
}
} else if (elm.hasAttribute('class')) {
segs.unshift(elm.localName.toLowerCase() + '[@class="' + elm.getAttribute('class') + '"]');
} else {
for (i = 1, sib = elm.previousSibling; sib; sib = sib.previousSibling) {
if (sib.localName == elm.localName) i++; };
segs.unshift(elm.localName.toLowerCase() + '[' + i + ']');
};
};
return segs.length ? '/' + segs.join('/') : null;
};
function lookupElementByXPath(path) {
var evaluator = new XPathEvaluator();
var result = evaluator.evaluate(path, document.documentElement, null,XPathResult.FIRST_ORDERED_NODE_TYPE, null);
return result.singleNodeValue;
}
Вот функциональная функция стиля программирования ES6 для работы:
function getXPathForElement(element) {
const idx = (sib, name) => sib
? idx(sib.previousElementSibling, name||sib.localName) + (sib.localName == name)
: 1;
const segs = elm => !elm || elm.nodeType !== 1
? ['']
: elm.id && document.querySelector(`#${elm.id}`) === elm
? [`id("${elm.id}")`]
: [...segs(elm.parentNode), `${elm.localName.toLowerCase()}[${idx(elm)}]`];
return segs(element).join('/');
}
function getElementByXPath(path) {
return (new XPathEvaluator())
.evaluate(path, document.documentElement, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue;
}
// Demo:
const li = document.querySelector('li:nth-child(2)');
const path = getXPathForElement(li);
console.log(path);
console.log(li === getElementByXPath(path)); // true
<div>
<table id="start"></table>
<div>
<ul><li>option</ul></ul>
<span>title</span>
<ul>
<li>abc</li>
<li>select this</li>
</ul>
</div>
</div>
Это будет использовать id
селектор, если только элемент не является первым с таким идентификатором. Селекторы классов не используются, потому что в интерактивных веб-страницах классы могут часто меняться.
Я адаптировал алгоритм, который Chromium использует для расчета XPath из инструментов разработчика ниже.
Чтобы использовать это, как написано, вы бы позвонили Elements.DOMPath.xPath(<some DOM node>, false)
. Последний параметр определяет, получите ли вы более короткий "Копировать XPath" (еслиtrue
) или "Скопировать полный XPath".
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Elements = {};
Elements.DOMPath = {};
/**
* @param {!Node} node
* @param {boolean=} optimized
* @return {string}
*/
Elements.DOMPath.xPath = function (node, optimized) {
if (node.nodeType === Node.DOCUMENT_NODE) {
return '/';
}
const steps = [];
let contextNode = node;
while (contextNode) {
const step = Elements.DOMPath._xPathValue(contextNode, optimized);
if (!step) {
break;
} // Error - bail out early.
steps.push(step);
if (step.optimized) {
break;
}
contextNode = contextNode.parentNode;
}
steps.reverse();
return (steps.length && steps[0].optimized ? '' : '/') + steps.join('/');
};
/**
* @param {!Node} node
* @param {boolean=} optimized
* @return {?Elements.DOMPath.Step}
*/
Elements.DOMPath._xPathValue = function (node, optimized) {
let ownValue;
const ownIndex = Elements.DOMPath._xPathIndex(node);
if (ownIndex === -1) {
return null;
} // Error.
switch (node.nodeType) {
case Node.ELEMENT_NODE:
if (optimized && node.getAttribute('id')) {
return new Elements.DOMPath.Step('//*[@id="' + node.getAttribute('id') + '"]', true);
}
ownValue = node.localName;
break;
case Node.ATTRIBUTE_NODE:
ownValue = '@' + node.nodeName;
break;
case Node.TEXT_NODE:
case Node.CDATA_SECTION_NODE:
ownValue = 'text()';
break;
case Node.PROCESSING_INSTRUCTION_NODE:
ownValue = 'processing-instruction()';
break;
case Node.COMMENT_NODE:
ownValue = 'comment()';
break;
case Node.DOCUMENT_NODE:
ownValue = '';
break;
default:
ownValue = '';
break;
}
if (ownIndex > 0) {
ownValue += '[' + ownIndex + ']';
}
return new Elements.DOMPath.Step(ownValue, node.nodeType === Node.DOCUMENT_NODE);
};
/**
* @param {!Node} node
* @return {number}
*/
Elements.DOMPath._xPathIndex = function (node) {
// Returns -1 in case of error, 0 if no siblings matching the same expression,
// <XPath index among the same expression-matching sibling nodes> otherwise.
function areNodesSimilar(left, right) {
if (left === right) {
return true;
}
if (left.nodeType === Node.ELEMENT_NODE && right.nodeType === Node.ELEMENT_NODE) {
return left.localName === right.localName;
}
if (left.nodeType === right.nodeType) {
return true;
}
// XPath treats CDATA as text nodes.
const leftType = left.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType;
const rightType = right.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType;
return leftType === rightType;
}
const siblings = node.parentNode ? node.parentNode.children : null;
if (!siblings) {
return 0;
} // Root node - no siblings.
let hasSameNamedElements;
for (let i = 0; i < siblings.length; ++i) {
if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
hasSameNamedElements = true;
break;
}
}
if (!hasSameNamedElements) {
return 0;
}
let ownIndex = 1; // XPath indices start with 1.
for (let i = 0; i < siblings.length; ++i) {
if (areNodesSimilar(node, siblings[i])) {
if (siblings[i] === node) {
return ownIndex;
}
++ownIndex;
}
}
return -1; // An error occurred: |node| not found in parent's children.
};
/**
* @unrestricted
*/
Elements.DOMPath.Step = class {
/**
* @param {string} value
* @param {boolean} optimized
*/
constructor(value, optimized) {
this.value = value;
this.optimized = optimized || false;
}
/**
* @override
* @return {string}
*/
toString() {
return this.value;
}
};
Аналогичное решение дает функция getXPathForElement в MDN:
function getXPathForElement(el, xml) {
var xpath = '';
var pos, tempitem2;
while(el !== xml.documentElement) {
pos = 0;
tempitem2 = el;
while(tempitem2) {
if (tempitem2.nodeType === 1 && tempitem2.nodeName === el.nodeName) { // If it is ELEMENT_NODE of the same name
pos += 1;
}
tempitem2 = tempitem2.previousSibling;
}
xpath = "*[name()='"+el.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']["+pos+']'+'/'+xpath;
el = el.parentNode;
}
xpath = '/*'+"[name()='"+xml.documentElement.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']"+'/'+xpath;
xpath = xpath.replace(/\/$/, '');
return xpath;
}
Также стоит попробовать XMLSerializer.
function getElementXPath (element) {
if (!element) return null
if (element.id) {
return `//*[@id=${element.id}]`
} else if (element.tagName === 'BODY') {
return '/html/body'
} else {
const sameTagSiblings = Array.from(element.parentNode.childNodes)
.filter(e => e.nodeName === element.nodeName)
const idx = sameTagSiblings.indexOf(element)
return getElementXPath(element.parentNode) +
'/' +
element.tagName.toLowerCase() +
(sameTagSiblings.length > 1 ? `[${idx + 1}]` : '')
}
}
console.log(getElementXPath(document.querySelector('#a div')))
<div id="a">
<div>def</div>
</div>
Я проверил все представленные здесь решения, но ни одно из них не работает с svg
элементы (код getElementByXPath(getXPathForElement(elm)) === elm
возвращается false
за svg
или path
элементы)
Поэтому я добавил в SVG затруднительного Тока в к решению trincot в и получил этот код:
function getXPathForElement(element) {
const idx = (sib, name) => sib
? idx(sib.previousElementSibling, name||sib.localName) + (sib.localName == name)
: 1;
const segs = elm => !elm || elm.nodeType !== 1
? ['']
: elm.id && document.getElementById(elm.id) === elm
? [`id("${elm.id}")`]
: [...segs(elm.parentNode), elm instanceof HTMLElement
? `${elm.localName}[${idx(elm)}]`
: `*[local-name() = "${elm.localName}"][${idx(elm)}]`];
return segs(element).join('/');
}
Разница в том, что он возвращается *[local-name() = "tag"][n]
вместо того tag[n]
если элемент не является экземпляром HTMLElement
(SVG SVGElement
но я решил не останавливаться на проверке только svg).
Пример:
Перед:.../div[2]/div[2]/span[1]/svg[1]/path[1]
После:.../div[2]/div[2]/span[1]/*[local-name() = "svg"][1]/*[local-name() = "path"][1]
Просто передайте элемент в функцию getXPathOfElement
и вы получите Xpath
,
function getXPathOfElement(elt)
{
var path = "";
for (; elt && elt.nodeType == 1; elt = elt.parentNode)
{
idx = getElementIdx(elt);
xname = elt.tagName;
if (idx > 1) xname += "[" + idx + "]";
path = "/" + xname + path;
}
return path;
}
function getElementIdx(elt)
{
var count = 1;
for (var sib = elt.previousSibling; sib ; sib = sib.previousSibling)
{
if(sib.nodeType == 1 && sib.tagName == elt.tagName) count++
}
return count;
}
Получите xPath, указав элемент dom
Эта функция возвращает полный селектор xPath (без идентификатора или класса). Этот тип селектора полезен, когда сайт генерирует случайный идентификатор или класс.
function getXPath(element) {
// Selector
let selector = '';
// Loop handler
let foundRoot;
// Element handler
let currentElement = element;
// Do action until we reach html element
do {
// Get element tag name
const tagName = currentElement.tagName.toLowerCase();
// Get parent element
const parentElement = currentElement.parentElement;
// Count children
if (parentElement.childElementCount > 1) {
// Get children of parent element
const parentsChildren = [...parentElement.children];
// Count current tag
let tag = [];
parentsChildren.forEach(child => {
if (child.tagName.toLowerCase() === tagName) tag.push(child) // Append to tag
})
// Is only of type
if (tag.length === 1) {
// Append tag to selector
selector = `/${tagName}${selector}`;
} else {
// Get position of current element in tag
const position = tag.indexOf(currentElement) + 1;
// Append tag to selector
selector = `/${tagName}[${position}]${selector}`;
}
} else {
//* Current element has no siblings
// Append tag to selector
selector = `/${tagName}${selector}`;
}
// Set parent element to current element
currentElement = parentElement;
// Is root
foundRoot = parentElement.tagName.toLowerCase() === 'html';
// Finish selector if found root element
if(foundRoot) selector = `/html${selector}`;
}
while (foundRoot === false);
// Return selector
return selector;
}