可扩展标记语言XML是一套机器可读的编码文件形式。XML是互联网上流行的用于共享数据的格式。网站经常会更新它们的内容,比如新闻网站,博客,它们通常会提供一个XML格式的数据源,这样方便外部程序来及时同步改变的内容。因此对于网络应用来说,上传和解析XML数据就成了共同的任务。这节课主要讲解怎样解析XML文档以及使用它们的数据。
选择解析器 Choose a Parser
我们推荐XmlPullParser,其在android中对XML的解析是高效且可维护的。从过去来看,android已经拥有该接口的两个实现:
- KXmlParser,通过XmlPullParserFactory.newPullParser()创建。
 - ExpatPullParser,通过Xml.newPullParser()创建。
 
任意的选择都是可以的,这节中的例子用的是Xml.newPullParser()创建的ExpatPullParser。
分析数据源 Analyze the Feed
解析数据源的第一步是决定哪些属性字段是你需要的,
解析器将提取你需要的属性字段而忽略其他的。以下显示的片段,是来自例子程序中正在被解析的数据源。每一个指向StackOverflow.com的请求都做为一个entry标签显示在数据源中,并且每一个entry标签都含有几个内嵌标签:
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ;  | 
 <?xml version"utf-8"?> <feed xmlns"http://backend.userland.com/creativeCommonsRssModule" ..."> <title type="text">newest questions tagged android - Stack Overflow</title> ... <entry> ... </entry> <entry> <id>http://stackoverflow.com/q/9439999</id> <re:rank scheme="http://stackoverflow.com">0</re:rank> <title type="text">Where is my data file?</title> <category schemeandroid&sort"android"/> <category schemeandroid&sort"file"/> <author> <name>cliff2310</name> <uri>http://stackoverflow.com/users/1128925</uri> </author> <link rel"http://stackoverflow.com/questions/9439999/where-is-my-data-file" /> <published>2012-02-25T00:30:54Z</published> <updated>2012-02-25T00:30:54Z</updated> <summary type="html"> <p>I have an Application that requires a data file...</p>
  </summary> </entry> <entry> ... </entry> ... </feed>
  ;  | 
这个例子程序将会获取entry以及它的内嵌标签title,link和summary的数据。
实例化解析器 Instantiate the Parser*
下一步就是实例化解析器并且开始解析过程了。在以下的代码片段中,实例化了一个不处理命名空间的解析器,并且将提供的InputStream作为输入参数。在调用nextTag)以及readFeed方法后开始解析过程。这两个方法负责获取并处理应用需要的数据:
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ;  | 
 public class StackOverflowXmlParser {           // 我们不需要使用命名空间           private static final String ns = null;
            public List parse(InputStream in) throws XmlPullParserException, IOException {                     try {                               XmlPullParser parser = Xml.newPullParser();                               parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);                               parser.setInput(in, null);                               parser.nextTag();                               return readFeed(parser);                     } finally {                         in.close();               } } ... }
  ;  | 
