在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