XML解析技术2-使用JAXP开发包对XML进行SAX解析

上一篇

XML解析技术1
已经总结过了,使用dom解析的缺点是内存消耗太大,容易导致内存溢出。

而SAX解析允许在读取文档的时候,就一边对文档进行处理,而不必等到整个文档装载完再进行操作。

SAX采用事件处理的方式解析XML文件,利用SAX解析XML文档涉及两个部分:解析器和时间处理器。

解析器可以使用JAXP的API创建,创建出SAX解析器后,就可以指定解析器去解析某个XML文档;
解析器采用SAX方式在解析某个XML文档时,只要解析到XML文档的一个组成部分,都会去调用事件处理器的一个方法,解析器在调用事件处理器的方法时,会把当前解析到的XML文件内容作为方法的参数传递给事件处理器。
事件处理器由程序员自己编写,程序员通过事件处理器种方法的参数,可以很轻松的得到sax解析到的数据,从而可以决定如何对数据进行处理。

创建sax解析器的过程:

  • 1.先要创建saxParserFactory解析工厂
  • 2.然后创建saxparser解析器
    (这两步和dom解析器是一样的,dom里也是先创建解析工厂,然后创建解析器)
  • 3.然后通过saxparser解析器创建一个读取器saxreader
    (这一步dom解析是没有的,dom解析拿到解析器之后直接就有parse方法把xml解析成document对象了)
  • 4.然后通过saxreader读取器去读取xml文档,解析其内容。解析的时候,解析一部分就会交给事件处理器对xml解析出来的内容进行处理

sun公司的jaxp提供的不同类型的事件处理器有ContentHandler、ErrorHandler、DTDHandler、EntityResolver四个。其中最常用的只有ContentHandler。其他基本用不到,我们要自己实现事件处理器的时候,就实现ContentHandler这个接口的相关方法。

我们查看ContentHandler的方法列表

在这里插入图片描述

可以看到,这里面传入的参数就是解析结果

我们还是以之前的dom解析使用的一样的book.xml文档做数据来对过程进行一个测试。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<book category="CHILDREN">
    <title>Harry Potter</title>
    <author>J K. Rowling</author>
    <year>2005</year>
    <price>29.99</price>
</book>
<book category="XMLPARSE">
    <title>Learning XML</title>
    <author>Erik T. Ray</author>
    <year>2003</year>
    <price>39.95</price>
</book>

然后我们按照步骤开始进行sax解析:

       //创建工厂
       SAXParserFactory  saxParserFactory=SAXParserFactory.newInstance();
       //创建解析器
       SAXParser saxParser=saxParserFactory.newSAXParser();
       //创建读取器
       XMLReader xmlReader=saxParser.getXMLReader();

下一步进行读取之前,必须创建事件处理器,因为在开始解析的时候,都会调用事件处理器的方法,都是自动调用的。如果在之后才去设置的话,开始解析就已经结束了。

事件处理器的四种都是接口,需要我们实现具体操作。选择ContentHandler进行实现,在下面写一个实现类:

这里面实现接口的时候,所有方法里我们选择只实现其中的三个方法,关注xml文档的内容进行输出,不进行其他操作。因此其他方法不进行实现。
(有没有发现里面的方法实在太多?)

 @Override
 public void startElement(String uri, String localName,  String qName, Attributes atts) throws SAXException {
       // TODO Auto-generated method stub
       System.out.println("<"+qName+">");//开头元素。在这里我们只对名字进行输出,不做其他处理
 }
 @Override
 public void endElement(String uri, String localName,  String qName) throws SAXException {
       // TODO Auto-generated method stub
       System.out.println("</"+qName+">");//结束的元素。在这里我们和开始元素一样不做其他处理
 }
 @Override
 public void characters(char[] ch, int start, int length)  throws SAXException {
       // TODO Auto-generated method stub
       System.out.println(new String(ch,start,length));//这个方法的参数是内容的参数,我们不做其他处理,调用string方法输出整个内容
 }

