XPath と Lens で XML を扱う

はじめに

OpenGL の仕様が XML で書かれていてこれについて調べたりしてました。 XML 扱う機会があまりなくてほとんど使ったことがなかったんですが調べてみると色々と XML 使われているところはあるのだなと感じました。 XML から検索したりコード生成したりしたくて調べたことをまとめておきます。

XPath

developer.mozilla.org

ja.wikipedia.org

XPathXML Path Language の略です。 XPath を使って XML から必要な情報を検索したりできるようです。

JSON でいうところの JMESPath や jq などに近いものだと思います。

XPath は HTML にも使えるようで JavaScript やクローラでも使われたりする例を見かけました。 XPath 3.1 までバージョンが出てるらしいんですが、新しいものは対応しているツールがあまり無いようでした。

CLI から XPath を扱うことができるツールがあって XMLStarlet や xmllint が使えるようです。

xmlstar.sourceforge.net

xmlsoft.org

xmlstarlet では XML の更新も行える機能があるようなのでこちらを使ってみてます。

問い合わせの場合は sel のサブコマンドを使います。 ドキュメントはこちらです。

xmlstar.sourceforge.net

Lens

XPath を使って CLI で検索したりはできたんですが Haskell などから扱う場合にどうするのかなと調べていたところ Lens を使うと似たようなことができそうでした。

Lens についてはあまりよくわかっていないので詳しくは他の情報を参照してください。 とりあえず使うだけであれば Getter や Setter として使えるというぐらいの理解でもいいような気がします。

XML を扱う Haskell のライブラリはいくつかあるんですがあまり更新されてないものもあってどれを使うか迷いました。 XML 自体の仕様が変わっているわけでは無いので最近更新されていなくてもそんなに問題なく使えるような気もします。

hackage.haskell.org

xml-conduit は比較的更新されていそうだったためこれを使ってみています。

hackage.haskell.org

xml-conduit の Lens ライブラリとして xml-lens がありました。

hackage.haskell.org

Lens 自体のライブラリはこれです。

なお JSON にも同じように aeson というパーサーのライブラリと lens-aeson という Lens のライブラリがあります。

XPath と Lens の対応

<?xml version="1.0" ?>
<animals>
    <animal id="1">
        <name>rat</name>
    </animal>
    <animal id="2">
        <name>cow</name>
    </animal>
    <animal id="3">
        <name>tiger</name>
    </animal>
    <animal id="4">
        <name>rabbit</name>
    </animal>
</animals>

上のような XML をサンプルとして扱う場合の一部の例です。

XPath Lens
ルートの要素 / doc ^.? root
要素の階層を指定して取得 /animals/animal/name doc ^.. root . el "animals" ./ el "animal" ./ el "name"
属性に条件を付けて取得 /animals/animal[@id = "2"]/name doc ^.. root . el "animals" ./ el "animal" . attributeIs "id" "2" ./ el "name"
doc ^.. root . el "animals" ./ el "animal" . filtered ((Just "2" ==) . (^? attr "id")) ./ el "name"
子の要素に条件を付けて属性を取得 /animals/animal[name = "cow"]/@id doc ^.. root . el "animals" ./ el "animal" . filtered ((== Just "cow") . (^? plate . el "name" . text)) . attr "id"
名前が一致する要素を取得 //animal doc ^.. root . entire . named "animal"

データ型に変換する

ほとんどの場合は xml-conduit の結果の型そのままだと使いにくいと思います。 ここから自分で使いたいデータ型に変換して使うことになると思うのでデータ型に変換するあたりのコード例を書いておきます。 Maybe モナドや Either モナドでパーサーと似たような処理を書いて変換できました。

import Data.Text
import qualified  Text.XML as XML
import Text.XML.Lens

data Animal = Animal
    { animalId :: !Int
    , animalName :: !Text
    } deriving (Show)

parseAnimal :: XML.Element -> Maybe Animal
parseAnimal a = do
    aid <- a ^? attr "id"
    aname <- a ^? el "name" . text
    return (Animal aid aname)

parseAnimals :: XML.Document -> Maybe [Animal]
parseAnimals doc = mapM parseAnimal elems
    where
    elems = doc ^.. root . el "animals" ./ el "animal"