本文由大关收集整理所得,不保证内容的正确性,转载请标明出处,谢谢!
上一次,我们讲述了使用Dom方式解析XML,并通过修改Dom树结构,最终改变XML文档内容。本次主要讲解使用SAX(Simple API for XML)方式和StAX(Stream API for XML)方法对XML文档进行分析和生成操作。
Dom方式解析XML先要在内存中创建Dom树,然后通过遍历或修改树节点,来对XML文档中的内容进行处理,如果要将Dom树保存成XML文档需要借助Transformer,具体内容请参考我的上篇文档。Dom方式的优点在于,能够将整个文档读入到内存中,可以在任意时刻修改Dom树的任意节点的内容,并且可以使用XPath接口辅助查询和处理。但是Dom最大的缺陷是在内存中创建Dom树,如果将一个很大的XML文档读入内存,这不仅是个很耗时的操作,同时也可能造成内存空间不足,进一步恶化了XML处理速度。
SAX方式和StAX方式与Dom方式不同,它们使用的是基于流机制的处理方式。在处理XML文档时,SAX和StAX并不会将整个文档读入内存,而是,从头开始,读取一段,处理一段。SAX使用的是观察者模式(参看GOF设计模式)而StAX使用一种被称为拉(pull)的模式,稍后我们会简要探讨这两种模式的区别。
由于在处理大规模的XML文档时,DOM显得很无力,所以下面的内容将要围绕流处理机制分析XML进行探讨。(注意DOM在形成DOM树的过程中,使用SAX进行分析)。
1. SAX分析XML文档
与DOM的方式相同,很多厂商提供了对SAX的实现,我们可以通过使用SAXParserFactory来设置具体使用哪个实现的版本。之后可以通过SAX工厂实例创建一个SAXParser分析器(一般系统中都提供了默认的SAX解析器,如果没有特别必要,在不提供参数的情况下,即可使用系统的默认SAX分析器(一般是Sun提供的))。调用SAXParser的parser方法,即可实现对XML文档的分析。注意parser有两个参数,一个是要分析的XML文档的路径,另外一个是一个访问者(观察者模式)。通过对DefaultHandler中的一些方法进行重写,到达分析XML文档的目的。Demo-1给出了一个使用SAX分析XML文档的例子,在这个例子中,我们希望获取XML文档中出现的所有学号。
示例xml文档student.xml:
Lord
S09080408
New York
13865445633
Math
90
Chinese
88
Mary
S09080707
Seattle
0456-5462148
Math
100
实例程序:
package com.upc.upcgrid.guan.SAXTest;
import java.io.File;
import java.io.IOException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public class SAXParserXMLDemo {
public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory spf = SAXParserFactory.newInstance();//创建SAX解析器工厂
spf.setNamespaceAware(true);//设置名称空间属性
SAXParser sp = spf.newSAXParser();//创建解析器
//开始解析
sp.parse(new File(System.getProperty("user.dir")+File.separator+"XMLFolder"+File.separator+"student.xml"),
new DefaultHandler(){
boolean isStudentId = false;//是否是学号信息
@Override
public void startElement(String uri, String localName,
String qName, Attributes attributes) throws SAXException {
super.startElement(uri, localName, qName, attributes);
if("student_id".equals(localName))//如果接收到的元素是student_id
{
isStudentId = true;//标记这个是学号信息,在接收字符信息时需要输出学号
System.out.print(localName+" : ");//输出元素名称
}
}
@Override
public void characters(char[] ch, int start, int length)//接收到字符信息
throws SAXException {
super.characters(ch, start, length);
if(isStudentId)//如果当前信息是学号
{
System.out.println(new String(ch,start,length));//输出这个学号
}
}
@Override
public void endElement(String uri, String localName, String qName)//元素终止
throws SAXException {
super.endElement(uri, localName, qName);
isStudentId = false;//标记当前元素不是学号
}
});
}
}
输出结果:
student_id : S09080408
student_id : S09080707
SAX分析过程是这样的,SAX先从XML文档开始部分进行处理,当其发现第一个元素的时候(这个例子中第一个元素是students,注意这不是第一个事件,我们仅分析关心的事件),触发一个startElement事件,并将元素的名字和所有属性都传入处理函数,而处理函数正是我们提供的DefaultHandler被重写的startElement,因此就由这个函数进行处理。所以这是一个观察者模式。SAXParser是一个观察者,他发现了访问者(DefaultHandler)关心的事件后,就将这个事件交给这个访问者处理,整个流程很像C语言中的回调函数。我们在一个元素的开始时,判断这个元素是不是student_id,如果这个元素是,那么接下来必然读到元素的文本值(因为我们知道student_id元素内仅有文本节点),会触发characters事件,然后,我们在characters中输出文本内容,最后在student_id标签结束的时候,修改状态。在DefaultHandler中还有其他的一些事件,比如文档开始,文档结束,前缀开始,前缀结束等内容,不过一般我们只关心元素开始、元素终止和字符三个事件,此外,需要注意的是在元素开始时,需要提供对这个元素的所有属性的处理(因为属性在元素开始标签内部)。关于Attributes和DefaultHandler的其他操作可以参考javaAPI文档,这里不在累述。
1.2 StAX
StAX是在java1.6之后提供的又一种XML分析方式,StAX与SAX都是基于流模式对XML文档进行分析的,但是StAX并不是使用观察者模式,而是使用一种被称为pull模式。与SAX相似,也需要先创建一个XMLInputFactory工厂,之后通过工厂创建分析器。Demo-2中给出了一个用StAX实现的与Demo-1相同功能的分析过程。
Demo-2 StAX分析示例:
package com.upc.upcgrid.guan.SAXTest;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
public class StAXParserXMLDemo {
public static void main(String[] args) throws FileNotFoundException, XMLStreamException {
//创建InputStream
InputStream in = new FileInputStream(System.getProperty("user.dir")+File.separator+"XMLFolder"+File.separator+"student.xml");
XMLInputFactory xif = XMLInputFactory.newInstance();//创建StAX分析工厂
XMLStreamReader reader = xif.createXMLStreamReader(in);//创建分析器
while(reader.hasNext())//迭代
{
int event = reader.next();//读取下一个事件
if(event == XMLStreamReader.START_ELEMENT)//如果这个事件是元素开始
{
if("student_id".equals(reader.getLocalName()))//判断元素是不是student_id
{//如果是student_id则输出元素的文本内容
System.out.print(reader.getLocalName()+" : ");
System.out.println(reader.getElementText());
}
}
}
}
}
输出结果:
student_id : S09080408
student_id : S09080707
在Demo-2与Demo-1的对比中可以看出,StAX并不是使用观察者模式,StAX这种模式被称为是pull模式。在观察者模式中,分析器是主体,在整个分析过程中,分析器分析了一个事件,之后调用使用者提供的处理方法,处理产生的事件,而在pull模式中,使用者是主体,使用者调用一次分析器的next方法,分析器就产生一个新的事件,之后使用者分析这个事件,然后对这个事件进行特殊的响应。从设计角度来说,使用pull模式更令人容易接受些,但是两种设计本身并没有优劣之分,不过似乎StAX使用起来更便捷,对于普通使用者也更容易理解些,并且StAX是java1.6之后提供的,新的东西必然有其长处才会被推广。
1.3 使用StAX写入XML
StAX提供了用来写入的XMLStreamWriter类,可以使用这个类生成XML文档。这个类比较简单,这里仅提供代码,可以参考代码进行理解,更多内容请参考java API文档。
package com.upc.upcgrid.guan.SAXTest;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
public class StAXGenerateXMLDemo {
public static void writeXML(XMLStreamWriter writer) throws XMLStreamException
{
writer.writeStartDocument("UTF-8", "1.0");//开始写文档
writer.writeStartElement("", "students");//写出一些内容
writer.writeStartElement("student");
writer.writeStartElement("student_id");
writer.writeCharacters("S09080709");
writer.writeEndElement();
writer.writeStartElement("student_name");
writer.writeCharacters("mary");
writer.writeEndElement();
writer.writeEndElement();
writer.writeStartElement("student");
writer.writeStartElement("student_id");
writer.writeCharacters("S0900121");
writer.writeEndElement();
writer.writeStartElement("student_name");
writer.writeCharacters("Lord");
writer.writeEndElement();
writer.writeEndElement();
writer.writeEndElement();
writer.writeEndDocument();//文档写出结束
writer.flush();//刷新缓冲
}
public static void main(String[] args) throws XMLStreamException {
XMLOutputFactory xof = XMLOutputFactory.newInstance();//创建输出工厂
XMLStreamWriter writer = xof.createXMLStreamWriter(System.out,"UTF-8");//创建XML写出流
writer.setPrefix("", "");//没提供特殊的前缀
writeXML(writer);//执行写入一些XML信息
writer.close();//关闭写出流
}
}
输出结果:
<?xml version="1.0" encoding="UTF-8"?>S09080709maryS0900121Lord
注意输出的内容无法换行。更多的内容可查看java API文档,关于XMLStreamWriter的用法。
1.4 使用SAX生成XML文档
SAX没有提供写XML文档的功能,因此,我们需要像DOM一样借助Transformer将SAX内容写到XML文档中。这看起来比较复杂,Demo-4给出了一个与Demo-3功能相似,使用SAX实现的写XML文档的功能。
Demo-4 使用SAX生成XML文档
package com.upc.upcgrid.guan.SAXTest;
import java.io.IOException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import org.xml.sax.ContentHandler;
import org.xml.sax.DTDHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;
public class SAXGenerateXMLDemo {
static class SAXReaderImpl implements XMLReader{
@Override
public boolean getFeature(String name)
throws SAXNotRecognizedException, SAXNotSupportedException{
return false;
}
@Override
public void setFeature(String name, boolean value)
throws SAXNotRecognizedException, SAXNotSupportedException{
}
@Override
public Object getProperty(String name)
throws SAXNotRecognizedException, SAXNotSupportedException{
return null;
}
@Override
public void setProperty(String name, Object value)
throws SAXNotRecognizedException, SAXNotSupportedException{
}
@Override
public void setEntityResolver(EntityResolver resolver) {
}
@Override
public EntityResolver getEntityResolver() {
return null;
}
@Override
public void setDTDHandler(DTDHandler handler) {
}
@Override
public DTDHandler getDTDHandler() {
return null;
}
@Override
public void setContentHandler(ContentHandler handler) {
this.handler = handler;
}
@Override
public ContentHandler getContentHandler() {
return null;
}
@Override
public void setErrorHandler(ErrorHandler handler) {
}
@Override
public ErrorHandler getErrorHandler() {
return null;
}
@Override
public void parse(InputSource input) throws IOException, SAXException {
if(handler == null)
throw new SAXException("content handler is null");
handler.startDocument();
handler.startElement("", "students", "students", null);
handler.startElement("", "student", "student", null);
handler.startElement("", "student_name", "student_name", null);
handler.characters("Mary".toCharArray(), 0, "Mary".length());
handler.endElement("", "student_name", "student_name");
handler.startElement("", "student_id", "student_id", null);
handler.characters("S09070934".toCharArray(), 0, "S09070934".length());
handler.endElement("", "student_id", "student_id");
handler.endElement("", "student", "student");
handler.startElement("", "student", "student", null);
handler.startElement("", "student_name", "student_name", null);
handler.characters("Lord".toCharArray(), 0, "Mary".length());
handler.endElement("", "student_name", "student_name");
handler.startElement("", "student_id", "student_id", null);
handler.characters("S09070808".toCharArray(), 0, "S09070808".length());
handler.endElement("", "student_id", "student_id");
handler.endElement("", "student", "student");
handler.endElement("", "students", "students");
handler.endDocument();
}
@Override
public void parse(String systemId) throws IOException, SAXException {
}
private ContentHandler handler;
}
public static void main(String[] args) throws TransformerException {
TransformerFactory tff = TransformerFactory.newInstance();
Transformer tf = tff.newTransformer();
tf.setOutputProperty(OutputKeys.INDENT, "yes");//设置转换的属性
tf.setOutputProperty(OutputKeys.METHOD, "xml");
tf.transform(new SAXSource(new SAXReaderImpl(),null), new StreamResult(System.out));
}
}
结果:
Mary
S09070934
Lord
S09070808
这个实现看起来很复杂,首先是Transformer类的transform方法需要一个Source和一个Result,很明显这个Source就是我们提供的SAXSource,而SAXSource需要两个参数,第一个参数是一个XMLReader类型的实例,第二个参数是一个文件流。正常情况是XMLReader读取文件中的数据,之后调用SAXSource的ContentHandler对读取的事件进行处理。这是一种观察者模式。其中XMLReader是一个观察者,它负责处理传入的文本流,当XMLReader发现相应的事件后,调用访问者(SAXSource中的ContentHandler)的方法,完成处理过程。这里我们不需要传入流,因为我们仅是生成文档而已。XMLReader直接触发相应的事件,而SAXSource将相应的事件传递给Transformer,Transformer将事件传给ResultSource,最后输出内容。整个过程就是一个观察者模式,其中观察者是XMLReader,而访问者形成一条链,分别是ContentHandler、SAXSource、Transformer、ResultSource,这种分析模式是级联式的,很有过滤器风格(参考REST(表述状态转移))。
1.5 总结
SAX和StAX使用流模式对XML文档进行处理。这解决了DOM中无法解决的处理大规模XML文档问题。但是从上面的例子可以看出SAX和StAX这种模式在解析XML文档过程中无法修改XML文档的内容,如果要对XML中的内容进行修改,我们很可能需要解析文档,对没必要修改的内容直接拷贝到另外一个文档,而对于需要修改的内容进行特殊处理后在输出到新文档中,之后删除原来文档,将新文档改名为原来文档名字。此外基于流机制访问过的内容除非从头重新开始访问,否则无法返回访问过的节点(也因此无法使用XPath语句)。
StAX提供了专用的输出类,而DOM和SAX需要借助Transformer类(Transformer类设计不仅是输出XML,更多的是对XML文档进行转换)。StAX设计成pull模式,利于初学者理解和使用。
到目前为止,基于jaxp的三种XML文档分析模式我们已经介绍结束,但是,有一点需要提及,对于XML的验证我们并未提及,因为我工作的原因,所以对验证并不关注(也是由于验证对Schema的支持比较一般,而我又很少使用DTD),如果对XML验证感兴趣,可以参考《java core volume II》中关于XML章节的内容。
关于XML的下一个主题就是XOM就是XML与对象映射问题。在下面几篇文章中,准备对这部分内容进行探讨。