以第一个方法为例,在文档里,可以看到方法的描述是:
【void startElement(String uri,String localName,String qName,Attributes atts)throws SAXException】
接收元素开头的通知。解析器将在XML文档中的每个元素的开头调用此方法; 每个startElement事件将会有一个相应的endElement事件(即使该元素为空)。 所有元素的内容将按照相应的endElement事件的顺序进行报告。

这样,这个新的实现类我们其名为ListHandler。

回到前面进行解析的下一个步骤,必须先创建事件处理器,那么我们就创建一个ListHandler实例,然后进行解析,完整的过程五个步骤就是:

       //创建工厂
       SAXParserFactory  saxParserFactory=SAXParserFactory.newInstance();
       
       //创建解析器
       SAXParser saxParser=saxParserFactory.newSAXParser();
       
       //创建读取器
       XMLReader xmlReader=saxParser.getXMLReader();
       
       //设置事件处理器ContentHandler,必须在读取器读取之前,在读取的时候是要调用内容处理器的,如果在之后,那时已经读取完了
       xmlReader.setContentHandler(new ListHandler());
       
       //读取xml
       xmlReader.parse("src/xml/book.xml");

运行一下,产生输出结果:

在这里插入图片描述

对比我们的xml原本的文档,看到只关注了里面的标签(元素)和元素内容部分,但是属性没有输出。想要输出属性,我们查看到接口ContentHandler里的startElement方法还有一个参数是Attributes atts,因此我们可以点开这个Attributes类,查看里面的方法,返回的是多个属性,然后根据里面的getlength方法得到索引,对应访问每一个属性的名称和值。我们更改startElement方法的实现:

 @Override
 public void startElement(String uri, String localName,  String qName, Attributes atts) throws SAXException {
       // TODO Auto-generated method stub
       System.out.print("< "+qName);//元素的开头。在这里我们只对名字进行输出,不做其他处理
       for (int i = 0; i < atts.getLength(); i++) {
            String attName=atts.getQName(i);
            String attValue=atts.getValue(i);
            System.out.print(attName+" = \""+attValue+"\"  ");
       }
       System.out.print(">");
 }

这里面,如果所有的标签都是没有属性的 也就是这个属性列表是空的,那么在for循环进行的时候,可能att.length方法的返回结果就是一个null值,可能会出现异常,我们为了严谨,应该在判断条件里增加对attr的判断。

 @Override
 public void startElement(String uri, String localName,  String qName, Attributes atts) throws SAXException {
       // TODO Auto-generated method stub
       System.out.print("< "+qName);//元素的开头。在这里我们只对名字进行输出,不做其他处理
       for (int i = 0;atts!=null && i < atts.getLength(); i++) {
            String attName=atts.getQName(i);
            String attValue=atts.getValue(i);
            System.out.print(attName+" = \""+attValue+"\"  ");
       }
       System.out.print(">");
 }

再次运行:

在这里插入图片描述

可以看到属性也进行输出了。

这样,我们的处理器采用sax方法进行xml的文档的解析已经完成了。

在日常开发中,我们需要的解析结果肯定是更多的要求,绝不是把xml文档的所有内容都获取就可。显然,对应不同的要求就是实现不同的事件处理器(最多肯定还是内容处理器ContentHandler)

但是上面已经发现了,在ContentHandler里,有很多基本用不到的方法,并且我们没有必要实现,查看api文档可以发现,**其实已经有很多默认的实现类可以调用,**我们只需要继承其中的实现类,覆盖里面的一些方法,或者增加自己想要的方法就可以了。(常用的还是一个DefaultHandler,其实里面什么也没做,和接口无异,需要更改我们就进行重写具体的方法,但是继承就比实现接口的代码严要简洁很多)

