深入分析android中用SAX解析XML文件并纠错

在android中解析XML文件有很多方法,今天主要介绍下SAX解析。

1、SAX简介

SAX是基于事件驱动模型,可以捕获到读取文档过程中产生的事件,比如开始文档、结束文档、开始元素、结束元素、文本内容事件等。通过定义一个事件处理器,在这些事件触发后,来实现数据的获取。通过使用XMLReader类来注册事件处理器,在Android中有如下4个事件处理器接口,如下图:

补充:事件驱动模型是事件源发出事件,监听器捕获事件并做出相应的过程。这里,事件源就是发出事件的对象,监听器则是对事件感兴趣的对象。


图片引用:http://www.cnblogs.com/felix-hua/archive/2012/01/10/2317404.html

注:在实际应用中不需要全部实现这4个接口,SDK提供了DefaultHandler类来处理。DefaultHandler类已经实现了这4个接口,用户只需继承该类即可。

XMLReader中的方法:

//注册处理XML文档解析事件ContentHandler
public void setContentHandler(ContentHandler handler)

//开始解析一个XML文档
public void parse(InputSorce input) throws SAXException

在上述四个接口中,最重要的就是ContentHandler这个接口,下面是对这个接口方法的详细说明:

//开始XML文档的回调函数,在文档开头时调用,其中可做预处理工作
public void startDocument()throws SAXException

//元素开始标签的回调函数,处理元素开始事件。
//参数:namespacesURI为元素所在的名称空间,localName为不带命名空间前缀的标签名,qName为带命名空间前缀的标签名,atts为元素的属性名和值集合
public void startElement(String namespacesURI , String localName , String qName , Attributes atts) throws SAXException

//元素结束标签的回调函数,处理元素结束事件。
public void endElement(String namespacesURI , String localName , String qName) throws SAXException

//处理元素的字符内容,从参数中可以获得内容
//参数:ch为文件的字符串内容,start为读到的字符串在这个数组中的起始位置,length为读到的字符串在这个数组中的长度.
public void characters(char[] ch , int start , int length)  throws SAXException
	
//结束XML文档的回调函数,在文档结束时调用,其中可做一些回收工作。
public void endElement(String uri, String localName, String qName)throws SAXException


2、SAX实现解析的步骤

//1、新建一个工厂类SAXParserFactory,代码如下:
SAXParserFactory factory = SAXParserFactory.newInstance();
//2、让工厂类产生一个SAX的解析类SAXParser,代码如下:
SAXParser parser = factory.newSAXParser();
//3、从SAXPsrser中得到一个XMLReader实例,代码如下:
XMLReader reader = parser.getXMLReader();
//4、自己实现一个事件处理类handler,并将之注册到XMLReader中,一般最重要的就是ContentHandler。代码如下:
ParserPerson parserPerson = new ParserPerson();//自己实现的handler,详细代码后面有介绍
reader.setContentHandler(handler);
//5、将一个xml文档或者资源变成一个java可以处理的InputStream流in后,开始解析,代码如下:
parser.parse(in);  

3、SAX的优缺点:

优点:解析速度快,占用内存少。解析时不用调入整个文档,所有占用资源少。尤其在嵌入式环境中,如android,极力推荐使用SAX解析。

缺点:流式处理,因而不像DOM解析一样将文档长期驻留在内存中,数据不是持久的。需要在事件中保存数据。


4、应用实例:

代码中所使用的类如下,另外考虑代码的可扩展性,本例采用了策略模式。对每个事件处理器进行封装,将每一个事件处理器封装到一个具有共同接口的独立类ParseXML中。从而使得它们可以在不影响客户端情况下发生,也能相互替换。

//Person:人员基本信息,是一个bean类。
//ParseXML类:抽象类,定义一个抽象方法,用来获取解析的结果。
//ParsePersonXML类:ParseXML的子类,其含有继承DefaultHandler类的内嵌类ParsePerson,该类实现了SAX解析的事件处理器。
//ContextClient类:封装事件处理器,屏蔽高层模块对事件处理器的直接访问。
//MainActivity类: 通过一个TextView来显示解析的结果。

首先定义一个XML文件PersonItem,其目录为res/raw/person_item.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<Persons>
	<Person  account="zhangsan">
	    <name>张三</name>
	    <email>zhangsan@gmail.com</email>
	    <tel>18866661111</tel>
	    <boss>manager</boss>
	    <newProject>1</newProject>
	    <superPower>1</superPower>
	</Person>
	<Person account="lisi">
	    <name>李四</name>
	    <email>lisi@gmail.com</email>
	    <tel>16688883333</tel>
	    <boss>manager</boss>
	    <newProject>0</newProject>
	    <superPower>0</superPower>
	</Person>
