XPath.scala

Parsování XML a (špatného) HTML je nejspíš moje životní poslání. V nekonečné snaze najít ideální nástroj pro zpracování těchto formátů jsem napsal další mikro-knihovnu. Tentokrát jde o XPath.scala – jednoduchý Scala wrapper nad Java XPath API, který se v mnohém se podobá mojí dřívější PHP kreaci Atrox\Matcher.
Javovské rozhraní pro práci s XPath je jednak netypované (vrací objekt typu
Any
, který si musíme sami přetypovat) a druhak dodržuje Javoskou tradici a je
až legendárně ukecané. Oba tyto nedostatky jsem v XPath.scala vyřešil pomocí
type-class, funkcionální abstrakce a haldou implicitních konverzí.
Základem je funkce xpath
, které musíme předat požadovaný typ výsledku a XPath
cestu. Výsledkem je jiná funkce, která na DOM dokumentu provede
XPath dotaz a vrátí výsledek správného typu. XPath dotaz se zkompiluje a připraví ve funkci xpath
, následující funkce ho už jenom vykonává.
def xpath[T](path: String): Any => T
Pužití je velice jednoduché:
// připravíme dotaz val getTitle = xpath[String]("//article//h1") // dotaz použijeme (nejlépe opakovaně) val title: String = getTitle(domDocument)
A protože je výsledkem obyčejná funkce, můžeme jí skládat s dalšími funkcemi:
// funkce, která naparsuje titulek def parseTitle(title: String): Title = Title(title, decodeSatanicMessages(title)) // nejdřív extrahujeme titulek, pak ho zkonvertujeme do objektu Title val getAndParseTitle: AnyRef => Title = xpath[String]("//article//h1") andThen parseTitle
V případě kolekcí se nemusíme spoléhat jenom na typ NodeList
z Javovského
API, ale můžeme požadovat plně typované sekvence:
// chceme extrahovat sekvenci integerů xpath[Seq[Int]]("//post/id") // když se id nevejde do integeru, můžeme použít jiný typ xpath[Seq[Long]]("//post/id") // popřípadě vlastní typ implicit val MyTypeConverter = ConvertNode[MyType] { n: Node => MyType(n.getTextContent) } xpath[Seq[MyType]]("//post/id") // což je obdoba tohoto xpath[Seq[Node]]("//post/id") map (n => MyType(n.getTextContent)) // nebo tohohle xpath[Seq[String]]("//post/id") map (MyType(_))
Volání jako například xpath[Seq[Int]]
funguje jen proto, že funkce xpath
si přes mechanismus implicitních argumentů najde type class pro Seq
a ta pak
si zase najde příslušnou type class pro Int
.
S trochou představivosti se dá xpath.scala použít podobně jako Atrox\Matcher:
val matcher = xpath[Seq[Node]]("//post") andThen { nodes => nodes.map { node => new Post( title = xpath[String]("title", node) text = xpath[String]("text", node) tags = xpath[Seq[String]]("tag", node) map (_.trim) ) } }