Hatena::Grouprubyist

Going My Ruby Way このページをアンテナに追加 RSSフィード

Ruby ロゴ (C) Ruby Association LLC

2011年07月22日(金)

PYX 形式の XML パーザ その 3

| 22:38 | PYX 形式の XML パーザ その 3 - Going My Ruby Way を含むブックマーク はてなブックマーク - PYX 形式の XML パーザ その 3 - Going My Ruby Way PYX 形式の XML パーザ その 3 - Going My Ruby Way のブックマークコメント

(2011/07/23 Obsoletes by

PYX 形式の XML パーザ その 4 - Going My Ruby Way - Rubyist)

(2011/07/22 Updates by PYX 形式の XML パーザ その2 - Going My Ruby Way - Rubyist)

----

このパーザは基本的にストリームパースをしますが、Treeパースする処理をクラスメソッドとして追加しました。(といっても前書いたサンプルをクラスの中に取りこんだだけですが)

前の日記で書いた object.rb も使ってます。(cascade を使うためです。代わりに config メソッドは廃止しました)

#!/usr/bin/env ruby
# -*- coding: UTF-8 -*-

require 'object'

class PYXParser
  def initialize(source=nil, &block)
    @source = source

    cascade(&block)
  end

  attr_accessor :source

  def listener(c, &block)
    (@listeners ||= {})[c] = (block || @listeners[c] || proc {})
  end

  def parse(source=nil, &block)
    (source || @source || ARGF).grep(/^(.)((\S+)(?:\s+(.+))?)/) {
      [$1, $2, $3, $4]
    }.map{ | c, text, name, value|

      text.gsub!(/\\(.)/) {{'r'=>"\r",'n'=>"\n",'t'=>"\t"}[$1] || $1}
      
      dispatcher = proc {|c, *a| listener(c).call(*a) }

      (block || dispatcher).call(c, name, value, text)
    }
  end

  def self.parse(source=nil)
    stack = [doc = [nil, nil, []]]

    new(source).cascade do
      listener('(') do |name,|
        stack.last[2] << elem = [name, {}, []]
        stack << elem
      end

      listener(')') do |name,|
        stack.pop
      end

      listener('A') do |name, value,|
        stack.last[1][name] = value
      end

      listener('-') do |name, value, text|
        stack.last[2] << text
      end

      listener('?') do |name, value, text|
        # ignore
      end

      parse
    end

    doc
  end
end

if $0 == __FILE__
=begin
#
# example 1
#
PYXParser.new do |parser|
  parser.parse do |c, name, val, line|
    puts "c:'#{c}' name:#{name} val:#{val} |#{line}|"
  end
end
=end

=begin
#
# example 2
#
PYXParser.new do |parser|
  parser.listener('(') do |name,|
    puts "START TAG: '#{name}'"
  end

  parser.listener(')') do |name,|
    puts "END TAG: '#{name}'"
  end

  parser.listener('A') do |name, value,|
    puts %|Attribute: #{name}="#{value}"|
  end

  parser.listener('-') do |name, value, text|
    puts "TEXT: '#{text}'"
  end

  parser.listener('?') do |name, value, text|
    puts "PI: '?#{name} #{value}?'"
  end

  parser.parse
end
=end

#
# example 3
#
require 'pp'

doc = PYXParser.parse
pp doc[2]
=begin
=end

end

# vi:set ts=2 sw=2 et fenc=UTF-8:

使い方は example 3 のところの通りです。

----

ノードツリー構造についてです。

ノードツリー構造は Array で表現された Element の入れ子です。

  Element : [ tag, {}, [] ]     : [String, Hash, Array]
  Document: [ nil, nil, [] ]    : [nil, nil, Array]
  Text    : "..."               : String

Element の 0番目の要素がタグ、1番目の要素が属性(属性名、値とも String)、2番目の要素が子要素のArray(ノードリスト)の Array です。子要素のArray(ノードリスト)に Element, Text などが格納されてツリー構造を成します。

Document は Element の特殊な形です。子要素のArrayだけが有効に使用されます。

Text は String で表現されます。

カスケード (Object の拡張)

| 22:08 |  カスケード (Object の拡張) - Going My Ruby Way を含むブックマーク はてなブックマーク -  カスケード (Object の拡張) - Going My Ruby Way  カスケード (Object の拡張) - Going My Ruby Way のブックマークコメント

Object を拡張してカスケードパターンを実装します。

Object のようなコアな部分を拡張するのはどうかという議論もありますが、ここは Going My Ruby Way (略してGMRW) です。

カスケードは、同じレシーバに複数のメッセージを送るパターンです。

Smalltalk ベストプラクティス・パターン』に紹介されています。

(解釈が正しいかはあんまり自信がありません)

----

object.rb

#!/usr/bin/env ruby
# -*- coding :UTF-8 -*-

