RubyでXML操作

(2007.1.14 全体的に更新。)

Rubyの標準添付XMLライブラリ、REXMLの使い方。

REXMLの特徴

REXMLは、

Ruby 1.8.5にはREXML 3.1.4が添付されている。チュートリアル、APIリファレンスは下記で見られる。が、APIリファレンスのほうは不十分かつ誤りもある。使いかたをまとめてみた。

DOMツリーの生成と要素の取り出し

何はともあれ、XML文書を読み込み、パースしないと始まらない。

まずはDOMツリーを生成してみる。REXMLのパーサは、整形式 (well-formed) なXML文書を(妥当性検査せずに)パースし、DOMツリーを生成する。

[POPUP]
  1. require "rexml/document"
  2. source = <<EOF
  3. <?xml version="1.0"?>
  4. <foo><bar />
  5. <baz>ho&lt;ge</baz>
  6. </foo>
  7. EOF
  8. doc = REXML::Document.new source
  9. p doc.root, doc.root.class
  10. #=> <foo> ... </>
  11. #=> REXML::Element
  12. p doc.elements
  13. #=> #<REXML::Elements:0x4844cbc @element=<UNDEFINED> ... </>>
  14. p doc.elements['/foo/baz'].text
  15. #=> "ho<ge":String # 実体参照が変換される。&lt;と < は区別されない。
  16. # 区別したいときはこうすればいい。
  17. p doc.elements['/foo/baz'].get_text #=> "ho&lt;ge"
  18. p doc.elements['/foo/baz'].get_text.class #=> REXML::Text

REXML::Document.newは、文字列またはIOオブジェクトを引数に取り、パースしてDOMツリーを生成する。読み込んだデータが整形式でないときは、REXML::ParseException 例外が投げられる。

Documentオブジェクトは、eachメソッドでXML宣言、要素などを順に取得できる。each メソッドでは直接の子要素がブロックに渡される。

[POPUP]
  1. require "rexml/document"
  2. doc = nil
  3. File.open("xmlsample.xml") {|fp|
  4. doc = REXML::Document.new fp
  5. }
  6. i = 0
  7. doc.each {|e|
  8. s = e.to_s.strip.split("\n")[0]
  9. print "#{i}: #{s} #<#{e.class}>\n"
  10. i += 1
  11. }

XML文書を作る

(2007.11.30 この節追加。)

今度は、何もないところからXML文書を作ってみます。次のスクリプトはATOMフィードのひな形を作ります。

単にDocument.new を呼び出すと、空のXML文書になります。

<<オペレータ、add_element(), add_text() で、外側の要素から順に作っていきます。

[POPUP]
  1. require 'rexml/document'
  2. doc = REXML::Document.new
  3. doc << REXML::XMLDecl.new('1.0', 'UTF-8')
  4. # add_element は追加した子要素を返す
  5. feed = doc.add_element("feed", {"xmlns" => "http://www.w3.org/2005/Atom"})
  6. feed.add_element("title", {'type' => 'text'}).add_text "Netsphere Feed"
  7. doc.write STDOUT

次のXML文書が出力されます。(手で整形しています。実際の出力では要素の間に空白、改行はありません。)

[POPUP]
  1. <?xml version='1.0' encoding='UTF-8'?>
  2. <feed xmlns='http://www.w3.org/2005/Atom'>
  3. <title type='text'>Netsphere Feed</title>
  4. </feed>

XPathで要素を取り出す

REXMLは、[]メソッドにXPath式を指定して、XML文書から柔軟に要素を取り出せます。

TODO:

主なクラス

(2007.12.4 更新)

REXMLでは,次の表にある左の項目は,右のクラスのオブジェクトとなります。

項目クラス
XML宣言REXML::XMLDecl
文書型宣言REXML::DocType
処理命令REXML::Instruction
要素REXML::Element
コメントREXML::Comment
CDATAセクションREXML::CData
文字データ,参照REXML::Text

