JavaScript获取节点的XPath

84

有没有办法在Javascript中返回DOM元素的XPath字符串?

9个回答

96

我从另一个示例中进行了重构。它将尝试检查是否存在唯一的ID,如果是,则使用该用例缩短表达式。

请注意,如果其中一个节点具有使用相同class属性值的兄弟节点,则XPath将无法区分您要选择哪个兄弟节点

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

1
太棒了!我已经寻找类似的东西有一段时间了,这确实是我见过最完整的解决方案。你得到了我的赞。谢谢! - Alejandro Piad
1
segs 在此处成为全局变量。 - mattsven
2
这在此页面上无法工作(http://www.icanvas.com/anderson-design-group-marthas-vineyard-maryland-blue-canvas-print-art.html?utm_source=google+utm_medium=cse+utm_campaign=GoogleProducts&gclid=CI2ghPf4isUCFYpgfgod6l8Ang),例如:
  1. 在Chrome开发工具中,单击未选择的DOM元素以获取价格,其中价格不是第一个列出的价格。将该元素保存到变量中。
  2. 在该元素上运行算法。
  3. 它会将您带回该窗格中的第一个元素。
- yangmillstheory
4
当两个兄弟节点拥有相同的“class”属性时,第一个节点总是会被选中,因此“not entirely accurate(不完全准确)”。 - Andrei Roba
1
@AndreiRoba,我也遇到了同样的问题。当存在兄弟节点时,只有第一个子节点的xpath被选中。你找到了替代方案吗? - oldpride
显示剩余4条评论

49

一个节点没有唯一的XPath路径,因此您需要决定构建路径的最合适方式。是否使用可用的ID?文档中的数字位置?相对于其他元素的位置?

查看这个答案中的getPathTo(),其中提供了一种可能的方法。


1
嘿,谢谢,看起来这是一个不错的函数。我提出了另一个更合适、更有上下文的问题:http://stackoverflow.com/questions/2661918/javascript-crazy-idea-finding-a-node。回想起来,我应该编辑这个问题...哎呀,糟糕了。 - Louis
“一个节点没有唯一的XPath”(以及可行的替代方案)。 - dakab
2
XPath 被定义为从文档根节点到某个节点的路径。 - Tomáš Zato
我理解“没有唯一的XPath”是指“有很多种方法可以解决这个问题”。 - Nick Grealy

26
这是一个用函数式编程风格编写的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 的第一个元素。不使用类选择器,因为在交互式网页中类可能经常更改。


1
使用document.getElementById(elm.id)代替document.querySelector(`#${elm.id}`),因为当id只包含数字时,前者在Chrome上会失败。 - erdos
我只是想指出Rohit Luthra对这个答案进行的修改,以解决SVG元素的问题。https://dev59.com/tnE85IYBdhLWcg3wr1ge#55793129。 - Regular Jo
1
不使用类选择器,因为在交互式网页中,类可能经常更改--这也适用于XPaths。这是我刚刚在开发工具中复制的示例:id("mG61Hd")/div[2]/div[1]/div[2]/div[12]/div[1]/div[1]/div[2]/div[1]/div[4]/div[1]/div[1]/div[2] <-- 这不会成为一个非常可靠的选择器。 - thdoan
1
@thdoan,没错,但这个问题是关于xpath的... ;-) 如果我们必须在xpath的上下文中进行选择,那么在结构上打赌比在上更可靠。 - trincot

20
我已经改编了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;
    }
};

2022年08月14日更新:这里有一个TypeScript版本。


您提供的链接已经失效了,我认为他们可能更新了源代码,因为我下载了最新版本,但找不到这段代码的任何痕迹。 然而,您提供的代码非常有效! 您能告诉我在哪个文件中找到了这段原始代码吗? - Vasco
2
已将链接与 Github 镜像交换,现在应该可以使用了。 - dcmorse
如果我们想要获取类而不是xpath,该怎么办?您能否在此处与我分享代码 https://stackoverflow.com/questions/60524774/is-there-any-way-to-get-class-of-the-page-in-pyqt5-in-browser? @dcmorse - Abhay Salvi
2
@dcmorse,您能否提供一个简短的代码块来说明如何使用您上面发布的代码?谢谢:)(非JS开发人员) - octopus
2
@octopus,只需复制上面的代码并传递元素window.onclick = function (e) { // alert(e.target) x = Elements.DOMPath.xPath(e.target) alert(x) } - PankajKushwaha

6

MDN上的函数getXPathForElement提供了类似的解决方案。

以下函数允许传入一个元素和一个XML文档,以找到一条唯一的字符串XPath表达式,以引导回该元素。

请注意,该函数适用于XML文档,由于HTML对nodeName值的大写化,在HTML文档上可能无法正常工作...

此外,这个函数可能不会产生一个"唯一的字符串XPath";它既不是在任何情况下都能定位到给定元素的唯一XPath,也不是生成的XPath只能识别一个元素(此XPath仅通过元素名称进行搜索,因此可能会识别具有相同元素名称的多个兄弟元素)。

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


请注意,document.documentElement.nodeName 返回 大写的 'HTML' 如文档所述。但是,如果您使用 /*[name()='HTML'] 进行搜索,则没有结果document.evaluate("/*[name()='HTML']", document.documentElement).iterateNext() 会产生 null。而如果您使用小写的 /*[name()='html'],则会找到结果。XPath name() 函数返回 QName,似乎区分大小写! - Nate Anderson
如果你正在处理 HTML / Element API,考虑使用 el.localName 属性 代替 el.nodeName,以及 local-name() XPath 函数 代替 name() XPath 函数。 - Nate Anderson
我的担忧可能仅适用于处理HTML文档时,nodeName属性在HTML中是大写的,但"在XML / XHTML文档中可能以不同的大小写形式出现)"; 稍后它说:“在XML DOM树中元素的标记名称以与它们在原始XML文件中编写的大小写形式相同的方式返回”。 - Nate Anderson

6

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>


2
你应该在你的回答中添加一个解释。 - S4NDM4N

3

我查看了这里提供的所有解决方案,但是它们都不能与svg元素一起使用(对于svgpath元素,代码getElementByXPath(getXPathForElement(elm)) === elm返回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('/');
}

区别在于,如果元素不是HTMLElement的实例(SVG是SVGElement,但我决定不仅检查SVG),它返回的是*[local-name() = "tag"][n]而不是tag[n]

例如:

之前:
.../div[2]/div[2]/span[1]/svg[1]/path[1]

之后:
.../div[2]/div[2]/span[1]/*[local-name() = "svg"][1]/*[local-name() = "path"][1]


这么多的分支条件......嗨呀...... - Mike Warren

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

0

通过给定的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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接