可扩展标记语言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(); } ; |