使用 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的话,你必须满足下面的要求:
实现了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上,在这篇文章中就不涉及了。下面是我们要关注的接口和类:
回页首 理解并使用SAXSAX的设计实现与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***()来完成的。详细介绍请见下表:
在这四个处理器接口中,对我们最重要的是ContentHandler接口。下面让我们看一下对其中方法的说明:
这里再介绍一下org.xml.sax.XMLReader中的方法,然后让我们看一个具体的例子。XMLReader是所有兼容SAX2的解析器都要实现的接口,由它的方法开始解析文档,并且调用它的注册方法来注册各种事件处理器。请看下表:
回页首 一个实例让我们通过例子来看一下使用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 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 length ) throws SAXException { //将元素内容累加到StringBuffer中 buf.append(chars,start,length); } public void ignorableWhitespace( char[] chars,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:saxclasses>java com.javausr.saxexample.MySAXApp d:book.xml *******开始解析文档******* 元素: [我的书架] 开始解析! 元素: [技术书籍] 开始解析! 元素: [图书] 开始解析! 元素: [书名] 开始解析! 内容是: JAVA 2编程详解 元素: [书名] 解析结束! 元素: [价格] 开始解析! 属性名称:货币单位 属性值:人民币 内容是: 150 元素: [价格] 解析结束! 元素: [购买日期] 开始解析! 内容是: 2000,24 元素: [购买日期] 解析结束! 元素: [图书] 解析结束! 元素: [技术书籍] 解析结束! 前缀映射: book 开始! 它的URI是:http://javausr.com 元素: [book:文学书籍] 开始解析! 元素: [book:文学书籍] 解析结束! 前缀映射: book 结束! 元素: [历史书籍] 开始解析! 元素: [历史书籍] 解析结束! 元素: [我的书架] 解析结束! *******解析文档结束******* 上面就是使用SAX解析一个XML文档的基本过程,但是MyContentHandler只是处理了解析过程中和文档内容相关的事件,如果在解析过程中出现了错误那我们需要实现ErrorHandler接口来处理。如果不注册一个错误处理器来处理的话,那么错误事件将不会被报告,而且解析器会出现不可预知的行为。在解析过程中产生的错误被分成了3类,它们分别是warning,error,以及fatalerror,也就是说在ErrorHandler中有这么三个相应的方法来处理这些错误事件。下面是对这三个错误处理方法的介绍:
下面是实现了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:saxclasses>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的应用程序。一般步骤如下:
使用DefaultHandler
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,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 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 length ) throws SAXException { //将元素内容累加到StringBuffer中 buf.append(chars,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中有两个方法:
我们不需要自己实现org.xml.sax.XMLFilter接口,在SAX API 中提供了一个org.xml.sax.helper.XMLFilterImpl类,它不仅实现了org.xml.sax.XMLFilter接口而且还实现了其他四个核心处理器接口,我们只需要继承它即可完成我们的过滤器。刚开始使用XMLFilterImpl比较容易让人迷惑,你只需要记住:
下面让我们结合已有的例子来演示过滤器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,Attributes attributes ) throws SAXException { currentElement = localName; if ( !localName.equals("技术书籍") ) { super.startElement(namespaceURI,localName,fullName,attributes); } } /** * 过滤掉元素<技术书籍>的结束事件 **/ public void endElement(String namespaceURI,String fullName) throws SAXException { if ( !localName.equals("技术书籍") ) { super.endElement(namespaceURI,fullName); } } /** * 过滤掉元素<技术书籍>中的内容 **/ public void characters(char[] buffer,int length) throws SAXException { if ( !currentElement.equals("技术书籍") ) { super.characters( buffer,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:saxclasses>java com.javausr.saxexample.MySAXApp d:book.xml *******开始解析文档******* 元素: [我的书架] 开始解析! 元素: [图书] 开始解析! 元素: [书名] 开始解析! 内容是: JAVA 2编程详解 元素: [书名] 解析结束! 元素: [价格] 开始解析! 属性名称:货币单位 属性值:人民币 内容是: 150 元素: [价格] 解析结束! 元素: [购买日期] 开始解析! 内容是: 2000,24 元素: [购买日期] 解析结束! 元素: [图书] 解析结束! 前缀映射: book 开始! 它的URI是:http://javausr.com 元素: [book:文学书籍] 开始解析! 元素: [book:文学书籍] 解析结束! 前缀映射: book 结束! 元素: [历史书籍] 开始解析! 元素: [历史书籍] 解析结束! 元素: [我的书架] 解析结束! *******解析文档结束******* 一些值得注意的问题首先是有关元素内容的问题,在SAX API定义中元素内容可以在一次事件(由characters()方法处理)中返回,也可以在多次事件中返回,这样我们就应该考虑不能一次得到所有内容数据的情况。一般的解决办法是定义一个StringBuffer由它来保存内容数据,在元素结束或者新元素开始的时候清空这个StringBuffer从而可以保存新的内容数据。请参考上面的相应的源代码。 还有在SAX API中特意提到从characters(char[] ch,int length)方法中提取数据时一定不要从返回的字符数组范围之外读取,这一点我们也要切记。 另一个值得注意的问题是,在startElement()方法中返回的Attributes属性列表中的属性顺序并没有被特意规定,在不同的SAX实现中也各不相同。所以我们在编写程序时不要把属性顺序想成一定的。 SAX与DOM的比较通过上面的介绍我想大家对SAX已经有了一个基本的了解。每一个进行XML开发的编程人员都知道DOM,那为什么在有了DOM这个功能强大的文档对象模型之后,我们还需要SAX?这就要从它们根本不同的实现方法上来分析。DOM解析器是通过将XML文档解析成树状模型并将其放入内存来完成解析工作的,而后对文档的操作都是在这个树状模型上完成的。这个在内存中的文档树将是文档实际大小的几倍。这样做的好处是结构清除、操作方便,而带来的麻烦就是极其耗费系统资源。而SAX正好克服了DOM的缺点。SAX解析器的处理过程是通读整个文档,根据文档内容产生事件,而把对这些事件的处理交由事件处理器处理。SAX不需要在内存中保存整个文档,它对系统资源的节省是显而易见的。这样在一些需要处理大型XML文档和性能要求比较高的场合就要用SAX了。 下面的表格列出了SAX和DOM在一些方面的对照:
通过对SAX和DOM的分析,它们各有自己的不同应用领域: SAX适于处理下面的问题:
DOM适于处理下面的问题:
对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 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |