Analizza dati XML

Extensible Markup Language (XML) è un insieme di regole per la codifica di documenti in formato leggibile. XML è un formato popolare per la condivisione di dati su internet.

I siti web che aggiornano spesso i propri contenuti, come i siti di notizie o i blog, spesso forniscono un feed XML in modo che i programmi esterni possano tenersi aggiornati sulle modifiche ai contenuti. Il caricamento e l'analisi dei dati XML è un'attività comune per le app connesse alla rete. Questo argomento spiega come analizzare i documenti XML e come utilizzarli.

Per scoprire di più sulla creazione di contenuti basati sul web nella tua app per Android, consulta Contenuti basati sul web.

Scegli un parser

Consigliamo XmlPullParser, che è un modo efficiente e gestibile per analizzare il codice XML su Android. Android ha due implementazioni di questa interfaccia:

Entrambe le opzioni va bene. L'esempio in questa sezione utilizza ExpatPullParser e Xml.newPullParser().

Analizzare il feed

Il primo passaggio dell'analisi di un feed consiste nel decidere quali campi ti interessano. Il parser estrae i dati per questi campi e ignora il resto.

Consulta il seguente estratto di un feed analizzato nell'app di esempio. Ogni post su StackOverflow.com viene visualizzato nel feed come tag entry contenente diversi tag nidificati:

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

L'app di esempio estrae i dati per il tag entry e i relativi tag nidificati title, link e summary.

Crea l'istanza del parser

Il passaggio successivo dell'analisi di un feed consiste nel creare un parser e avviare la procedura di analisi. Questo snippet inizializza un parser in modo che non elabori gli spazi dei nomi e per utilizzare il valore InputStream fornito come input. Avvia il processo di analisi con una chiamata all'indirizzo nextTag() e richiama il metodo readFeed(), che estrae ed elabora i dati a cui l'app è interessata:

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

Leggi il feed

Il metodo readFeed() si occupa dell'elaborazione effettiva del feed. Cerca gli elementi contrassegnati con "entry" come punto di partenza per l'elaborazione ricorsiva del feed. Se un tag non è entry, viene ignorato. Una volta che l'intero feed è stato elaborato in modo ricorsivo, readFeed() restituisce un List contenente le voci (inclusi i membri di dati nidificati) che ha estratto dal feed. Questo valore List viene quindi restituito dal 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;
}

Analizza XML

Per analizzare un feed XML, procedi nel seguente modo:

  1. Come descritto in Analizzare il feed, identifica i tag da includere nella tua app. Questo esempio estrae i dati per il tag entry e i relativi tag nidificati: title, link e summary.
  2. Crea i seguenti metodi:

    • Un metodo "read" per ogni tag da includere, ad esempio readEntry() e readTitle(). Il parser legge i tag dal flusso di input. Quando incontra un tag denominato, in questo esempio, entry, title, link o summary, richiama il metodo appropriato per quel tag. In caso contrario, il tag viene ignorato.
    • Metodi per estrarre i dati per ogni tipo di tag diverso e per far avanzare il parser al tag successivo. In questo esempio, i metodi pertinenti sono i seguenti:
      • Per i tag title e summary, l'analizzatore sintattico chiama readText(). Questo metodo estrae i dati per questi tag chiamando parser.getText().
      • Per il tag link, il parser estrae i dati dei link determinando innanzitutto se il link è il tipo a cui sono interessati. Poi utilizza parser.getAttributeValue() per estrarre il valore del collegamento.
      • Per il tag entry, l'analizzatore sintattico chiama readEntry(). Questo metodo analizza i tag nidificati della voce e restituisce un oggetto Entry con i membri dei dati title, link e summary.
    • Un metodo skip() helper ricorsivo. Per ulteriori informazioni su questo argomento, vedi Ignorare i tag che non ti interessano.

Questo snippet mostra in che modo il parser analizza voci, titoli, link e riepiloghi.

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

Ignora i tag che non ti interessano

Il parser deve saltare i tag che non gli interessano. Ecco il metodo skip() del parser:

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

