ざる魂

真似ぶ魂、学ぶの本質。知られざる我が魂

Clojureでxmlを読みこむ方法

clojureでxmlを処理する方法のメモ。随時更新予定。

基本

下記のxmlをtest.xmlとしてプロジェクト直下に保存。

<parent>
    <child>
        hello
    </child>
</parent>

clojure.xml/parse関数を使うとmapデータに変換できる。

user> (require '[clojure.xml :as xml])
user> (xml/parse (slurp "test.xml"))
{:tag :parent, :attrs nil, :content [{:tag :child, :attrs nil, :content ["\n\t\thello\n\t"]}]}

ここでは、slurp関数を使ってXMLファイルを直接読んで処理したが、 slurp以外の関数で読み込んだxmlを扱いたいときなどは、xmlは既に文字列になっていることが多い。 clojure.xml/parse関数は文字列を直接パースできないので下記のようなラッパー関数を用意する。

(defn xml-parse [s]
  (xml/parse (java.io.ByteArrayInputStream. (.getBytes s))))

これで文字列としてのxmlもパースできるようになった。

user> (def xml-doc "<parent> <child> hello </child> </parent>")
#'user/xml-doc
user> (xml-parse xml-doc)
{:tag :parent, :attrs nil, :content [{:tag :child, :attrs nil, :content [" hello "]}]}

xmlのデータが画面に表示されると、場合によってはスクロールが重くなるので、 そんな時はC-c M-o でバッファクリアすると良い感じ。

xml-seq

xmlをmapデータにしただけだと、各要素へのアクセスが大変なので xml-seq を使用して、各要素をトラバースした状態のシーケンスを用意することができる。 この関数を使用すると、それぞれのタグが先頭になった状態シーケンスが得られる。

user> (xml-seq (xml-parse xml-doc))
({:tag :parent, ;; 最初のparentタグが先頭
  :attrs nil,
  :content [{:tag :child, :attrs nil, :content [" hello "]}]}
  {:tag :child, ;; 子供のchidleタグが先頭
   :attrs nil,
   :content [" hello "]}
  " hello "     ;; 一番最後の要素
 )

こうすることで、ツリー構造なXMLデータがフラットなシーケンスデータになる。 つまり for などの従来のシーケンス関数がそのまま使用できるようになる。

<parent>
    <child name="taro">hello</child>
    <child name="hanako">wao</child>
</parent>
user>  (def xml-doc2 " <parent> <child name=\"taro\">hello</child> <child name=\"hanako\">hi</child> </parent>")
#'user/xml-doc2
user> (for [x (xml-seq (xml-parse xml-doc2)) :when (= :child (:tag x))] (:name (:attrs x)))
("taro" "hanako")

xml-zip

xml-seq でxmlデータをシーケンスにできるのは良いが、正直使いづらい。 そこでもうちょっと直感的に扱える clojure.zip/xml-zip 関数を使用してみる。

user> (require '[clojure.zip :as zip])
user> (zip/xml-zip (xml-seq (xml-parse xml-doc)))
[({:tag :parent,
   :attrs nil,
   :content [
      {:tag :child,
       :attrs nil,
       :content [
          " hello "]}]}
  {:tag :child,
   :attrs nil,
   :content [
       " hello "]}
 " hello ") nil]

zipperとは、ツリー構造を扱うためのライブラリっぽい。

XPATH

clj-xpath というライブラリを使用すれば、xpathを使うことができる。 しかし、XPATHは正規表現のようなミニ言語であり、それなりに学習コストがかかる。 また、要素を指定する部分が文字列になるため、実行時エラーの可能性と、 コンパイルで時間がかかる可能性などがあるため、現時点では使用しない。