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


XPath.scala

11. 11. 2012 — k47 (CC by)

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 Jaxa 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]("//div[@class='post']//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
val parseTitle: String => Title = ???

// nejdřív extrahujeme titulek, pak ho zkonvertujeme do objektu Title
val getAndParseTitle: AnyRef => Title =
  xpath[String]("//div[@class='post']//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 to jenom 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 { _ map { post => new {
  val title = xpath[String]("title", post)
  val text  = xpath[String]("text", post)
  val tags  = xpath[Seq[String]]("tag", post) map (_.trim)
}}}
píše k47 & hosté, ascii@k47.cz