class Object
  def cascade!(&block)
    block.arity > 0 ? tap(&block) : instance_eval(&block)
    self
  end

  def cascade(&block)
    block ? cascade!(&block) : self
  end
end

# vi:set ts=2 sw=2 et fenc=UTF-8:

cascade! はブロックが渡されるのを期待します。渡されないと怒ります!

cascade はブロックが渡された場合のみブロックを実行します。温厚な性格です。

引数なしブロックが渡された場合は、ブロックを self の instance_eval で評価します。

引数ありのブロックが渡された場合は、self をブロックの1番目の引数として渡します。

casecade、casecade! とも self を返します。

----

例です。

>> "hello".cascade { upcase! }
=> "HELLO"

>> "hello".cascade {|s| s.upcase! }
=> "HELLO"

ブロックの中の upcase!upcase の場合、返る値は "hello" です。

casecade、casecade! とも self を返しますので。

======

ここまで、日記を書いて cascade! の実装を block.call(&block) から tap(&block) に変更しました。

cascade は、結局 tap のちょっとしたバリエーションな感じです。

----

参考書籍

アクセサ定義 property

| 21:42 | アクセサ定義 property - Going My Ruby Way を含むブックマーク はてなブックマーク - アクセサ定義 property - Going My Ruby Way アクセサ定義 property - Going My Ruby Way のブックマークコメント

Ruby ベストプラクティス』第3章に「2つの目的を兼ねたアクセサ」として

c = C.new
c.font_size(30)   # c.font_size = 30 と同じ働き

のような例が書かれてました。が、本の紹介は自分的にはイマイチだったので自分流カスタマイズです。(GMRW(=Going My Ruby Way)です。改題したばっかですし)

以下の日記で書いた proc_accessor の改良(?)でもあります。

----

インスタンス変数へのアクセサを定義するメソッド propertyです。

以下のように使います。

class C
  extend Property

  property :foo
end

c = C.new
c.foo(10)   # インスタンス変数 @foo に 10 が設定されます
c.foo       # => 10  (@foo を返しています)

本と変わりませんね。

さらに、こうも使えます。proc を設定できます。

c.foo do |s|  # c.foo = proc {|s| s + "!!"} と同じです。
  s + "!!"
end

c.foo.call("hello")  # => "hello!!"  

前の日記で書いた proc_accessor の機能の取り入れです。

さらに、2 番目の引数でデフォルト値を記述できます。

デフォルト値は module_eval で評価するので文字列で与えます。

シングルクォーテーションで囲って渡しましょう。

class C
  property :val, '10'
  property :str, '"hello"'
  property :ary, '[]'
  property :hsh, '{}'
  property :sym, ':x'
  property :prc, 'proc {|s| s * 2}'
  property :hol, 'Hash.new {|h,k| h[k] = k * 3}'
end

c.val           # => 10
c.str           # => "hello"
c.ary           # => []
c.hsh           # => {}
c.sym           # => :x
c.prc.call(10)  # => 20
c.hol[3]        # => 9
c.hol["A"]      # => "AAA"

数値、truefalsenil に関してはクォーテーションで囲まなくとも大丈夫です。

(リテラルを含む) [] や {} も大丈夫です。

class C
  property :x, 10
  property :y, true
  property :z, false
  property :w, nil
end

c.x    # => 10
c.y    # => true
c.z    # => false
c.w    # => nil

Symbol をクォーテーションで囲まずに渡すと(本当の)シンボルに評価されます。

class C
  property :bar, :str
end

c.bar    # => "hello"

:str はシンボル str に評価されてメソッド str を呼び出しました。(str の返す値 "hello" を返しています)

デフォルト値の評価は、最初にアクセサを呼び出した時に行なわれます。

c.str("goodbye")  # この後で、@str を変更しても
c.bar             # "hello" が返る

また、property では、foo=() の定義はされません。

必要ならば attr_writer を使いましょう。(property は attr_reader の置き換え相当です)

----

で、コードです。

property.rb

#!/usr/bin/env ruby
# -*- coding : UTF-8 -*-

module Property
  extend self

  def property(name, default='')
    module_eval %-  
      def #{name}(*a, &block)
        @#{name} = (#{default}) unless instance_variable_defined? :@#{name}

        @#{name} = !a.empty? ? a[0]
                 : block     ? block
                 : @#{name}
      end
    -
  end
end

class Object
  extend Property
end

# vi:se ts=2 sw=2 et fenc=UTF-8:

(2011/07/23 変更: class Objectextend Property するようにした。property.rb を require したら extend しなくとも各Class定義で property が使えます。(というか強制的に使えるようになってしまう)

日記改題

20:51 | 日記改題 - Going My Ruby Way を含むブックマーク はてなブックマーク - 日記改題 - Going My Ruby Way 日記改題 - Going My Ruby Way のブックマークコメント

日記を改題しました。(旧題「lnznt の Ruby 日記」)

----

君は、君の道を行き給え。

私は、私の道を行く。