Как рассчитать положение XPath элемента с помощью Javascript?

Допустим, у меня есть большой HTML-файл с различными видами тегов, похожий на Stackru, который вы сейчас просматриваете.

Теперь предположим, что вы щелкаете элемент на странице, как будет выглядеть функция Javascript, которая вычисляет самый простой XPath, который ссылается на этот конкретный элемент?

Я знаю, что существуют бесконечные способы обращения к этому элементу в XPath, но я ищу что-то, что просто смотрит на дерево DOM, без учета идентификаторов, классов и т. Д.

Пример:

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

Допустим, вы нажимаете на яблоки. Функция Javascript будет возвращать следующее:

/html/body/ol/li[2]

По сути, он будет работать вверх по дереву DOM вплоть до элемента HTML.

Просто чтобы уточнить, обработчик событий "по щелчку" не является проблемой. Я могу сделать эту работу. Я просто не уверен, как рассчитать положение элемента в дереве DOM и представить его как XPath.

PS Любой ответ с использованием или без использования библиотеки JQuery приветствуется.

PPS Я полностью новичок в XPath, поэтому я мог даже ошибиться в приведенном выше примере, но вы поймете, что идея.

Редактирование от 11 августа 2010 г.: Похоже, кто-то задал похожий вопрос: сгенерируйте / получите Xpath для выбранного текстового узла.

12 ответов

Решение

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

Функция, которую я использую для получения 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;
}

Маленькая, мощная и чистая функция 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)+']' : '')
}

Возможно, вам понадобится добавить оболочку для IE8, которая не поддерживает метод [].filter: эта страница MDN дает такой код.

использование

Получение xpath для узла:
var xp = xpath(elementNode)
Выполнение xpath:
var iterator = xpath("//h2")
var el = iterator.iterateNext();
while (el) {
  // work with element
  el = iterator.iterateNext();
}

Реализация firebug может быть немного изменена, чтобы проверить element.id далее вверх по дереву dom:

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

Я только что изменил решение 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;
};

Нет ничего встроенного, чтобы получить xpath элемента HTML, но обратная ситуация обычна, например, с помощью селектора jQuery xpath.

Если вам нужно определить xpath элемента HTML, вам нужно будет предоставить специальную функцию для этого. Вот пара примеров javascript/jQuery для вычисления xpath.

Просто для удовольствия, однострочная реализация XPath 2.0:

string-join(ancestor-or-self::*/concat(name(),
                                       '[',
                                       for $x in name() 
                                          return count(preceding-sibling::*
                                                          [name() = $x]) 
                                                 + 1,
                                       ']'),
            '/')

Приведенное ниже решение является предпочтительным, если вам необходимо надежно определить абсолютный XPath элемента.

Некоторые другие ответы либо частично основаны на идентификаторе элемента (который не является надежным, поскольку потенциально может быть несколько элементов с одинаковыми идентификаторами), либо они генерируют XPath, которые фактически указывают больше элементов, чем заданный (ошибочно опуская в определенных обстоятельствах индекс родственного элемента),

Код был адаптирован из исходного кода Firebug путем исправления вышеупомянутых проблем.

getXElementTreeXPath = 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;

        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();

        // *always* include the sibling index
        var pathIndex = "[" + (index+1) + "]";

        paths.unshift( tagName + pathIndex );
    }

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

Посмотрите мой пример, который по крайней мере попытается сократить выражение, если есть уникальный идентификатор. Javascript получить XPath узла

function getPath(event) {
  event = event || window.event;

  var pathElements = [];
  var elem = event.currentTarget;
  var index = 0;
  var siblings = event.currentTarget.parentNode.getElementsByTagName(event.currentTarget.tagName);
  for (var i=0, imax=siblings.length; i<imax; i++) {
      if (event.currentTarget === siblings[i] {
        index = i+1; // add 1 for xpath 1-based
      }
  }


  while (elem.tagName.toLowerCase() != "html") {
    pathElements.unshift(elem.tagName);
    elem = elem.parentNode;
  }
  return pathElements.join("/") + "[" + index + "]";
}

ИЗМЕНЕНО ДЛЯ ДОБАВЛЕНИЯ ИНДЕКСА ИНДЕКСА СИГНАЛА

Я столкнулся с этой проблемой, и мне было трудно решить ее полностью. так как в моем случае это давало половину xpath. поэтому я немного изменил его, чтобы указать полный путь. вот мой ответ.

      window.onclick = (e) => {
    let pathArr = e.path;
    let element = pathArr[0];
    var xpath = '';
if(pathArr.length<=2 && pathArr[0].nodeType!=1){
    for (let i = 0; i < pathArr.length - 1 && pathArr[i].nodeType == 1; i++) {
        element = pathArr[i];
        var id = $(element.parentNode).children(element.tagName).index(element) + 1;
        id > 1 ? (id = '[' + id + ']') : (id = '');
        xpath = '/' + element.tagName.toLowerCase() + id + xpath;
    }
}
else{
  xpath="/html/document"
}
    return xpath;

Используйте https://github.com/KajeNick/jquery-get-xpath

<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="../src/jquery-get-xpath.js"></script> 

<script>
    jQuery(document).ready(function ($) {

        $('body').on('click', 'ol li', function () {
           let xPath = $(this).jGetXpath();

           console.log(xPath);
        });

    });
</script>

Консоль покажет: /html / body / ol / li [2]

Другие вопросы по тегам