k47.cz
mastodon twitter RSS
bandcamp explorer 0xDEADBEEF

XPath.scala

— 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 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)
    )
  }
}
píše k47, ascii@k47.cz