使用 StAX 解析 XML

2 篇文章 0 订阅

1、前言


本文大部分摘抄于IBM developerworks(主要是理论),详下面三篇文章,摘抄主要是为了使自己理解更深一点儿,仅当作笔记而已...也是为了以后再次使用时有个参考!摘抄并不全面,原文内容要丰富地多,详见原文。

参考文章:

使用 StAX 解析 XML,第 1 部分: Streaming API for XML (StAX) 简介:http://www.ibm.com/developerworks/cn/xml/x-stax1.html

使用 StAX 解析 XML,第 2 部分: 拉式解析和事件:http://www.ibm.com/developerworks/cn/xml/x-stax2.html

使用 StAX 解析 XML,第 3 部分: 使用定制事件和编写 XML:http://www.ibm.com/developerworks/cn/xml/x-stax3.html


2、概述


开始的时候,Java API for XML processiong(JAXP)提供了两种XML处理方式:文档对象模型(DOM)和Simple API for XML(SAX),JSR-173提出了一种面向流的新方法:Streaming API for XML(StAX)。其最终版本于 2004 年 3 月发布,并成为了 JAXP 1.4(包含在 Java 6 中)的一部分。

顾名思义,StAX把重点放在流上,StAX使应用程序能够把 XML 作为一个事件流来处理;其实SAX方式也是基于事件流的XML处理方法,但二者不同之处在于,SAX是基于观察者模式,我们需要提供事件处理程序并注册到解析器,解析器在指定事件发生时回调我们提供的程序;而StAX允许我们的程序把事件逐个”拉“出来,这样StAX就有更大的灵活性,对于我们不感兴趣的事件就没有必要将其”拉“出来处理。

StAX提供了两套API用来处理XML,分别提供了不同程度的抽象。基于指针的 API 把 XML 作为一个标记(或事件)流来处理;应用程序可以检查解析器的状态,获得解析的上一个标记的信息,然后再处理下一个标记,依此类推。这是一种低层 API,尽管效率高,但是没有提供底层 XML 结构的抽象。基于迭代器的 API 把 XML 作为一系列事件对象来处理。应用程序只需要确定解析事件的类型,将其转换成对应的具体类型,然后利用其方法获得属于该事件的信息。


3、基本原理

不管我们使用哪种API,首先要做的都是获取解析器工厂实例,然后根据需要配置该实例可设置的定制或者预先定义好的属性(其名称在类 XMLInputFactory 中定义),最后创建解析器,如下:

XMLInputFactory inputFactory = XMLInputFactory.newFactory();
XMLEventReader eventReader = inputFactory.createXMLEventReader(new FileInputStream("E:\\PDFPATH_6.xml"));

这个例子创建的是基于事件对象的解析器XMLEventReader,如果需要使用基于指针的 API,可以调用某个 createXMLStreamReader 方法获得一个 XMLStreamReader;基于事件迭代器的 API比基于指针的API 具有更多的面向对象特征,因为当前的解析器状态反映在事件对象中,所以在处理事件的时候不需要访问解析器,所需要的信息都被封装在获得的事件对象中了。


4、基于指针的 API


基于指针的 API通过在 XML 标记流中移动逻辑指针来处理 XML。基于指针的解析器实质上是一个状态机,在事件的驱动下从一个状态转移到另一个状态。这里的触发事件是随着应用程序使用适当的方法推动解析器在标记流中前进而解析出来的 XML 标记。在每个状态,都可使用一组方法获得上一个事件的信息。一般来说,并非每个状态下都能使用所有的方法。

基于指针的 API 是解析 XML 的低层方法。使用这种方法,应用程序沿着 XML 标记流移动指针,在每一步中检查解析器的状态来了解解析内容的更多信息。这种方法效率很高,特别适用于资源受限的环境。

使用基于指针的API首先必须通过调用上面创建的XMLInputFactory 实例的 createXMLStreamReader 方法从 XMLInputFactory 得到 XMLStreamReader。该方法有多个重载版本,支持不同类型的输入。


4.1、XMLStreamReader 接口


XMLStreamReader 接口基本上定义了基于指针的 API(标记常量在其超类 XMLStreamConstants 接口中定义)。之所以称为基于指针,是因为读取器就像是底层标记流上的指针。应用程序可以沿着标记流向前推进指针并分析当前指针所在位置的标记。

XMLStreamReader 提供了多种方法导航标记流。为了确定当前指针所指向的标记(或事件)的类型,应用程序可以调用 getEventType()。该方法返回接口 XMLStreamConstants 中定义的一个标记常量。移动到下一个标记,应用程序可以调用 next()。该方法也返回解析的标记的类型,如果接着调用 getEventType() 则返回的值相同。只有当方法 hasNext() 返回 true 时(就是说还有其他标记需要解析)才能调用该方法(以及其他移动读取器的方法)。

示例代码:

// create an XMLStreamReader
XMLStreamReader r = ...;
int event = r.getEventType();
while (true) {
	switch (event) {
	case XMLStreamConstants.START_DOCUMENT:
		// do something
		break;
	case XMLStreamConstants.START_ELEMENT:
		// do something
		break;
	// add cases for each event of interest
	}

	if (!r.hasNext())
		break;
 
	event = r.next();
}

还与其他几种方法可以移动 reader。 nextTag() 方法将跳过所有的空白、注释或处理指令,直到遇到 START_ELEMENT 或 END_ELEMENT。该方法在解析只含元素的内容时很有用,如果在发现标记之前遇到非空白文本(不包括注释或处理指令),就会抛出异常。getElementText() 方法返回元素的开始和关闭标签(即 START_ELEMENT 和 END_ELEMENT)之间的所有文本内容。如果遇到嵌套的元素就会抛出异常。

请注意,这里的 “标记” 和 “事件” 可以互换使用。虽然基于指针的 API 的文档说的是事件,但把输入源看成标记流很方便。而且不容易造成混乱,因为还有一整套基于事件的 API(那里的事件是真正的对象)。不过,XMLStreamReader 的事件本质上并非都是标记。比方说,START_DOCUMENT 和 END_DOCUMENT 事件不需要对应的标记。前一个事件是解析开始之前发生,后者则在没有更多解析工作要做的时候发生(比如解析完成最后一个元素的关闭标签之后,读取器处于 END_ELEMENT 状态,但是如果没有发现更多的标记需要解析,读取器就会切换到 END_DOCUMENT 状态)。


4.2、处理XML文档


创建之后,XMLStreamReader 将从 START_DOCUMENT 状态开始(即 getEventType() 返回 START_DOCUMENT)。处理标记的时候应考虑到这一点。和迭代器不同是不需要先移动指针(使用 next())进入合法的状态。同样当读取器转换到最终状态 END_DOCUMENT 之后,应用程序也不应再移动它。在这种状态下,hasNext() 方法将返回 false。

START_DOCUMENT 事件提供了获取关于文档本身信息的方法,如 getEncoding()、getVersion() 和 isStandalone()。应用程序也可调用 getProperty(String) 获得命名属性的值,不过一些属性仅在特定状态做了定义(比方说,如果当前事件是 DTD,则属性 javax.xml.stream.notations 和 javax.xml.stream.entities 分别返回所有的符号和实体声明)。

在 START_ELEMENT 和 END_ELEMENT 事件中,可以使用和元素名称以及名称空间有关的方法(如 getName()、getLocalName()、getPrefix() 和 getNamespaceXXX()),在 START_ELEMENT 事件中还可使用与属性有关的方法(getAttributeXXX())。

ATTRIBUTE 和 NAMESPACE 也被识别为独立的事件,虽然在解析 典型的 XML 文档时不会用到。但是当 ATTRIBUTE 或 NAMESPACE 节点作为 XPath 查询结果返回时可以使用。

基于文本的事件(如 CHARACTERS、CDATA、COMMENT 和 SPACE),可使用各种 getTextXXX() 方法取得文本。可以分别使用 getPITarget() 和 getPIData() 检索 PROCESSING_INSTRUCTION 的目标和数据。ENTITY_REFERENCE 和 DTD 也支持 getText(),ENTITY_REFERENCE 还支持 getLocalName()。

解析完成后,应用程序关闭读取器并释放解析过程中获得的资源。请注意这样并没有关闭底层的输入源。


4.3、StreamFilter筛选器


通过调用 XMLInputFactory 的带有基本读取器的 createFilteredReader 方法和一个应用程序定义的筛选器(即实现 StreamFilter 的类实例),可以创建筛选过的 XMLStreamReader。导航筛选过的读取器时,读取器每次移动到下一个标记之前都会询问筛选器。如果筛选器认可了当前事件,就将其公开给筛选过的读取器。否则跳过这个标记并检查下一个,依此类推。这种方法可以让开发人员创建一个仅处理解析内容子集的基于指针的 XML 处理程序,并与针对不同的扩展的内容模型的筛选器结合使用。