</Persons>
然后相应代码实现:

Person类:

public class Person {
	  String mName;//姓名
	  String mAccount;//账户
	  String mTel;//电话
	  String mEmail;//邮箱
	  String mBoss;//上司
	  String mCompetence;//超级权限
	  String mProprietor;//新建项目权利

	  public Person()
	  {
	    this.mAccount = "";
	    this.mName = "";
	    this.mTel = "";
	    this.mEmail = "";
	    this.mBoss = "";
	    this.mCompetence = "";
	  }

	  public Person(String paramString1, String paramString2,
			  String paramString3, String paramString4, 
			  String paramString5, String paramString6)
	  {
	    this.mAccount = paramString1;
	    this.mName = paramString2;
	    this.mTel = paramString3;
	    this.mEmail = paramString4;
	    this.mBoss = paramString5;
	    this.mCompetence = paramString6;
	  }

	  public String getmAccount()
	  {
	    return this.mAccount;
	  }

	  public String getmBoss()
	  {
	    return this.mBoss;
	  }

	  public String getmCompetence()
	  {
	    return this.mCompetence;
	  }

	  public String getmEmail()
	  {
	    return this.mEmail;
	  }

	  public String getmName()
	  {
	    return this.mName;
	  }

	  public String getmProprietor()
	  {
	    return this.mProprietor;
	  }

	  public String getmTel()
	  {
	    return this.mTel;
	  }

	  public void setmAccount(String paramString)
	  {
	    this.mAccount = paramString;
	  }

	  public void setmBoss(String paramString)
	  {
	    this.mBoss = paramString;
	  }

	  public void setmCompetence(String paramString)
	  {
	    this.mCompetence = paramString;
	  }

	  public void setmEmail(String paramString)
	  {
	    this.mEmail = paramString;
	  }

	  public void setmName(String paramString)
	  {
	    this.mName = paramString;
	  }

	  public void setmProprietor(String paramString)
	  {
	    this.mProprietor = paramString;
	  }

	  public void setmTel(String paramString)
	  {
	    this.mTel = paramString;
	  }
}
ParseXML类:

import java.io.InputStream;
import java.util.ArrayList;

public abstract class ParseXML {

	public abstract ArrayList<Object> getXMLInfoByList(InputStream in);
	
}
ParsePersonXML类:
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

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;

import android.util.Log;

public class ParsePersonXML extends ParseXML {
	
	@Override
	public ArrayList<Object> getXMLInfoByList(InputStream in){
		Log.d("ParsePersonXML","not execution yet");
		ParserPerson parserPerson = new ParserPerson();
		SAXParserFactory localSAXParseFaxtory = SAXParserFactory.newInstance();
		try{
			SAXParser saxParser = localSAXParseFaxtory.newSAXParser();
			saxParser.parse(in, parserPerson);
		}catch(ParserConfigurationException e){
			e.printStackTrace();
		}catch(SAXException e){
			e.printStackTrace();
		}catch(IOException e){
			e.printStackTrace();
		}
		Log.d("ParsePersonXML","ParsePersonXML:"+parserPerson.getPersons().size());
		return parserPerson.getPersons();
	}

	private class ParserPerson extends DefaultHandler{
		private StringBuilder builder;
		//存储当前正在解析结点的标签名。
		private String currentElement;
		private Person person;
		private ArrayList<Object> personList;
		
