本周的编程提高群大作业中涉及到了XML的解析问题。
我在自学JavaScript的过程中,已经使用了N次JSON.parse()这样的函数了。但以前只是这样轻轻松松地调用一下,未去细细思考。其实无论是JSON还是XML还是其他的结构化的文档,其解析的实现方法都是很值得讨论的话题。
在动手开始码本次作业的代码之前,我还在设想自己能不能实现一个自己的XML Parser,但考虑了一下代码量以及需要实现一个完备的Parser所需要考虑的复杂的情形,还是放弃了,转而去寻找合适的库来完成。
不过在找库的时候我也自己思考了以下问题:
- XML是层级结构,那么要在解析完一个XML后存储起来,必然会使用到树。
- XML解析时的读取和判断又是怎么样的?以作业中使用的struts.xml中的一行作为例子:
<result name="success">/jsp/homepage.jsp</result>
那么,它的解析过程是不是这样
- 遇到一个
<
,解析器就会判断出自己开始“读一个元素” - 遇到一个
>
,解析器就会判断出自己已经完成了这个元素的读取 - 遇到第一个空格,解析器就会认为自己已经取出了元素的名字(也就是例子中的"result")
- 遇到一个
=
,解析器就会认为自己之前读的一串是一个key,接下来在碰到空格之前读到的东西则是它的value - 遇到第二个以及以后的空格,解析器就会认为自己已经完成了这个key/value的读取
- (事实上以上分析并不严谨= =)
上面的第一点思考,说明了XML本质上也是一种“树”;第二点思考,说明了XML的解析的实现方式,可能就像上面所述的那样,让XML Parser一个字符一个字符地读取,在碰到特定的字符时,就进入或退出一种状态。这说明了解析的过程,事实上是可以用事件驱动这一软件设计的常见模式来说明的。
后来我带着自己的这个似对非对的结论,去找Java中常用的XML解析库的时候,很快就找到了XML解析库的两大分类:
- DOM式
- 使用方式是将XML全部读入,并形成一个对象以供我们访问其中的各个节点上的属性
- 由此可知DOM式解析的特点是
- 可读可写,解析完成后的访问简单,适合对一份XML文件的多种多样的读取和写入操作
- 因为需要将XML文件全部读取,假设XML文件很大时,会消耗大量内存
- SAX(Simple API for XML)式
- 使用方式是,解析器会在读取XML产生的事件上暴露接口,我们可以通过这些接口拿到事件发生时所获取到的节点属性
- SAX解析器会将XML文件内容当做流来处理,一边读取一边判断。如果判断已经读取到了需要的节点属性,则不再继续读取
- SAX解析器不会记录整个XML结构,只会通过事件的回调向外提供XML的相关数据,记录需要将其赋给我们自己定义的变量
- 由此可知SAX式解析的特点是
- 对内存消耗少
- 只可读不可写,适合简单的读取XML中特定属性的任务。但因为SAX解析器不会记录层级关系,使得通过层级关系来查找特定属性比较复杂
关于DOM式和SAX式解析的比较,在SAX的官网上有一篇文章讲得不错,请参考: Events vs. Trees - SAX Project。
另外,除了DOM和SAX式以外,Java中还有以下两种模式来处理�XML。分别是
- StAx式,使用迭代器式的hasNext(), next()接口,完成XML的读和写
- JAXB,我们需要提供Java对象的实例,JAXB可以将其编译成对应的XML或JSON,也可以进行反向操作
这里不再赘述
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
public class SAXParserDemo {
public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
//在回调函数外面定义好自定义变量用来保存解析结果
ArrayList<String> tags = new ArrayList<>();
ArrayList<String> innerTexts = new ArrayList<>();
ArrayList<HashMap<String, String>> attributesList = new ArrayList<>();
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
parser.parse("src/test.xml", new DefaultHandler(){
//解析器开始扫描时被调用,只调用一次
@Override
public void startDocument() throws SAXException {
}
//解析器扫描至一个元素的开始时被调用(遇到一个"<",且不为"</时)
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
tags.add(qName);
HashMap<String, String> attrMap = new HashMap<>();
for (int i = 0; i < attributes.getLength(); i++) {
String name = attributes.getQName(i);
String value = attributes.getValue(i);
attrMap.put(name,value);
}
attributesList.add(attrMap);
}
//解析器扫描至一个元素的结束时被调用(遇到一个"</"时)
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
}
//解析器扫描到字符串时,可通过这个方法取出<tag></tag>之间的内容
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String innerText = new String(ch, start, length);
//注意: 每一个XML中的换行符"\n",也会出发characters事件,所以需要从结果中剔除
if(!Objects.equals(innerText.substring(0, 1), "\n")){
innerTexts.add(innerText);
}
}
//解析器完成扫描时被调用,只调用一次
@Override
public void endDocument() throws SAXException {
//打印结果
System.out.println(tags);
System.out.println(innerTexts);
System.out.println(attributesList);
}
});
}
}