Javascript 获取节点的 XPath

在 Javascript 中是否有返回 DOM 元素的 XPath 字符串的方法?

87257 次浏览

节点没有唯一的 XPath,因此必须决定构造路径的最合适方法。在可用的地方使用 ID?文件中的数字位置?相对于其他元素的位置?

请参阅 这个答案中的 getPathTo()了解一种可能的方法。

我从另一个例子中重构了这一点。它将尝试检查或肯定有一个唯一的 id,如果是这样,使用该情况下缩短表达式。

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;
}

MDN 上的函数 GetXPathForElement也给出了类似的解决方案:

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也值得一试。

下面是这项工作的 函数式程序设计风格 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.getElementById(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选择器,除非该元素不是第一个具有该 id 的元素。不使用类选择器,因为在交互式网页中类可能经常更改。

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>

只要传递函数 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;
}

我已经通过下面的 devtools 对 Chromium 使用的算法进行了调整,以计算 XPath。

要使用按编写的方法,您可以调用 Elements.DOMPath.xPath(<some DOM node>, false)。最后一个参数控制是否获得较短的“ CopyXPath”(如果是 true)或“ CopyfulXPath”。

// 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;
}
};

更新2022-08-14: 给你是一个 TypeScript 版本。

我检查了这里提供的所有解决方案,但是它们都不能与 svg元素一起工作(代码 getElementByXPath(getXPathForElement(elm)) === elmsvgpath元素返回 false)

所以我把 Touko 的 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('/');
}

区别在于,如果 element 不是 HTMLElement的实例,它返回 *[local-name() = "tag"][n]而不是 tag[n](svgs 是 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]

通过给出一个 dom 元素来获取 xPath

此函数返回完整的 xPath 选择器(没有任何 id 或类)。 当站点生成随机 ID 或类时,这种类型的选择器非常有用

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;
}