Elementクラス (以下、本文ではREXML::は省略します。) はParentから、また、ParentとTextはChildクラスから派生しています。各クラスの関係は次のクラス図のとおりです。

DocumentクラスはElementクラスから派生しています。XML文書はただ一つのroot要素を持ちますが、だからと言ってElementから派生させるのはどうかと思わなくもありません (aDocument is_a Element, Parent or Child ではない)。

Note.

W3C Document Object Model (DOM) Coreでは、Documentインターフェイス、Elementインターフェイスは両方ともNodeから派生している。

いろいろなパーサ

REXMLには、ストリームパーサ、pull-parserも付いている。これらのパーサは、XML文書の使い方によって使い分ける。あるいは別のプログラミング言語からの移植の場合でも適宜使い分けることになるだろう。

ストリームパーサ

ストリームパーサは、特定の要素や属性にしか用事がないときなどに使う。ストリームパーサを使うときは、パーサにリスナオブジェクトを渡して、適宜コールバックさせる。

Parsers::StreamParserクラスには、StreamListenerモジュールが対応する。StreamListenerモジュールをincludeし、コールバックさせたい項目のメソッドを定義すればいい。

[POPUP]
  1. require 'rexml/parsers/streamparser'
  2. require 'rexml/parsers/baseparser'
  3. require 'rexml/streamlistener'
  4. # 全部のメソッドを実装しなくても、
  5. # StreamListenerモジュールをincludeすればいい。
  6. class MyListener
  7. include REXML::StreamListener
  8. # attrs ハッシュ
  9. # 属性がないときは、{}
  10. def tag_start(name, attrs)
  11. p name, attrs
  12. end
  13. def tag_end x
  14. p "tag_end: #{x}"
  15. end
  16. # encoding, standaloneは、指定がなければnil
  17. def xmldecl(version, encoding, standalone)
  18. p version, encoding, standalone
  19. end
  20. end
  21. source = File.read "01rep.xml"
  22. listener = MyListener.new
  23. REXML::Parsers::StreamParser.new(source, listener).parse

Pullパーサ

今度は、ノードを一つずつ取り出すパーサ。APIドキュメントの例は誤っている。

Parsers::PullParserオブジェクトは、has_next?メソッドとpullメソッドを持つ。pull() の戻り値はParsers::PullEventインスタンス。

[POPUP]
  1. require "rexml/parsers/pullparser"
  2. source = File.read "01rep.xml"
  3. parser = REXML::Parsers::PullParser.new(source)
  4. while parser.has_next?
  5. res = parser.pull
  6. if res.event_type == :start_element
  7. res[0..-1].each_with_index {|e, i|
  8. puts "#{i}: #{e} / #{e.class}" #=> "addressBook", {}
  9. }
  10. end
  11. end

SAX2リスナ

Parsers::StreamParserと似たパーサとして、SAX2パーサがある。

[POPUP]
  1. require "rexml/parsers/sax2parser"
  2. require "rexml/sax2listener"
  3. class MyEventListener
  4. def receive event
  5. p event
  6. end
  7. end
  8. # すべてのメソッドを実装しないときは、SAX2Listenerモジュールをincludeすればいい。
  9. class MySAXListener
  10. include REXML::SAX2Listener
  11. def start_document
  12. puts "start."
  13. end
  14. def xmldecl version, encoding, standalone
  15. p version
  16. end
  17. end
  18. source = File.read "01rep.xml"
  19. parser = REXML::Parsers::SAX2Parser.new(source)
  20. # イベントリスナをattachする。
  21. parser.add_listener(MyEventListener.new)
  22. # SAX2リスナをattachする。
  23. parser.listen(MySAXListener.new)
  24. parser.parse

XML文書の妥当性検査

XML文書の妥当性検査のための仕様 DTD, W3C XML Schema, RELAX NG のうち、REXMLでサポートされている。

サイト内リンク

(2007.12.6 ページを分けました)