Android解析 XML 數(shù)據(jù)

2018-08-02 17:41 更新

編寫:kesenhoo - 原文:http://developer.android.com/training/basics/network-ops/xml.html

Extensible Markup Language(XML)是一組將文檔編碼成機器可讀形式的規(guī)則,也是一種在網(wǎng)絡(luò)上共享數(shù)據(jù)的普遍格式。頻繁更新內(nèi)容的網(wǎng)站,比如新聞網(wǎng)站或者博客,經(jīng)常會提供 XML 提要(XML feed)來使得外部程序可以跟上內(nèi)容的變化。下載與解析 XML 數(shù)據(jù)是網(wǎng)絡(luò)連接相關(guān) app 的一個常見功能。 這一課會介紹如何解析 XML 文檔并使用它們的數(shù)據(jù)。

示例NetworkUsage.zip

選擇一個 Parser

我們推薦 XmlPullParser,它是 Android 上一個高效且可維護的解析 XML 的方法。 Android 上有這個接口的兩種實現(xiàn)方式:

兩個選擇都是比較好的。下面的示例中是通過 Xml.newPullParser() 得到 ExpatPullParser。

分析 Feed

解析一個 feed 的第一步是決定我們需要獲取的字段。這樣解析器便去抽取出那些需要的字段而忽視其他的字段。

下面的XML片段是章節(jié)概覽示例 app 中解析的 Feed 的片段。StackOverflow.com 上每一個帖子在 feed 中以包含幾個嵌套的子標(biāo)簽的 entry 標(biāo)簽的形式出現(xiàn)。

<?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"  rel="external nofollow" target="_blank"  />
        <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>

示例 app 從 entry 標(biāo)簽與它的子標(biāo)簽 title,link 和 summary 中提取數(shù)據(jù).

實例化 Parser

下一步就是實例化一個 parser 并開始解析的操作。在下面的片段中,一個 parser 被初始化來處理名稱空間,并且將 InputStream 作為輸入。它通過調(diào)用 nextTag() 開始解析,并調(diào)用 readFeed() 方法,readFeed() 方法會提取并處理 app 需要的數(shù)據(jù):

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();
        }
    }
 ...
}

讀取Feed

readFeed() 方法實際的工作是處理 feed 的內(nèi)容。它尋找一個 "entry" 的標(biāo)簽作為遞歸處理整個 feed 的起點。readFeed() 方法會跳過不是 entry 的標(biāo)簽。當(dāng)整個 feed 都被遞歸處理后,readFeed() 會返回一個從 feed 中提取的包含了 entry 標(biāo)簽內(nèi)容(包括里面的數(shù)據(jù)成員)的 List。然后這個 List 成為 parser 的返回值。

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 feed 的步驟如下:

  1. 正如在上面分析 Feed 所說的,判斷出應(yīng)用中想要的標(biāo)簽。這個例子抽取了 entry 標(biāo)簽與它的內(nèi)部標(biāo)簽 title,link 和 summary 中的數(shù)據(jù)。
  2. 創(chuàng)建下面的方法:
  3. 為每一個我們想要獲取的標(biāo)簽創(chuàng)建一個 "read" 方法。例如 readEntry()readTitle() 等等。解析器從輸入流中讀取標(biāo)簽。當(dāng)讀取到 entry,title,link 或者 summary 標(biāo)簽時,它會為那些標(biāo)簽調(diào)用相應(yīng)的方法。否則,跳過這個標(biāo)簽。

  4. 為每一個不同的標(biāo)簽創(chuàng)建提取數(shù)據(jù)的方法,和使 parser 繼續(xù)解析下一個標(biāo)簽的方法。例如:

    • 對于 title 和 summary 標(biāo)簽,解析器調(diào)用 readText()。這個方法通過調(diào)用 parser.getText() 來獲取數(shù)據(jù)。

    • 對于 link 標(biāo)簽,解析器先判斷這個 link 是否是我們想要的類型。然后再使用 parser.getAttributeValue() 來獲取 link 標(biāo)簽的值。

    • 對于 entry 標(biāo)簽,解析器調(diào)用 readEntry()。這個方法解析 entry 的內(nèi)部標(biāo)簽并返回一個帶有 title,link 和 summary 數(shù)據(jù)成員的 Entry 對象。

  5. 一個遞歸的輔助方法:skip()。關(guān)于這部分的討論,請看下面一部分內(nèi)容:跳過不關(guān)心的標(biāo)簽。

下面的代碼演示了如何解析 entries,titles,links 與 summaries。

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;
}
  ...
}

跳過不關(guān)心的標(biāo)簽

上面描述的 XML 解析步驟中有一步就是跳過不關(guān)心的標(biāo)簽,下面演示解析器的 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;
        }
    }
}

下面解釋這個方法如何工作:

  • 如果當(dāng)前事件不是一個 START_TAG,拋出異常。
  • 它消耗掉 START_TAG 以及接下來的所有內(nèi)容,包括與開始標(biāo)簽配對的 END_TAG。
  • 為了保證方法在遇到正確的 END_TAG 時停止,而不是在最開始的 START_TAG 后面的第一個標(biāo)簽,方法隨時記錄嵌套深度。

因此如果目前的標(biāo)簽有子標(biāo)簽, 那么直到解析器已經(jīng)處理了所有位于 START_TAG 與對應(yīng)的 END_TAG 之間的事件之前,depth 的值不會為 0。例如,看解析器如何跳過 <author> 標(biāo)簽,它有2個子標(biāo)簽,<name> 與 <uri>

  • 第一次循環(huán), 在 <author> 之后 parser 遇到的第一個標(biāo)簽是 <name> 標(biāo)簽的 START_TAG。depth 值變?yōu)?。
  • 第二次循環(huán), parser 遇到的下一個標(biāo)簽是 END_TAG </name>。depth 值變?yōu)?。
  • 第三次循環(huán), parser 遇到的下一個標(biāo)簽是 START_TAG <uri>。depth 值變?yōu)?。
  • 第四次循環(huán), parser 遇到的下一個標(biāo)簽是 END_TAG </uri>。depth 值變?yōu)?。
  • 第五次同時也是最后一次循環(huán), parser 遇到的下一個標(biāo)簽是 END_TAG </author>。 depth 值變?yōu)?。表明成功跳過了 <author> 標(biāo)簽。

使用 XML 數(shù)據(jù)

示例程序是在 AsyncTask 中獲取與解析 XML 數(shù)據(jù)的。這會在主 UI 線程之外進行處理。當(dāng)處理完畢后,app 會更新 main activity(NetworkActivity)的 UI。

在下面示例代碼中,loadPage() 方法做了下面的事情:

  • 初始化一個帶有 URL 地址的字符串變量,用來訂閱 XML feed。
  • 如果用戶設(shè)置與網(wǎng)絡(luò)連接都允許,會調(diào)用 new DownloadXmlTask().execute(url)。這會初始化一個新的 DownloadXmlTask 對象(AsyncTask 的子類)并且開始執(zhí)行它的 execute() 方法,這個方法會下載并解析 feed,并返回展示在 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,實現(xiàn)了 AsyncTask 的如下方法:

  • doInBackground() 執(zhí)行 loadXmlFromNetwork() 方法。它以 feed 的 URL 作為參數(shù)。loadXmlFromNetwork() 獲取并處理 feed。當(dāng)它完成時,返回一個結(jié)果字符串。
  • onPostExecute() 接收返回的字符串并將其展示在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);
    }
}

下面是 DownloadXmlTask 中調(diào)用的 loadXmlFromNetwork() 方法做的事情:

  1. 實例化一個 StackOverflowXmlParser。它同樣創(chuàng)建一個 Entry 對象(entries)的 List,和 title,url,summary,來保存從 XML feed 中提取的值。
  2. 調(diào)用 downloadUrl(),它會獲取 feed, 并將其作為 InputStream 返回。
  3. 使用 StackOverflowXmlParser 解析 InputStream。StackOverflowXmlParser 用從 feed 中獲取的數(shù)據(jù)填充 entries 的 List。
  4. 處理 entries 的 List,并將 feed 數(shù)據(jù)與 HTML 標(biāo)記結(jié)合起來。
  5. 返回一個 HTML 字符串,AsyncTask 的 onPostExecute() 方法會將其展示在 main activity 的 UI 上。
// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.【這里可以看出應(yīng)該是Download】
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.
【關(guān)于Timeout具體應(yīng)該設(shè)置多少,可以借鑒這里的數(shù)據(jù),當(dāng)然前提是一般情況下】
// 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();
}


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號