8. 解析XML

##8.1 问题
应用程序需要解析从API或其他资源返回的XML格式的响应结果。

##8.2 解决方案
(API Level 1)
可以通过实现org.xml.sax.helpers.DefaultHandler的一个子类来解析数据,它使用的是基于事件的SAX方式(Simple API for XML)。android有三种用于解析XML数据的主要方式:DOM(文档对象模型)、SAX和Pull。这其中最容易实现的就是SAX解析器,它也是内存效率最高的。SAX解析通过遍历XML数据来实现,并在每个元素的开头和结尾产生回调事件。

##8.3 实现机制
为了进一步介绍如何解析XML,先来看一下请求RSS/ATOM新闻源时返回的XML格式数据(参见以下代码)。
RSS基本结构

<rss version ="2.0">
    <channel>
        <item>
            <title></title>
            <link></link>
            <description></description>
        </item>
        <item>
            <title></title>
            <link></link>
            <description></description>
        </item>
        ...
    </channel>
</rss>

在各组、和标签之间就是每个项的值。我们可以使用SAX将这段数据解析成一个项数组,应用程序可以很方便地在列表中将数据呈现给用户(参见以下代码):<br/> <strong>自定义的RSS解析处理程序</strong>

public class RSSHandler extends DefaultHandler {

    public class NewsItem {
        public String title;
        public String link;
        public String description;
        
        @Override
        public String toString() {
            return title;
        }
    }
    
    private StringBuffer buf;
    private ArrayList<NewsItem> feedItems;
    private NewsItem item;
    
    private boolean inItem = false;
    
    public ArrayList<NewsItem> getParsedItems() {
        return feedItems;
    }
    
    //在每个新元素开始时调用
    @Override
    public void startElement(String uri, String name, String qName, Attributes atts) {
        if("channel".equals(name)) {
            feedItems = new ArrayList<NewsItem>();
        } else if("item".equals(name)) {
            item = new NewsItem();
            inItem = true;
        } else if("title".equals(name) && inItem) {
            buf = new StringBuffer();
        } else if("link".equals(name) && inItem) {
            buf = new StringBuffer();
        } else if("description".equals(name) && inItem) {
            buf = new StringBuffer();
        }
    }
    
    //在每个元素结束时调用
    @Override
    public void endElement(String uri, String name, String qName) {
        if("item".equals(name)) {
            feedItems.add(item);
            inItem = false;
        } else if("title".equals(name) && inItem) {
            item.title = buf.toString();
        } else if("link".equals(name) && inItem) {
            item.link = buf.toString();
        } else if("description".equals(name) && inItem) {
            item.description = buf.toString();
        }
        
        buf = null;
    }
    
    //调用元素中的字符数据
    @Override
    public void characters(char ch[], int start, int length) {
        //Don't bother if buffer isn't initialized
        if(buf != null) {
            for (int i=start; i<start+length; i++) {
                buf.append(ch[i]);
            }
        }
    }
}

在每个元素开始和结束时都会通过startElement()方法通过RSSHandler。在这之间,组成元素值的字符会传递给character()回调方法。当解析器遍历文档,会产生如下步骤:
(1)当解析器碰到第一个元素时,会初始化项列表。
(2)对于遇到的每个项元素,会初始化一个新的NewsItem模型。
(3)在每个项元素的内部,数据元素被置入一个StringBuffer中,然后插入NewsItem的成员中。
(4)当到达每个项的结尾时,会把NewsItem添加到列表中。
(5)解析完成后,feedItems中包含了源数据中的所有项。
接下来,使用第6节的API示例中介绍的一些技巧来下载最新的RSS格式的Google新闻内容(参见以下代码)。
解析XML并显示各个项内容的Activity

public class FeedActivity extends Activity implements ResponseCallback {
    private static final String TAG = "FeedReader";
	private static final String FEED_URI = "http://news.google.com/?output=rss";
	
	private ListView mList;
	private ArrayAdapter<NewsItem> mAdapter;
	private ProgressDialog mProgress;
	
	@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        mList = new ListView(this);
        mAdapter = new ArrayAdapter<NewsItem>(this, android.R.layout.simple_list_item_1, android.R.id.text1);
        mList.setAdapter(mAdapter);
        mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
                NewsItem item = mAdapter.getItem(position);
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setData(Uri.parse(item.link));
                startActivity(intent);
            }
        });
        
        setContentView(mList);
    }
	
    @Override
    public void onResume() {
        super.onResume();
    	//获取RSS源数据
        try{
            RestTask task = RestUtil.obtainGetTask(FEED_URI);
            task.setResponseCallback(this);
            task.execute();
            mProgress = ProgressDialog.show(this, "Searching", "Waiting For Results...", true);
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
    
    @Override
    public void onRequestSuccess(String response) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        //处理响应数据
        try {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser p = factory.newSAXParser();
            RSSHandler parser = new RSSHandler();
            p.parse(new InputSource(new StringReader(response)), parser);
            
            mAdapter.clear();
            for(NewsItem item : parser.getParsedItems()) {
                mAdapter.add(item);
            }
            mAdapter.notifyDataSetChanged();
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
    
    @Override
    public void onRequestError(Exception error) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        //显示错误
        mAdapter.clear();
        mAdapter.notifyDataSetChanged();
        Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show();
    }
}

这个示例修改之后会显示一个ListView,其中的数据就是从RSS源解析出来的。在这个示例中,我们为列表添加一个OnItemClickListener,用户点击时会在浏览器中加载新闻项的链接。
当数据从API的响应回调方法返回时,Android内置的SAX解析器会遍历XML字符串。SAXParser.parse()会使用RSSHandler的实例来处理XML,从XML中解析的内容会用来填充RSSHandler的feedItems列表。接收器在逐个处理解析出来的项,将其添加到ArrayAdapter中,最终显示在ListView中。

XMLPullParser
由框架提供的XmlPullParser是另一种高效解析传入的XML数据的方式。和SAX一样,解析过程也是基于流的,由于解析开始之前并不需要加载整个XML数据结构,因此在解析大文档源时也就不需要太多的内存。下面让我们看一下使用XmlPullParser解析RSS源数据的实例。但与SAX不同,我们必须手动地干预每一步的数据流解析过程,即使是我们不感兴趣的标签元素。
以下代码包含一个工厂类,它会迭代源数据以构造元素模型。

用来将XML解析成模型对象的工厂类

public class NewsItemFactory {

    /* 数据模型类 */
    public static class NewsItem {
        public String title;
        public String link;
        public String description;
        
        @Override
        public String toString() {
            return title;
        }
    }
    
    /*
     * 将 RSS 源解析为一个NewsItem 元素的列表
     */
    public static List<NewsItem> parseFeed(XmlPullParser parser) throws XmlPullParserException, IOException {
        List<NewsItem> items = new ArrayList<NewsItem>();
        
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }
                
            if (parser.getName().equals("rss") ||
                    parser.getName().equals("channel")) {
                //跳过这些元素,但允许解析它们内部的元素
            } else if (parser.getName().equals("item")) {
                NewsItem newsItem = readItem(parser);
                items.add(newsItem);
            } else {
                //跳过其他元素以及它们的子元素
                skip(parser);
            }
        }
        //返回解析后的列表
        return items;
    }
    
    /*
     *将每个 <item> 元素解析为一个NewsItem
     */
    private static NewsItem readItem(XmlPullParser parser) throws XmlPullParserException, IOException {
        NewsItem newsItem = new NewsItem();
        
        //开头必须是有效的 <item> 元素
        parser.require(XmlPullParser.START_TAG, null, "item");
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }
            
            String name = parser.getName();
            if (name.equals("title")) {
                parser.require(XmlPullParser.START_TAG, null, "title");
                newsItem.title = readText(parser);
                parser.require(XmlPullParser.END_TAG, null, "title");
            } else if (name.equals("link")) {
                parser.require(XmlPullParser.START_TAG, null, "link");
                newsItem.link = readText(parser);
                parser.require(XmlPullParser.END_TAG, null, "link");                
            } else if (name.equals("description")) {
                parser.require(XmlPullParser.START_TAG, null, "description");
                newsItem.description = readText(parser);
                parser.require(XmlPullParser.END_TAG, null, "description");
            } else {
                //跳过其他元素以及它们的子元素
                skip(parser);
            }
        }
        
        return newsItem;
    }
    
    /*
     * 读取当前元素的文本内容,该内容start和end标签之间包含的数据
     */
    private static String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
        String result = "";
        if (parser.next() == XmlPullParser.TEXT) {
            result = parser.getText();
            parser.nextTag();
        }
        return result;
    }
    
    /*
     * 辅助方法,用来跳过当前元素以及该元素的子元素
     */
    private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            throw new IllegalStateException();
        }
        
        /*
         * 对于每个新标签,会把一个depth计数器加1。到达每个标签的结尾时会把
         * 计时器减1并且在end标签与开始时的标签匹配时会返回
         */
        int depth = 1;
        while (depth != 0) {
            switch (parser.next()) {
            case XmlPullParser.END_TAG:
                depth--;
                break;
            case XmlPullParser.START_TAG:
                depth++;
                break;
            }
        }
    }
}

Pull解析过程的工作原理就是把数据流作为一系列的事件来处理。应用程序通过调用next()方法或该方法的一个或多个指定变体来告诉解析器处理下一个事件。以下是解析器会处理的事件类型:

  • START_DOCUMENT :当解析器首次初始化时会返回这个事件。在首次调用next()、nextToken()或nextTag()之前,解析器都会是这个状态。
  • START_TAG :解析器刚刚读取标签元素的开始部分。标签的名称可以通过getName()获得,里面的任何属性也可以通过getAttributeValue()和相关的方法获得。
  • TEXT :读取标签元素内部的字符数据,可以通过getText()获取。
  • END_TAG :解析器刚刚读取标签元素的结尾部分。和它相匹配的开始标签的名称可以通过getName()获得。
  • END_DOCUMENT :表明到达了数据量的结尾。

由于必须自己操作解析器,因此我们创建了一个辅助方法skip(),它可以帮助解析器跳过我们不感兴趣的标签。这个方法从当前位置开始遍历所有的内嵌子元素,直到找到匹配的结束标签,并把它们全部跳过。这里使用了一个depth计数器,碰到每个开始标签时会递增,碰到每个结束标签时会递减。当depth计数器到达0时,我们就找到了与开始位置相匹配的结束标签了。
本例中,在调用parseFeed()方法时,解析器首先会迭代数据流来查找可以转换为NewsItem的标签。除了和,所有不是的元素都可以跳过。这是因为所有的项都是内嵌在这两个标签之中的,因此即使我们对它们不直接感兴趣,也不能把它们交给skip()处理,否则所有的项都会被跳过。
分析每个元素的工作是由readItem()方法完成的,它会构造一个新的NewsItem,该NewsItem的内容来自于内部的数据。readItem()方法首先会调用require(),它是一种安全性检查能够确保XML是我们希望的格式。如果当前的解析器事件和传入的命名空间、标签名称相匹配的话,这个方法会静默地返回;否则,它会抛出异常。当我们遍历子元素时,我们主要查找title、link和description标签,这样就可以把它们的值读取到模型数据中。查找到所需的标签后,readText()会操作解析器并把相关字符数据取出。同样,在内部有一些其他元素我们并没有解析,对于不需要的标签只需要调用skip()即可。
可见XmlPullParser非常灵活,原因是可控制整个过程的每一步,但这也要求写更多的代码来完成相同的结果。以下代码清单展示了使用新的解析器来完成源数据显示的Activity。
显示解析的XML源的Activity

public class PullFeedActivity extends Activity implements ResponseCallback {
    private static final String TAG = "FeedReader";
	private static final String FEED_URI = "http://news.google.com/?output=rss";
	
	private ListView mList;
	private ArrayAdapter<NewsItem> mAdapter;
	private ProgressDialog mProgress;
	
	@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        mList = new ListView(this);
        mAdapter = new ArrayAdapter<NewsItem>(this, android.R.layout.simple_list_item_1, android.R.id.text1);
        mList.setAdapter(mAdapter);
        mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
                NewsItem item = mAdapter.getItem(position);
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setData(Uri.parse(item.link));
                startActivity(intent);
            }
        });
        
        setContentView(mList);
    }
	
    @Override
    public void onResume() {
        super.onResume();
    	//获取RSS 源数据
        try{
            RestTask task = RestUtil.obtainGetTask(FEED_URI);
            task.setResponseCallback(this);
            task.execute();
            mProgress = ProgressDialog.show(this, "Searching", "Waiting For Results...", true);
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
    
    @Override
    public void onRequestSuccess(String response) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        //处理响应数据
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(new StringReader(response));
            //跳过第一个标签
            parser.nextTag();
            
            mAdapter.clear();
            for(NewsItem item : NewsItemFactory.parseFeed(parser)) {
                mAdapter.add(item);
            }
            mAdapter.notifyDataSetChanged();
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
    
    @Override
    public void onRequestError(Exception error) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        //显示错误
        mAdapter.clear();
        mAdapter.notifyDataSetChanged();
        Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show();
    }
}

使用Xml.newPullParser()可以实例化一个新的XmlPullParser,通过setInput()可以将数据源的输入流作为一个Reader。本例中,从Web服务器返回的数据已经是字符串了,所以我们把它封装成一个StringReader来让解析器解析。我们可以把解析器传给NewsItemFactory,之后会返回NewsItem元素的列表,我们把它添加到ListAdapter中,然后像之前那样显示出来。

提示:
还可以使用XmlPullParser解析应用程序中绑定的本地XML数据。把你的原始XML放到资源文件中(如res/xml),然后你就可以实例化一个XmlResourceParser,它会使用Resourse.getXml()预加载你的本地数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值