Języki

Erudis - your road to knowledge
XML w Scali

Obsługa XML-a w języku Scala wyróżnia ją zdecydowanie spośród innych języków programowania. Na szczęście elementem wyróżniającym jest prostota i łatwość użycia. Jak zobaczymy, implementacja funkcjonalności polegającej na pobraniu informacji z kanału RSS i zapisaniu ich na dysku jest nadspodziewanie łatwym zadaniem, wymagającym zaledwie paru linijek kodu. Zanim jednak do tego przejdziemy, zajmiemy się szczegółami przetwarzania XML-a. W końcu Czytelnik mógłby poczuć pewien niedosyt, jeżeli główny temat artykułu zostałby sprowadzony do demonstracji krótkiego programu.

Popatrzmy najpierw na Listing 6, gdzie widzimy w jaki sposób możemy tworzyć obiekty reprezentujące dokument XML. Pierwszy sposób jest bardzo intuicyjny. Definiując zmienną podajemy znaczniki XML-owe, a Scala sama je konwertuje na odpowiedni obiekt typu scala.xml.Elem. Bardzo wygodne, zwłaszcza, że możemy dowolne fragmenty XML-a generować dynamicznie, umieszczając w nawiasach klamrowych kod Scali. Ciekawszy i bardziej praktyczny przykład widzieliśmy na Listingu 5, w metodzie getXML. Drugi sposób tworzenia XML-a, demonstrowany na Listingu 6, wygląda mniej ekscentrycznie – tworzymy hierarchię obiektów Elem.

Listing 6. Sposoby tworzenia obiektów typu Elem, które reprezentują dokumenty XML

 object XML extends Application {
   val xmlByTags = 
      <rss version="2.0">
         <channel>
           <title>Lift Off News</title>
           <link>http://liftoff.msfc.nasa.gov</link>
           <description>Liftoff to Space Exploration.</description>
           <item>
             <title>{title}</title>
             <link>{link}</link>
             <description>{description}</description>
             <guid>{guid}</guid>
           </item>
         </channel>
      </rss>
   println(xmlByTags)
   import scala.xml._
   val title = "Star City"
   val link = "http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp"
   val description = "How do Americans get..."
   val guid = "http://liftoff.msfc.nasa.gov/2003/06/03.html#item573"
   val xmlByObjects = Elem(null, "rss", new UnprefixedAttribute("version","2.0", Null), TopScope,
      Elem(null, "channel", Null, TopScope,
         Elem(null, "title", Null, TopScope, Text("Lift Off News")),
         Elem(null, "link", Null, TopScope, Text("http://liftoff.msfc.nasa.gov")),
         Elem(null, "description", Null, TopScope, Text("Liftoff to Space Exploration.")),
         Elem(null, "item", Null, TopScope,
            Elem(null, "title", Null, TopScope, Text(title)),
            Elem(null, "link", Null, TopScope, Text(link)),
            Elem(null, "description", Null, TopScope, Text(description)),
            Elem(null, "guid", Null, TopScope, Text(guid))  
         )
      )
   )
   println(new PrettyPrinter(80 /*width*/,3 /*indent*/).format(xmlByObjects))
}

Klasa Elem jest najważniejsza w całym interfejsie programistycznym XML-a w Scali i dlatego przyjrzymy się jej nieco dokładniej. Jest ona zdefiniowana jako case class.

Klasa tego typu ma automatycznie zaimplementowane metody equals i hashCode, dzięki temu jej obiekty mogą być wykorzystywane przez mechanizm porównywania wzorców. Ponadto uproszczony jest sposób tworzenia obiektów klasy case: nie jest potrzebne słowo kluczowe new, dodatkowo, dla wygody programisty, zmienne umieszczone w konstruktorze klasy są automatycznie publicznymi polami tej klasy.

Konstruktor klasy Elem na pierwszy rzut oka nie wygląda przyjaźnie, trzeba podać co najmniej cztery parametry, spośród których oczywiste znaczenia ma tylko nazwa znacznika. Pozostałe parametry służą do definiowania XML-owych przestrzeni nazw. Na Listingu 7 mamy deklarację klasy Elem z opisanymi atrybutami oraz przykład znacznika XML-owego wyposażonego we wszelkie możliwe dodatki.

Listing 7. Deklaracja klasy Elem oraz przykład znacznika XML ze zdefiniowaną przestrzenią nazw

/* Deklaracja klasy Elem
case class Elem(
      val prefix: String,           // prefix przestrzeni nazw
      val label: String,            // nazwa znacznika (lokalna)
      val attributes: MetaData,     // atrybuty znacznika
      val scope: NamespaceBinding,  // deklaracja przestrzeni nazw
      val child: Node*) extends Node { ... } // znaczniki, które są 
                                             // zawarte wewnątrz naszego znacznika
*/
object FullXmlTag extends Application {
   import scala.xml._
   val attributes = new UnprefixedAttribute("attr1","v1", 
              new PrefixedAttribute("prefix","attr2","v2", Null))
   val namespace = new NamespaceBinding("prefix", "http://erudis.pl", TopScope)
   val e = Elem("prefix", "tagname", attributes, namespace)
   println(e) // wydrukuje:
   // <prefix:tagname attr1="v1" prefix:attr2="v2" xmlns:prefix="http://erudis.pl"/>
}