		/**
		 * 开始XML文档的回调函数,在文档开头时调用,其中可做预处理工作
		 */
		@Override
		public void startDocument() throws SAXException {
			// TODO Auto-generated method stub
			this.personList = new ArrayList<Object>();
			super.startDocument();
		}
		/**
		 * 元素开始标签的回调函数,处理元素开始事件。
		 * uri: 命名空间。
		 * localName: 不带命名空间前缀的标签名。
		 * qName:带命名空间前缀的标签名。
		 * attributes: 属性名和值对的集合。
		 */
		@Override
		public void startElement(String uri, String localName, String qName,
				Attributes attributes) throws SAXException {
			// TODO Auto-generated method stub
			this.currentElement = localName;
			if("Person".equals(this.currentElement)){
				this.person = new Person();
				this.personList.add(this.person);
				//获取属性值
				person.setmAccount(attributes.getValue("account"));
			}
			
		}
		/**
		 * 结束XML文档的回调函数,在文档结束时调用,其中可做一些回收工作。
		 */
		@Override
		public void endElement(String uri, String localName, String qName)
				throws SAXException {
			// TODO Auto-generated method stub
			//Person标签解析完毕,将其存储到集合中,并清空person,准备下一个Person的解析。
			this.currentElement = null;
		}
		/**
		 * 结束XML文档的回调函数,文档结束时调用,其中可做一些回收工作。
		 */
		@Override
		public void endDocument() throws SAXException {
			// TODO Auto-generated method stub
			super.endDocument();
		}
		/**
		 * 处理元素的字符内容,从参数中可以获得内容
		 * ch: 文件的字符串内容.
		 * start: 读到的字符串在这个数组中的起始位置.
		 * length: 读到的字符串在这个数组中的长度.
		 * 使用new String(ch,start,length)就可以获取内容。
		 */
		@Override
		public void characters(char[] ch, int start, int length)
				throws SAXException {
			// TODO Auto-generated method stub
			if(this.currentElement == null)
				return;
			//根据标签名,读取相应的值。
			String str = new String(ch,0,length);
			if("name".equals(this.currentElement)){
				person.setmName(str);
			}
			else if("email".equals(this.currentElement)){
				person.setmEmail(str);
			}
			else if("tel".equals(this.currentElement)){
				person.setmTel(str);
			}
			else if("boss".equals(this.currentElement)){
				person.setmBoss(str);
			}
			else if("newProject".equals(this.currentElement)){
				person.setmProprietor(str);
			}
			else if("superPower".equals(this.currentElement)){
				person.setmCompetence(str);
			}
		}
		
		public ArrayList<Object> getPersons(){
			Log.d("ParsePersonXML","ParserPerson:"+this.personList.size());
			return this.personList;
		}
	}

}

ContextClient类的实现:

import java.io.InputStream;
import java.util.ArrayList;

public class ContextClient {
	private ParseXML mParseXML;
	
	public ContextClient(ParseXML parseXML){
		this.mParseXML = parseXML;
	}
	
	public ArrayList<Object> getParseInfo(InputStream in){
		return this.mParseXML.getXMLInfoByList(in);
	}
}

MainActivity类的实现:

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = (TextView)findViewById(R.id.tv1);
        try{
        	ParsePersonXML parsePerson = new ParsePersonXML();
        	ContextClient client = new ContextClient(parsePerson);
        	InputStream in = getResources().openRawResource(R.raw.person_item);
        	ArrayList<Object> list = client.getParseInfo(in);
        	in.close();
        	for(Object p: list)
        	{
        		Person person = (Person)p;
        		if(person != null)
        		{
	        		String str1 = ("1".equals(person.getmProprietor()))?"超级权限":"普通权限";
	        		String str2 = ("1".equals(person.getmCompetence()))?"读写项目":"只读项目";
	        		StringBuilder builder = new StringBuilder();
	        		tv.append(builder.toString());
	        		tv.append("\n\r姓名: "+person.getmName()+"\r\n账户:"+person.getmAccount()
	        				+ "\n\r电话: "+person.getmTel()+"\n\r邮箱: "+person.getmEmail()
	        				+"\n\r权限:  "+str1+" && "+str2+"\n\r ");
        		}
        	}
        }catch(IOException e){
        	e.printStackTrace();
        }
        
    }

}

下面是效果图:



5、易犯错误纠正

  在继承DefaultHandler实现的ParsePerson事件处理器中,一定要理清解析的逻辑,再进行相应的处理。不然很容易解析结果为空之类的错误。网上有些blog在处理文档开头、结束等回调函数上,都会犯一些错误,这在某些情况下会直接导致解析结果为空,因而手机端不会显示结果为空。根本原因没有掌握好事件处理器的调用机制,解析的逻辑。

1)currentElement存储当前解析的标签,一定要注意初始化和清空的时机。在startElement函数中初始化,个人建议首先初始化然后再判断处理,这样比较符合解析逻辑。好些资料包括某些书里,在startElement中都是先判断处理再初始化。这在某些情况下是能顺利解析出来的,但有些时候却不行,比如只有一个标签<Person account="zhangsan"><Person>,就解析不出数据的。最简单的方法就是不用currentElement,后面会贴出标准代码。

2)分配Person对象和将该对象添加到集合的时机。添加Person对象建议在分配空间时就添加到集合中,或者在endElement函数中添加。不建议在characters函数中添加,因为当前的currentElement不一定是Person。

