2010年3月1日月曜日

XML, DOM, XSLT, NekoHtmlを使う

Bloggerを始めるにあたり、SmartDocが生成したHTMLをBlogger用に変換するScalaプログラムsdoc2bloggerを書いた。Scalaで、XML、DOM、XSLT、NekoHtmlを使う実例として参考になるかもしれない。

Scalaでは極めて簡単にXMLを扱うことができる。まず、XMLリテラルが使えるのが極めて大きい。プログラム内で生成したXMLデータ(scala.xml.Node, scala.xml.Elem)をtoStringで文字列に変換できるのも便利。さらに、XMLリテラル中に「{}」を使って式を埋め込むことができる。XMLを処理するプログラムを書くのにScalaは手離せない。

ただ、ScalaのXML操作系は独自体系(scala.xml.Node, scala.xml.Elem)になっているので、既存のDOMベースのコンポーネントを利用するにはscala.xml.NodeとDOMの相互変換が必要になる。sdoc2bloggerではSAXによる変換(DOMからscala.xml.Nodeへの変換)とテキストによる変換(XMLリテラルによるXSLTをDOMのTransformerに設定)を使っている。

sdoc2bloggerではDOMベースのコンポーネントとしてXSLTとNekoHtmlを使っている。XSLTによるXML変換はこれからニーズが高まりそうな技術の一つ。この例から分かるとおり、Scalaからも比較的簡単に行うことができるけど、もっと簡単に使えるようなラッパーがあるのが望ましい。ただし、XSLT的な変換はScalaのList処理でもっと簡単にできそうなので、XSLTプログラマが絶滅しかかっている現状では、あえてXSLTをサポートする誘引は少ないかもしれない。

NekoHtmlは、ブロークンなHTMLを読み込むことができるHTMLパーサー。今回はこれを使いたくてDOMを使うことになった。

XSLTで余分な空白を取り除く方法が分からなかったので、Scalaプログラムの方で処理を行っている。XSLTでやり方が見つからなかった場合、無理をしてXSLTの利用方法を追求するよりScalaプログラム側で対処した方が現実的である。

この他にXML要素をas-isで残す方法も見つけることができなかった。これはXSLTに地道にパターンを設定することで対処。

いずれにしてもXSLTは難しい。XSLTを触るのはかなり久々なのでカンも戻っていない。

それはともかく、XSLTは技を使わないと手続き的な処理を実現できないので、副作用のない変換はXSLTを使い、Scalaプログラムで補完するのが実用的な使い方かなと考えている。

Main.scala
package org.xmlsmartdoc.sdoc2blogger

import scala.xml._
import scala.xml.parsing._
import java.io.{FileReader, StringReader}
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.sax.SAXResult
import javax.xml.transform.stream.StreamSource
import org.xml.sax.InputSource
import org.cyberneko.html.parsers.DOMParser

/*
 * @since   Feb. 28, 2010
 * @version Mar.  1, 2010
 * @author ASAMI, Tomoharu
 */
object Main {
  val xslt = 
<xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
  <xsl:template match="/HTML/BODY">
    <div>
      <xsl:apply-templates />
    </div>
  </xsl:template>
  <xsl:template match="P">
    <p>
      <xsl:apply-templates />
    </p>
  </xsl:template>
  <xsl:template match="A">
    <a>
      <xsl:attribute name="href">
        <xsl:value-of select="@href"/>
      </xsl:attribute>
      <xsl:apply-templates />
    </a>
  </xsl:template>
  <xsl:template match="PRE[@class='program']">
<pre style="background-color: gainsboro;font-family: courier, \
    monospace;padding: 5pt;">
      <xsl:value-of select="."/>
    </pre>
  </xsl:template>
  <xsl:template match="PRE[@class='console']">
<pre style="background-color: black;color: white;font-family: courier, \
    monospace;padding: 5pt;">
      <xsl:value-of select="."/>
    </pre>
  </xsl:template>
  <xsl:template match="DIV[@class='caption']">
    <div style="background: lavender;font-weight: bold;">
      <xsl:value-of select="."/>
    </div>
  </xsl:template>
  <xsl:template match="DIV[@class='date']">
  </xsl:template>
  <xsl:template match="DIV[@class='author']">
  </xsl:template>
  <xsl:template match="DIV[@style='text-align:right']">
  </xsl:template>
  <xsl:template match="H1">
  </xsl:template>
</xsl:stylesheet>

  def main(args: Array[String]) {
    val reader = new FileReader(args(0))
    val is = new InputSource(reader)
    val parser = new DOMParser()
    parser.parse(is)
    val dom = parser.getDocument
    //
    val saxHandler = new NoBindingFactoryAdapter()
    saxHandler.scopeStack.push(TopScope)
    val transFactory = TransformerFactory.newInstance
    val ss = new StreamSource(new StringReader(xslt.toString))
    val trans = transFactory.newTransformer(ss)
    trans.transform(new DOMSource(dom), new SAXResult(saxHandler))
    saxHandler.scopeStack.pop
val xml = <div>{distill_child_elements(saxHandler.rootElem.child)}</div>
    println(xml)
  }

  private def distill_child_elements(nodes: Seq[Node]): Seq[Node] = {
    nodes.filter(_.isInstanceOf[Elem])
  }
}

0 件のコメント:

コメントを投稿