XPath と Lens で XML を扱う
はじめに
OpenGL の仕様が XML で書かれていてこれについて調べたりしてました。 XML 扱う機会があまりなくてほとんど使ったことがなかったんですが調べてみると色々と XML 使われているところはあるのだなと感じました。 XML から検索したりコード生成したりしたくて調べたことをまとめておきます。
XPath
XPath は XML Path Language の略です。 XPath を使って XML から必要な情報を検索したりできるようです。
JSON でいうところの JMESPath や jq などに近いものだと思います。
XPath は HTML にも使えるようで JavaScript やクローラでも使われたりする例を見かけました。 XPath 3.1 までバージョンが出てるらしいんですが、新しいものは対応しているツールがあまり無いようでした。
CLI から XPath を扱うことができるツールがあって XMLStarlet や xmllint が使えるようです。
xmlstarlet では XML の更新も行える機能があるようなのでこちらを使ってみてます。
問い合わせの場合は sel のサブコマンドを使います。 ドキュメントはこちらです。
Lens
XPath を使って CLI で検索したりはできたんですが Haskell などから扱う場合にどうするのかなと調べていたところ Lens を使うと似たようなことができそうでした。
Lens についてはあまりよくわかっていないので詳しくは他の情報を参照してください。 とりあえず使うだけであれば Getter や Setter として使えるというぐらいの理解でもいいような気がします。
XML を扱う Haskell のライブラリはいくつかあるんですがあまり更新されてないものもあってどれを使うか迷いました。 XML 自体の仕様が変わっているわけでは無いので最近更新されていなくてもそんなに問題なく使えるような気もします。
xml-conduit は比較的更新されていそうだったためこれを使ってみています。
xml-conduit の Lens ライブラリとして xml-lens がありました。
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"