Расширяемый язык разметки XML представляет собой набор правил для кодирования документов в машиночитаемой форме. XML является популярным форматом для обмена данными в Интернете. Сайты, которые часто обновляют свой контент, например, новостные сайты или блоги, часто предоставляют XML канал, чтобы внешние программы были в курсе изменений контента. Отправка и разбор XML-данных является общей задачей для приложений с сетевым подключением. Этот урок объясняет, как выполнить разбор XML документов и использовать их данные.

Выбор синтаксического анализатора

Мы рекомендуем XmlPullParser, который является эффективным и удобным способом разбора XML на Android. Исторически, в Android было две реализации этого интерфейса:

Любой выбор хорош. Пример в этом разделе использует ExpatPullParser, через Xml.newPullParser().

Анализ канала

Первым шагом в разборе канала является решение о том, в какие полях данные вы заинтересованы. Анализатор извлекает заданные поля и игнорирует все остальное.

Вот фрагмент канала, который будет разбираться в примере приложения. Каждый пост на StackOverflow.com появляется в канале, как 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 канала следующие:

  1. Как описано в Анализ канала, определите теги, которые необходимы вашему приложению. Этот пример извлекает данные из entry тега и его вложенных тегов title, link, и summary.
  2. Создайте следующие методы:

    • "read" метод для каждого тега, который вам нужен. Например, readEntry(), readTitle(), и так далее. Анализатор считывает теги из входного потока. Когда он встречает тег с именем entry, title, link или summary, он вызывает соответствующий метод для найденного тега. В противном случае, он пропускает тег.
    • Методы для извлечения данных для каждого типа тега и продвижение анализатора к следующему тегу. Например:
      • Для title и summary тегов, анализатор вызывает readText(). Этот метод извлекает данные из этих тегов, вызывая parser.getText().
      • Для link тега, анализатор извлекает данные для ссылок, определяя в начале заинтересовано ли приложение в этой ссылке. Затем он использует parser.getAttributeValue() для извлечение значения ссылки.
      • Для entry тегов, анализатор вызывает readEntry(). Этот метод анализирует вложенные теги и возвращает Entry объект с членами данных title, link, и summary.
    • Вспомогательный skip() метод рекурсивный. Более детального обсуждения этой темы см. Пропуск элементов, который вам не нужны.

Этот фрагмент показывает, как анализатор анализирует entry, title, link, и summary.

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> элемент, который имеет 2 вложенных элемента, <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 данных

Пример приложения получает и анализирует XML канал в AsyncTask. Обработка выполняется вне основного потока пользовательского интерфейса. Когда обработка завершена, приложение обновляет пользовательский интерфейс в основной деятельности(NetworkActivity).

Во фрагменте представленном ниже, loadPage() метод делает следующее:

  • Инициализирует строковую переменную значением URL, указывающим на XML канал.
  • Если настройки пользователя и подключение к сети позволяют, вызывает new DownloadXmlTask().execute(url). Это создает новый DownloadXmlTask объект(AsyncTask подкласс) и выполняет его execute() метод, который загружает и анализирует канал и возвращает строковый результат, который будет отображаться в пользовательском интерфейсе.
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() получает и обрабатывает канал. Когда он заканчивает обработку, он передает обратно результирующую строку.
  • onPostExecute() принимает возвращенную строку и отображает её в пользовательском интерфейсе.
// 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. Он делает следующее:

  1. Создает экземпляр StackOverflowXmlParser. Он также создает переменные для List Entry объектов(entries), и title, url, и summary, для хранения значений, извлеченных из XML канала, для этих полей.
  2. Вызывает downloadUrl(), который загружает канал и возвращает его как InputStream.
  3. Использует StackOverflowXmlParser для разбора InputStream. StackOverflowXmlParser заполняет List entries данными из канала.
  4. Обрабатывает entries List, и объединяет в себе данные канала с HTML разметкой.
  5. Возвращает HTML строку, отображаемую в пользовательском интерфейсе главной деятельности, AsyncTask в методе onPostExecute().
// 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();
}