如何使用Javascript计算元素的XPath位置?

IT技术 javascript jquery html xml xpath
2021-02-03 01:38:35

假设我有一个包含不同类型标签的大型 HTML 文件,类似于您现在正在查看的 StackOverflow 文件。

现在假设您单击页面上的一个元素,计算引用该特定元素的最基本 XPath 的 Javascript 函数会是什么样的?

我知道在 XPath 中有多种引用该元素的方法,但我正在寻找一些只查看 DOM 树的东西,而不考虑 ID、类等。

例子:

<html>
<head><title>Fruit</title></head>
<body>
<ol>
  <li>Bananas</li>
  <li>Apples</li>
  <li>Strawberries</li>
</ol>
</body>
</html>

假设您单击ApplesJavascript 函数将返回以下内容:

/html/body/ol/li[2]

它基本上只是沿着 DOM 树向上一直工作到 HTML 元素。

只是为了澄清,“点击”事件处理程序不是问题所在。我可以做到这一点。我只是不确定如何计算元素在 DOM 树中的位置并将其表示为 XPath。

PS 任何使用或不使用 JQuery 库的答案都值得赞赏。

PPS 我对 XPath 完全陌生,所以我什至可能在上面的例子中犯了一个错误,但你会明白的。

2010 年 8 月 11 日编辑:看起来其他人问了类似的问题:为选定的文本节点生成/获取 Xpath

6个回答

Firebug 可以做到这一点,而且它是开源的 ( BSD ),因此您可以重用它们的实现,不需要任何库。

第三方编辑

这是上述链接来源的摘录。以防万一上面的链接会改变。请检查源以受益于更改和更新或提供的完整功能集。

Xpath.getElementXPath = function(element)
{
    if (element && element.id)
        return '//*[@id="' + element.id + '"]';
    else
        return Xpath.getElementTreeXPath(element);
};

上面的代码调用了这个函数。注意我添加了一些换行以避免水平滚动条

Xpath.getElementTreeXPath = function(element)
{
    var paths = [];  // Use nodeName (instead of localName) 
    // so namespace prefix is included (if any).
    for (; element && element.nodeType == Node.ELEMENT_NODE; 
           element = element.parentNode)
    {
        var index = 0;
        var hasFollowingSiblings = false;
        for (var sibling = element.previousSibling; sibling; 
              sibling = sibling.previousSibling)
        {
            // Ignore document type declaration.
            if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
                continue;

            if (sibling.nodeName == element.nodeName)
                ++index;
        }

        for (var sibling = element.nextSibling; 
            sibling && !hasFollowingSiblings;
            sibling = sibling.nextSibling)
        {
            if (sibling.nodeName == element.nodeName)
                hasFollowingSiblings = true;
        }

        var tagName = (element.prefix ? element.prefix + ":" : "") 
                          + element.localName;
        var pathIndex = (index || hasFollowingSiblings ? "[" 
                   + (index + 1) + "]" : "");
        paths.splice(0, 0, tagName + pathIndex);
    }

    return paths.length ? "/" + paths.join("/") : null;
};
@OneWorld 是的,但它只检查您正在获取 xpath 的元素,而不是它的父元素。看我的回答,它产生一个更像谷歌浏览器检查器的 xpath。
2021-03-17 01:38:35
在当前版本中,提到的实现可以在这里找到:github.com/firebug/firebug/blob/master/extension/content/...
2021-04-04 01:38:35
包含此方法的文件是哪个?我需要类似的东西,但萤火虫已转移到 github,我找不到方法
2021-04-08 01:38:35
我喜欢 Firebug 实现 (getElementXPath),因为如果元素有任何元素,它更喜欢使用 ID 而不是 xpath 树。
2021-04-09 01:38:35
Firebug 的实现在第 1365 行有一个错误:它只添加索引,(例如“[3]”),如果之前有相同类型的兄弟。这是错误的,因为没有索引的 XPath 将匹配该类型的所有兄弟,甚至更远的兄弟。例如:/p/b将匹配所有 b下标签 p在根标签。如果 Firebug 没有找到相同类型的先前兄弟姐妹,它只会跳过索引
2021-04-12 01:38:35

我用来获取与您的情况类似的 XPath 的函数,它使用 jQuery:

