将一组简单的 xpath 与 SAX 匹配

Match set of simple xpaths with SAX

提问人:cachius 提问时间:10/27/2022 更新时间:10/27/2022 访问量:41

问:

我有一组简单的 xpath,只涉及标签和属性,没有谓词。我的 XML 输入大小为几 MB,因此我想使用流式 XML 解析器。

如何将流式 XML 解析器与一组 xapths 进行匹配,以便为每个 xpath 检索一个值?

关键似乎是从 xpath 集合中构建正确的数据结构,以便可以根据 xml 事件对其进行评估。

这似乎是一个相当常见的任务,但我找不到任何现成的解决方案。

Java XPath 事件 XML 解析 SAX

评论

1赞 f1sh 10/27/2022
“几 MB”不是不使用支持 xpath 表达式的公共库的理由。

答:

1赞 Ahmed Mohamed 10/27/2022 #1

若要将流式处理 XML 分析器与一组简单的 xpath 进行匹配,可以使用以下步骤:

  • 创建一个 来存储 xpath 及其相应的值。将值初始化为 。Map<String, String>null
  • 创建一个 以跟踪 XML 元素的当前路径。Stack<String>
  • 创建 a 和 a 以分析 XML 输入。SAXParserDefaultHandler
  • 在处理程序的方法中,将元素名称推送到堆栈并将其追加到当前路径。然后,检查当前路径是否与地图中的任何 xpath 匹配。如果是,请设置一个标志以指示应提取该值。startElement
  • 在处理程序的方法中,从堆栈中弹出元素名称并将其从当前路径中删除。然后,重置该标志以指示不应提取该值。endElement
  • 在处理程序的方法中,检查是否设置了标志。如果是,请将字符数据附加到映射中匹配的 xpath 的值。characters
  • 解析 XML 输入后,返回包含 xpath 及其值的映射。

解释

流式 XML 解析器(如 )按顺序读取 XML 输入,并在遇到文档的不同部分(如开始标记、结束标记、文本等)时触发事件。它不会在内存中构建文档的树结构,这使得它对于大型 XML 输入更有效。SAXParser

xpath 是一种用于从 XML 文档中选择节点的语法。它由一系列步骤组成,用斜杠分隔,用于描述所需节点的位置。例如,选择 bookstore 元素的 book 元素的 title 元素。/bookstore/book/title

一个简单的 xpath 只涉及标签和属性,不涉及谓词。例如,选择具有 value 的属性的 book 元素的 title 元素。/bookstore/book[@lang='en']/titlelangen

为了将流式 XML 解析器与一组简单的 xpath 进行匹配,我们需要在解析输入时跟踪 XML 元素的当前路径,并将其与集合中的 xpath 进行比较。如果我们找到匹配项,我们需要提取节点的值并将其存储在地图中。我们还需要处理节点值跨越多个字符事件的情况,或者节点在文档中有多个实例的情况。

假设我们有以下 XML 输入:

<bookstore>
  <book lang="en">
    <title>Harry Potter and the Philosopher's Stone</title>
    <author>J. K. Rowling</author>
    <price>10.99</price>
  </book>
  <book lang="fr">
    <title>Le Petit Prince</title>
    <author>Antoine de Saint-Exupéry</author>
    <price>8.50</price>
  </book>
</bookstore>

以及以下一组简单的 xpath:

  • /bookstore/book/title
  • /bookstore/book/author
  • /bookstore/book[@lang='fr']/price

我们可以使用以下 Java 代码将流式 XML 解析器与一组 xpath 进行匹配:

import java.io.*;
import java.util.*;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

public class XPathMatcher {

  public static Map<String, String> match(InputStream xmlInput, Set<String> xpaths) throws Exception {
    // Create a map to store the xpaths and their values
    Map<String, String> map = new HashMap<>();
    for (String xpath : xpaths) {
      map.put(xpath, null);
    }

    // Create a stack to keep track of the current path
    Stack<String> stack = new Stack<>();

    // Create a SAXParser and a DefaultHandler to parse the XML input
    SAXParserFactory factory = SAXParserFactory.newInstance();
    SAXParser parser = factory.newSAXParser();
    DefaultHandler handler = new DefaultHandler() {

      // A flag to indicate if the value should be extracted
      boolean extract = false;

      // A variable to store the current path
      String currentPath = "";

      // A variable to store the matching xpath
      String matchingXPath = "";

      @Override
      public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        // Push the element name to the stack and append it to the current path
        stack.push(qName);
        currentPath += "/" + qName;

        // Check if the current path matches any of the xpaths in the map
        for (String xpath : map.keySet()) {
          // If the xpath has an attribute, extract the attribute name and value
          String attrName = "";
          String attrValue = "";
          if (xpath.contains("[@")) {
            int start = xpath.indexOf("[@") + 2;
            int end = xpath.indexOf("=");
            attrName = xpath.substring(start, end);
            start = end + 2;
            end = xpath.indexOf("]");
            attrValue = xpath.substring(start, end - 1);
          }

          // If the xpath matches the current path, and either has no attribute or has a matching attribute, set the flag and the matching xpath
          if (xpath.startsWith(currentPath) && (attrName.isEmpty() || attrValue.equals(attributes.getValue(attrName)))) {
            extract = true;
            matchingXPath = xpath;
            break;
          }
        }
      }

      @Override
      public void endElement(String uri, String localName, String qName) throws SAXException {
        // Pop the element name from the stack and remove it from the current path
        stack.pop();
        currentPath = currentPath.substring(0, currentPath.length() - qName.length() - 1);

        // Reset the flag and the matching xpath
        extract = false;
        matchingXPath = "";
      }

      @Override
      public void characters(char[] ch, int start, int length) throws SAXException {
        // Check if the flag is set
        if (extract) {
          // Append the character data to the value of the matching xpath in the map
          String value = map.get(matchingXPath);
          if (value == null) {
            value = "";
          }
          value += new String(ch, start, length);
          map.put(matchingXPath, value);
        }
      }
    };

    // Parse the XML input
    parser.parse(xmlInput, handler);

    // Return the map with the xpaths and their values
    return map;
  }

  public static void main(String[] args) throws Exception {
    // Create an input stream from the XML file
    InputStream xmlInput = new FileInputStream("bookstore.xml");

    // Create a set of simple xpaths
    Set<String> xpaths = new HashSet<>();
    xpaths.add("/bookstore/book/title");
    xpaths.add("/bookstore/book/author");
    xpaths.add("/bookstore/book[@lang='fr']/price");

    // Match the streaming XML parser against the set of xpaths
    Map<String, String> map = match(xmlInput, xpaths);

    // Print the results
    for (String xpath : map.keySet()) {
      System.out.println(xpath + " = " + map.get(xpath));
    }
  }
}

代码的输出为:

/bookstore/book/title = Harry Potter and the Philosopher's StoneLe Petit Prince
/bookstore/book/author = J. K. RowlingAntoine de Saint-Exupéry
/bookstore/book[@lang='fr']/price = 8.50