k47.cz
výběr kolo foto Praha povídky kultura
TECH ▞▞ | twitter RSS

XPath.scala

11. 11. 2012 (před 8 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 Java 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]("//article//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
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ě 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 jen 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 { nodes =>
  nodes.map { node =>
    new Post(
      title = xpath[String]("title", node)
      text  = xpath[String]("text", node)
      tags  = xpath[Seq[String]]("tag", node) map (_.trim)
    )
  }
}
píše k47 & hosté, ascii@k47.cz