内容


使用 SAX 处理 XML 文档

SAX的基本情况

SAX同DOM一样也是一个访问XML文档的接口。SAX是Simple API for XML的缩写。它不像DOM那样是W3C的推荐标准。它是由XML-DEV邮件列表的成员开发维护,由David Megginson领导(david@megginson.com)的一个Public Domain软件。SAX是一个彻底的自由软件,它的作者放弃了对它的所有权利,并且它也被许可用于任何目的(在文章最后附录了它的版权声明)。

到现在为止SAX的版本已经发展到2.0。在这个最新版本中增加了对名称空间(Namespaces)的支持,而且可以通过对features以及properties的设置来对解析器做全面的配置,这其中包括设置解析器是否对文档进行有效性验证,以及怎样来处理带有名称空间的元素名称等。SAX1中的接口已经不再使用了,这里只会讨论有关SAX2的开发。在本文中提到SAX只是指SAX 2。另外,本文的所有例子都是用java编写,SAX解析器也使用的是JAVA版本。

像DOM一样,SAX并不是一个实际可以使用的XML文档解析器,而是其他兼容SAX的解析器要实现的接口和帮助类的集合。如果你想使用SAX的话,你必须满足下面的要求:

  1. 系统中包含Java 1.1 或者更高版本。
  2. 在Java classpath中包含进你的SAX类库。
  3. 在Java classpath中包含进你要使用的兼容SAX的XML解析器类库。

实现了SAX的解析器有很多,比如Apache的Xerces,Oracle的XML Parser等等。在本文中的例子程序使用的都是Xerces解析器,你可以从 http://xml.apache.org 得到它。让我们下载得到xerces.jar文件然后将其加入到classpath中去,这样我们就已经建立好环境(在xerces.jar中已经包含了SAX接口,所以不必特意再去寻找SAX类库)。

在SAX API中有两个包,org.xml.sax和org.xml.sax.helper。其中org.xml.sax中主要定义了SAX的一些基础接口,如XMLReader、ContentHandler、ErrorHandler、DTDHandler、EntityResolver等。而在org.xml.sax.helper中则是一些方便开发人员使用的帮助类,如缺省实现所有处理器接口的帮助类DefaultHandler、方便开发人员创建XMLReader的XMLReaderFactory类等等。在这两个包中还有一些应用于SAX1的接口,同时还有几个类它们只是为了便于将在SAX1上开发的应用移植到SAX2上,在这篇文章中就不涉及了。下面是我们要关注的接口和类:

Package org.xml.sax介绍
Interfaces接口
Attributes定义了一个属性列表接口,供访问元素的属性列表而用。
ContentHandler处理解析文档内容时产生的事件。
DTDHandler处理解析DTD时的相应事件。
EntityResolver处理外部实体。
ErrorHandler处理解析过程中所遇到的文档错误事件。
Locator为了定位解析中产生的内容事件在文档中的位置而准备的一个定位器接口。
XMLFilter提供了一个方便应用开发的过滤器接口。
XMLReader任何兼容SAX2的解析器都要实现这个接口,这个接口让应用程序可以设置或查找features和properties,注册各种事件处理器,以及开始解析文档。
Classes
InputSource为XML实体准备的输入源。
Exceptions
SAXException包装了一般的SAX错误和警告。
SAXNotRecognizedException为识别不出某些标识而抛出的异常。
SAXNotSupportedException为不支持某个操作而抛出的异常。
SAXParseException包装了一个关于XML解析的错误或者警告。
Package org.xml.sax.helpers帮助类所在的包
Classes
AttributesImpl对Attributes接口的缺省实现
NamespaceSupport提供名称空间支持。
DefaultHandler缺省实现了四个处理器接口,方便用户开发,在开发过程中会经常用到。
LocatorImpl提供了一个对Locator接口的实现
XMLFilterImpl对过滤器接口的实现,使用过滤器进行应用程序开发时,继承这个类很方便。
XMLReaderFactory为方便创建不同的XMLReader而提供。也会经常用到。

理解并使用SAX

SAX的设计实现与DOM是完全不同的!DOM处理XML文档是基于将XML文档解析成树状模型,放入内存进行处理。而SAX则是采用基于事件驱动的处理模式,它将XML文档转化成一系列的事件,由单独的事件处理器来决定如何处理。为了了解如何使用SAX API处理XML文档,这里先介绍一下SAX所使用的基于事件驱动的处理模式。

这种基于事件的处理模式是一种通用的程序设计模式,被广泛应用于GUI设计。在JAVA的AWT,SWING以及JAVA BEANS中就有它的身影。而SAX的基于事件驱动的处理模式就与上面三者中的非常相像。

