Analizuj dane XML

Extensible Markup Language (XML) to zestaw reguł kodowania dokumentów w postaci czytelnej dla komputerów. Format XML to popularny format udostępniania danych w internecie.

Witryny, które często aktualizują treść, takie jak serwisy informacyjne czy blogi, często dostarczają kanał XML, dzięki czemu zewnętrzne programy mogą na bieżąco śledzić zmiany treści. Przesyłanie i analizowanie danych XML to częste zadanie w przypadku aplikacji połączonych z siecią. W tym temacie dowiesz się, jak analizować dokumenty XML i wykorzystywać zawarte w nich dane.

Więcej informacji o tworzeniu treści internetowych w aplikacji na Androida znajdziesz w artykule Treści internetowe.

Wybierz parser

Zalecamy korzystanie z XmlPullParser – efektywnego i łatwego w utrzymaniu sposobu analizowania kodu XML na Androidzie. Android ma 2 implementacje tego interfejsu:

Każda z tych opcji jest odpowiednia. W przykładzie w tej sekcji użyto znaczników ExpatPullParser i Xml.newPullParser().

Analizowanie karty

Pierwszym krokiem przy analizowaniu pliku danych jest określenie, które pola Cię interesują. Parser wyodrębnia dane z tych pól i ignoruje pozostałe.

Zobacz przykładowy fragment przeanalizowanego kanału w przykładowej aplikacji. Każdy post na stronie StackOverflow.com pojawia się w kanale jako tag entry zawierający kilka zagnieżdżonych tagów:

<?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>

Przykładowa aplikacja wyodrębnia dane do tagu entry i jego zagnieżdżonych tagów title, link i summary.

Utwórz instancję parsera

Następnym krokiem analizy pliku danych jest zainicjowanie parsera i rozpoczęcie procesu analizy. Ten fragment inicjuje parser, aby nie przetwarzać przestrzeni nazw i używać podanej InputStream jako danych wejściowych. Rozpoczyna proces analizy od wywołania funkcji nextTag() i wywołuje metodę readFeed(), która wyodrębnia i przetwarza dane interesujące dla aplikacji:

Kotlin

// We don't use namespaces.
private val ns: String? = null

class StackOverflowXmlParser {

    @Throws(XmlPullParserException::class, IOException::class)
    fun parse(inputStream: InputStream): List<*> {
        inputStream.use { inputStream ->
            val parser: XmlPullParser = Xml.newPullParser()
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
            parser.setInput(inputStream, null)
            parser.nextTag()
            return readFeed(parser)
        }
    }
 ...
}

Java

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

Czytanie kart

Metoda readFeed() sama przetwarza plik danych. Szuka elementów oznaczonych tagiem „entry” jako punktu wyjścia dla rekurencyjnego przetwarzania pliku danych. Jeśli tag nie jest tagiem entry, zostanie pominięty. Gdy cały plik danych zostanie przetworzony rekurencyjnie, readFeed() zwróci wartość List zawierającą wpisy (w tym zagnieżdżone dane) wyodrębnione z pliku. Ta wartość List jest następnie zwracana przez parser.

Kotlin

@Throws(XmlPullParserException::class, IOException::class)
private fun readFeed(parser: XmlPullParser): List<Entry> {
    val entries = mutableListOf<Entry>()

    parser.require(XmlPullParser.START_TAG, ns, "feed")
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.eventType != XmlPullParser.START_TAG) {
            continue
        }
        // Starts by looking for the entry tag.
        if (parser.name == "entry") {
            entries.add(readEntry(parser))
        } else {
            skip(parser)
        }
    }
    return entries
}

Java

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

Analizuj XML

Aby przeanalizować plik danych XML:

  1. Jak opisano w sekcji Analizowanie pliku danych, wskaż tagi, które chcesz umieścić w aplikacji. W tym przykładzie wyodrębniamy dane z tagu entry i jego zagnieżdżonych tagów: title, link i summary.
  2. Utwórz te metody:

    • Metoda „odczyt” w przypadku każdego tagu, który chcesz uwzględnić, np. readEntry() i readTitle(). Parser odczytuje tagi ze strumienia wejściowego. Gdy napotka tag o nazwie entry, title, link lub summary, wywołuje odpowiednią dla niego metodę. W przeciwnym razie tag zostanie pominięty.
    • Metody wyodrębniania danych z poszczególnych typów tagów i przekazywania parsera do następnego tagu. W tym przykładzie odpowiednie metody są następujące:
      • W przypadku tagów title i summary parser wywołuje metodę readText(). Ta metoda wyodrębnia dane dla tych tagów przez wywołanie parser.getText().
      • W przypadku tagu link parser wyodrębnia dane na potrzeby linków, określając najpierw, czy dany link jest tym typem, który Cię interesuje. Następnie za pomocą metody parser.getAttributeValue() wyodrębnia wartość linku.
      • W przypadku tagu entry parser wywołuje metodę readEntry(). Ta metoda analizuje zagnieżdżone tagi wpisu i zwraca obiekt Entry z elementami danych title, link i summary.
    • Rekursywna metoda skip(). Więcej informacji na ten temat znajdziesz w artykule Pomijanie tagów, które Cię nie interesują.

Ten fragment pokazuje, jak parser analizuje wpisy, tytuły, linki i podsumowania.

Kotlin

data class Entry(val title: String?, val summary: String?, val link: String?)

// 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.
@Throws(XmlPullParserException::class, IOException::class)
private fun readEntry(parser: XmlPullParser): Entry {
    parser.require(XmlPullParser.START_TAG, ns, "entry")
    var title: String? = null
    var summary: String? = null
    var link: String? = null
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.eventType != XmlPullParser.START_TAG) {
            continue
        }
        when (parser.name) {
            "title" -> title = readTitle(parser)
            "summary" -> summary = readSummary(parser)
            "link" -> link = readLink(parser)
            else -> skip(parser)
        }
    }
    return Entry(title, summary, link)
}

// Processes title tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readTitle(parser: XmlPullParser): String {
    parser.require(XmlPullParser.START_TAG, ns, "title")
    val title = readText(parser)
    parser.require(XmlPullParser.END_TAG, ns, "title")
    return title
}

// Processes link tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readLink(parser: XmlPullParser): String {
    var link = ""
    parser.require(XmlPullParser.START_TAG, ns, "link")
    val tag = parser.name
    val relType = parser.getAttributeValue(null, "rel")
    if (tag == "link") {
        if (relType == "alternate") {
            link = parser.getAttributeValue(null, "href")
            parser.nextTag()
        }
    }
    parser.require(XmlPullParser.END_TAG, ns, "link")
    return link
}

// Processes summary tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readSummary(parser: XmlPullParser): String {
    parser.require(XmlPullParser.START_TAG, ns, "summary")
    val summary = readText(parser)
    parser.require(XmlPullParser.END_TAG, ns, "summary")
    return summary
}

// For the tags title and summary, extracts their text values.
@Throws(IOException::class, XmlPullParserException::class)
private fun readText(parser: XmlPullParser): String {
    var result = ""
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.text
        parser.nextTag()
    }
    return result
}
...

Java

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

Pomiń tagi, które Cię nie interesują

Parser musi pomijać tagi, którymi nie jest zainteresowany. Oto metoda skip() parsera:

Kotlin

@Throws(XmlPullParserException::class, IOException::class)
private fun skip(parser: XmlPullParser) {
    if (parser.eventType != XmlPullParser.START_TAG) {
        throw IllegalStateException()
    }
    var depth = 1
    while (depth != 0) {
        when (parser.next()) {
            XmlPullParser.END_TAG -> depth--
            XmlPullParser.START_TAG -> depth++
        }
    }
}

Java

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

Proces przebiega następująco:

  • Występuje wyjątek, jeśli bieżące zdarzenie nie jest zdarzeniem START_TAG.
  • Wykorzystuje START_TAG i wszystkie zdarzenia, aż do pasującego elementu END_TAG włącznie.
  • Śledzi głębokość gniazdowania, aby upewnić się, że kończy się na prawidłowym elemencie END_TAG, a nie na pierwszym tagu, który napotka po pierwotnym tagu START_TAG.

Jeśli więc bieżący element zawiera elementy zagnieżdżone, wartość depth nie będzie wynosić 0, dopóki parser nie przetworzy wszystkich zdarzeń między oryginalnym elementem START_TAG a pasującym elementem END_TAG. Zobacz na przykład, jak parser pomija element <author>, który ma 2 zagnieżdżone elementy – <name> i <uri>:

  • Przy pierwszym uruchomieniu pętli while kolejnym tagiem, który parser napotka po <author>, jest START_TAG dla <name>. Wartość depth rośnie do 2.
  • Przy drugim w pętli while kolejnym tagiem, który napotka parser, jest </name> END_TAG. Wartość depth zmniejsza się do 1.
  • Przy 3 pętli while kolejnym tagiem, który napotka parser, jest START_TAG <uri>. Wartość depth rośnie do 2.
  • Przy czwartym pętli while następnym tagiem, który napotka parser, jest END_TAG </uri>. Wartość depth zmniejsza się do 1.
  • Piąty raz i ostatni w pętli while. Następnym tagiem, który napotka parser, jest END_TAG </author>. Wartość pola depth zmniejsza się do 0, co oznacza, że element <author> został pominięty.

Wykorzystywanie danych XML

Przykładowa aplikacja asynchronicznie pobiera i analizuje kanał XML. Wyłącza to przetwarzanie w głównym wątku interfejsu użytkownika. Po zakończeniu przetwarzania aplikacja zaktualizuje interfejs w swoim głównym działaniu: NetworkActivity.

W tym fragmencie metoda loadPage() wykonuje te działania:

  • Inicjuje zmienną ciągu tekstowego z adresem URL kanału XML.
  • Wywołuje metodę downloadXml(url), jeśli pozwalają na to ustawienia użytkownika i połączenie sieciowe. Ta metoda pobiera i analizuje plik danych, a następnie zwraca wynik w postaci ciągu znaków do wyświetlenia w interfejsie.

Kotlin

class NetworkActivity : Activity() {

    companion object {

        const val WIFI = "Wi-Fi"
        const val ANY = "Any"
        const val SO_URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"
        // Whether there is a Wi-Fi connection.
        private var wifiConnected = false
        // Whether there is a mobile connection.
        private var mobileConnected = false

        // Whether the display should be refreshed.
        var refreshDisplay = true
        // The user's current network preference setting.
        var sPref: String? = null
    }
    ...
    // Asynchronously downloads the XML feed from stackoverflow.com.
    fun loadPage() {

        if (sPref.equals(ANY) && (wifiConnected || mobileConnected)) {
            downloadXml(SO_URL)
        } else if (sPref.equals(WIFI) && wifiConnected) {
            downloadXml(SO_URL)
        } else {
            // Show error.
        }
    }
    ...
}

Java

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;
    ...
    // Asynchronously downloads the XML feed from stackoverflow.com.
    public void loadPage() {

        if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) {
            downloadXml(URL);
        }
        else if ((sPref.equals(WIFI)) && (wifiConnected)) {
            downloadXml(URL);
        } else {
            // Show error.
        }
    }

Metoda downloadXml wywołuje w Kotlin te metody:

  • lifecycleScope.launch(Dispatchers.IO), który używa korekty Kotlin do uruchomienia metody loadXmlFromNetwork() w wątku zamówienia reklamowego. Przekazuje adres URL kanału jako parametr. Metoda loadXmlFromNetwork() pobiera i przetwarza plik danych. Po zakończeniu zwraca ciąg znaków wynikowy.
  • withContext(Dispatchers.Main), który korzysta z współprogramów Kotlin do powrotu do wątku głównego, pobiera zwrócony ciąg i wyświetla go w interfejsie.

W języku Java proces przebiega w następujący sposób:

  • Executor wykonuje metodę loadXmlFromNetwork() w wątku w tle. Przekazuje adres URL kanału jako parametr. Metoda loadXmlFromNetwork() pobiera i przetwarza plik danych. Po zakończeniu zwraca ciąg znaków wynikowy.
  • Handler wywołuje metodę post, aby wrócić do wątku głównego, pobiera zwrócony ciąg znaków i wyświetla go w interfejsie użytkownika.

Kotlin

// Implementation of Kotlin coroutines used to download XML feed from stackoverflow.com.
private fun downloadXml(vararg urls: String) {
    var result: String? = null
    lifecycleScope.launch(Dispatchers.IO) {
        result = try {
            loadXmlFromNetwork(urls[0])
        } catch (e: IOException) {
            resources.getString(R.string.connection_error)
        } catch (e: XmlPullParserException) {
            resources.getString(R.string.xml_error)
        }
        withContext(Dispatchers.Main) {
            setContentView(R.layout.main)
            // Displays the HTML string in the UI via a WebView.
            findViewById<WebView>(R.id.webview)?.apply {
                loadData(result?: "", "text/html", null)
            }
        }
    }
}

Java

// Implementation of Executor and Handler used to download XML feed asynchronously from stackoverflow.com.
private void downloadXml(String... urls) {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Handler handler = new Handler(Looper.getMainLooper());
    executor.execute(() -> {
        String result;
            try {
                result = loadXmlFromNetwork(urls[0]);
            } catch (IOException e) {
                result = getResources().getString(R.string.connection_error);
            } catch (XmlPullParserException e) {
                result = getResources().getString(R.string.xml_error);
            }
        String finalResult = result;
        handler.post(() -> {
            setContentView(R.layout.main);
            // Displays the HTML string in the UI via a WebView.
            WebView myWebView = (WebView) findViewById(R.id.webview);
            myWebView.loadData(finalResult, "text/html", null);
        });
    });
}

Metoda loadXmlFromNetwork(), która jest wywoływana z elementu downloadXml, jest widoczna w następnym fragmencie. Wykonuje te działania:

  1. Tworzy instancję StackOverflowXmlParser. Tworzy też zmienne dla List obiektów Entry (entries) oraz dla title, url i summary, aby przechowywać wartości wyodrębnione z pliku XML dla tych pól.
  2. Wywołuje downloadUrl(), który pobiera plik danych i zwraca go jako InputStream.
  3. Używa StackOverflowXmlParser do analizowania InputStream. StackOverflowXmlParser wypełnia pole List dla wartości entries danymi z pliku danych.
  4. Przetwarza entries List i łączy dane z pliku ze znacznikami HTML.
  5. Zwraca ciąg znaków HTML wyświetlany w głównym interfejsie aktywności.

Kotlin

// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.
@Throws(XmlPullParserException::class, IOException::class)
private fun loadXmlFromNetwork(urlString: String): String {
    // Checks whether the user set the preference to include summary text.
    val pref: Boolean = PreferenceManager.getDefaultSharedPreferences(this)?.run {
        getBoolean("summaryPref", false)
    } ?: false

    val entries: List<Entry> = downloadUrl(urlString)?.use { stream ->
        // Instantiates the parser.
        StackOverflowXmlParser().parse(stream)
    } ?: emptyList()

    return StringBuilder().apply {
        append("<h3>${resources.getString(R.string.page_title)}</h3>")
        append("<em>${resources.getString(R.string.updated)} ")
        append("${formatter.format(rightNow.time)}</em>")
        // 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.
        entries.forEach { entry ->
            append("<p><a href='")
            append(entry.link)
            append("'>" + entry.title + "</a></p>")
            // If the user set the preference to include summary text,
            // adds it to the display.
            if (pref) {
                append(entry.summary)
            }
        }
    }.toString()
}

// Given a string representation of a URL, sets up a connection and gets
// an input stream.
@Throws(IOException::class)
private fun downloadUrl(urlString: String): InputStream? {
    val url = URL(urlString)
    return (url.openConnection() as? HttpURLConnection)?.run {
        readTimeout = 10000
        connectTimeout = 15000
        requestMethod = "GET"
        doInput = true
        // Starts the query.
        connect()
        inputStream
    }
}

Java

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