Java SAX解析

15

我需要解析一个XML流。由于我只需要执行一次并构建我的Java对象,所以SAX看起来是自然的选择。我正在扩展DefaultHandler并实现startElement,endElement和characters方法,在类中具有成员变量,其中我保存当前读取的值(在characters方法中获取)。

我没有问题地完成了我需要做的事情,但是我的代码变得相当复杂,我确信没有必要这样做,我可以用不同的方式处理。我的XML结构大致如下:

<players>
  <player>
    <id></id>
    <name></name>
    <teams total="2">
      <team>
        <id></id>
        <name></name>
        <start-date>
          <year>2009</year>
          <month>9</month>
        </start-date>
        <is-current>true</is-current>
      </team>
      <team>
        <id></id>
        <name></name>
        <start-date>
          <year>2007</year>
          <month>11</month>
        </start-date>
        <end-date>
          <year>2009</year>
          <month>7</month>
        </end-date>
      </team>
    </teams>
  </player>
</players>

当我意识到文件的几个区域中使用了相同的标签名称时,我的问题就开始了。例如,id和name都用于一个球员和一支队伍。我想创建我的Java类Player和Team的实例。在解析过程中,我保持布尔标志以告诉我是否在团队部分,这样在endElement中,我将知道该名称是团队的名称,而不是球员的名称等。

以下是我的代码:

public class MyParser extends DefaultHandler {

    private String currentValue;
    private boolean inTeamsSection = false;
    private Player player;
    private Team team;
    private List<Team> teams;

    public void characters(char[] ch, int start, int length) throws SAXException {
        currentValue = new String(ch, start, length);
    }

    public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
        if(name.equals("player")){
            player = new Player();
        }
        if (name.equals("teams")) {
            inTeamsSection = true;
            teams = new ArrayList<Team>();
        }
        if (name.equals("team")){
            team = new Team();
        }
    }   

    public void endElement(String uri, String localName, String name) throws SAXException {
        if (name.equals("id")) {
            if(inTeamsSection){
                team.setId(currentValue);
            }
            else{
                player.setId(currentValue);
            }
        }
        if (name.equals("name")){
            if(inTeamsSection){
                team.setName(currentValue);
            }
            else{
                player.setName(currentValue);
            }
        }
        if (name.equals("team")){
            teams.add(team);
        }
        if (name.equals("teams")){
            player.setTeams(teams);
            inTeamsSection = false;
        }
    }
}

因为在我的实际场景中,除了团队之外,我还有更多的节点与玩家相关,并且这些节点也有像名称和ID这样的标签。我发现自己陷入了许多布尔值类似于inTeamsSection的混乱中,而我的endElement方法变得又长又复杂,有许多条件。

我应该怎么做才能不同呢?例如,我如何知道名称标签属于哪个节点?

谢谢!


1
我会说,在大约7年前使用SAX是一个自然的选择。目前,自然的选择是使用JAXB(或Xtream、XmlBeans或JibX)。 - Tarlog
有时候你必须手动解析数据。当你处理几兆字节的XML文件并将其转换为Java对象时,这并不是一个很好的想法。 - Oleg Mikheev
@ʘleg - 如果您使用JAXB从StAX XMLStreamReader反编组对象,则可以反编组较大文档的子部分以管理内存限制。 - bdoughan
“subsections” 意思是子树或 XML 的一部分吗?如果有一个简单的(单层级)XML,例如 <player1/> <player2/> <player3/>,是否可能使用 JAXB 逐个取消编组它们,而不是全部一起取消编组? - Oleg Mikheev
5个回答

23

当编写 SAX 解析器时,有一个巧妙的技巧:可以在解析过程中更改 XMLReader 的 ContentHandler。这允许将不同元素的解析逻辑分离到多个类中,使解析更具模块化和可重用性。当一个处理程序看到其结束元素时,它会切换回其父级处理程序。你可以根据需要实现多少处理程序。代码可能如下所示:

public class RootHandler extends DefaultHandler {
    private XMLReader reader;
    private List<Team> teams;

    public RootHandler(XMLReader reader) {
        this.reader = reader;
        this.teams = new LinkedList<Team>();
    }

    public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
        if (name.equals("team")) {
            // Switch handler to parse the team element
            reader.setContentHandler(new TeamHandler(reader, this));
        }
    }
}

public class TeamHandler extends DefaultHandler {
    private XMLReader reader;
    private RootHandler parent;
    private Team team;
    private StringBuilder content;

    public TeamHandler(XMLReader reader, RootHandler parent) {
        this.reader = reader;
        this.parent = parent;
        this.content = new StringBuilder();
        this.team = new Team();
    }

    // characters can be called multiple times per element so aggregate the content in a StringBuilder
    public void characters(char[] ch, int start, int length) throws SAXException {
        content.append(ch, start, length);
    }

    public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
        content.setLength(0);
    }

    public void endElement(String uri, String localName, String name) throws SAXException {
        if (name.equals("name")) {
            team.setName(content.toString());
        } else if (name.equals("team")) {
            parent.addTeam(team);
            // Switch handler back to our parent
            reader.setContentHandler(parent);
        }
    }
}

如果有子团队、玩家等,它们不都必须相互引用,这将导致非常紧密的耦合吗? - Oleg Mikheev
1
每个处理程序都必须知道其父处理程序和可能的子处理程序,因此肯定存在一些耦合。但例如,“start-date”的处理程序不需要知道“player”的处理程序。 - Jörn Horstmann
谢谢,我现在正在使用这个技巧,它对我非常有效。正是我需要的用例。 - Haji

3

如果不了解您的具体需求,就很难给出建议。但是,您对“我的代码变得非常复杂”感到惊讶,这表明您在选择SAX时并不明确。SAX是一个低级编程接口,具有非常高的性能,但这是因为解析器为您做了更少的工作,因此您需要自己做更多的工作。


1

我强烈建议停止手动解析,使用好的XML数据绑定库。XStream(http://x-stream.github.io/)是我个人最喜欢的,但还有许多不同的库可供选择。它甚至可以在现场解析您的POJOs,无需任何配置(如果您使用属性名称和复数形式来匹配XML结构)。


1
我做了类似的事情,但是我不是使用布尔标志来告诉我我的状态,而是测试playerteam是否为非null。这使得事情变得更加整洁。这需要在检测到每个元素的结尾后将它们设置为null,在将其添加到相关列表之后。

0
如果您需要更美观的代码,请使用StAX。所有XML解析API的比较表明,StAX是一个更好的选择。
在大多数测试中,StAX的性能都比其他API实现更好。
因此,我个人认为除非您正在进行一些与遗留相关的编程,否则没有任何理由继续使用SAX。

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