基于事件的处理模式主要是围绕着事件源以及事件处理器(或者叫监听器)来工作的。一个可以产生事件的对象被称为事件源,而可以针对事件产生响应的对象就被叫做事件处理器。事件源和事件处理器是通过在事件源中的事件处理器注册方法连接的。这样当事件源产生事件后,调用事件处理器相应的处理方法,一个事件就获得了处理。当然在事件源调用事件处理器中特定方法的时候,会传递给事件处理器相应事件的状态信息,这样事件处理器才能够根据事件信息来决定自己的行为。

在SAX接口中,事件源是org.xml.sax包中的XMLReader,它通过parse()方法来开始解析XML文档并根据文档内容产生事件。而事件处理器则是org.xml.sax包中的ContentHandler,DTDHandler,ErrorHandler,以及EntityResolver这四个接口。它们分别处理事件源在解析过程中产生的不同种类的事件(其中DTDHandler是为解析文档DTD时而用)。而事件源XMLReader和这四个事件处理器的连接是通过在XMLReader中的相应的事件处理器注册方法set***()来完成的。详细介绍请见下表:

处理器名称所处理事件注册方法
org.xml.sax.ContentHandler跟文档内容有关的所有事件:
  1. 文档的开始和结束
  2. XML元素的开始和结束
  3. 可忽略的实体
  4. 名称空间前缀映射开始和结束
  5. 处理指令
  6. 字符数据和可忽略的空格
XMLReader中的setContentHandler(ContentHandler handler)方法
org.xml.sax.ErrorHandler处理XML文档解析时产生的错误。如果一个应用程序没有注册一个错误处理器类,会发生不可预料的解析器行为。setErrorHandler(ErrorHandler handler)
org.xml.sax.DTDHandler处理对文档DTD进行解析时产生的相应事件setDTDHandler(DTDHandler handler)
org.xml.sax.EntityResolver处理外部实体setEntityResolver(EntityResolver resolver)

在这四个处理器接口中,对我们最重要的是ContentHandler接口。下面让我们看一下对其中方法的说明:

方法名称方法说明
public void setDocumentLocator(Locator locator) 设置一个可以定位文档内容事件发生位置的定位器对象
public void startDocument() throws SAXException 用于处理文档解析开始事件
public void endDocument() throws SAXException 用于处理文档解析结束事件
public void startPrefixMapping(java.lang.String prefix, java.lang.String uri) throws SAXException 用于处理前缀映射开始事件,从参数中可以得到前缀名称以及所指向的uri
public void endPrefixMapping(java.lang.String prefix) throws SAXException 用于处理前缀映射结束事件,从参数中可以得到前缀名称
public void startElement(java.lang.String namespaceURI,java.lang.String localName,java.lang.String qName,Attributes atts) throws SAXException 处理元素开始事件,从参数中可以获得元素所在名称空间的uri,元素名称,属性列表等信息
public void endElement(java.lang.String namespaceURI, java.lang.String localName, java.lang.String qName) throws SAXException 处理元素结束事件,从参数中可以获得元素所在名称空间的uri,元素名称等信息
public void characters(char[] ch, int start, int length) throws SAXException 处理元素的字符内容,从参数中可以获得内容
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException 处理元素的可忽略空格
public void processingInstruction(java.lang.String target, java.lang.String data) throws SAXException 处理解析中产生的处理指令事件

这里再介绍一下org.xml.sax.XMLReader中的方法,然后让我们看一个具体的例子。XMLReader是所有兼容SAX2的解析器都要实现的接口,由它的方法开始解析文档,并且调用它的注册方法来注册各种事件处理器。请看下表:

方法名称方法介绍
public Boolean getFeature(java.lang.String name)throws SAXNotRecognizedException,SAXNotSupportedException 得到某个feature的值
public void setFeature(java.lang.String name,boolean value) throws SAXNotRecognizedException,SAXNotSupportedException 设置某个feature的值,例如,如果需要解析器支持对文档进行验证那么就这么调用本方法。myReader.setFeature(http://xml.org/sax/features/validation,true);其中myReader是XMLReader的实例。
public java.lang.Object getProperty(java.lang.String name)throws SAXNotRecognizedException,SAXNotSupportedException 返回一个property的值
public void setProperty(java.lang.String name,java.lang.Object value)throws SAXNotRecognizedException,SAXNotSupportedException 设置一个property的值
public void setEntityResolver(EntityResolver resolver) 注册处理外部实体的EntityResolver
public EntityResolver getEntityResolver() 得到系统中注册的EntityResolver
public void setDTDHandler(DTDHandler handler) 注册处理DTD解析事件的DTDHandler
public DTDHandler getDTDHandler() 得到系统中注册的DTDHandler
public void setContentHandler(ContentHandler handler) 注册处理XML文档内容解析事件的ContentHandler
public ContentHandler getContentHandler() 得到系统中注册的ContentHandler
public void setErrorHandler(ErrorHandler handler) 注册处理文档解析错误事件的ErrorHandler
public ErrorHandler getErrorHandler() 得到系统中注册的ErrorHandler
public void parse(InputSource input)throws java.io.IOException,SAXException 开始解析一个XML文档。
public void parse(java.lang.String systemId)throws java.io.IOException,SAXException 开始解析一个使用系统标识符标识的XML文档。这个方法只是上面方法的一个快捷方式它等同于:parse(new InputSource(systemId));

一个实例

让我们通过例子来看一下使用SAX解析XML文档的应用程序是如何建立的。下面是在应用程序中被处理的XML文档。为了说明SAX对名称空间的支持,我在这里特意加了一个有名称空间的元素,在这里会产生相应的前缀映射开始和结束事件。

<?xml version="1.0" encoding="GB2312"?>
<我的书架 >
    <技术书籍>
        <图书>
            <书名>JAVA 2编程详解</书名>
            <价格 货币单位="人民币">150</价格>
            <购买日期>2000,1,24</购买日期>
        </图书>      
    </技术书籍>
    <book:文学书籍 xmlns:book="http://javausr.com"/>
    <历史书籍/>
</我的书架>

这里的例子程序只是简单地将遇到的事件信息打印出来。我们首先实现ContentHandler接口来处理在XML文档解析过程中产生的和文档内容相关的事件,代码如下所示MyContentHandler.java: package com.javausr.saxexample;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
public class MyContentHandler implements ContentHandler {
    private StringBuffer buf;
    public void setDocumentLocator( Locator locator ) {
    }
    public void startDocument() throws SAXException {
        buf=new StringBuffer();
        System.out.println("*******开始解析文档*******");
    }
    public void endDocument() throws SAXException {
        System.out.println("*******解析文档结束*******");
    }
    public void processingInstruction( String target, String instruction )
        throws SAXException {
    }
    public void startPrefixMapping( String prefix, String uri ) {
          System.out.println("\n前缀映射: " + prefix +" 开始!"+ "  它的URI是:" + uri);
    }
    public void endPrefixMapping( String prefix ) {
          System.out.println("\n前缀映射: "+prefix+" 结束!");
    }
    public void startElement( String namespaceURI, String localName,
                                  String fullName, Attributes attributes )
                          throws SAXException {
        System.out.println("\n 元素: " + "["+fullName+"]" +" 开始解析!");
        // 打印出属性信息
        for ( int i = 0; i < attributes.getLength(); i++ ) {
            System.out.println("\t属性名称:" + attributes.getLocalName(i)
                + " 属性值:" + attributes.getValue(i));
        }
    }
    public void endElement( String namespaceURI, String localName,
                                                      String fullName )
                          throws SAXException {
        //打印出非空的元素内容并将StringBuffer清空                  
      String nullStr="";
        if (!buf.toString().trim().equals(nullStr)){
           System.out.println("\t内容是: " + buf.toString().trim());
        }
        buf.setLength(0);
        //打印元素解析结束信息
        System.out.println("元素: "+"["+fullName+"]"+" 解析结束!");              
    }
    public void characters( char[] chars, int start, int length )
                                throws SAXException {
          //将元素内容累加到StringBuffer中                
          buf.append(chars,start,length);
    }
    public void ignorableWhitespace( char[] chars, int start, int length )
                                  throws SAXException {
    }
    public void skippedEntity( String name ) throws SAXException {
    }
}

下面让我们创建一个调入了xerces解析器来实现XMLReader接口、并使用刚才创建的MyContentHandler来处理相应解析事件的MySAXApp.java类: package com.javausr.saxexample;

import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import java.io.IOException;
public class MySAXApp {
  public static void main( String[] args ) {
    
    if ( args.length != 1 ) {
      System.out.println("输入: java MySAXApp ");
      System.exit(0);
    }
    try {
        // 初始化reader
        XMLReader reader = XMLReaderFactory.createXMLReader
                          ("org.apache.xerces.parsers.SAXParser") ;
        // 创建ContentHandler的实例
        ContentHandler contentHandler = new MyContentHandler();
        // 在reader中注册实例化的ContentHandler
        reader.setContentHandler( contentHandler );
        // 开始解析文档
        reader.parse(args[0]);
    } catch ( IOException e ) {
        System.out.println("读入文档时错: " + e.getMessage());
    } catch ( SAXException e ) {
        System.out.println("解析文档时错: " + e.getMessage());
    }
  }
}

下面让我们来看一下执行结果:

D:\sax\classes>java com.javausr.saxexample.MySAXApp d:\book.xml
*******开始解析文档*******
元素: [我的书架] 开始解析!
元素: [技术书籍] 开始解析!
元素: [图书] 开始解析!
元素: [书名] 开始解析!
        内容是: JAVA 2编程详解
元素: [书名] 解析结束!
元素: [价格] 开始解析!
        属性名称:货币单位 属性值:人民币
        内容是: 150
元素: [价格] 解析结束!
元素: [购买日期] 开始解析!
        内容是: 2000,1,24
元素: [购买日期] 解析结束!
元素: [图书] 解析结束!
元素: [技术书籍] 解析结束!
前缀映射: book 开始!  它的URI是:http://javausr.com
元素: [book:文学书籍] 开始解析!
元素: [book:文学书籍] 解析结束!
前缀映射: book 结束!
元素: [历史书籍] 开始解析!
元素: [历史书籍] 解析结束!
元素: [我的书架] 解析结束!
*******解析文档结束*******

上面就是使用SAX解析一个XML文档的基本过程,但是MyContentHandler只是处理了解析过程中和文档内容相关的事件,如果在解析过程中出现了错误那我们需要实现ErrorHandler接口来处理。如果不注册一个错误处理器来处理的话,那么错误事件将不会被报告,而且解析器会出现不可预知的行为。在解析过程中产生的错误被分成了3类,它们分别是warning,error,以及fatalerror,也就是说在ErrorHandler中有这么三个相应的方法来处理这些错误事件。下面是对这三个错误处理方法的介绍:

方法名称方法介绍
warning()SAX解析器将用这个方法来报告在XML1.0规范中定义的非错误(error)或者致命错误(fatal error)的错误状态。对这个错误缺省的行为是什么也不做。SAX解析器必须在调用这个方法后继续提供正常的解析事件:应用程序应该能继续处理完文档。
error()这个方法对应在W3C XML 1.0规范的1.2部分中定义的"error"概念。例如,一个带有有效性验证的解析器会使用这个方法来报告违反有效性验证的情况。一个带有有效性验证的解析器会使用这个方法来报告违背有些性约束的情况。缺省的行为是什么也不做。SAX解析器必须在调用这个方法后继续提供正常的解析事件:应用程序应该能继续处理完文档。如果应用程序做不到这样,则解析器即使在XML1.0规范没有要求的情况下也要报告一个致命错误。
fatalError()这个方法对应在W3C XML1.0规范的1.2部分定义的"fatal error"概念。例如,一个解析器会使用这个方法来报告违反格式良好约束的情况。在解析器调用这个方法后应用程序必须表明这个文档是不可使用的,而且应该只是为了收集错误信息而继续进行处理(如果需要的话):实际上,一旦在这个方法被调用后SAX解析器可以停止报告任何事件。

下面是实现了ErrorHandler接口的MyErrorHandler.java类: package com.javausr.saxexample;

import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;
import org.xml.sax.SAXException;
public class MyErrorHandler implements ErrorHandler {
    public void warning( SAXParseException exception ) {
        System.out.println("*******WARNING******");
        System.out.println("\t行:\t" + exception.getLineNumber());
        System.out.println("\t列:\t" + exception.getColumnNumber());
        System.out.println("\t错误信息:\t" + exception.getMessage());
        System.out.println("********************");
    }
    public void error( SAXParseException exception ) throws SAXException{
        System.out.println("******* ERROR ******");
        System.out.println("\t行:\t" + exception.getLineNumber());
        System.out.println("\t列:\t" + exception.getColumnNumber());
        System.out.println("\t错误信息:\t" + exception.getMessage());
        System.out.println("********************");
    }
    public void fatalError( SAXParseException exception ) throws SAXException {
        System.out.println("******** FATAL ERROR ********");
        System.out.println("\t行:\t" + exception.getLineNumber());
        System.out.println("\t列:\t" + exception.getColumnNumber());
        System.out.println("\t错误信息:\t" + exception.getMessage());
        System.out.println("*****************************");
    }
}

我们也要对MySAXApp.java类做一些修改(在源代码中蓝色标出的部分)使它使用MyErrorHandler.java: package com.javausr.saxexample;

import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import org.xml.sax.ContentHandler;
//引入ErrorHandler
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import java.io.IOException;
public class MySAXApp {
    
        public static void main( String[] args ) {
        
          if ( args.length != 1 ) {
            System.out.println("输入: java MySAXApp ");
            System.exit(0);
        }
        try {
            // 初始化reader
            XMLReader reader = XMLReaderFactory.createXMLReader
                               ("org.apache.xerces.parsers.SAXParser") ;
            // 创建ContentHandler的实例
            ContentHandler contentHandler = new MyContentHandler();
            // 在reader中注册实例化的ContentHandler
            reader.setContentHandler( contentHandler );
            // 创建ErrorHandler的实例
            ErrorHandler errorHandler = new MyErrorHandler();
            // 在reader中注册实例化的ErrorHandler
            reader.setErrorHandler( errorHandler );
            // 开始解析文档
            reader.parse(args[0]);
    } catch ( IOException e ) {
        System.out.println("读入文档时错: " + e.getMessage());
    } catch ( SAXException e ) {
        System.out.println("解析文档时错: " + e.getMessage());
    }
  }

让我们人为制造些错误来检查一下我们的错误处理器工作情况。删除元素<购买日期>的闭合标记,这样会产生一个fatal error,下面是执行结果: D:\sax\classes>java com.javausr.saxexample.MySAXApp d:\book.xml

*******开始解析文档*******
元素: [我的书架] 开始解析!
元素: [技术书籍] 开始解析!
元素: [图书] 开始解析!
元素: [书名] 开始解析!
元素: [书名] 开始解析!
        内容是: JAVA 2编程详解
元素: [书名] 解析结束!
元素: [价格] 开始解析!
        属性名称:货币单位 属性值:人民币
        内容是: 150
元素: [价格] 解析结束!
元素: [购买日期] 开始解析!
******** FATAL ERROR ********
        行:     8
        列:     7
        错误信息: The element type "购买日期" must be 
        terminated by the matching end-tag "</购买日期>".
*****************************
解析文档时错: Stopping after fatal error: 
The element type "购买日期" 
must be terminated by the matching end-tag "</购买日期>".

现在总结一下如何书写基于SAX的应用程序。一般步骤如下:

  1. 实现一个或多个处理器接口(ContentHandler, ErrorHandler, DTDHandler ,or EntityResover)。
  2. 创建一个XMLReader类的实例。
  3. 在新的XMLReader实例中通过大量的set*****() 方法注册一个事件处理器的实例
  4. 调用XMLReader的parse()方法来处理文档。

使用DefaultHandler


现在的程序是比较完整了,但还有许多可以改进的地方。首先在我们实现的MyContentHandler.java中,你会发现有很多方法实际上什么也没有做,但为了实现ContentHandler接口,不得不把它们写出来,这样很是麻烦。SAX API已经考虑到这个问题,在它的org.xml.sax.helper包中为我们提供了一个方便实现各种处理器接口的帮助类DefaultHandler。这个类缺省实现了上面提到的4个处理器接口。这样我们只需继承这个类,然后覆盖我们想要实现的事件处理方法即可。下面我们来新建一个继承了DefaultHandler的MyDefaultHandler.java类,然后把在MyContentHandler.java和MyErrorHandler.java中实现的事件处理方法照搬到MyDefaultHandler.java类中,那些没有使用的方法就不必重复了。这里是MyDefaultHandler.java: package com.javausr.saxexample;

import org.xml.sax.*;
import org.xml.sax.helpers.*;
import java.io.*;
public class MyDefaultHandler extends DefaultHandler {
    private StringBuffer buf;
    public void startDocument() throws SAXException {
        buf=new StringBuffer();
        System.out.println("*******开始解析文档*******");
    }
    public void endDocument() throws SAXException {
        System.out.println("*******解析文档结束*******");
    }
    public void startPrefixMapping( String prefix, String uri ) {
System.out.println("\n前缀映射: " + prefix +" 开始!"+ "  它的URI是:"+uri);
    }
    public void endPrefixMapping( String prefix ) {
       System.out.println("\n前缀映射: "+prefix+" 结束!");
    }
    public void startElement( String namespaceURI, String localName,
                                  String fullName, Attributes attributes )
                          throws SAXException {
        System.out.println("\n元素: " + "["+fullName+"]" +" 开始解析!");
        // 打印出属性信息
        for ( int i = 0; i < attributes.getLength(); i++ ) {
            System.out.println("\t属性名称:" + attributes.getLocalName(i)
                + " 属性值:" + attributes.getValue(i));
        }
    }
    public void endElement( String namespaceURI, String localName,
                                                      String fullName )
                          throws SAXException {
       //打印出非空的元素内容并将StringBuffer清空
       String nullStr="";
       if (!buf.toString().trim().equals(nullStr)){
          System.out.println("\t内容是: " + buf.toString().trim());
       }
       buf.setLength(0);
       //打印元素解析结束信息
        System.out.println("元素: "+"["+fullName+"]"+" 解析结束!");
    }
    public void characters( char[] chars, int start, int length )
                                throws SAXException {
       //将元素内容累加到StringBuffer中
       buf.append(chars,start,length);
    }
    public void warning( SAXParseException exception ) {
        System.out.println("*******WARNING******");
        System.out.println("\t行:\t" + exception.getLineNumber());
        System.out.println("\t列:\t" + exception.getColumnNumber());
        System.out.println("\t错误信息:\t" + exception.getMessage());
        System.out.println("********************");
    }
    public void error( SAXParseException exception ) throws SAXException{
        System.out.println("******* ERROR ******");
        System.out.println("\t行:\t" + exception.getLineNumber());
        System.out.println("\t列:\t" + exception.getColumnNumber());
        System.out.println("\t错误信息:\t" + exception.getMessage());
        System.out.println("********************");
    }
    public void fatalError( SAXParseException exception ) throws SAXException {
        System.out.println("******** FATAL ERROR ********");
        System.out.println("\t行:\t" + exception.getLineNumber());
        System.out.println("\t列:\t" + exception.getColumnNumber());
        System.out.println("\t错误信息:\t" + exception.getMessage());
        System.out.println("*****************************");
    }
}

我们也要对MySAXApp.java做相应的修改,修改已在源代码中标出: package com.javausr.saxexample;

import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
//引入DefaultHandler
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.SAXException;
import java.io.IOException;
public class MySAXApp {
  public static void main( String[] args ) {
        
      if ( args.length != 1 ) {
        System.out.println("输入: java MySAXApp ");
        System.exit(0);
      }
    try {
        // 初始化reader
        XMLReader reader = XMLReaderFactory.createXMLReader
                         ("org.apache.xerces.parsers.SAXParser") ;
        // 创建DefaultHandler的实例
        DefaultHandler defaultHandler=new MyDefaultHandler();
        //在reader中将defaultHandler注册为ContentHandler
        reader.setContentHandler(defaultHandler);
        //在reader中将defaultHandler注册为ErrorHandler
        reader.setErrorHandler(defaultHandler);
        // 开始解析文档
        reader.parse(args[0]);
    } catch ( IOException e ) {
        System.out.println("读入文档时错: " + e.getMessage());
    } catch ( SAXException e ) {
        System.out.println("解析文档时错: " + e.getMessage());
    }
  }
}

使用过滤器

在SAX API中还提供了一个过滤器接口org.xml.sax.XMLFilter,以及对它的缺省实现org.xml.sax.helper.XMLFilterImpl。使用它们可以很容易的开发出复杂的SAX应用。这里要先介绍一下过滤器设计模式。这个设计模式很好理解,就像一个净化水的过程。自然界中的水流过一个个的过滤器得到最后的饮用水。这些过滤器,有的是清除水中的泥沙,有的是杀灭水中的细菌,总之不同的过滤器完成不同的任务。在应用开发中,我们让被改造的对象(这里是事件流)通过这些过滤器对象从而得到改造后符合要求的对象。这样,在过滤器的帮助之下,我们可以非常方便的在每个过滤器中实现一个特定功能,从而创建结构复杂的应用程序。在应用程序中你可以构造任意多个过滤器,将它们串接起来完成任务。

在SAX API中org.xml.sax.XMLFilter接口继承了org.xml.sax.XMLReader接口。它与XMLReader不同的是它不像XMLReader那样通过解析文档来获取事件,而是从其他XMLReader中获取事件,当然这也包括从其他的XMLFilter中获取事件。在org.xml.sax.XMLFilter中有两个方法:

方法名称方法描述
Public void setParent(XMLReader parent)设置父XMLReader。这个方法让应用程序将这个过滤器连接到它的父XMLReader (也可能是另一个过滤器)。
Public XMLReader getParent()获取父XMLReader。这个方法让应用程序可以查询父XMLReader(也可能是另一个过滤器)。最好不要在父XMLReader中直接进行任何操作:让所有的事件通过这个过滤器来处理。

我们不需要自己实现org.xml.sax.XMLFilter接口,在SAX API 中提供了一个org.xml.sax.helper.XMLFilterImpl类,它不仅实现了org.xml.sax.XMLFilter接口而且还实现了其他四个核心处理器接口,我们只需要继承它即可完成我们的过滤器。刚开始使用XMLFilterImpl比较容易让人迷惑,你只需要记住:

  1. 在你继承的XMLFilterImpl类中用set****()方法这册的事件处理器是给过滤后的事件流而用的。
  2. 在你继承的XMLFilterImpl类中实现的那些事件处理方法,比如startDocument()、startElement()、characters()等才是这个过滤器实现它自身功能的地方。而通过继承XMLFilterImpl而实现的这个类会被造型成各种处理器(它本身实现了四个处理器接口)用在它的父XMLReader中。这个步骤会在你调用自己创建的过滤器的parse()方法开始解析文档时被自动执行(请参见SAX源代码)。
  3. 如果不是使用带参数的构造器创建XMLFilter对象,务必使用setParent(XMLReader parent)方法连接它的父XMLReader。
  4. 如果使用多个过滤器的话,执行顺序是从父亲到最后的过滤器。但是开始解析却要调用最后一个过滤器的parse()方法。

下面让我们结合已有的例子来演示过滤器org.xml.sax.XMLFilter的作用。我们在这个过滤器中要过滤掉<技术书籍>这个元素,最后得到的事件流还是由上边实现的MyDefaultHandler来处理。源代码如下MyFilter.java: package com.javausr.saxexample;

import org.xml.sax.*;
import org.xml.sax.helpers.*;
import java.io.*;
public class MyFilter extends XMLFilterImpl {
   private String currentElement;
   public MyFilter( XMLReader parent ) {
      super(parent);
   }
   /**
    * 过滤掉元素<技术书籍>的开始事件
    **/
   public void startElement( String namespaceURI, String localName,
                             String fullName, Attributes attributes )
      throws SAXException {
         currentElement = localName;
         if ( !localName.equals("技术书籍") ) {
           super.startElement(namespaceURI, localName, fullName, attributes);
         }
      }
   /**
    * 过滤掉元素<技术书籍>的结束事件
    **/
   public void endElement(String namespaceURI, String localName, String
                          fullName)
      throws SAXException {
         if ( !localName.equals("技术书籍") ) {
            super.endElement(namespaceURI, localName, fullName);
         }
    }
   /**
    * 过滤掉元素<技术书籍>中的内容
    **/
    public void characters(char[] buffer, int start, int length) 
throws SAXException {
        if ( !currentElement.equals("技术书籍") ) {
          super.characters( buffer,start,length );
        }
    }
}

同样我们还要修改MySAXApp.java,修改后的代码如下所示: package com.javausr.saxexample;

import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import org.xml.sax.helpers.DefaultHandler;
//引入XMLFilter
import org.xml.sax.XMLFilter;
import org.xml.sax.SAXException;
import java.io.IOException;
public class MySAXApp {
  public static void main( String[] args ) {
    
    if ( args.length != 1 ) {
      System.out.println("输入: java MySAXApp ");
      System.exit(0);
    }
    try {
            // 初始化reader
        XMLReader reader = XMLReaderFactory.createXMLReader
                           ("org.apache.xerces.parsers.SAXParser") ;
        //初始化过滤器
        XMLFilter myFilter=new MyFilter(reader);
        // 创建DefaultHandler的实例
        DefaultHandler defaultHandler=new MyDefaultHandler();
        //为过滤后的事件流设置ContentHandler
        myFilter.setContentHandler(defaultHandler);
        //为过滤后的事件流设置ErrorHandler
        myFilter.setErrorHandler(defaultHandler);
            // 开始解析文档,注意是使用myFilter中的解析方法
        myFilter.parse(args[0]);
      } catch ( IOException e ) {
            System.out.println("读入文档时错: " + e.getMessage());
      } catch ( SAXException e ) {
            System.out.println("解析文档时错: " + e.getMessage());
    }
  }
}

这里是最后的执行结果,我们可以发现有关<技术书籍>的全部事件已经被过滤掉了。认真看一下结果,你一定觉得奇怪,为什么<技术书籍>元素的孩子元素仍然存在。请记住SAX是把XML文档解析成事件流,所有没有被过滤的事件都会保留下来。这就是SAX和DOM的最大不同。在DOM中文档被解析成了树状模型,如果你删除一个元素,那么这个元素以及它的孩子元素就都会被删除,这符合树状模型的特点。

D:\sax\classes>java com.javausr.saxexample.MySAXApp d:\book.xml

*******开始解析文档*******
元素: [我的书架] 开始解析!
元素: [图书] 开始解析!
元素: [书名] 开始解析!
        内容是: JAVA 2编程详解
元素: [书名] 解析结束!
元素: [价格] 开始解析!
        属性名称:货币单位 属性值:人民币
        内容是: 150
元素: [价格] 解析结束!
元素: [购买日期] 开始解析!
        内容是: 2000,1,24
元素: [购买日期] 解析结束!
元素: [图书] 解析结束!
前缀映射: book 开始!  它的URI是:http://javausr.com
元素: [book:文学书籍] 开始解析!
元素: [book:文学书籍] 解析结束!
前缀映射: book 结束!
元素: [历史书籍] 开始解析!
元素: [历史书籍] 解析结束!
元素: [我的书架] 解析结束!
*******解析文档结束*******

一些值得注意的问题

首先是有关元素内容的问题,在SAX API定义中元素内容可以在一次事件(由characters()方法处理)中返回,也可以在多次事件中返回,这样我们就应该考虑不能一次得到所有内容数据的情况。一般的解决办法是定义一个StringBuffer由它来保存内容数据,在元素结束或者新元素开始的时候清空这个StringBuffer从而可以保存新的内容数据。请参考上面的相应的源代码。

还有在SAX API中特意提到从 characters(char[] ch,int start,int length)方法中提取数据时一定不要从返回的字符数组范围之外读取,这一点我们也要切记。

另一个值得注意的问题是,在 startElement()方法中返回的Attributes属性列表中的属性顺序并没有被特意规定,在不同的SAX实现中也各不相同。所以我们在编写程序时不要把属性顺序想成一定的。

SAX与DOM的比较

通过上面的介绍我想大家对SAX已经有了一个基本的了解。每一个进行XML开发的编程人员都知道DOM,那为什么在有了DOM这个功能强大的文档对象模型之后,我们还需要SAX?这就要从它们根本不同的实现方法上来分析。DOM解析器是通过将XML文档解析成树状模型并将其放入内存来完成解析工作的,而后对文档的操作都是在这个树状模型上完成的。这个在内存中的文档树将是文档实际大小的几倍。这样做的好处是结构清除、操作方便,而带来的麻烦就是极其耗费系统资源。而SAX正好克服了DOM的缺点。SAX解析器的处理过程是通读整个文档,根据文档内容产生事件,而把对这些事件的处理交由事件处理器处理。SAX不需要在内存中保存整个文档,它对系统资源的节省是显而易见的。这样在一些需要处理大型XML文档和性能要求比较高的场合就要用SAX了。

下面的表格列出了SAX和DOM在一些方面的对照:

SAXDOM
顺序读入文档并产生相应事件,可以处理任何大小的XML文档在内存中创建文档树,不适于处理大型XML文档。
只能对文档按顺序解析一遍,不支持对文档的随意访问。可以随意访问文档树的任何部分,没有次数限制。
只能读取XML文档内容,而不能修改可以随意修改文档树,从而修改XML文档。
开发上比较复杂,需要自己来实现事件处理器。易于理解,易于开发。
对开发人员而言更灵活,可以用SAX创建自己的XML对象模型。已经在DOM基础之上创建好了文档树。

通过对SAX和DOM的分析,它们各有自己的不同应用领域:

  1. SAX适于处理下面的问题:
  2. 对大型文档进行处理。
  3. 只需要文档的部分内容,或者只需要从文档中得到特定信息。
  4. 想创建自己的对象模型的时候。

DOM适于处理下面的问题:

  1. 需要对文档进行修改
  2. 需要随机对文档进行访问,例如XSLT解析器。

对SAX的介绍到这里就告一段落了,希望能对大家有所帮助:),本文的绝大部分参考资料都来源于http://www.megginson.com/SAX/ 以及SAX API(虽然说SAX有了自己新的网站http://sax.sourceforge.net/ 但我从来没有成功访问过!) ,感谢David Megginson和其他SAX开发人员给我们提供了这么一个好东东。本文如有错误和不妥的地方还请大家指正。

附录: SAX的版权声明

SAX2 is Free!

I hereby abandon any property rights to SAX 2.0 (the Simple API for XML), and release all of the SAX 2.0 source code, compiled code, and documentation contained in this distribution into the Public Domain. SAX comes with NO WARRANTY or guarantee of fitness for any purpose.

David Megginson, david@megginson.com

2000-05-05


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=21072
ArticleTitle=使用 SAX 处理 XML 文档
publish-date=11012001