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>

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

自定义的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()方法或该方法的一个或多个指定变体来告诉解析器处理下一个事件。以下是解析器会处理的事件类型:

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容