读取数据源 Read the Feed
readFeed方法用来实际处理数据源,它将entry标签作为递归处理数据源的一个起点,如果标签不是entry,则会跳过它。一旦整个数据源被递归处理,readFeed方法会返回包含entry类型的List对象(包括内嵌标签),而这些数据都是从数据源获取。之后这个List会被解析器返回。
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ;  | 
 private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException {           List entries = new ArrayList();
            parser.require(XmlPullParser.START_TAG, ns, "feed");           while (parser.next() != XmlPullParser.END_TAG) {                     if (parser.getEventType() != XmlPullParser.START_TAG) {                               continue;                     }                     String name = parser.getName();                     // 从寻找entry标签开始                     if (name.equals("entry")) {                               entries.add(readEntry(parser));                     } else {                         skip(parser);               } } return entries; }
  ;  | 
解析XML Parse XML
以下是解析XML数据源的具体步骤:
跟(上面提到的)分析数据源一样,先分析出你应用程序想要的标签。这个例子中就是获取entry标签以及它内嵌的title,link和summary标签的数据。
创建以下方法:
- 为每一个你想要的标签创建“read”方法。比如:readEntry(),readTitle()等等。
 - 解析器从输入流读取标签,当它遇到名为entry,title,link或者summary时,它会调用相对应标签的方法。不然就跳过标签。
 
为获取每个不同类型标签的数据并推进到下一个标签定义方法。比如:
- 对于title和summary标签,实例化的解析器会调用readText()方法,而这个方法获取这些标签的数据则是调用了 parser.getText()方法。
 
- 对于link标签,解析器在获取数据的时候会根据第一次的判断,这个链接是否是应用需要的,然后用parser.getAttributeValue()方法获取链接的值。
 
- 对于entry标签,解析器会调用readEntry()方法,这个方法会解析entry标签的内嵌标签,title,link和summary,并返回一个Entry对象。
 
定义一个用于辅助遍历的方法skip(),更多关于skip的信息,详���(跳过你不需要的标签 Skip Tags You Don't Care About)
以下代码片段展示的是解析器如何解析Entry,title,link,以及summary标签:
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 ;  | 
 public static class Entry {           public final String title;           public final String link;           public final String summary;
            private Entry(String title, String summary, String link) {                     this.title = title;                     this.summary = summary;                     this.link = link;           } }
  // 解析entry标签的内容,如果遇到title,summary,或者link标签,则在他们各自的"读"方法中处理. 否则跳过标签. private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException {           parser.require(XmlPullParser.START_TAG, ns, "entry");           String title = null;           String summary = null;           String link = null;           while (parser.next() != XmlPullParser.END_TAG) {                     if (parser.getEventType() != XmlPullParser.START_TAG) {                               continue;                     }                     String name = parser.getName();                     if (name.equals("title")) {                               title = readTitle(parser);                     } else if (name.equals("summary")) {                               summary = readSummary(parser);                     } else if (name.equals("link")) {                               link = readLink(parser);                     } else {                         skip(parser);               } } return new Entry(title, summary, link); }
  // 处理title标签. private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException {           parser.require(XmlPullParser.START_TAG, ns, "title");           String title = readText(parser);           parser.require(XmlPullParser.END_TAG, ns, "title");           return title; }
  // 处理link标签. private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException {           String link = "";           parser.require(XmlPullParser.START_TAG, ns, "link");           String tag = parser.getName();           String relType = parser.getAttributeValue(null, "rel");           if (tag.equals("link")) {                     if (relType.equals("alternate")){                               link = parser.getAttributeValue(null, "href");                               parser.nextTag();                     }           }           parser.require(XmlPullParser.END_TAG, ns, "link");           return link; }
  // 处理summary标签. private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException {           parser.require(XmlPullParser.START_TAG, ns, "summary");           String summary = readText(parser);           parser.require(XmlPullParser.END_TAG, ns, "summary");           return summary; }
  // 为title和summary标签获取他们的文本值. private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {           String result = "";           if (parser.next() == XmlPullParser.TEXT) {                     result = parser.getText();                     parser.nextTag();           }           return result; } ... }
  ;  | 
跳过你不需要的标签 Skip Tags You Don't Care About
在上述解析XML的描述中,其中的一个步骤是跳过你不需要的标签,以下是解析器的skip方法:
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ;  | 
 private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {           if (parser.getEventType() != XmlPullParser.START_TAG) {                     throw new IllegalStateException();           }           int depth = 1;           while (depth != 0) {                     switch (parser.next()) {                           case XmlPullParser.END_TAG:                               depth--;                               break;                           case XmlPullParser.START_TAG:                               depth++;                               break;                     }           } }
  ;  | 
以下为执行过程:
如果当前事件不是START_TAG就抛出一个异常。
解析START_TAG并依此解析至其他的(START_TAG)事件,包括匹配END_TAG事件
为了确保在对应的END_TAG停止,而不是在原START_TAG后面遇到的第一个标签,它会保存内嵌(标签)深度的轨迹。
因此,如果当前标签元素有内嵌标签,depth变量值就不会为0,直到解析器将原START_TAG与相应的END_TAG之前的事件解析完成。
例如,考虑如何让解析器跳过标签,并且这个标签中还内嵌了以及标签:
- 第一次通过while循环,解析器在标签之后遇到的第一个标记是标签的START_TAG,这时,depth变量值增为2.
 - 第二次通过while循环,解析器遇到的是END_TAG,也就是标签,这时,depth变量会减至1.
 - 第三次通过while循环,解析器遇到的下一个标记是START_TAG,也就是标签,depth变量增为2.
 - 第四次通过while循环,解析与遇到的是END_TAG.也就是标签,depth变量值减至1.
 - 第五次也是最后一次通过while循环,解析器遇到的是END_TAG,也就是,这时depth变量值减至0,这也说明标签已经成功跳过。
 
使用XML数据 Consume XML Data
这个应用例子使用AsyncTask获取并解析XML数据源,此操作会脱离UI主线程(也就是所谓的异步),当操作完毕,应用会在主activity中修改UI(NetworkActivity)。
以下代码片段中,loadPage方法主要执行:
初始化一个指向XML数据源的Url字符型变量
如果用户的设置以及网络连接允许,则执行new DownloadXmlTask.execute(url)实例化一个新的DownloadXmlTask对象(AsyncTask子类),并运行其execute方法,
下载并解析数据源,然后返回一个用户UI显示的字符串。
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ;  | 
 public class NetworkActivity extends Activity {           public static final String WIFI = "Wi-Fi";           public static final String ANY = "Any";           private static final String URL android&sort=newest";
            // 是否是Wi-Fi连接.           private static boolean wifiConnected = false;           // 是否是手机移动网络连接.           private static boolean mobileConnected = false;           // 显示是否要被刷新.           public static boolean refreshDisplay = true;           public static String sPref = null;
            ...
            // 使用AsyncTask从stackoverflow.com下载XML数据源.           public void loadPage() {
                      if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) {                               new DownloadXmlTask().execute(URL);                     }                     else if ((sPref.equals(WIFI)) && (wifiConnected)) {                               new DownloadXmlTask().execute(URL);                     } else {                         // 显示错误               } }
  ;  | 
AsyncTask的子类(如下)DownloadXmlTask类实现了AsyncTask的以下方法:
doInBackground(),接收数据源的Url作为参数传入,并调用loadXmlFormNetwork方法,该方法获取并处理数据源,当处理完毕则返回一个字符串。
onPostExecute(),接收返回的字符串并显示在界面上(UI)。
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ;  | 
 // AsyncTask的实现类用来从stackoverflow.com下载XML数据源. private class DownloadXmlTask extends AsyncTask<String, Void, String> {           @Override           protected String doInBackground(String... urls) {                     try {                               return loadXmlFromNetwork(urls[0]);                     } catch (IOException e) {                         return getResources().getString(R.string.connection_error);               } catch (XmlPullParserException e) {                   return getResources().getString(R.string.xml_error); } }
  @Override protected void onPostExecute(String result) {               setContentView(R.layout.main);               // 通过WebView在UI上显示HTML字符串               WebView myWebView = (WebView) findViewById(R.id.webview);               myWebView.loadData(result, "text/html", null); } }
  ;  | 
下方的loadXmlFromNetwork在DownloadXmlTask类被执行,其主要执行以下:
1.实例化一个StackOverflowXmlParser,并且创建一个Entry类型的List对象,以及title,url和summary变量来保存从XMl数据源中获取的相对应的字段。
2.调用downloadUrl方法,遍历数据并返回一个InputStream对象。
3.使用StackOverflowXmlParser解析InputStream,并将解析完毕的数据填充List变量entries。
4.处理entries对象,将解析的数据与HTML标签合并。
5.返回一个HTML字符串对象,并在AsyncTask类的onPostExecute方法中显示在主activity的界面上。
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 ;  | 
 // 上传从stackoverflow.com获取的XML数据, 解析并合并其HTML标记,返回一个HTML字符串 private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException {           InputStream stream = null;           // 实例化解析器           StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser();           List<Entry> entries = null;           String title = null;           String url = null;           String summary = null;           Calendar rightNow = Calendar.getInstance();           DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa");
            // 检查用户是否设置首选项           SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);           boolean pref = sharedPrefs.getBoolean("summaryPref", false);
            StringBuilder htmlString = new StringBuilder();           htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>");           htmlString.append("<em>" + getResources().getString(R.string.updated) + " " +                   formatter.format(rightNow.getTime()) + "</em>");
            try {                     stream = downloadUrl(urlString);                     entries = stackOverflowXmlParser.parse(stream);                 // 确保应用用完InputStream后关闭它           } finally {               if (stream != null) {                         stream.close();               } }
  // StackOverflowXmlParser返回一个名为entries的List<Entry>对象 // 每一个Entry对象都表示在XML数据源中独立的项 // 本节处理entries对象,将每一个entry对象和HTML标签合并 // 每个entry都作为一个包含文本摘要的链接显示在UI上 for (Entry entry : entries) {               htmlString.append("<p><a href='");               htmlString.append(entry.link);               htmlString.append("'>" + entry.title + "</a></p>");               // 如果用户设置包含摘要文本,追加到显示中               if (pref) {                         htmlString.append(entry.summary);               } } return htmlString.toString(); }
  // 给一个URL, 设置连接并获取输入流 private InputStream downloadUrl(String urlString) throws IOException {           URL url = new URL(urlString);           HttpURLConnection conn = (HttpURLConnection) url.openConnection();           conn.setReadTimeout(10000 /*  milliseconds * /);           conn.setConnectTimeout(15000 /*  milliseconds * /);           conn.setRequestMethod("GET");           conn.setDoInput(true);           // 启动查询           conn.connect();           InputStream stream = conn.getInputStream(); } ;  |