Scheme:SXML:SXPath

Scheme:SXML:SXPath

SXPath の S 式表現がよくわからなかったのでまとめてみました。 -- leque(2007/02/11 01:26:05 PST)

sxpath 手続き

sxpath 手続きの定義は以下のようになっている。

;; $Id: sxpath.scm,v 1.1 2003/07/22 11:22:11 shirok Exp $
...
(define (sxpath path . ns-binding)
  (let loop (...)
    (cond
      ((null? path)  ; parsing is finished
       (lambda (node . root-var-binding)
         ...))
      ...)))

手続きの直前のコメントには省略記法の展開ルールが書いてある(GaucheRefj:sxpath 参照)。

ここで path は SXPath のパス表現、ns-binding

'((example . "http://www.example.com"))

のような形式で名前空間接頭辞のマッピングを指定する省略可能な引数。root-var-bindingrootvar-binding という省略可能な引数で、root には SXML ツリーのルート、var-binding には XPath の変数の束縛関係を連想リストで指定する。root が指定できるようになっているのは、SXPath に ".." などが含まれていた場合、SXML が線型リストであるために、単純に親ノードを参照することができないため。

path にはリスト形式の SXPath だけでなく XPath 記法を文字列で直接指定することもできる。

XPath との比較

XPath と、それに対応する SXPath を比較してみる。対象とする SXML ツリーは以下の通り。

(define *test-sxml*
  '(*TOP*
    (table (@ (border "1")
              (summary ""))
           (caption "test table")
           (tr (@ (class "odd"))
               (td "1") (td "2") (td "3"))
           (tr (@ (class "even"))
               (td "4") (td "5") (td (@ (id "six")) "6"))
           (tr (@ (class "odd"))
               (td "7") (td "8") (td (em "9"))))
    (p "paragraph")))

比較を簡単にするために gauche.test を利用する。

(use gauche.test)
(use sxml.sxpath)
(use sxml.tools)
(use util.list)

(define-syntax test-sxpath
  (syntax-rules ()
    ((_ s txp sxp)
     (test* (format "‾A: ‾S & `‾S" s txp 'sxp)
            ((sxpath txp) *test-sxml* *test-sxml*)
            ((sxpath `sxp) *test-sxml* *test-sxml*)))
    ((_ s txp sxp vars)
     (test* (format "‾A: ‾S & `‾S with variables `‾S" s txp 'sxp 'vars)
            ((sxpath txp) *test-sxml* *test-sxml* `vars)
            ((sxpath `sxp) *test-sxml* *test-sxml* `vars)))))

識別用のコメントと、XPath、それと同等の意味になる SXPath 式を指定する。二番目の形式ではさらに XPath 変数の束縛を指定する。

(test-start "XPath & SXPath test")

子要素

子要素へのパスは単純にその名前を書けばよい。

(test-sxpath "children" "table" (table))

さらに子要素をたどる場合は後ろに続けて書く。

(test-sxpath "children(2)" "table/caption" (table caption))

ワイルドカードを使うこともできる。

(test-sxpath "all the children" "*" (*))

属性

属性ノードは @ で表す(SXML の属性ノードは名前が @ であるノードである)。

(test-sxpath "attribute" "table/@border" (table @ border))

ここでもワイルドカードを使うことができる。

(test-sxpath "all the attributes" "table/@*" (table @ *))

テキストノード

テキストノードは *text* で表す。

(test-sxpath "text node" "table/caption/text()" (table caption *text*))

子孫ノード

子孫ノードは XPath と同様、// と書くことができる。

(test-sxpath "descendants" "//td" (// td))

not@, or@

not@, or@ を使うとある特定の種類のノードだけを選択するようなパスを簡単に書くことができる(not@, or@ 以下に書くことのできる「判定基準」は GaucheRefj:ntype?? のものと同じ)。

(test-sxpath "not@"
             "*[name() != 'table' and name() != 'div']"
             ((not@ table div)))

(test-sxpath "or@"
             "*[name() = 'p' or name() = 'ul']"
             ((or@ p ul)))

Scheme の手続きをパスの要素として使う

Scheme の手続きをつかってノードを選択することもできる。

;; コンテキストノードは *TOP* なので select-kids を使う。
(test-sxpath "Scheme procedure as SXPath"
             "*[name() = 'table']"
             (,(lambda (node root vars)
                 ((select-kids
                   (lambda (n)
                     (eq? (sxml:name n) 'table)))
                  node))))

引数の root, vars は sxpath 手続きの返した手続きの引数の rootvar-binding と同じ。

XPath 文字列をそのまま使う

XPath 式の文字列表現をそのまま sxpath 手続きに渡すだけでなく、S 式表現の SXPath のなかに混在させることもできる。

(test-sxpath "mixed pattern"
             "table/tr/td[em]"
             (table tr "td[em]"))

適宜 XPath 記法を使用した方が見通しがよくなることが多い。

ノード述語

`(sxpath0 sxpath1 ...)' と書くと sxpath0 にマッチし、かつそのノードをコンテキストノードとしたとき sxpath1 ... にマッチするようなノードを選択することができる(sxpath0 にはシンボルを指定することもできる。この場合は (sxpath0) と同じ意味になる)。

(test-sxpath "predicate - the text of the current context node"
             "//td[. = '4']"
             (// (td ((equal? "4")))))

(test-sxpath "predicate - text"
             "//tr[td = '4']"
             (// (tr (td (equal? "4")))))

(test-sxpath "predicate - attribute"
             "//tr[@class = 'even']"
             (// (tr (@ (equal? (class "even"))))))

(test-sxpath "predicate with the text of the decendant node"
             "table[tr/td/em = '9']"
             ((table (tr td (em ((equal? "9")))))))

最初のものは td のうち、ノードの値が "4" であるものを選択している。次は tr のうち、その子に、td でノード値が "4" があるノードのあるものを選択している。三番目のものは、tr で、属性ノード内に (class "even") に等しいノード(属性)のあるものを選択している。

述語の and は、単純に条件となる SXPath を複数並べればよい。

(test-sxpath "predicate - and"
             "//td[@id = 'six' and . = '6']"
             (// (td (@ (equal? (id "six")))
                     ((equal? "6")))))

このとき条件式は左から順に適用され、上の例では、ある td について (@ (equal? (id "six"))) にマッチするもの、そのマッチしたものについてさらに ((equal? "6")) にマッチするもの……というようにノードを絞り込んで行く。

not や or には簡易記法がない(?)ので手続きを使う。

(test-sxpath "predicate - not"
             "//td[. != '6']"
             (// (td (,(lambda (node root vars)
                         ((sxml:filter
                           (lambda (n)
                             (not (equal? (sxml:string-value n) "6"))))
                          node))))))

(test-sxpath "predicate - or"
             "//td[. = '4' or . = '8']"
             (// (td (,(lambda (node root vars)
                         ((sxml:filter
                           (lambda (n)
                             (member (sxml:string-value n)
                                     '("4" "8"))))
                          node))))))

この場合は以下のように XPath 記法を混在させるか、すべて XPath 記法で書いた方が簡単である。

(test-sxpath "predicate - not(mixed)"
             "//td[. != '6']"
             (// "td[. != '6']"))

(test-sxpath "predicate - or(mixed)"
             "//td[. = '4' or . = '8']"
             (// "td[. = '4' or . = '8']"))

ノードの位置による選択

条件部に数値を書くと、ノードの位置による選択をすることができる。

(test-sxpath "indexed element" "table/tr[1]" (table (tr 1)))

(test-sxpath "indexed attribute" "table/@*[1]" (table @ (* 1)))

XPath の last() は -1 と書く。

(test-sxpath "last element" "table/tr[last()]" (table (tr -1)))

もちろん階層化することもできる。

(test-sxpath "more indexed element" "table/tr[2]/td[2]" (table (tr 2) (td 2)))

ほかの述語と同時に使うこともできる。

(test-sxpath "predicate & index"
             "//tr[@class = 'odd'][2]"
             (// (tr (@ (equal? (class "odd")))
                     2)))

XPath の変数

XPath では $var の形式で変数の値を参照することができるが、SXPath でもこれは問題なく使用することができる。変数の束縛関係は sxpath 手続きの返した手続きの第三引数に指定する。

(test-sxpath "xpath variable"
             "table/tr/td[@id = $x]"
             (table tr (td (,(lambda (node root vars)
                               ((sxml:filter
                                 (lambda (node)
                                   (and-let* ((val (sxml:attr-u node 'id)))
                                     (equal? (assoc-ref vars 'x #f) val))))
                                node)))))
             ((x . "six")))

(test-end)

passed.

参考文献

関連文書


サンプルコードがあまり見当たらないようなので書いてみました。 -- satoru (2004/03/24 17:37:36 PST)

XML からデータを取り出すには次のようなコードを書けばいいようだ。 ssax:xml->sxml で XML を SXML (S式) に変換し、sxpath で探索する。

  (use sxml.ssax)
  (use sxml.sxpath)
  
  (define xml
  "<blog>
    <title>ブログ</title>
    <author>誰か</author>
    <author><ghost>もう一人</ghost></author>
  </blog>")
  
  (define (test sxml path)
    (format #t "~s => ~s\n" path ((sxpath path) sxml)))
  
  (let1 sxml (call-with-input-string xml 
               (lambda (in) (ssax:xml->sxml in '())))
    (test sxml '(blog title))
    (test sxml '(blog author))
    (test sxml '(blog author ghost))
    (test sxml '(blog date)))

実行結果:

  % gosh sxml.scm
  (blog title) => ((title "ブログ"))
  (blog author) => ((author "誰か") (author (ghost "もう一人")))
  (blog author ghost) => ((ghost "もう一人"))
  (blog date) => ()

Shiro: sxpathは私も手探りで使ってみてるところです。 とりあえず、こんなこともできます。

gosh> (let1 sxml (call-with-input-string xml (cut ssax:xml->sxml <> '()))
        (test sxml '(blog "author[1]"))
        (test sxml '(blog "author[2]")))  ;; インデックスによる選択
(blog "author[1]") => ((author "誰か"))
(blog "author[2]") => ((author (ghost "もう一人")))

gosh> (let1 sxml (call-with-input-string xml (cut ssax:xml->sxml <> '()))
        (test sxml '(blog (or@ author title))))  ;; どちらかにマッチ
(blog (or@ author title)) => ((title "ブログ") (author "誰か") (author (ghost "もう一人")))

gosh> (let1 sxml (call-with-input-string xml (cut ssax:xml->sxml <> '()))
        (test sxml '(// ghost)))  ;; // で複数の階層を表す。
(// ghost) => ((ghost "もう一人"))

リスト中の文字列はXPathとして解釈されるので、XPathを 知っていれば、リスト中にそのまま放りこんでやればXPathと同等のことが できる(そうです)。属性によって選択したりする場合はXPath記法がいいみたい。

また、ノード選択手続きをSchemeで書いといてsxpathに渡すリスト中に 埋め込むこともできるらしいです。

質問

namespace-prefix-assig ってのは具体的にどのようなものでしょ?

(GaucheRefj:ssax:xml->sxml)

 Function: ssax:xml->sxml port namespace-prefix-assig 

参考

SXMLの例: http://okmij.org/ftp/Scheme/SXML.html#Examples

Tags: SXML, sxml.sxpath, SXPath

More ...