比如我们如果要进行解析的目的是,获取指定标签的值,我们继承DefaultHandler,因为是根据标签的名字获取具体的标签的值,需要重写的方法就有获取标签(元素)内容的character方法、获取标签开始的和标签结束的方法:

 private String currentTag;//当前解析到的标签名
 private int needId=2;//需要解析第几个标签
 private int currentId;//当前解析到第几个标签
 @Override
 public void startElement(String uri, String localName,  String qName, Attributes attributes) throws SAXException {
       currentTag=qName;
       if (currentTag.equals("author")) {
            //我们以获取的目标标签名是author为例
            currentId++;
       }
 }
 @Override
 public void endElement(String uri, String localName,  String qName) throws SAXException {
 }
 @Override
 public void characters(char[] ch, int start, int length)  throws SAXException {
       if (currentTag.equals("author")&&currentId==needId) {
            //获取到的是目标标签的内容,就输出
            System.out.println(new String(ch,start,length));
       }
 }

这样运行结果是:

在这里插入图片描述

输出了第二个author标签的内容。

对于面向对象来说,上述案例的xml数据我们肯定希望拿到的是每本书作为一个对象的数据。

因此我们首先要设置一个Bean来存放从xml中解析出来的book信息:

package xml;
/**
* 一个bookbean类,封装从xml文档里进行sax解析出来的内容
* @author John
*
*/
public class bookBean {
     private String category;
    private String title;
    private String author;
    private String year;
    private double price;
     public String getCategory() {
           return category;
     }
     public void setCategory(String category) {
           this.category = category;
     }
     public String getTitle() {
           return title;
     }
     public void setTitle(String title) {
           this.title = title;
     }
     public String getAuthor() {
           return author;
     }
     public void setAuthor(String author) {
           this.author = author;
     }
     public String getYear() {
           return year;
     }
     public void setYear(String year) {
           this.year = year;
     }
     public double getPrice() {
           return price;
     }
     public void setPrice(double price) {
           this.price = price;
     }
    
    
}

然后在解析的测试类里,前面的步骤和正常的步骤没有区别,当然,我们要实现一个新的事件处理器,实现的方法仍然是三个,元素开始、元素内容和元素结束分别进行处理,因为要求的是对每本book的内容进行封存,我们用bookBean组成一个list进行返回。事件处理器的实现代码是这样的:

/**
*
* 获取一个book的数据为一整个对象的事件处理器,命名成BeanListHandler
* 封装成的对象应该是放在一个List里面,关于一个book的类我们另外设计一个Bean叫bookBean
* @author John
*
*/
class BeanListHandler extends DefaultHandler{
     private List<bookBean> list=new ArrayList<bookBean>();
     private String currentTag;//当前解析到的标签
     private bookBean book;//一个bookBean对象用来保存解析内容
     
     /*
      * 元素开始,由于是获取整个对象的开始,在遇到book标签的时候就new出来bookBean对象,然后接着进行解析字标签,内容都放在这个里
      */
     @Override
     public void startElement(String uri, String localName,  String qName, Attributes attributes) throws SAXException {
           // TODO Auto-generated method stub
           currentTag=qName;
           if ("book".equals(currentTag)) {
                book=new bookBean();
                book.setCategory(attributes.getValue(0));;
           }
     }
     
     /*
      * 元素内容,注意对于不同元素,先进行判断元素名,然后将内容放在不同的字符串里
      * 对于sax解析,每一次边读取边调用事件处理器解析,碰到的每个标签都需要进行判断
      */
     @Override
     public void characters(char[] ch, int start, int length)  throws SAXException {
           // TODO Auto-generated method stub
           if ("title".equals(currentTag)) {
                String title=new String(ch, start, length);
                book.setTitle(title);
           }
           if ("author".equals(currentTag)) {
                String author=new String(ch, start, length);
                book.setAuthor(author);
           }
           if ("year".equals(currentTag)) {
                String year=new String(ch, start, length);
                book.setYear(year);
           }
           if ("price".equals(currentTag)) {
                String price=new String(ch, start, length);
                book.setPrice(Double.parseDouble(price));
           }
     }
     /*
      * 元素结束,一个变量currentTag多次使用,每次遇到元素结尾都置空
      * 同时,如果整个book元素已经结束了,那么一个对象就可以完毕了,将他加入到List里,也置空
      */
     @Override
     public void endElement(String uri, String localName,  String qName) throws SAXException {
           // TODO Auto-generated method stub
           if (qName.equals("book")) {
                list.add(book);
                book=null;
           }
           currentTag=null;
     }
     /**
      *
      * 这个处理器的结果是一个List集合,作为private属性提供get方法获取,为了用户友好改名成getBooks
      * @return the list
      */
     public List<bookBean> getBooks() {
           return list;
     }
}

