Parsovanie XML pomocou API Android SAX

SAX. DOM. StAX. Ktorú reprezentáciu XML zvoliť? Hja, klasická dilema. Android samozrejme podporuje všetky z nich (pretože ťaží z API, ktoré poskytuje samotná Java). A aby ste nemali ľahký výber, dodáva ešte jednu, poloznámu možnosť, a to v podobe tried android.sax.

Ten predstavuje hybrid medzi SAXom a DOMom. Prvé API je udalosťami riadené, pamäťovo nenáročné, ale oprogramovať komplexné dokumenty vedie k podivno-nečitateľnému kódu (a jednoduchosť z názvu, teda Simple API for XML, rozkošnej skratky zo skratiek, mizne v hmle). Na druhej strane, v DOMe je radosť pracovať so stromami (alebo aj nie, lebo API tiež nevyhralo cenu krásy), lež na úkor pamäte.

Ukážme si jeho použitie na príklade parsera RSS kanálov. Napríklad z obľúbeného mienkotvorného denníka Nový čas:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
   <channel>
      <title>cas.sk</title>
      <link>http://www.cas.sk</link>
      <description>Najčítanejší denník na internete</description>
      <language>sk</language>
      <image>
         <title>cas.sk</title>
         <url>http://img.cas.sk/images/cas-2012/logo-cas.sk.png</url>
         <link>http://www.cas.sk</link>
      </image>
      <pubDate>Mon, 06 May 2013 11:15:16 +0200</pubDate>
      <item>
         <title><![CDATA[Zlodej ukradol pravoslávnemu kňazovi 20 litrov omšového vína!]]></title>
         <link>http://www.cas.sk/clanok/249939/zlodej-ukradol-pravoslavnemu-knazovi-20-litrov-omsoveho-vina.html</link>
         <pubDate>Mon, 06 May 2013 11:10:00 +0200</pubDate>
         <guid isPermaLink="false">69747a7f93e3449a06145cc6fe5e8b16</guid>
         <description><![CDATA[Policajti pátrajú po zlodejovi, ktorý z domu právoslávneho kňaza ukradol 20 litrov omšového vína.]]></description>
         <enclosure url="http://img.cas.sk/img/4/title/1758012-img-vino-grange-syrah-2008-penfolds.jpg" length="0" type="image/jpeg" />
      </item>
      <item>
         <title><![CDATA[Psychologička o utopených bratoch: Keby Alexko za Nikolajkom neskočil, vyčítal by si to celý život!]]></title>
         <link>http://www.cas.sk/clanok/249903/psychologicka-o-utopenych-bratoch-keby-alexko-za-nikolajkom-neskocil-vycital-by-si-to-cely-zivot.html</link>
         <pubDate>Mon, 06 May 2013 11:00:00 +0200</pubDate>
         <guid isPermaLink="false">21e1a553361e1e402d1f16f67156da87</guid>
         <description><![CDATA[Bračeka tak veľmi miloval, že za ním neváhal skočiť do rozbúreného Dunaja!]]></description>
         <enclosure url="http://img.cas.sk/img/4/title/1757532-img-alexej-nikolaj-dunaj-utopeny-utopeni-deti-chlapci-horny-bar-hasici-hasicjsky-cln-potapaci-zachranari-bicykel-hradza.jpg" length="0" type="image/jpeg" />
      </item>
   </channel>
</rss>

Štruktúra dokumentu je nasledovná:

rss
 |-channel
    |-title
    |-link
    |-description
    |-language
    |-item*
        |-title
        |-link
        |-guid
        |-enclosure
        |-description

Úloha dňa č. 1 znie:

Vypíšte všetky nadpisy článkov!

Riešenie bude:

  • vybudujeme hierarchiu elementov dokumentu
  • na príslušné elementy navešiame poslucháčov, ktorí spracujú ich telá
  • získame klasický SAXovský ContentHandler
  • a napcháme ho do tradičného XML parsera

Budovanie hierarchie elementov

Podľa štruktúry dokumentu začneme budovať hierarchiu elementov. Začneme vytvorenim inštancie RootElementu:

RootElement rssElement = new RootElement("rss");

Z koreňového elementu získame objekt typu android.sax.Element prislúchajúci <channel>u:

Element channelElement = rssElement.getChild("channel");

Trik zopakujeme pre <item> a jeho dieťa <title>:

Element itemElement = channelElement.getChild("item");
Element titleElement = itemElement.getChild("title");

Vešanie listenerov

Ako sa dostaneme k nadpisom? Na každý Element môžeme navešať poslucháča, ktorý sa zavolá pri výskyte klasických SAXovských udalostí. Môžeme počúvať na:

Použime tretí bod: ak na elemente <title> nastane koniec textového uzla (teda vnútra), zozbierame text a vypíšeme ho:

titleElement.setEndTextElementListener(new EndTextElementListener() {           
    @Override
    public void end(String body) {
        System.out.println(body);
    }
});

Vytvorili sme anonymnú vnútornú triedu implementujúcu EndTextElementListener a prekryli sme jedinú metódu end(). Obsah textového uzla zozbierame veľmi jednoducho: objaví sa v parametri body.

Ak poznáte bežné SAXovské ContentHandlery, už tušíte, že toto je obrovské zjednodušenie. Vôbec nemusíme kumulovať jednotlivé znaky v StringBuilderi a dávať pozor na to, v ktorom uzle sa práve nachádzame.

Získanie inštancie ContentHandlera

Keď sme takto radostne navešali poslucháčov na elementy, z koreňového elementu vieme získať inštanciu ContentHandlera:

ContentHandler contentHandler = rssElement.getContentHandler();

Napchanie do XML parsera

Android ponúka užitočnú triedu android.util.Xml, z ktorej vieme získať inštanciu XML parsera pomocou metódy parse()

try {
    String url = "http://www.cas.sk";
    InputStream inputStream = new URL(url).openStream();
    Xml.parse(inputStream, Encoding.UTF_8, rssElement.getContentHandler());
} catch (MalformedURLException e) {
    throw new RssParseException("Cannot access URL " + url, e);
} catch (IOException e) {
    throw new RssParseException("Cannot parse RSS " + url
            + " due to I/O error", e);
} catch (SAXException e) {
    throw new RssParseException("Cannot parse RSS " + url
            + " due to syntax error", e);
}

Bohužiaľ, musíme uviesť kódovanie vstupného súboru, a to dokonca z pevnej množiny. Android 4.2 podporuje len UTFká, ISO-8859-1 a klasické ASCII. Pokiaľ máte civilizované XML, nie je s tým problém, ale taký denník SME produkuje svoje RSS vo windowsovskom kódovaní, ktoré takýmto spôsobom nenaparsujete.

Vrátte všetky nadpisy článkov!

Urobme teraz kultúrnejšiu verziu predošlého programu. Vytvorme triedu, ktorá vezme URL a vráti zoznam názvov článkov ako List<String>ov.

Najprv si vytvorme pomocnú metódu, ktorá vybuduje hierarchiu Elementov a vráti výsledný ContentHandler. Je tu však zádrheľ: v skutočnosti potrebujeme vrátiť aj zoznam nadpisov.

Pomocná metóda pre budovanie ContentHandlera

Riešenie môže byť céčkovské: metóda zoberie ako parameter zoznam List a v rámci ContentHandlera ho naplní dátami:

private ContentHandler getContentHandler(final List<String> titles) {
    RootElement rssElement = new RootElement("rss");
    Element channelElement = rssElement.getChild("channel");

    Element itemElement = channelElement.getChild("item");
    Element titleElement = itemElement.getChild("title");
    titleElement.setEndTextElementListener(new EndTextElementListener() {           
        @Override
        public void end(String body) {
            titles.add(body);
        }
    });

    return rssElement.getContentHandler();
}

Všimnime si ako napĺňanie prebehne vo vnútri ĘndTextElementListenera, čo je zároveň dôvod, prečo je parameter List<String> označený ako final.

Parsovanie pomocou ContentHandler

Pomocnú metódu použijeme vo verejnej metóde parse():

public List<String> parse(String url) throws RssParseException {
    try {
        List<String> titles = new LinkedList<String>();
        InputStream inputStream = new URL(url).openStream();
        Xml.parse(inputStream, Encoding.UTF_8, getContentHandler(titles));
        return titles;
    } catch (MalformedURLException e) {
        throw new RssParseException("Cannot access URL " + url, e);
    } catch (IOException e) {
        throw new RssParseException("Cannot parse RSS " + url
                + " due to I/O error", e);
    } catch (SAXException e) {
        throw new RssParseException("Cannot parse RSS " + url
                + " due to syntax error", e);
    }
}

