Analyser des données XML

XML (Extensible Markup Language) est un ensemble de règles permettant d'encoder des documents dans un format lisible par un ordinateur. Le format XML est couramment utilisé pour partager des données sur Internet.

Les sites qui mettent fréquemment à jour leur contenu, comme les sites d'actualités ou les blogs, fournissent souvent un flux XML afin que les programmes externes puissent suivre l'évolution du contenu. L'importation et l'analyse des données XML sont des tâches courantes pour les applications connectées au réseau. Cette rubrique vous explique comment analyser des documents XML et utiliser leurs données.

Pour en savoir plus sur la création de contenu Web dans votre application Android, consultez la section Contenu Web.

Choisir un analyseur

Nous vous recommandons d'utiliser XmlPullParser, une interface efficace et facile à gérer pour analyser du code XML sur Android. Android propose deux implémentations de cette interface :

Vous pouvez utiliser n'importe laquelle. Dans cette section, l'exemple utilise ExpatPullParser et Xml.newPullParser().

Analyser le flux

Pour analyser un flux, vous devez tout d'abord déterminer les champs qui vous intéressent. L'analyseur extrait les données de ces champs et ignore les autres.

Consultez l'extrait suivant d'un flux analysé dans l'application exemple. Chaque article publié sur StackOverflow.com apparaît dans le flux en tant que balise entry contenant plusieurs balises imbriquées :

<?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'application exemple extrait des données pour la balise entry, ainsi que ses balises imbriquées title, link et summary.

Instancier l'analyseur

L'étape suivante de l'analyse d'un flux consiste à instancier un analyseur et à lancer le processus d'analyse. Cet extrait initialise un analyseur qui ne traite pas les espaces de noms et qui utilise la classe InputStream fournie comme entrée. Il lance le processus d'analyse avec un appel à nextTag() et appelle la méthode readFeed() qui extrait et traite les données dont l'application a besoin :

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

Lire le flux

La méthode readFeed() procède au traitement effectif du flux. Elle recherche des éléments avec la balise "entry" comme point de départ pour traiter le flux de manière récursive. Si une balise n'est pas de type entry, elle est ignorée. Une fois que l'ensemble du flux a été traité de manière récursive, readFeed() renvoie une List contenant les entrées (y compris les membres de données imbriqués) qu'il a extraites du flux. Cette List est ensuite renvoyée par l'analyseur.

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

Analyser un flux XML

Pour analyser un flux XML, procédez comme suit :

  1. Comme indiqué dans la section Analyser le flux, identifiez les balises que vous souhaitez inclure dans votre application. Cet exemple extrait des données pour la balise entry, et ses balises imbriquées title, link et summary.
  2. Créez les méthodes suivantes :

    • Une méthode de lecture pour chaque balise que vous souhaitez inclure, par exemple readEntry() et readTitle(). L'analyseur lit les balises du flux d'entrée. S'il trouve une balise nommée (entry, title, link ou summary, dans cet exemple), il appelle la méthode appropriée. Dans le cas contraire, il l'ignore.
    • Des méthodes pour extraire des données pour chaque type de balise et faire passer l'analyseur à la balise suivante. Dans cet exemple, les méthodes pertinentes sont les suivantes :
      • Pour les balises title et summary, l'analyseur appelle readText(). Cette méthode extrait des données pour ces balises en appelant parser.getText().
      • Pour la balise link, l'analyseur extrait des données pour les liens en déterminant d'abord si le lien est du type dont il a besoin. Ensuite, il utilise parser.getAttributeValue() pour extraire la valeur du lien.
      • Pour la balise entry, l'analyseur appelle readEntry(). Cette méthode analyse les balises imbriquées de l'entrée, et renvoie un objet Entry avec les membres de données title, link et summary.
    • Une méthode d'assistance skip() récursive. Pour plus d'informations à ce sujet, consultez Ignorer les balises qui ne vous intéressent pas.

Cet extrait montre comment l'analyseur analyse les entrées, les titres, les liens et les résumés.

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

Ignorer les balises qui ne vous intéressent pas

L'analyseur doit ignorer les balises qui ne l'intéressent pas. Voici la méthode skip() de l'analyseur :

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

