k47.cz  — každý den dokud se vám to nezačne líbit
výběr foto Praha povídky kultura
TECH ▞▞ kolo | twitter RSS

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

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

XML a XPath dotazy.

Scala.xml

Scala.xml je sou­část stan­dardní knihovny Scaly pro práci s XML. Jednou z nej­více vy­zdvi­ho­va­ných vlast­ností jsou xpath-like se­lek­tory. Pro­tože jde o stan­dardní sou­část jazyka, pod­po­ruje na­tivní xml li­te­rály a pat­tern matching xml dat.

Bo­hu­žel Scala.xml má vážné ne­do­statky, kvůli kterým se s ní ne­pra­cuje zrovna nej­lépe. Jedním z nej­vět­ších pře­šlapů je cyk­lická hi­e­rar­chie tříd:

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

NodeSeq je po­to­mek Seq[Node], Node je po­to­mek NodeSeq a navíc exis­tuje im­pli­citní kon­verze z Seq[Node] na NodeSeq. Node má pak po­tomky: Atom, Comment, Elem, EntityRef, Group, PCData, ProcInstr, SpecialNode, TextUnparsed.

Tenhle cyklus má za ná­sle­dek, že skoro nikdy není jasné s čím vlastně člověk pra­cuje, zdali jde o jednu Node nebo NodeSeq, který ob­sa­huje jednu Node.

Další pro­blé­mem je to, že No­de­Seq není im­mu­table.

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>)

Vy­tvo­ř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)

Do­ta­zo­vání:

Xpath-like dotazy mohou vy­pa­dat 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 dis­po­zici máme dvě metody: \ pro mělký dotaz a \\ pro dotaz, který jde do nej­větší možné hloubky. Se­lek­tor může být pouze string. Podle jeho for­mátu se určí, co vlastně bude hledat:

Vý­sle­dek dotazu je vždycky No­de­Seq.


Anti-xml

Anti-xml je dílo Da­niela Spiewaka, které má za cíl udělat správně všechno, co stan­dardní Scala.xml dělá špatně, přidat ještě pár věcí navrch a v bu­doucnu na­hra­dit Scala.xml ze stan­dardní knihovny.

Je­li­kož jde o ex­terní ná­stroj, nemá na­tivní pod­poru xml li­te­rálů ani pat­tern matchingu (ale to se může změnit díky SIP-11 (String In­ter­po­lation)SIP-16 (Self-cle­a­ning Macros)).

Anti-xml má jed­no­du­chou hi­e­rar­chií ne­měn­ných da­to­vých typů a Node (a jeho po­tomci ProcInstr, Elem, Text, CData, EntityRef) jsou jasně od­dě­leni od xml ko­lekcí GroupZipper (fan­tas­tický zipper tady nebudu vy­svět­lo­vat, pro­tože jde o černou magii a pokud pro­vá­díte trans­for­mace xml, tak zipper je důvod proč si vybrat Anti-xml a za­po­me­nout všechno ostatní).

Vy­tvo­ř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)

Do­ta­zo­vání:

Anti-xml nabízí stejné ope­rá­tory pro hle­dání v XML stromu: \\\, ale navíc při­dává \\!, který hledá do hloubky stejně jako \\, ale jakmile najde to co hledal, za­staví se a v daných pod­stro­mech už nejde hlou­běji.

Na rozdíl od scala.xml, se­lek­tor není string, ale par­ci­ální funkce. Se­lek­tor při­jímá všechny nody pro které je funkce de­fi­no­vaná a typ vý­sledku je určen ná­vra­to­vým typem této funkce. Takže na­pří­klad hle­dání proti Selector[Elem] vrátí Group[Elem]. Člověk tak přesně ví, s čím pra­cuje, což je mnohem pří­vě­ti­vější než scala.xml, kde se vždycky vrátil NodeSeq. Když se­lek­tor vrací něco, co není po­to­mek Elem, pak je vý­sld­kem 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 im­pli­citně zkon­ver­tuje na se­lek­tor, který vybírá ele­menty s daným jménem.

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

Anti-xml ob­sa­huje ještě dva ve­sta­věné se­lek­tory: text, který vybírá tex­tový obsah ele­mentů 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 se­lek­tor atri­butů, který by mohl vy­pa­dat např. takto: attr("wantedAttribute"). De­fi­no­vat 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

Po­slední mož­ností jak ex­tra­ho­vat data x xml/html je DOM a XPath, které jsou sou­částí stan­dardní knihovny Javy. XPath jako takový není typově bez­pečný, ale zato je velice kom­paktní a nabízí plnou sílu XPath dotazů (může zpětně od­ka­zo­vat na ele­menty výše ve xml stromu a pod­po­ruje všechny osy do­ta­zo­vání).

Vy­tvo­ř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 tvor­bou DOMu spoustu sraní. Do­ta­zo­vání je na tom velice po­dobně.

Do­ta­zo­vá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ž od­hléd­neme od bo­i­ler­platu a nějak ro­zumně ho abs­tra­hu­jeme, velkou vý­ho­dou XPath je jeho ob­rov­ská struč­nost.

Pro srov­ná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 ne­do­statků se dá obejít pár řádky Scaly a ně­ko­lika ty­pec­lass-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 pro­blémů a ne­pra­cuje se s ním nej­lépe. Anti-xml vět­šinu pro­blémů opra­vuje a při­dává pár cuk­rá­tek navrch. Pokud vám jde jenom o ex­tra­ho­vání in­for­mací z xml/html do­ku­mentu, který hned potom za­ho­díte, tak jako nej­lepší volba se mi jeví oby­čejný XPath. Když abs­tra­hu­jete bo­i­ler­plate tvorby DOMu a dotazů, jde o nej­struč­nější va­ri­antu. Pokud už znáte XPath, bude to pro vás i ta nej­při­ro­ze­nější va­ri­anta. Typová bez­peč­nost v mnoha pří­pa­dech není dů­le­žitá. Když par­su­jete data, nad kte­rými nemáte kon­t­rolu a jejich forma není nijak spe­ci­fi­ko­vaná, typy vám ne­po­mů­žou. Stačí když vám druhá strana pošle trochu jiné xml/html a se­be­víc typově bez­pečný pro­gram stejně vy­bouchne.

Takže tak.

píše k47 & hosté, ascii@k47.cz