我该何时选择SAX而不是StAX?

86

像SAX和StAX这样的流式XML解析器比DOM解析器构建树结构更快,更节省内存。 SAX是推送解析器,意味着它是观察者模式(也称为监听器模式)的实例。 SAX先出现了,但接着出现了StAX - 一种拉动解析器,基本上就像一个迭代器。

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

在JAXP StAX的Java教程中,StAX被模糊地介绍为DOM和SAX之间的中间状态:“它比SAX更容易使用,比DOM更有效率”。然而,我从未发现任何线索表明StAX比SAX更慢或内存效率更低。

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

6个回答

86
概述
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
}

如果遇到“Building”开始元素,你需要确定正在解析的是地址,然后将XML事件路由到负责解释地址的方法。 StAX
StAX“拉取”XML事件,让你决定在程序/数据中接收XML事件的位置。
// method in standard StAX reader
int event = reader.next();
if(event == XMLStreamConstants.START_ELEMENT) {
    // .. your logic here for start element
}

当然,您总是希望在负责解释地址的方法中接收到“Building”事件。
讨论
SAX和StAX之间的区别在于推和拉。在两种情况下,都必须以某种方式处理解析状态。
这意味着对于SAX,方法B是典型的,而对于StAX,则是方法A。此外,SAX必须提供B个单独的XML事件,而StAX可以通过传递XMLStreamReader实例来提供多个事件(A)。

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

结论
StAX使您可以根据XML结构组织解析(数据绑定)代码; 因此,与SAX相比,对于StAX,“状态”从程序流中是隐含的,而对于大多数事件调用,在SAX中,您总是需要保留某种状态变量+根据该状态路由流程。

我建议使用StAX来解析除最简单文档以外的所有文档。稍后再将其优化为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>

同样,缺失的子树也是如此,你明白了吧。
在调用子解析方法之后,它们被称为开始元素,并在相应的结束元素之后返回,即解析器比方法调用前低一级(上述模式)。
请注意,这种方法完全忽略了“可忽略的”空格,以实现更健壮的实现。
解析器 使用Woodstox获取大多数功能或使用Aalto-xml提高速度。

在你的开场白中,它写着"...whereas in SAX..."。这是一个打字错误吗?(应该是"StAX"而不是"SAX")无论如何,感谢您的回答。如果我理解正确,您是说相对于需要跟踪StAX方法中xml树的位置,SAX方法中的隐式状态是一种优势。 - Rinke
感谢你的(现在更详细的)回答。恐怕我仍然看不出为什么使用SAX而不是StAX会是一个好的理由。你的回答对两个处理器如何工作提供了很好的解释。 - Rinke
对于简单的文档,它们是相同的。例如,请查看此模式:http://mpeg.chiariglione.org/technologies/mpeg-21/mp21-did/index.htm,并且使用StAX会更实用。 - ThomasRS
简而言之,由于您已经在编写代码,您了解正在解析的文档的哪一部分,即将SAX事件映射到正确代码的所有逻辑都是浪费的。 - ThomasRS

24

一般来说,我认为 StAX 在效率上可以与 SAX 相媲美。随着 StAX 设计的改进,除非在处理遗留代码时,我真的找不到任何情况下会优先选择 SAX 解析。

编辑:根据这篇博客Java SAX vs. StAXStAX 不提供模式验证功能。


2
在stax上添加验证并不太难。我前几天就自己实现了这个功能。 - jtahlborn
有关验证的更多详细信息:https://dev59.com/U2025IYBdhLWcg3wzZP2 - Ben

17

@Rinke: 我想只有当您不需要处理 XML 内容时,我才会考虑使用 SAX 而不是 STAX;例如,您只想检查传入的 XML 是否格式正确,并且只想处理错误(如果有)... 在这种情况下,您可以直接调用 SAX 解析器上的 parse() 方法并指定错误处理程序以处理任何解析问题... 因此,基本上在您想要处理内容时,STAX 是明显更好的选择,因为编写 SAX 内容处理程序太困难了...

这种情况的一个实际例子可能是,如果您的企业系统中有一系列 SOAP 节点,并且入门级 SOAP 节点仅允许那些格式正确的 SOAP XML 通过下一阶段,则我不认为我会使用 STAX。我只会使用 SAX。


我选择了这个回答作为目前为止最好的一个。虽然它是一个很好的回答,但我觉得它并不完全权威和清晰。欢迎新的回答。 - Rinke

1

一切都是平衡。

你可以使用阻塞队列和一些线程技巧将SAX解析器转换为拉取解析器,因此对我来说,两者之间的差异要小得多。

我相信目前StAX需要通过第三方jar打包,而SAX在javax中免费提供。

我最近选择了SAX,并围绕它构建了一个拉取解析器,因此我不需要依赖第三方jar。

未来的Java版本几乎肯定会包含StAX实现,因此问题就解决了。


1
Java SE 6 包含 StAX。但例如 Android 实现不包括它。 - Bjarne Boström

0

StAX 可以让你创建快速的双向 XML 解析器。它在性能和可用性方面都比其他方法(如 DOM 和 SAX)更好,是一个更好的选择。

你可以在 Java StAX 教程 中了解更多关于 StAX 的内容。


-2

2
我阅读了这篇论文,获胜者是使用游标API的StAX,如XMLStreamReader - Roland
非常有趣:),你是指乌龟比赛的获胜者吗:) - vtd-xml-author
我刚刚重新阅读了这篇论文,确实StaX比vtd更优秀,速度更快,内存消耗更少。那么你的观点是什么? - Roland
获胜者是stAX,以什么方式?您指的是论文的哪一部分?修改文档、选择还是区分?显然,论文的作者得出了不同的结论。但他们完全可能是错的... - vtd-xml-author
2
根据结果(图11和图12),我们可以看出StAX是性能最好的API,其次是VTD。然而,VTD消耗了相当大的内存。内存消耗可能成为提供有限功能的环境的瓶颈。 - Roland
一些操作在VTD中更快,例如差异操作。因此,如果您需要使用该操作,请考虑使用VTD。 - Roland

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