注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。
原文链接: http://developer.android.com/training/basics/network-ops/xml.html
可扩展标记语言(XML)是一种将文档编码为机器可阅读的形式的规则集合。XML是一种在互联网中分享数据的比较流行的格式。那些频繁更新内容的网站(如新的站点或者博客),经常会提供一个XML源,这样外部程序就可以与内容变更保持同步。上传及解析XML数据对于需要联网的应用来说是一个很平常的任务。这节课将讲解如何解析XML文档并使用它们的数据。
一). 选择一个解析器
我们推荐使用 XmlPullParser ,它是一个在Android上解析XML的一种比较有效及稳定的方法。历史中Android有两种实现该接口的方法:
-
通过
XmlPullParserFactory.newPullParser()
实现的
KXmlParser 。 - 通过 Xml.newPullParser() 实现的 ExpatPullParser 。
每一种选择都是可以的。不过这里我们使用第二个例子。
二). 分析源
解析源的第一步是决定哪些字段是你感兴趣的。解析器会提取这些你感兴趣的字段数据并把其余的忽略。
下面是在应用中被解析的源的一段摘录。每一个到 StackOverflow.com 的推送都会在源中显示为一个 entry 标签,并 包含若干 entry 子标签:
<?
xml version="1.0" encoding="utf-8"
?>
<
feed
xmlns
="http://www.w3.org/2005/Atom"
xmlns:creativeCommons
="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
scheme
="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags"
term
="android"
/>
<
category
scheme
="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags"
term
="file"
/>
<
author
>
<
name
>
cliff2310
</
name
>
<
uri
>
http://stackoverflow.com/users/1128925
</
uri
>
</
author
>
<
link
rel
="alternate"
href
="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 子标签的数据。
三). 初始化解析器
下一步是初始化解析器,并启动解析的步骤。在下面的代码片段中,一个不处理命名空间的解析器被初始化,并使用 InputStream 作为参数。通过调用 nextTag() 开始解析的步骤,并激活 readFeed() 方法,该方法提取并处理应用感兴趣的数据:
public
class
StackOverflowXmlParser {
//
We don't use namespaces
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();
}
}
...
}
四). 阅读源
readFeed()
方法执行一些工作来处理源。它寻找
entry
标签作为开始递归处理的起始点。如果一个标签不是
entry
标签,那么就忽略它。一点整个源都被递归处理完了,
readFeed()
方法返回一个包含它从源中提取的字段的
List
(包含子数据成员)。该
List
被解析器返回。
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();
//
Starts by looking for the entry tag
if
(name.equals("entry"
)) {
entries.add(readEntry(parser));
}
else
{
skip(parser);
}
}
return
entries;
}
五). 解析XML
解析一个XML源的步骤如下:
- 如第二节中所述,在你的应用中标识出你希望包含的标签。该例子中提取的数据为 entry 标签及其子标签: title , link 和 summary 子标签的数据。
- 创建下列方法:
-
- 为每个你感兴趣的标签创建“ read ”方法。例如, readEntry(), readTitle()等。解析器从输入流中读取标签。当它遇到了名为 entry, title, link或 summary时,它会为标签调用相应的方法。否则就略过该标签。
-
为每个不同类型标签提取数据并将解析器推进到下一个标签的方法。例如:
- 对于 title和 summary标签,解析器调用 readText()。该方法提取通过调用 parser.getText(),从这些标签中提取数据。
- 对于 link标签,解析器首先确定该link是否是自己感兴趣的,如果是的话就调用 parser.getAttributeValue()来提取它的值。
- 对于 entry标签,解析器会调用 readEntry()。该方法解析entry中的子标签,并返回一个 Entry 对象,其中包含了数据成员: title, link和 summary。
- 一个用以辅助的方法 skip()。更多信息可以阅读: Skip Tags You Don't Care About 。
下列代码片段展示了如何解析上述标签:
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;
}
}
//
Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off
//
to their respective "read" methods for processing. Otherwise, skips the tag.
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);
}
//
Processes title tags in the feed.
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;
}
//
Processes link tags in the feed.
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;
}
//
Processes summary tags in the feed.
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;
}
//
For the tags title and summary, extracts their text values.
private
String readText(XmlPullParser parser)
throws
IOException, XmlPullParserException {
String result
= ""
;
if
(parser.next() ==
XmlPullParser.TEXT) {
result
=
parser.getText();
parser.nextTag();
}
return
result;
}
...
}
六). 跳过你不关注的标签
上面所描述的解析XML步骤中,其中有一步是解析器跳过我们不关注的标签。下面是skip()方法的代码:
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 ,以及之后遇到的内容,并匹配 END_TAG 。
- 为了确保它在正确的 END_TAG 停止,而不是在 START_TAG 之后遇到的第一个标签,它会一直向子标签深度搜索。
因此如果当前标签含有子标签,那么depth的值不会变成0,直到解析器处理了所有在原始的
START_TAG
和与它匹配的
END_TAG
之间的所有标签。例如,考虑该解析器如何略过<
author
>标签,该标签含有两个子标签
<name>和
<uri>:
- 第一次while循环,解析器在 <author>之后 遇到了 START_TAG: <name> ,此时 depth的值增加到2。
-
第二次while循环,解析器遇到了
END_TAG:
</name>
。此时 depth的值减少到 1 。 -
第三次while循环,解析器遇到了
START_TAG: <uri> 。此时 depth的值增加到2 。 -
第四次while循环,解析器遇到了END_TAG:
</uri>
。此时 depth 的值减少到 1 。 - 最后一次while循环,解析器遇到了 END_TAG: </author>。此时depth的值减少到0,表明 <author>已经被成功忽略了。
七). 处理XML数据
样例代码中,使用了 AsyncTask 获取并解析XML源。这样该过程就不会再UI主线程中执行。当处理执行完毕,应用会更新主Activity( NetworkActivity )的UI。
在下面摘录的代码片段中, loadPage()方法进行了如下的处理:
用XML源的URL初始化一个String变量。
在用户的设置及网络连接允许的情况下,调用 new DownloadXmlTask().execute(url)。这将初始化一个新的 DownloadXmlTask对象( AsyncTask 的子类)并运行它的 execute() 方法,它会下载并解析源并将结果以String的形式返回,显示在UI上。
public
class
NetworkActivity
extends
Activity {
public
static
final
String WIFI = "Wi-Fi"
;
public
static
final
String ANY = "Any"
;
private
static
final
String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"
;
//
Whether there is a Wi-Fi connection.
private
static
boolean
wifiConnected =
false
;
//
Whether there is a mobile connection.
private
static
boolean
mobileConnected =
false
;
//
Whether the display should be refreshed.
public
static
boolean
refreshDisplay =
true
;
public
static
String sPref =
null
;
...
//
Uses AsyncTask to download the XML feed from stackoverflow.com.
public
void
loadPage() {
if
((sPref.equals(ANY)) && (wifiConnected ||
mobileConnected)) {
new
DownloadXmlTask().execute(URL);
}
else
if
((sPref.equals(WIFI)) &&
(wifiConnected)) {
new
DownloadXmlTask().execute(URL);
}
else
{
//
show error
}
}
AsyncTask 的子类:DownloadXmlTask如下所示,它实现了下列 AsyncTask 的方法:
doInBackground() 执行 loadXmlFromNetwork(),它将源的URL作为参数传入。 loadXmlFromNetwork()方法获取并处理源。当它结束以后,它会返回String作为结果。
onPostExecute() 接收结果String并将它显示在UI上。
//
Implementation of AsyncTask used to download XML feed from stackoverflow.com.
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);
//
Displays the HTML string in the UI via a WebView
WebView myWebView =
(WebView) findViewById(R.id.webview);
myWebView.loadData(result,
"text/html",
null
);
}
}
下面是方法:loadXmlFromNetwork(),它被 DownloadXmlTask调用,它执行下列任务:
- 初始化一个 StackOverflowXmlParser,它也创建一个装载entry对象的 List ( entries ),以及 title, url,和 summary,来存储从XML源中相应字段里提取出的数据。
- 调用 downloadUrl(),它获取源并以 InputStream 的形式返回
- 使用 StackOverflowXmlParser来解析 InputStream 。 StackOverflowXmlParser会用源中的数据填充 entries这个 List 。
- 处理 List ,并将源数据和HTML标记向结合。
- 返回 HTML字符串,由 AsyncTask 的 onPostExecute() 方法将它 显示在主Activity UI上的。
//
Uploads XML from stackoverflow.com, parses it, and combines it with
//
HTML markup. Returns HTML string.
private
String loadXmlFromNetwork(String urlString)
throws
XmlPullParserException, IOException {
InputStream stream
=
null
;
//
Instantiate the parser
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"
);
//
Checks whether the user set the preference to include summary text
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);
//
Makes sure that the InputStream is closed after the app is
//
finished using it.
}
finally
{
if
(stream !=
null
) {
stream.close();
}
}
//
StackOverflowXmlParser returns a List (called "entries") of Entry objects.
//
Each Entry object represents a single post in the XML feed.
//
This section processes the entries list to combine each entry with HTML markup.
//
Each entry is displayed in the UI as a link that optionally includes
//
a text summary.
for
(Entry entry : entries) {
htmlString.append(
"<p><a href='"
);
htmlString.append(entry.link);
htmlString.append(
"'>" + entry.title + "</a></p>"
);
//
If the user set the preference to include summary text,
//
adds it to the display.
if
(pref) {
htmlString.append(entry.summary);
}
}
return
htmlString.toString();
}
//
Given a string representation of a URL, sets up a connection and gets
//
an input stream.
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
);
//
Starts the query
conn.connect();
return
conn.getInputStream();
}