Využitie triedy

Použiť našu triedu môžeme nasledovne:

RssParser parser = new RssParser();
List<String> titles = parser.parse("http://www.cas.sk/rss");
for (String title : titles) {
    System.out.println(title);
}

Rozšírenie parsera na položky

Parser si zaslúži rozšírenie: namiesto reťazcov s názvami článkov vracajme celé objekty so vybranými podpoložkami. Pre jednoduchosť sa dohodnime na triede, ktorá bude držať len niektoré z nich:

public class RssItem {
    private URL source; 

    private String title;

    private URL link;

    private String description;

    //.. gettre a settre

}

Pomocná metóda, ktorá vráti ContentHandler využije známy trik s parametrom typu List a návratovým typom ContentHandler. Logika je nasledovná:

  • na začiatku nainicializuj novú položku RssItem
  • pri elementoch <title>, <link> a <description> nastav príslušné inštančné premenné na položke
  • pri koncovom elemente </item> pridaj dohotovenú položku do zoznamu a vytvor novú inštanciu položky, ktorá sa naplní pri ďalšom začiatočnom elemente <item>.

Všetko toto by fungovalo bez problémov, keby nebolo syntaktických obmedzení Javy. Premenná pre aktuálnu položku RssItem musí byť označená ako final, aby ju bolo možné použiť vo vnútri anonymných vnútorných tried pre poslucháčov. Na druhej strane, v momente, keď budeme chcieť vytvoriť novú inštanciu položky, nebudeme ju môcť do takejto premennej priradiť.

Čas na ďalší špinavý trik: stačí vytvoriť kontajner pre náš premenlivý objekt. Samotná premenná s kontajnerom bude final, teda nie je možné do nej priradiť, ale vnútro môžeme meniť dľa ľubovole.

Na implementáciu holdera použijeme jednoprvkové pole:

final RssItem[] holder = new RssItem[] { new RssItem() };

Touto deklaráciou sme jedným šmahom vytvorili jednoprvkové pole, kde sme nultý prvok rovno inicializovali novou inštanciou položky RssItem.

Čítanie a zapisovanie do tejto položky uskutočníme cez holdera: pristúpime k nultému prvku poľa a zmeníme ho podľa požiadaviek (všimnite si kód v poslucháčoch).

Teraz už uveďme kompletný kód:

protected ContentHandler getContentHandler(final List<RssItem> rssItems) {
    final RssItem[] holder = new RssItem[] { new RssItem() };

    RootElement rssElement = new RootElement("rss");
    Element channelElement = rssElement.getChild("channel");

    Element itemElement = channelElement.getChild("item");
    itemElement.setEndElementListener(new EndElementListener() {
        @Override
        public void end() {
            // položka <item> bola spracovaná, pridajme ju do zoznamu
            rssItems.add(holder[0]);
            // a vytvorme čerstvú inštanciu pre budúce položky <item>
            holder[0] = new RssItem();
        }
    });

    Element titleElement = itemElement.getChild("title");
    titleElement.setEndTextElementListener(new EndTextElementListener() {           
        @Override
        public void end(String body) {
            holder[0].setTitle(body);
        }
    });

    Element linkElement = itemElement.getChild("link");
    linkElement.setEndTextElementListener(new EndTextElementListener() {
        @Override
        public void end(String body) {
            holder[0].setLink(body);
        }
    });

    Element descriptionElement = itemElement.getChild("description");
    descriptionElement.setEndTextElementListener(new EndTextElementListener() {
        @Override
        public void end(String body) {
            holder[0].setDescription(body);
        }
    });

    return rssElement.getContentHandler();
}

Metóda pre spracovanie je veľmi podobná jednoduchšej verzii z predošlej časti:

public List<RssItem> parse(String url) throws RssParseException {
    final List<RssItem> items = new LinkedList<RssItem>();

    try {
        InputStream inputStream = new URL(url).openStream();
        Xml.parse(inputStream, Encoding.UTF_8, getContentHandler(items));
        return items;
    } catch (MalformedURLException e) {
        throw new RssParseException("Cannot access URL " + url, e);
    } catch (IOException e) {
        throw new RssParseException("Cannot parse RSS " + url
                + " due to I/O error", e);
    } catch (SAXException e) {
        throw new RssParseException("Cannot parse RSS " + url
                + " due to syntax error", e);
    }
}

Ďalšie zdroje

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *