k47.cz
mastodon twitter RSS
bandcamp explorer

Srovnání Scala.xml, Anti-xml a XPath

18. 6. 2012 (před 11 lety) — k47 (CC by-nc-sa)

Scala.xml

Scala.xml je součást standardní knihovny Scaly pro práci s XML. Jednou z nejvíce vyzdvihovaných vlastností jsou xpath-like selektory. Protože jde o standardní součást jazyka, podporuje nativní xml literály a pattern matching xml dat.

Bohužel Scala.xml má vážné nedostatky, kvůli kterým se s ní nepracuje zrovna nejlépe. Jedním z největších přešlapů je cyklická hierarchie tříd:

Node <: NodeSeq <: Seq[Node]
           ^          │
           | implicit │
           ╰──────────╯

NodeSeq je potomek Seq[Node], Node je potomek NodeSeq a navíc existuje implicitní konverze z Seq[Node] na NodeSeq. Node má pak potomky: Atom, Comment, Elem, EntityRef, Group, PCData, ProcInstr, SpecialNode, TextUnparsed.

Tenhle cyklus má za následek, že skoro nikdy není jasné s čím vlastně člověk pracuje, zdali jde o jednu Node nebo NodeSeq, který obsahuje jednu Node.

Další problémem je to, že NodeSeq není immutable.

val arr = Array(<a/>, <b/>)       // měnitelné pole
val ns = xml.NodeSeq.fromSeq(arr) // použiji pole jako základ pro NodeSeq
// ns = NodeSeq(<a></a>, <b></b>)

arr(0) = <updated/>  // změním pole
ns                   // změnil se i odvozený NodeSeq
// ns = NodeSeq(<updated></updated>, <b></b>)

Vytvoření:

// Pomocí XML literálů
val elem = <native>xml literals are</native>

// Z xml stringu (k dispozici jsou další metody pro načtení ze souboru, Readeru nebo InputSource)
val elem: xml.Elem = xml.XML.loadString(xmlString)

// Za pomocí knihovny TagSoup ze stringu obsahujícího HTML
val parser      = new org.ccil.cowan.tagsoup.jaxp.SAXFactoryImpl().newSAXParser
val adapter     = new scala.xml.parsing.NoBindingFactoryAdapter
val inputSource = new org.xml.sax.InputSource(new java.io.StringReader(data))
val node: scala.xml.Node = adapter.loadXML(inputSource, parser)

Dotazování:

Xpath-like dotazy mohou vypadat takto:

// nejdřív do hloubky vybere všechny elementy "post", pak vybere elementy "author"
// obsažené ve výsledku předchozího kroku a pak v nich vybere všechny atributy "name"
// odpovídá xpath dotazu "//post/author/@name"
xml \\ "post" \ "author" \ "@name"

// vybere všechny elementy "post", které obsahují element "author" jehož atribut "name" je rovný "Adam K."
// odpovídá xpath dotazu "//post[author/@name = 'Adam K.']"
xml \\ "post" filter { e => e \ "author" \ "@name" == "Adam K." }

Jak je vidět k dispozici máme dvě metody: \ pro mělký dotaz a \\ pro dotaz, který jde do největší možné hloubky. Selektor může být pouze string. Podle jeho formátu se určí, co vlastně bude hledat:

Výsledek dotazu je vždycky NodeSeq.


Anti-xml

Anti-xml je dílo Daniela Spiewaka, které má za cíl udělat správně všechno, co standardní Scala.xml dělá špatně, přidat ještě pár věcí navrch a v budoucnu nahradit Scala.xml ze standardní knihovny.

Jelikož jde o externí nástroj, nemá nativní podporu xml literálů ani pattern matchingu (ale to se může změnit díky SIP-11 (String Interpolation)SIP-16 (Self-cleaning Macros)).

Anti-xml má jednoduchou hierarchií neměnných datových typů a Node (a jeho potomci ProcInstr, Elem, Text, CData, EntityRef) jsou jasně odděleni od xml kolekcí GroupZipper (fantastický zipper tady nebudu vysvětlovat, protože jde o černou magii a pokud provádíte transformace xml, tak zipper je důvod proč si vybrat Anti-xml a zapomenout všechno ostatní).

Vytvoření:

import com.codecommit.antixml

// Z xml stringu (další možnosti: fromInputStream, fromReader a fromSource)
val elem: antixml.Elem = antixml.XML.fromString(xmlString)

// Za pomocí knihovny TagSoup z html stringu
val parser = new org.ccil.cowan.tagsoup.jaxp.SAXFactoryImpl().newSAXParser
val handler = new antixml.NodeSeqSAXHandler
parser.parse(new org.xml.sax.InputSource(new java.io.StringReader(htmlString)), handler)
val elem: antixml.Elem = handler.result().head

// Konverze scala.xml na anti-xml
val scalaElem: scala.xml.Elem = <test />
val antixmlElem: antixml.Elem = antixml.XMLConvertable.ElemConvertable(scalaElem)

Dotazování:

Anti-xml nabízí stejné operátory pro hledání v XML stromu: \\\, ale navíc přidává \\!, který hledá do hloubky stejně jako \\, ale jakmile najde to co hledal, zastaví se a v daných podstromech už nejde hlouběji.

