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

XPath.scala

11. 11. 2012 (před 7 lety) — k47 (CC by) (♪)

Par­so­vání XML a (špat­ného) HTML je nej­spíš moje ži­votní po­slání. V ne­ko­nečné snaze najít ide­ální ná­stroj pro zpra­co­vání těchto for­mátů jsem napsal další mikro-kni­hovnu. Ten­to­krát jde o XPath.scala – jed­no­du­chý Scala wrap­per nad Jaxa XPath API, který se v mnohém se podobá mojí dří­vější PHP kreaci Atrox\Matcher.

Ja­vov­ské roz­hraní pro práci s XPath je jednak ne­ty­po­vané (vrací objekt typu Any, který si musíme sami pře­ty­po­vat) a druhak do­dr­žuje Ja­voskou tra­dici a je až le­gen­dárně uke­cané. Oba tyto ne­do­statky jsem v XPath.scala vy­ře­šil pomocí type-class, funk­ci­o­nální abs­trakce a haldou im­pli­cit­ních kon­verzí.

Zá­kla­dem je funkce xpath, které musíme předat po­ža­do­vaný typ vý­sledku a XPath cestu. Vý­sled­kem je jiná funkce, která na DOM do­ku­mentu pro­vede XPath dotaz a vrátí vý­sle­dek správ­ného typu. XPath dotaz se zkom­pi­luje a při­praví ve funkci xpath, ná­sle­du­jící funkce ho už jenom vy­ko­nává.

def xpath[T](path: String): Any => T

Pužití je velice jed­no­du­ché:

// připravíme dotaz
val getTitle = xpath[String]("//div[@class='post']//h1")

// dotaz použijeme (nejlépe opakovaně)
val title: String = getTitle(domDocument)

A pro­tože je vý­sled­kem oby­čejná funkce, můžeme jí sklá­dat s dal­šími funk­cemi:

// 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ě ko­lekcí se ne­mu­síme spo­lé­hat jenom na typ NodeList z Ja­vov­ského API, ale můžeme po­ža­do­vat plně ty­po­vané sek­vence:

// 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 na­pří­klad xpath[Seq[Int]] fun­guje to jenom proto, že funkce xpath si přes me­cha­nis­mus im­pli­cit­ních ar­gu­mentů najde type-class pro Seq a ta pak si zase najde pří­sluš­nou type-class pro Int.

S tro­chou před­sta­vi­vosti se dá xpath.scala použít po­dobně 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