function getXPath( element )
{
    var xpath = '';
    for ( ; element && element.nodeType == 1; element = element.parentNode )
    {
        var id = $(element.parentNode).children(element.tagName).index(element) + 1;
        id > 1 ? (id = '[' + id + ']') : (id = '');
        xpath = '/' + element.tagName.toLowerCase() + id + xpath;
    }
    return xpath;
}
你是对的,我会像你一样写它,但我只是复制/粘贴......我前段时间发现了这个脚本并且从来没有费心去清理它。无论哪种方式,两种写法都是等价的。
2021-03-21 01:38:35
也能完美运行!谢谢
2021-04-01 01:38:35
第 7 行:从未见过这种语法,通常会这样写: id = id > 1 ? ('[' + id + ']') : '';
2021-04-03 01:38:35
BUG:在<div/><div/>它的第一个标签上返回“/div”而不是“/div[1]”
2021-04-11 01:38:35

小而强大的纯js函数

它返回元素的 xpath 和 xpath 的元素迭代器。

https://gist.github.com/iimos/e9e96f036a3c174d0bf4

function xpath(el) {
  if (typeof el == "string") return document.evaluate(el, document, null, 0, null)
  if (!el || el.nodeType != 1) return ''
  if (el.id) return "//*[@id='" + el.id + "']"
  var sames = [].filter.call(el.parentNode.children, function (x) { return x.tagName == el.tagName })
  return xpath(el.parentNode) + '/' + el.tagName.toLowerCase() + (sames.length > 1 ? '['+([].indexOf.call(sames, el)+1)+']' : '')
}

可能您需要为不支持 [].filter 方法的 IE8 添加一个 shim:这个 MDN 页面提供了这样的代码。

用法

获取节点的 xpath:
var xp = xpath(elementNode)
执行 xpath:
var iterator = xpath("//h2")
var el = iterator.iterateNext();
while (el) {
  // work with element
  el = iterator.iterateNext();
}

可以稍微修改 firebug 实现以检查 dom 树中的 element.id:

  /**
   * Gets an XPath for an element which describes its hierarchical location.
   */
  var getElementXPath = function(element) {
      if (element && element.id)
          return '//*[@id="' + element.id + '"]';
      else
          return getElementTreeXPath(element);
  };

  var getElementTreeXPath = function(element) {
      var paths = [];

      // Use nodeName (instead of localName) so namespace prefix is included (if any).
      for (; element && element.nodeType == 1; element = element.parentNode)  {
          var index = 0;
          // EXTRA TEST FOR ELEMENT.ID
          if (element && element.id) {
              paths.splice(0, 0, '/*[@id="' + element.id + '"]');
              break;
          }

          for (var sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) {
              // Ignore document type declaration.
              if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
                continue;

              if (sibling.nodeName == element.nodeName)
                  ++index;
          }

          var tagName = element.nodeName.toLowerCase();
          var pathIndex = (index ? "[" + (index+1) + "]" : "");
          paths.splice(0, 0, tagName + pathIndex);
      }

      return paths.length ? "/" + paths.join("/") : null;
  };
以下代码是否为以下 html 中的 div 返回正确的 xpath?
2021-03-26 01:38:35

我刚刚修改了 DanS 的解决方案,以便将它与 textNodes 一起使用。对序列化 HTML 范围对象非常有用。

/**
 * Gets an XPath for an node which describes its hierarchical location.
 */
var getNodeXPath = function(node) {
    if (node && node.id)
        return '//*[@id="' + node.id + '"]';
    else
        return getNodeTreeXPath(node);
};

var getNodeTreeXPath = function(node) {
    var paths = [];

    // Use nodeName (instead of localName) so namespace prefix is included (if any).
    for (; node && (node.nodeType == 1 || node.nodeType == 3) ; node = node.parentNode)  {
        var index = 0;
        // EXTRA TEST FOR ELEMENT.ID
        if (node && node.id) {
            paths.splice(0, 0, '/*[@id="' + node.id + '"]');
            break;
        }

        for (var sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) {
            // Ignore document type declaration.
            if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
                continue;

            if (sibling.nodeName == node.nodeName)
                ++index;
        }

        var tagName = (node.nodeType == 1 ? node.nodeName.toLowerCase() : "text()");
        var pathIndex = (index ? "[" + (index+1) + "]" : "");
        paths.splice(0, 0, tagName + pathIndex);
    }

    return paths.length ? "/" + paths.join("/") : null;
};