Na rozdíl od scala.xml, selektor není string, ale parciální funkce. Selektor přijímá všechny nody pro které je funkce definovaná a typ výsledku je určen návratovým typem této funkce. Takže například hledání proti Selector[Elem] vrátí Group[Elem]. Člověk tak přesně ví, s čím pracuje, což je mnohem přívětivější než scala.xml, kde se vždycky vrátil NodeSeq. Když selektor vrací něco, co není potomek Elem, pak je výsldkem Seq[Něco].

xml \ (s: Selector[Elem])   // Group[Elem]
xml \ (s: Selector[Text])   // Group[Text]
xml \ (s: Selector[String]) // Seq[String]

String nebo symbol se implicitně zkonvertuje na selektor, který vybírá elementy s daným jménem.

xml \\ "div"
xml \\ 'div
// jsou schodné s
xml \\ Selector { case e: Elem if e.name == "div" => e }

Anti-xml obsahuje ještě dva vestavěné selektory: text, který vybírá textový obsah elementů a *, který vybírá všechno.

xml \\ "post" \ "content"        // vrátí Group[Elem]
xml \\ "post" \ "content" \ text // vrátí Seq[String]

Jedna věc, která mi v anti-xml chybí je selektor atributů, který by mohl vypadat např. takto: attr("wantedAttribute"). Definovat ho naštěstí není nijak složité:

def attr(a: String) = antixml.Selector[String] {
  case e: antixml.Elem if e.attrs contains a => e.attrs(a)
}

XPath

Poslední možností jak extrahovat data x XML/HTML je DOM a XPath, které jsou součástí standardní knihovny Javy. XPath jako takový není typově bezpečný, ale zato je velice kompaktní a nabízí plnou sílu XPath dotazů (může zpětně odkazovat na elementy výše ve xml stromu a podporuje všechny osy dotazování).

Vytvoření:

// vytvoření DOMu z XML dat:

import org.w3c.dom.Document
import javax.xml.parsers.DocumentBuilderFactory

val domFactory = DocumentBuilderFactory.newInstance
domFactory.setNamespaceAware(true)
val doc: Document = domFactory.newDocumentBuilder.parse(xmlFileUri)
// vytvoření DOMu z html dat za pomocí TagSoup

import org.w3c.dom.Document
import org.ccil.cowan.tagsoup.Parser
import org.xml.sax.InputSource
import javax.xml.transform

val url = new java.net.URL(htmlFileUri)
val reader = new Parser
reader.setFeature(Parser.namespacesFeature, false)
reader.setFeature(Parser.namespacePrefixesFeature, false)

val transformer = transform.TransformerFactory.newInstance.newTransformer
val result = new transform.dom.DOMResult
transformer.transform(new transform.sax.SAXSource(reader, new InputSource(url.openStream)), result)
val doc: Document = result.getNode

Jak je vidět, tak či onak je s tvorbou DOMu spoustu sraní. Dotazování je na tom velice podobně.

Dotazování:

val xpath = XPathFactory.newInstance.newXPath
val expr = xpath.compile("//book/title")
val ns = expr.evaluate(doc, XPathConstants.NODESET).asInstanceOf[NodeList]
//                          ^                       ^
//                          tady řeknu jaký chci    vždycky vrátí Any
//                          vrátit výsledek         takže musím přetypovat

// NodeListem se nedá nijak inteligentně iterovat, má jenom dvě metody `getLength` a `item`
for (i <- 0 until ns.getLength) {
  println(ns.item(i).getTextContent)
}

Když odhlédneme od boilerplatu a nějak rozumně ho abstrahujeme, velkou výhodou XPath je jeho obrovská stručnost.

Pro srovnání:

//Xpath
"//div[@class='post']"

//Scala.xml
html \ "div" filter { e => (e \ "@class" text) == "post" }

//Anti-xml
html \ "div" filter { e => e.attrs.contains("class") && e.attrs("class") == "post" }

Většina nedostatků se dá obejít pár řádky Scaly a několika typeclass-ami a pak můžeme psát:

val expr = xpath[NodeList]("//book/title")
val ns = expr(doc)

for (n <- ns) {
  println(n.getTextContent)
}

Takže jak?

Scala.xml není příliš dobrý nástroj, který má mnoho problémů a nepracuje se s ním nejlépe. Anti-xml většinu problémů opravuje a přidává pár cukrátek navrch. Pokud vám jde jenom o extrahování informací z xml/html dokumentu, který hned potom zahodíte, tak jako nejlepší volba se mi jeví obyčejný XPath. Když abstrahujete boilerplate tvorby DOMu a dotazů, jde o nejstručnější variantu. Pokud už znáte XPath, bude to pro vás i ta nejpřirozenější varianta. Typová bezpečnost v mnoha případech není důležitá. Když parsujete data, nad kterými nemáte kontrolu a jejich forma není nijak specifikovaná, typy vám nepomůžou. Stačí když vám druhá strana pošle trochu jiné xml/html a sebevíc typově bezpečný program stejně vybouchne.

Takže tak.

píše k47, ascii@k47.cz