Uzbrojeni w taką wiedzę jesteśmy gotowi do tego, żeby zająć się przetwarzaniem dokumentów XML-owych. Najpierw będzie prosty przykład. Wyobraźmy sobie, że mamy dokument HTML, w którym występują zagnieżdżone znaczniki <p> (akapit) i <b> (tekst pogrubiony). Chcemy z tego dokumentu wyciągnąć wszystkie te fragmenty tekstu, które są napisane pogrubionym tekstem. Można to robić na dwa sposoby, co prezentuje Listing 8. Pierwszy sposób wykorzystuje dopasowywanie wzorców (metody getBolds1(Node) i getBolds2(Node)). Reguła dopasowania klasy do wzorca reaguje na pojawienie się znacznika <p>, wtedy dokument jest przetwarzany rekurencyjnie dalej, lub <b> i wtedy drukujemy tekst. Możemy się posługiwać się w kodzie tak samo wygodnie znacznikami XML-owymi jak i obiektami klasy Elem.

Listing 8. Przetwarzanie dokumentów XML-owych

/**
 * Przykład na podstawie http://scala.sygneca.com/code/xml-pattern-matching
 * Example based on http://scala.sygneca.com/code/xml-pattern-matching
 */
object XMLParser {
   import scala.xml._;
   def getBolds1(node: Node): unit = node match {
      case <b>{c}</b> => println("pogrubione: " + c)
      case <p>{c @ _ *}</p> => for (child <- c) getBolds1(child)
      case _ => { }
   } 
   def getBolds2(node: Node): unit = {
      node match {
         case Elem(_, "p", _, _, c @ _ *) => c.foreach(k => getBolds2(k))
         case Elem(_, "b", _, _, c @ _ *) => println("pogrubione: " + node.text);
         case _ => { }
      }
   }
   def getBolds3(doc: Node) = for (node <- doc \\ "b") println("pogrubione: " + node.text);
   def main(args: Array[String]) = {
      val doc = 
      <html>
          <body>
          <p>Scala is a <b>Java-like programming language</b>...
               ...mainstream languages – <p><b>Java</b> and C#</p>.    
          </p>
          </body>
      </html>
      val body = (doc \\ "p")
      println("Pierwsza wersja:");
      getBolds1(body(0));
      println("Druga wersja");
      getBolds2(body(0));
      println("Trzecia wersja");
      getBolds3(body(0));
   }
}

W praktyce najwygodniej jest korzystać z drugiego sposobu przetwarzania XML-a, jaki nam daje Scala – wyrażeń XPath. Wyrażenie doc \\ "b" w metodzie getBolds3(Node): zwraca listę obiektów reprezentujących znaczniki <b> i ich zawartość.

Klasa Node jest abstrakcyjną klasą reprezentującą węzeł dokumentu XML, znana nam już klasa Elem dziedziczy po niej. Omówione powyżej metody mają w swojej deklaracji wprost zaznaczony zwracany typ (unit) oznaczający, że metoda nie zwraca żadnej wartości. Musimy go wprost podać, gdyż nasze metody są wywoływane rekurencyjnie – taki jest wymóg Scali.

Wreszcie przyszedł czas, żeby dodać do naszej aplikacji, pobierającej informacje przez kanał RSS, ostatnią cegiełkę, czyli mechanizm łączący się z siecią i zapisujący dane do bazy CSVDb. Tak jak wspominałem, potrzebny do tego celu kod Scali jest śmiesznie prosty, przekonajmy się o tym naocznie, na Listingu 9.

Listing 9. Pobranie danych z kanału RSS i zapisanie ich w bazie CSVDb (Listing 5)

import scala.xml.XML
import java.net._
import java.io._
import pl.erudis.feedseater.model._
object FeedsEater extends Application{   
   val url = new URL("http://today.java.net/pub/q/articles_rss?x-ver=1.0")
   val rss = new BufferedReader(new InputStreamReader(url.openStream()))
   val data = XML.load(rss)
   val db = new CSVDb(new File("db.txt"))
   for(item <- data \\ "item") {
      var title = (item \\ "title").text
      var link = (item \\ "link").text
      //znaki końca linii psują naszą bazę danych, usuwamy je
      var description = (item \\ "description").text.replaceAll("\n"," ").trim
      var feed = new RSSFeedBean(title, new URL(link), description)
      db.add(feed)
      println(title) //dla sprawdzenia co się dzieje
   }
}

Zmienna rss reprezentuje strumień danych wyciągnięty przy pomocy standardowych klas obsługi wejścia-wyjścia dostępnych w Javie. Najcięższą pracę wykonuje obiekt XML, który przy pomocy metody load zamienia pobrany strumień na obiekt reprezentujący dokument XML-owy. Następnie przy pomocy poznanych przed chwilą wyrażeń XPath wyciągamy potrzebne nam dane i zapisujemy w bazie danych CSVDb. Proste, naprawdę proste!