赞
踩
##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>
在各组
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()方法或该方法的一个或多个指定变体来告诉解析器处理下一个事件。以下是解析器会处理的事件类型:
由于必须自己操作解析器,因此我们创建了一个辅助方法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()预加载你的本地数据。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。