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

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
, Text
a Unparsed
.
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:
"tag"
– obyčejný string hledá elementy"@attribut"
– string začínající zavináčem hledá atributy"_"
– podtržítko hledá všechno (funguje jako XPath*
)
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) a 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í Group
a Zipper
(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: \
a \\
, 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.