Ecco come funziona:

  • Genera un'eccezione se l'evento attuale non è un START_TAG.
  • Consuma il valore START_TAG e tutti gli eventi fino al END_TAG corrispondente.
  • Tiene traccia della profondità di nidificazione per assicurarsi che si fermi al END_TAG corretto e non al primo tag rilevato dopo il START_TAG originale.

Di conseguenza, se l'elemento corrente ha elementi nidificati, il valore di depth non sarà 0 finché l'analizzatore sintattico non consuma tutti gli eventi compresi tra l'elemento START_TAG originale e il END_TAG corrispondente. Ad esempio, considera in che modo il parser ignora l'elemento <author>, che ha due elementi nidificati, <name> e <uri>:

  • La prima volta nel loop while, il tag successivo rilevato dall'analizzatore sintattico dopo <author> è il tag START_TAG per <name>. Il valore di depth viene incrementato di 2.
  • La seconda volta nel loop while, il tag successivo rilevato dall'analizzatore sintattico è </name> END_TAG. Il valore di depth diminuisce a 1.
  • La terza volta nel loop while, il tag successivo rilevato dall'analizzatore sintattico è <uri> START_TAG. Il valore di depth viene incrementato fino a 2.
  • La quarta volta nel loop while, il tag successivo incontrato dall'analizzatore sintattico è </uri> END_TAG. Il valore di depth diminuisce a 1.
  • La quinta e ultima volta del loop while, il tag successivo rilevato dal parser è END_TAG </author>. Il valore di depth diminuisce a 0, per indicare che l'elemento <author> è stato saltato correttamente.

Utilizza dati XML

L'applicazione di esempio recupera e analizza il feed XML in modo asincrono. In questo modo l'elaborazione viene rimossa dal thread dell'interfaccia utente principale. Al termine dell'elaborazione, l'app aggiorna l'interfaccia utente nell'attività principale, NetworkActivity.

Nel seguente estratto, il metodo loadPage():

  • Inizializza una variabile stringa con l'URL per il feed XML.
  • Richiama il metodo downloadXml(url), se le impostazioni dell'utente e la connessione di rete lo consentono. Questo metodo scarica e analizza il feed e restituisce un risultato stringa da visualizzare nell'interfaccia utente.

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

Il metodo downloadXml chiama i seguenti metodi in Kotlin:

  • lifecycleScope.launch(Dispatchers.IO), che utilizza coroutine di Kotlin per avviare il metodo loadXmlFromNetwork() nel thread di IO. Trasmette l'URL del feed come parametro. Il metodo loadXmlFromNetwork() recupera ed elabora il feed. Al termine, restituisce una stringa di risultati.
  • withContext(Dispatchers.Main), che utilizza coroutine Kotlin per tornare al thread principale, prende la stringa restituita e la visualizza nell'interfaccia utente.

Nel linguaggio di programmazione Java, la procedura è la seguente:

  • Un Executor esegue il metodo loadXmlFromNetwork() su un thread in background. Trasmette l'URL del feed come parametro. Il metodo loadXmlFromNetwork() recupera ed elabora il feed. Al termine, restituisce una stringa di risultati.
  • Un Handler chiama post per tornare al thread principale, prende la stringa restituita e la visualizza nell'interfaccia utente.

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

Il metodo loadXmlFromNetwork() richiamato da downloadXml è mostrato nello snippet successivo. In questo modo:

  1. Crea un'istanza di StackOverflowXmlParser. Crea inoltre variabili per un List di Entry oggetti (entries) e per title, url e summary, per mantenere i valori estratti dal feed XML per questi campi.
  2. Richiama downloadUrl(), che recupera il feed e lo restituisce come InputStream.
  3. Utilizza StackOverflowXmlParser per analizzare InputStream. StackOverflowXmlParser compila un List di entries con i dati del feed.
  4. Elabora l'List entries e combina i dati del feed con il markup HTML.
  5. Restituisce una stringa HTML che viene visualizzata nell'interfaccia utente dell'attività principale.

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