5、基于事件对象的API


这种 API 以事件对象为中心。和基于指针的 API 一样也是一种基于”拉“的 XML 解析方法:应用程序使用提供的方法从解析器中拉出每个事件,按照需要处理该事件,依此类推,直到流解析完成(或者应用程序决定停止解析)。


5.1、XMLEventReader 接口简介


事件迭代器 API 的主要接口是 XMLEventReader。与 XMLStreamReader 相比它的方法要少很多。这是因为 XMLEventReader 用于迭代事件对象流(事实上 XMLEventReader 扩展了 java.util.Iterator)。关于解析事件的所有信息都封装在事件对象中而不是读取器中。

要使用基于事件迭代器的 API,应用程序首先必须从 XMLInputFactory 获得 XMLEventReader 的实例,与createXMLStreamReader 方法一样,createXMLEventReader也有多个重载版本,支持多种创建XMLEventReader 的输入源。需要注意的是createXMLEventReader方法还可以接受 XMLStreamReader 为参数创建 XMLEventReader。这种用法可以很好地说明基于事件迭代器的 API 是堆叠于基于指针的 API 之上的。事实上,实现通常要使用其他输入源创建一个 XMLStreamReader,然后再用它创建 XMLEventReader。


5.2、使用 XMLEventReader


创建 XMLEventReader 之后,应用程序可用它迭代表示底层 XML 流的 InfoSet 片段的事件。由于接口 XMLEventReader 扩展了 java.util.Iterator,可以使用标准迭代器方法如 hasNext() 和 next()。但是请注意,不支持 remove() 方法,如果调用该方法会抛出异常。

XMLEventReader 还提供了一些方便的方法来简化 XML 处理:

  • nextEvent()  本质上是一种等同于 Iterator 的 next() 方法的强类型方法,它返回一个 XMLEvent,它是所有事件对象的基本接口。
  • nextTag()  能够跳过所有无关紧要的空白直到下一个开始或结束标记。因此返回值将是 StartElement 或 EndElement 事件。该方法在处理纯元素(即文档类型声明 DTD 中声明为 EMPTY 的元素)内容时尤其有用。
  • getElementText()  可以访问纯文本元素的文本内容(开始标签到结束标签之间)。从 StartElement 作为下一个预期事件开始,该方法在遇到 EndElement 之前将所有字符连接起来并返回结果字符串。
  • peek()  可以得到迭代器将返回的下一个事件(如果有)但是不移动迭代器。

具体API请参阅JDK的API文档。


5.3、XMLEvent 的层次结构


XMLEventReader 在解析过程的每一步之后通过事件对象和应用程序通信自己的状态。整个 API 中使用的事件对象的标准类型定义在 javax.xml.stream.events 包中。接口 XMLEvent 表示类型层次结构的根,所有类型的事件必须扩展该接口。表示各种指针层事件类型(在基于指针的 API 中)定义在接口 XMLStreamConstants 中。不过,也可使用定制的接口(只要扩展了 XMLEvent)。

从解析器中检索到事件之后,应用程序通常需要将其向下转换成 XMLEvent 的子类型以便访问该特定类型的信息。XMLEvent 提供了 getEventType() 方法返回 XMLStreamConstants 中定义的事件常量,可基于该信息对事件进行向下类型转换;除此之外XMLEvent 还为此提供了布尔查询方法,比如isStartElement()返回true说明这是个StartElement ;asStartElement()、asEndElement() 和 asCharacters() 分别将相应的事件转换成 StartElement、EndElement 和 Characters。


5.4、EventFilter 筛选事件


StAX允许我们创建专门的事件读取器,即XMLEventReader 只能读取到我们指定的事件类型对象,这就需要用到EventFilter接口。只需要对 XMLInputFactory 实例调用 createXMLEventReader(XMLEventReader, EventFilter) 方法,并传递基本事件读取器和接受/拒绝从基本读取器获得的事件的简单筛选器。

比如:

eventReader = inputFactory.createFilteredReader(eventReader, new EventFilter() {
						public boolean accept(XMLEvent event) {
							int type = event.getEventType();
							return type == XMLStreamConstants.START_ELEMENT
									|| type == XMLStreamConstants.END_ELEMENT
									|| type == XMLStreamConstants.CHARACTERS;
						}
					});

如上代码我们得到的XMLEventReader就只能接受元素开始、元素结束、字符三种事件。


6、具体应用


