OSTRACISM CO.

ScalaとHaskellとPythonと...

XML

 epub2to3_H.hsで使っているXMLライブラリはText.XML.Lightである。

import Text.XML.Light

 このライブラリのドキュメントのどこにもDOMという言葉が出てこない。DOMに基づかない実装ということだ。パース後に得られるのはDOMではなくContentのリストだ。確かにDOMではない。

 で、これがまたなんというか非常に使いにくい。XMLで表現できることを正確に扱うためか、変に冗長なのである。なので、色々と自分で書いて補わないととてもやってられない。

delAttr :: QName -> Element -> Element
setAttr :: Attr -> Element -> Element
getDocumentRoot :: [Content] -> Maybe Element
findElementsByName :: String -> Element -> [Element]
findElementByName :: String -> Element -> Maybe Element
getAttrValueByName :: String -> Element -> String
mkAttr :: String -> String -> Attr
mkText :: String -> CData
getElementText :: Element -> String

 epub2to3_H.hs ではこれだけ機能を加えて記述を簡素化した。要素に属性を加える関数すらないのはいかがなものか。あと、showContentを使ってテキスト出力したらタブ文字が実体参照になっていたんだが、バグだよね。Haskellには他にもXMLライブラリがあるようなので、吟味したほうが良いです、マジで。

 ScalaはそもそもソースにXMLを書ける。元々XMLをサポートしてるという珍しい言語仕様だ。で、ScalaでのXMLも実はDOMではない。DOMである必要性がない。

 例えばepub2to3_S.scalaのfixOpf関数の冒頭、

val rootE = XML.load(ins)

 XMLをパースするとルートの要素が貰える。DOMだとかContentだとか、おそらく本当にそうであってもおかしくないNodeやNodeSeqを経由せず、そのものズバリ欲しい物であるElemが貰える。さすが解ってらっしゃる。

 XMLからXMLへの変換にRuleTransformerという予め用意されているクラスを使っている。HaskellとPythonにはないのでepub2to3シリーズはRuleTransformerの各言語への移植という側面を持つ。

 PythonでXMLといえばminidomである。名前から分かるように、これはDOMである。epub2to3_P.pyもminidomで普通にせっせかと書いた。minidomはたまに食えないXMLがあったり、出力時属性の順番を決められなかったりなど貧弱ではあるが、最初からあるってのは普通使うわな。以前、どうしても属性の順番を入力と出力で揃えたかった処理があったとき、ライブラリソースを書き換えた。Elementの初期化時に_attrsの初期値を{}からOrderdDict()に変えただけ。ライブラリソースを見ると名前空間ありなしで変数が独立にあり、割と残念な感じの実装だ。言い訳だかグチもコメントで書いてある。

def ruleTransformer(dom, trans, node):
    n = trans(dom, node.cloneNode(True))
    if not n:
        return None
    cs = []
    for c in n.childNodes:
        cs.append(ruleTransformer(dom, trans, c))
    while n.firstChild:
        n.removeChild(n.firstChild)
    for c in cs:
        if c:
            n.appendChild(c)
    return n

 ruleTransformer関数は変換用関数とノード(普通は要素)を受け取って、ノード以下のツリー上の全てのノードに変換用関数を再帰的に適用して変換結果を返す。パラメタのdomはPythonのminidom固有の事情。要素の生成にDocumentが必要だからってだけ。変換関数に渡してるnodeをクローンしてるのは一応保険。要素の削除機能を実現するため、ruleTransformer関数はNoneも返す。

 子ノードを全部削除するのに

while n.firstChild:
    n.removeChild(n.firstChild)

なんて書き方をしている。実にオブジェクト指向な手続き型言語だ。

ruleTransformer :: (Content -> Content) -> [Content] -> [Content]
ruleTransformer trans contents = 
  let
    onContent :: Content -> Content
    onContent (Elem ex) =
      let
        tx = trans (Elem ex)
        cs = case tx of
            Elem e -> map (\c -> onContent c) (elContent e)
            _ -> []
        fs = filter (\c -> not (isBlank c)) cs
      in
        case tx of
          Elem t -> Elem t { elContent = fs }
          _ -> tx
    onContent c = trans c
    isBlank :: Content -> Bool
    isBlank (Elem ex) = (elName ex == blank_name)
    isBlank c = False
  in
    map (\c -> onContent c) contents

 epub2to3_H.hsのruleTransformer関数はContentを受け取ってContentを返す関数とContentのリストを受け取ってContentのリストを返す。これ、もっと普通に再帰したかったが、ContentならまだしもContentのリストはツリーのルートにはならない。これはおそらくXMLがルート要素の前後にXML宣言があったり、任意の空白や改行があることを表現するためにこんな構造なんだろうが、パース結果はDocument相当の型を用意すべきで、こんな意味不明なContentのリストを返すのはどう考えてもおかしい。リストで表現できれば型を増やす必要がなくリーズナブルとでも思ったんだろうか。そんなわけで内部でonContent関数を用意して、Contentを受けてContentを返す形で再帰している。最上位ではそれをmapすることでリストにしている。

getDocumentRoot :: [Content] -> Maybe Element

 getDocumentRoot関数を用意してルート要素を得られるようにはしたが、結局再度XMLテキストを得るには前後の情報も必要なのでContentのリストをruleTransformerの引数にした。

 この問題は、結局Text.XML.Lightの作りの問題で、別にHaskellだから云々というわけではない。別のXMLライブラリなら別のアプローチをしてるんだろうし、Text.XML.Light は実際のところ、人気のあるライブラリではなさそうだ。Text.XML.Lightを使った最大の理由は、単に、

cabal install xml

でインストールされたからだ。名前の一等地のライブラリは責任重大なのである。下手なものを入れると全体の品質を疑われることになる。


2014.07.19


「インデックス」へ戻る


OSTRACISM CO.

OSTRA / Takeshi Yoneki