何时应选择 SAX 而不是 StAX?

2022-08-31 12:02:27

像 SAX 和 StAX 这样的流式 xml 解析器比构建 DOM 解析器等树结构的解析器更快、更节省内存。SAX 是一个推送解析器,这意味着它是观察者模式(也称为侦听器模式)的实例。SAX首先出现,但后来出现了StAX - 一个拉式解析器,这意味着它基本上像迭代器一样工作。

你可以找到为什么在任何地方都更喜欢StAX而不是SAX的原因,但它通常归结为:“它更容易使用”。

在Java教程中,JAXP StAX模糊地呈现为DOM和SAX之间的中间:“它比SAX更容易,比DOM更有效”。但是,我从未发现任何线索表明StAX会比SAX慢或内存效率更低。

所有这些都让我想知道:有什么理由选择SAX而不是StAX?


答案 1

概述
XML 文档是分层文档,其中相同的元素名称和命名空间可能出现在多个位置,具有不同的含义和不定式深度(递归)。像往常一样,解决大问题,就是把它们分成小问题。在 XML 解析的上下文中,这意味着在特定于该 XML 的方法中解析 XML 的特定部分。例如,一条逻辑将解析一个地址:

<Address>
    <Street>Odins vei</Street>    
    <Building>4</Building>
    <Door>b</Door>
</Address>

即你会有一个方法

AddressType parseAddress(...); // A

void parseAddress(...); // B

在逻辑中的某个位置,获取 XML 输入参数并返回一个对象(B 的结果可以在以后从字段中获取)。

SAX
SAX “推送”XML 事件,由您决定 XML 事件在程序/数据中的位置。

// method in stock SAX handler
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
    // .. your logic here for start element
}

对于“正在构建”的开始元素,您需要确定您实际上正在解析地址,然后将 XML 事件路由到其工作是解释 Address 的方法。

StAX
StAX“拉取”XML事件,由您决定在程序/数据中接收XML事件的位置。

// method in standard StAX reader
int event = reader.next();
if(event == XMLStreamConstants.START_ELEMENT) {
    // .. your logic here for start element
}

当然,您总是希望在解释地址的方法中接收“正在构建”事件。

讨论
SAX 和 StAX 的区别在于推拉。在这两种情况下,都必须以某种方式处理解析状态。

这转化为 SAX 的典型方法 B 和 StAX 的方法 A。此外,SAX 必须为 B 提供单独的 XML 事件,而 StAX 可以为 A 提供多个事件(通过传递 XMLStreamReader 实例)。

因此,B 首先检查解析的先前状态,然后处理每个单独的 XML 事件,然后将状态存储在字段中。方法 A 可以通过多次访问 XMLStreamReader 来一次处理所有 XML 事件,直到满意为止。

结论
StAX允许您根据XML结构构建解析(数据绑定)代码;因此,相对于 SAX,“状态”是从 StAX 的程序流中隐式的,而在 SAX 中,对于大多数事件调用,您始终需要保留某种状态变量 + 根据该状态路由流。

我推荐Statax用于除最简单的文档之外的所有文档。宁愿稍后移动到SAX作为优化(但到那时您可能希望采用二进制)。

使用 StAX 进行解析时,请遵循以下模式:

public MyDataBindingObject parse(..) { // provide input stream, reader, etc

        // set up parser
        // read the root tag to get to level 1
        XMLStreamReader reader = ....;

        do {
            int event = reader.next();
            if(event == XMLStreamConstants.START_ELEMENT) {
              // check if correct root tag
              break;
            }

            // add check for document end if you want to

        } while(reader.hasNext());

        MyDataBindingObject object = new MyDataBindingObject();
        // read root attributes if any

        int level = 1; // we are at level 1, since we have read the document header

        do {
            int event = reader.next();
            if(event == XMLStreamConstants.START_ELEMENT) {
                level++;
                // do stateful stuff here

                // for child logic:
                if(reader.getLocalName().equals("Whatever1")) {
                    WhateverObject child = parseSubTreeForWhatever(reader);
                    level --; // read from level 1 to 0 in submethod.

                    // do something with the result of subtree
                    object.setWhatever(child);
                }

                // alternatively, faster
                if(level == 2) {
                    parseSubTreeForWhateverAtRelativeLevel2(reader);
                    level --; // read from level 1 to 0 in submethod.

                    // do something with the result of subtree
                    object.setWhatever(child);
                }


            } else if(event == XMLStreamConstants.END_ELEMENT) {
                level--;
                // do stateful stuff here, too
            }

        } while(level > 0);

        return object;
}

因此,子方法使用大致相同的方法,即计数级别:

private MySubTreeObject parseSubTree(XMLStreamReader reader) throws XMLStreamException {

    MySubTreeObject object = new MySubTreeObject();
    // read element attributes if any

    int level = 1;
    do {
        int event = reader.next();
        if(event == XMLStreamConstants.START_ELEMENT) {
            level++;
            // do stateful stuff here

            // for child logic:
            if(reader.getLocalName().equals("Whatever2")) {
                MyWhateverObject child = parseMySubelementTree(reader);
                level --; // read from level 1 to 0 in submethod.

                // use subtree object somehow
                object.setWhatever(child);
            }

            // alternatively, faster, but less strict
            if(level == 2) {
              MyWhateverObject child = parseMySubelementTree(reader);
                level --; // read from level 1 to 0 in submethod.

                // use subtree object somehow
                object.setWhatever(child);
            }


        } else if(event == XMLStreamConstants.END_ELEMENT) {
            level--;
            // do stateful stuff here, too
        }

    } while(level > 0);

    return object;
}

然后最终你达到一个层次,你将在其中阅读基本类型。

private MySetterGetterObject parseSubTree(XMLStreamReader reader) throws XMLStreamException {

    MySetterGetterObject myObject = new MySetterGetterObject();
    // read element attributes if any

    int level = 1;
    do {
        int event = reader.next();
        if(event == XMLStreamConstants.START_ELEMENT) {
            level++;

            // assume <FirstName>Thomas</FirstName>:
            if(reader.getLocalName().equals("FirstName")) {
               // read tag contents
               String text = reader.getElementText()
               if(text.length() > 0) {
                    myObject.setName(text)
               }
               level--;

            } else if(reader.getLocalName().equals("LastName")) {
               // etc ..
            } 


        } else if(event == XMLStreamConstants.END_ELEMENT) {
            level--;
            // do stateful stuff here, too
        }

    } while(level > 0);

    // verify that all required fields in myObject are present

    return myObject;
}

这很简单,没有误解的余地。只需记住正确降低级别:

A.在你期望字符,但在某些标签中得到一个END_ELEMENT,其中应该包含字符(在上面的模式中):

<Name>Thomas</Name>

取而代之的是

<Name></Name>

对于缺少的子树也是如此,你明白了。

B.调用子解析方法后,这些方法在开始元素上调用,并在相应的结束元素之后返回,即解析器比方法调用之前低一个级别(上述模式)。

请注意,这种方法也完全忽略了“可忽略”的空格,以实现更健壮的实现。

解析器
Woodstox一起使用用于大多数功能,或Aaalto-xml用于速度。


答案 2

为了概括一下,我认为可以像一样有效。随着设计的改进,我真的找不到任何首选解析的情况,除非使用遗留代码。StAXSAXStAXSAX

编辑:根据此博客,Java SAX与StAX不提供架构验证。StAX


推荐