本次关注StAX的原因是项目中需要解析大XML文件,并将内容入库。XML文件超过100M,记录超过100万,所以用传统的DOM解析不大可行,也不能一次性将文件解析成Java对象,因为那样会在内存中出现上百万的Java对象,会吃掉很大一部分内容,所以只能采用边解析边入库的方式。

下面将本次的主要应用代码贴一下,用到的是基于事件对象的API。

XML文件大体结构:

<?xml version="1.0" encoding="gbk"?>
<TRS>
	<REC>
		<PDFPATH>\\1989YY05\R15\92257X\013\002\99130.pdf</PDFPATH><br/><br/>
		<UI>1989017091</UI><br/><br/>
		<ZHONGHUA>0</ZHONGHUA><br/><br/>
	</REC>
	<REC>
		<PDFPATH>\\1989YY02\R4\94093X\004\001\184114.pdf</PDFPATH><br/><br/>
		<UI>1989019986</UI><br/><br/>
		<ZHONGHUA>0</ZHONGHUA><br/><br/>
	</REC>
	// ...很多条
</TRS>

Java解析代码(由于并没有使用,所以比较粗糙,没有再优化..):

package com.ninemax.admin.action;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Stack;

import javax.xml.stream.EventFilter;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;

import org.apache.struts2.convention.annotation.Namespace;
import org.springframework.beans.factory.annotation.Autowired;

import com.ninemax.action.base.BaseActionSupport;
import com.ninemax.entity.Domain;
import com.ninemax.service.admin.IDomainService;

@Namespace("/")
public class LiteralUrlAction extends BaseActionSupport {
	public static final String ENTITY_TAG = "REC";		// 实体标记
	public static final String PATH_TAG = "PDFPATH";	// 属性标记
	public static final String UI_TAG = "UI";			// 属性标记
	public static final String FLAG_TAG = "ZHONGHUA";	// 属性标记
	
	@Autowired
	private IDomainService domainService;
	
	public String test() {
		XMLInputFactory inputFactory = XMLInputFactory.newFactory();
		Stack<Domain> stack = new Stack<Domain>();
		try {
			long s = System.currentTimeMillis();
			int i = 0;
			XMLEventReader eventReader = inputFactory.createXMLEventReader(new FileInputStream("E:\\PDFPATH_6.xml"));
			eventReader = inputFactory.createFilteredReader(eventReader, new EventFilter() {
						public boolean accept(XMLEvent event) {
							int type = event.getEventType();
							return type == XMLStreamConstants.START_ELEMENT
									|| type == XMLStreamConstants.END_ELEMENT
									|| type == XMLStreamConstants.CHARACTERS;
						}
					});
			while(eventReader.hasNext()) {
				XMLEvent event = eventReader.nextEvent();
				if (event.isStartElement()) {
					String tag = event.asStartElement().getName().getLocalPart();
					if (ENTITY_TAG.equalsIgnoreCase(tag)) {		// 如果是实体元素, 则创建一个新元素并压入栈顶
						Domain domain = new Domain();
						stack.push(domain);
					} else if (UI_TAG.equalsIgnoreCase(tag)) {		// 属性, 设置到当前实体中
						String ui = eventReader.nextEvent().asCharacters().getData();
						stack.lastElement().setUi(ui);
					} else if (PATH_TAG.equalsIgnoreCase(tag)) {	// 属性, 设置到当前实体中
						String path = eventReader.nextEvent().asCharacters().getData();
						stack.lastElement().setPath(path);
					} else if (FLAG_TAG.equalsIgnoreCase(tag)) {	// 属性, 设置到当前实体中
						String flag = eventReader.nextEvent().asCharacters().getData();
						stack.lastElement().setFlag(flag);
					}
				} else if (event.isEndElement()) {		// 元素结束事件 
					String tag = event.asEndElement().getName().getLocalPart();
					if (ENTITY_TAG.equalsIgnoreCase(tag) && stack.size() == 10000) {
						// 如果是实体元素结束事件并且栈内已达10000个实例, 进行一次入库
						domainService.saveEntity(stack);
						i += stack.size();
						stack.clear();	// 清空栈
					}
				}
			}
			if (stack.size() > 0) {		// 解析完毕后, 将剩下的入库
				domainService.saveEntity(stack);
				i += stack.size();
			}
			eventReader.close();
			System.out.println("共花费" + (System.currentTimeMillis() - s) + "毫秒时间.共" + i + "条记录");
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (XMLStreamException e) {
			e.printStackTrace();
		}
		return null;
	}
}

据测,性能还可以...


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值