Voici comment cela fonctionne :

  • Il génère une exception si l'événement en cours n'est pas une START_TAG.
  • Il utilise la START_TAG, ainsi que tous les événements jusqu'à la balise END_TAG correspondante (incluse).
  • Pour être sûr de s'arrêter à la END_TAG correcte, et non à la première balise rencontrée après la START_TAG d'origine, il effectue le suivi de la profondeur d'imbrication.

Ainsi, si l'élément actuel comporte des éléments imbriqués, la valeur de depth ne sera pas 0 tant que l'analyseur n'aura pas utilisé tous les événements entre la START_TAG d'origine et la END_TAG correspondante. Voyons, par exemple, comment l'analyseur ignore l'élément <author>, qui contient deux éléments imbriqués (<name> et <uri>) :

  • Lors du premier passage dans la boucle while, la balise suivante que l'analyseur rencontre après <author> est la START_TAG pour <name>. La valeur de depth passe à 2.
  • Lors du deuxième passage dans la boucle while, la balise suivante que l'analyseur rencontre est la END_TAG </name>. La valeur de depth passe à 1.
  • Lors du troisième passage dans la boucle while, la balise suivante que l'analyseur rencontre est la START_TAG <uri>. La valeur de depth passe à 2.
  • Lors du quatrième passage dans la boucle while, la balise suivante que l'analyseur rencontre est la END_TAG </uri>. La valeur de depth passe à 1.
  • Lors du cinquième et dernier passage dans la boucle while, la balise suivante que l'analyseur rencontre est la END_TAG </author>. La valeur de depth passe à 0, ce qui indique que l'élément <author> a bien été ignoré.

Utiliser des données XML

L'application exemple extrait et analyse le flux XML de manière asynchrone. Le traitement est alors effectué en dehors du thread UI principal. Une fois le traitement terminé, l'application met à jour l'UI dans l'activité principale (NetworkActivity).

Dans l'extrait suivant, la méthode loadPage() effectue les opérations suivantes :

  • Elle initialise une variable de chaîne avec l'URL du flux XML.
  • Elle appelle la méthode downloadXml(url) si les paramètres et la connexion réseau de l'utilisateur le permettent. Cette méthode télécharge et analyse le flux, et renvoie un résultat de chaîne à afficher dans l'interface utilisateur.

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

La méthode downloadXml appelle les méthodes suivantes en Kotlin :

  • lifecycleScope.launch(Dispatchers.IO), qui utilise les coroutines Kotlin pour lancer la méthode loadXmlFromNetwork() sur le thread d'E/S. Cette opération transmet l'URL du flux en tant que paramètre. La méthode loadXmlFromNetwork() extrait et traite le flux. Une fois l'opération terminée, elle renvoie une chaîne de résultat.
  • withContext(Dispatchers.Main), qui utilise des coroutines Kotlin pour revenir au thread principal, prend la chaîne renvoyée et l'affiche dans l'interface utilisateur.

Dans le langage de programmation Java, le processus se présente comme suit :

  • Un Executor exécute la méthode loadXmlFromNetwork() sur un thread d'arrière-plan. Cette opération transmet l'URL du flux en tant que paramètre. La méthode loadXmlFromNetwork() extrait et traite le flux. Une fois l'opération terminée, elle renvoie une chaîne de résultat.
  • Un Handler appelle post pour revenir au thread principal, prend la chaîne renvoyée et l'affiche dans l'interface utilisateur.

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

La méthode loadXmlFromNetwork() appelée à partir de downloadXml est présentée dans l'extrait suivant. Elle effectue les opérations suivantes :

  1. Elle instancie un StackOverflowXmlParser. Elle crée également des variables pour une List d'objets Entry (entries), ainsi que pour title, url et summary, afin de stocker les valeurs extraites du flux XML pour ces champs.
  2. Elle appelle downloadUrl(), qui extrait le flux et le renvoie en tant que InputStream.
  3. Elle utilise StackOverflowXmlParser pour analyser InputStream. StackOverflowXmlParser remplit une List d'entries avec les données du flux.
  4. Elle traite la List d'entries et combine les données du flux avec le balisage HTML.
  5. Elle renvoie une chaîne HTML qui est affichée dans l'UI de l'activité 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();
}