3)在解析一个完整的<name>value</name>时,characters函数是很有可能会执行多次的。遇到内容中有"\n"或“、“t”就会再次调用该函数执行,利用StringBuilder对象将其追加上去,这样就不会丢失内容。

最后我们可以得知上面的ParsePerson还有些繁琐,甚至可能会丢失部分数据。我参考网上资料,整理出标准的ParsePerson事件处理器。绕了这么一个大圈子,各位看官不要生气,我只是希望您能够真正理解SAX解析,希望下面的代码对大家有用,如下:

private class ParserPerson extends DefaultHandler{
		private StringBuilder builder = new StringBuilder();
		private Person person;
		private ArrayList<Object> personList;
		
		/**
		 * 开始XML文档回调函数,文档开头时调用,其中可做预处理工作
		 */
	//	@Override
		public void startDocument() throws SAXException {
			// TODO Auto-generated method stub
			this.personList = new ArrayList<Object>();
			super.startDocument();
		}
		/**
		 * 元素开始标签的回调函数
		 * uri: 命名空间。
		 * localName: 不带命名空间前缀的标签名。
		 * qName:带命名空间前缀的标签名。
		 * attributes: 属性名和值对的集合。
		 */
		@Override
		public void startElement(String uri, String localName, String qName,
				Attributes attributes) throws SAXException {
			// TODO Auto-generated method stub
			if("Person".equals(localName)){
				this.person = new Person();
				this.personList.add(this.person);
				//获取属性值
				person.setmAccount(attributes.getValue("account"));
			}
			this.builder.setLength(0);
		}
		/**
		 * 遇到元素结束标签的回调函数,
		 */
		@Override
		public void endElement(String uri, String localName, String qName)
				throws SAXException {
			// TODO Auto-generated method stub
			//Person标签解析完毕,将其存储到集合中,并清空person,准备下一个Person的解析。
			if("name".equals(localName)){
				person.setmName(this.builder.toString());
			}
			else if("email".equals(localName)){
				person.setmEmail(this.builder.toString());
			}
			else if("tel".equals(localName)){
				person.setmTel(this.builder.toString());
			}
			else if("boss".equals(localName)){
				person.setmBoss(this.builder.toString());
			}
			else if("newProject".equals(localName)){
				person.setmProprietor(this.builder.toString());
			}
			else if("superPower".equals(localName)){
				person.setmCompetence(this.builder.toString()); 
			}
		}
		/**
		 * 结束XML文档的回调函数,文档结束时调用,其中可做一些回收工作。
		 */
		@Override
		public void endDocument() throws SAXException {
			// TODO Auto-generated method stub
			super.endDocument();
		}
		/**
		 * 遇到元素值时回调此函数.
		 * ch: 文件的字符串内容.
		 * start: 读到的字符串在这个数组中的起始位置.
		 * length: 读到的字符串在这个数组中的长度.
		 * 使用new String(ch,start,length)就可以获取内容。
		 */
		@Override
		public void characters(char[] ch, int start, int length)
				throws SAXException {
			// TODO Auto-generated method stub
			//不管调用多少次该函数,都不会丢失内容。
			this.builder.append(ch,start,length);
		}
		
		public ArrayList<Object> getPersons(){
			Log.d("ParsePersonXML","ParserPerson:"+this.personList.size());
			return this.personList;
		}
	}

在这个代码里面,主要是用了不带命名空间前缀的标签名localName来做相应处理,如果XML含有命名空间前缀,大家自行修改。

下面贴出XML文件person_item

<?xml version="1.0" encoding="UTF-8"?>
<Persons>
    <Person account="test"></Person>
    <Person ></Person>
	<Person account="zhangsan">
	    <name>张三</name>
	    <email>zhangsan@gmail.com</email>
	    <tel>18866661111</tel>
	    <boss>manager</boss>
	    <newProject>1</newProject>
	    <superPower>1</superPower>
	</Person>
	<Person account="lisi">
	    <name>李四</name>
	    <email>lisi@gmail.com</email>
	    <tel>16688883333</tel>
	    <boss>manager</boss>
	    <newProject>0</newProject>
	    <superPower>0</superPower>
	</Person>
</Persons>
这是效果图,Person分配对象后也赋予了初始值,且在MainActivity中没进行相应判断处理,因而看起来有些怪异,见谅:



参考1:http://www.cnblogs.com/felix-hua/archive/2012/01/10/2317404.html

参考2:http://blog.csdn.net/feng88724/article/details/7013675




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值