其中重要的有三点,一是对每一个元素名称的判断,由于进行解析的时候是每解析一条数据都会进行自动调用事件处理器,因此解析出的不同元素我们要分开处理,就是分开存放到设计的bean对象里,然后每一次处理结束一个标签,也就是遇到endElement是这个book的时候,就代表一个完整的bean对象已经可用了,我们添加到list里去。

这里面再进行标签名称的判断过程中,开始使用 if (currentTag.equals(“title”))写法,报了空指针错误,找了很久最后更改为 if (“title”.equals(currentTag)),就正常运行了,之前并没有遇到这样的问题,查阅资料看到:

== 在String类型中,equals方法被重写了。==因此String类型中的equals方法是比较当前字符串与传进来的字符串是否相同。
如果将对象放在前面,调用equals方法时,若对象为空,会报空指针异常;
但若是将字符串放在前面,也就是相当于判断这个字符串的值,即使它为空,也不会报错。因此可以总结为:报不报错主要看调用equals的对象是否为空。
所以在使用equals方法时,通常将字符串放在前面,比如"userName" .equals (对象),这样就不用担心对象为空了。所以调换顺序更为规范。
但是,问题就在于,在我的处理过程里并不会出现currentTag为空的情况,一开始没有初始值,但是只要解析到标签的开始,就会赋值,只有遇到结束标签的时候才会置空,但是到下一个标签的开始标志,又赋了新的值了。

so why?

启用断点调试,从startElement开始执行,第一个标签是bookstore,到characters的时候本来应该直接跳过,因为不是book标签,应该到结束元素,但是程序还是进入了characters方法,并且报了空指针,我们查看xml文档,能够发现其中有什么内容呢?有,就是这些 东西:

在这里插入图片描述

前面已经说过,XML标签中出现的所有空格和换行,XML解析程序都会当作标签内容进行处理。

也就是说,在characters方法里传入了char[] ch参数,他的内容就是这里的空格和换行,然而作为字符对象,内容确实是null,因此就会产生空指针错误了。

反正记住用字符串常量.equals(对象)就不会出错就完事了。

同样的问题还存在于最后的currentTag=null;如果xml不解析空格和换行,那这个语句是可要可不要的,因为即使不置空,进入下一个标签之后也会重新覆盖成新的标签名。然而事实并不是这样,在结束book标签之后,book已经置空,而currentTag还是之前的price,这时候并不会直接到最后的bookstore,而是继续解析中间的空格和换行,注意这里又会调用到character方法去解析这里的内容,又会匹配到currentTag是price的情况,这样就又会对book的price重新进行set,而book已经置空了,就会出现空指针异常。

main方法的测试代码如下:

public static void main(String[] args) throws SAXException,  ParserConfigurationException, IOException {
           //解析工厂
           SAXParserFactory  saxParserFactory=SAXParserFactory.newInstance();
           //解析器
           SAXParser saxParser=saxParserFactory.newSAXParser();
           //读取器
           XMLReader xmlReader=saxParser.getXMLReader();
           //事件处理器
           
           BeanListHandler beanListHandler=new  BeanListHandler();
           xmlReader.setContentHandler(beanListHandler);
           //解析
           xmlReader.parse("src/xml/book.xml");
           
           //获取解析结果
           List<bookBean> list=beanListHandler.getBooks();
           
           System.out.println(list);
     }

因为懒得重写toString了,输出的是list的地址,我们用debug视图可以看到list的结果:

在这里插入图片描述

可以看到提取了xml文档里的所有内容。

版权声明:本文为weixin_42092787原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42092787/article/details/103357776