Hatena::Grouprubyist

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

Ruby ロゴ (C) Ruby Association LLC

2011年07月23日(土)

Hpricot::Doc を NodeTree に変換する

| 21:38 |  Hpricot::Doc を NodeTree に変換する - Going My Ruby Way を含むブックマーク はてなブックマーク -  Hpricot::Doc を NodeTree に変換する - Going My Ruby Way  Hpricot::Doc を NodeTree に変換する - Going My Ruby Way のブックマークコメント

(2011/07/23 Updates by Hpricot の拡張(実験) - Going My Ruby Way - Rubyist)

--------

nodetree/hpricot.rb

#!/usr/bin/env ruby

require 'hpricot'
require 'kconv'
require 'string'
require 'nodetree'

module NodeTree
  module HpricotAdapter
    def self.scan(doc)
      composer = NodeTree::Composer.new do
        encode       {|s| s.toutf8 }
        ignore_text  {|s| s.blank? }
      end

      retrieve = lambda do |nodelist|
        (nodelist||[]).each do |node|
          case node
            when Hpricot::Text
              composer << [:text, node.name]

            when Hpricot::Elem
              composer << [:stag, node.name]

              composer << [:attrs, node.attributes.to_hash]

              retrieve.call(node.children)

              composer << [:etag, node.name]
          end
        end
      end

      retrieve.call(doc.children)

      composer.doc
    end
  end
end

if __FILE__ == $0
  require 'open-uri'

  uri     = ARGV.shift
  source  = uri ? open(uri) : ARGF
  doc     = Hpricot(source)
  tree    = NodeTree::HpricotAdapter.scan(doc)

  NodeTree.view(tree.children)
end

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

composer の encode はテキストと属性の値に影響します。(toutf8upcase などに換えて試すと効果がよく分かります)

ignore_text は真になる(上の場合ブランクな場合)テキストは無視されます。

-------

このスクリプトにより、前に書いた hpricot-ex.rb (Hpricot を直接拡張したもの)は廃止です。

NodeTree を REXML::Document に変換する

| 21:32 |  NodeTree を REXML::Document に変換する - Going My Ruby Way を含むブックマーク はてなブックマーク -  NodeTree を REXML::Document に変換する - Going My Ruby Way  NodeTree を REXML::Document に変換する - Going My Ruby Way のブックマークコメント

nodetree/rexml.rb

#!/usr/bin/env ruby

require 'rexml/document'
require 'nodetree'

module NodeTree
  module REXMLAdapter
    extend self

    def to_doc(tree)
      build = lambda do |nodelist, cur|
        nodelist.each do |node|
          case node
            when String # Text
              cur.add_text(node)

            when Array  # Element
              cur.add_element(node.tag).add_attributes(node.attrs)

              build.call(node.children, cur.children.last)
          end
        end
      end

      (REXML::Document.new << REXML::XMLDecl.new).tap do |doc|
        build.call(tree.children, doc)
      end
    end

  end
end

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

ループと case を使ってる。

この構造の走査をする時は毎回同じ"手続き"を書かなくてはならなくなる。もっと関数型っぽくうまく書き直したい。

NodeTree の REXML ストリームパース対応

| 15:38 |  NodeTree の REXML ストリームパース対応 - Going My Ruby Way を含むブックマーク はてなブックマーク -  NodeTree の REXML ストリームパース対応 - Going My Ruby Way  NodeTree の REXML ストリームパース対応 - Going My Ruby Way のブックマークコメント

REXML::Document.parse_stream のリスナーとして渡すクラスです。

REXML でストリームパースして、listener.doc で NodeTree(ツリー構造)を取り出します。(ややこし。。)

nodetree/rexml/stream_listener.rb

#!/usr/bin/env ruby

require 'rexml/document'
require 'rexml/streamlistener'
require 'nodetree'
require 'property'

module NodeTree
  module REXMLAdapter
    class StreamListener
      include REXML::StreamListener

      def tag_start(tag, attrs)
        composer << [:stag, tag]
        composer << [:attrs, attrs]
      end

      def tag_end(tag)
        composer << [:etag, tag]
      end

      def text(s)
        composer << [:text, s]
      end

      def doc
        composer.doc
      end

      private
      property :composer, 'NodeTree::Composer.new'
    end
  end
end

if __FILE__ == $0
  listener = NodeTree::REXMLAdapter::StreamListener.new
  REXML::Document.parse_stream(xml=ARGF.read, listener)
  p listener.doc.children
end

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

PYX 形式の XML パーザ その 4

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

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

----

なんか毎日変えてますが。。。

PYXParser.parse の Tree 構造の構築部分を NodeTree::Composer クラスに任せるように変更です。

pyxparser.rb

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

require 'object'
require 'nodetree'

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)
    NodeTree::Composer.new {|composer|
      new(source).parse do |c, name, value, text|
        case c
          when '(' ; composer << [:stag, name]
          when ')' ; composer << [:etag, name]
          when 'A' ; composer << [:attr, name, value]
          when '-' ; composer << [:text, text]
          when '?' ; # ignore
        end
      end
    }.doc
  end
end

if $0 == __FILE__
#
# example 1 (stream parse)
#

#  (ここは略)

#
# example 2 (parsed tree)
#
require 'pp'

doc = PYXParser.parse
pp doc.children

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

以下は、変更した nodetree.rb です。Composer クラス、DocElem モジュール、が追加になり、NodeList モジュールが変更になりました。

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

require 'object'
require 'property'

module NodeTree
  module Attrs
    def to_s
      map{|n,v| %( #{n}="#{v}") }.join
    end
  end

  module ElemNode
    def tag       ; at(0)                  ; end
    def attrs     ; at(1).extend(Attrs)    ; end
    def children  ; at(2).extend(NodeList) ; end

    def [](i, *a)
      (i.kind_of?(Integer) ? children : attrs)[i, *a]
    end

    def to_s
      "<#{tag}#{attrs}>#{children}</#{tag}>"
    end
  end

  module DocNode
    def self.create(ary=[])
      ary.replace([nil, nil, []]).extend(DocNode)
    end

    def children
      at(2).extend(NodeList)
    end

    def [](i, *a)
      children[i, *a]
    end

    def to_s
      children.to_s
    end
  end

  module NodeList
    def self.to_node(node)
      node.kind_of?(Array) ? node.extend(ElemNode) : node
    end

    def [](i, *a)
      a.empty? ? at(i) : at(i)[*a]
    end

    def []=(i, node)
      super(i, NodeList.to_node(node))
    end

    def push(node)
      super(NodeList.to_node(node))
    end

    def unshift(node)
      super(NodeList.to_node(node))
    end

    alias << push
    alias >> unshift

    def to_s
      map{|s| s.to_s }.join
    end
  end

  class Composer
    def initialize(doc=[], &block)
      stack = [@doc = DocNode.create(doc)]

      @handlers = {
        :stag => proc {|nm,|
          stack.last.children << elem = [nm, {}, []]
          stack << elem
        },

        :etag => proc { stack.pop },

        :attr => proc {|nm, val,| stack.last.attrs[nm] = encode[val] },

        :text => proc {|s,| stack.last.children << encode[s] unless ignore_text[s] },
      }

      @handlers[:attrs] = proc {|attrs,| attrs.each(&@handlers[:attr])}

      cascade(&block)
    end

    attr_reader :doc
    property    :encode,      'proc {|s| s }'
    property    :ignore_text, 'proc {|s| false }'

    def push(key, *a)
      (@handlers[key] || proc {}).call(*a)
      self
    end

    def <<(a)
      push(*a)
    end
  end

  def self.view(nodelist, n=[])
    nodelist.each_with_index do |c, i|
      print (n+[i]).inspect + ' >>> '

      if c.kind_of?(Array)
        p c.slice(0,2)
        view(c.children, n+[i])
      else
        p c
      end
    end
  end
end

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

なぜか NodeTree が class になってたので module に修正しました。

Rinda の RingServer, RingProvider, RingFinger の拡張

| 13:09 |  Rinda の RingServer, RingProvider, RingFinger の拡張 - Going My Ruby Way を含むブックマーク はてなブックマーク -  Rinda の RingServer, RingProvider, RingFinger の拡張 - Going My Ruby Way  Rinda の RingServer, RingProvider, RingFinger の拡張 - Going My Ruby Way のブックマークコメント

Ring は Rinda のネームサービスです。プロバイダの提供する名前付きサービスを見つけることができます。

Rinda は dRuby 版の Linda (wikipedia:Linda)です。

----

参考リンク

----

拡張したのは 3 つのクラスのクラスメソッドです。

ring-ex.rb

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

require 'rinda/tuplespace'
require 'rinda/ring'
require 'object'

class << Rinda::RingServer
  def start(ts=Rinda::TupleSpace.new)
    ring_server = Rinda::RingServer.new(ts)
    DRb.thread.join
  end
end

class << Rinda::RingProvider
  def start(name, front=Rinda::TupleSpace.new, desc='', renewer = nil)
    provider = Rinda::RingProvider.new(name, front, desc, renewer)
    provider.provide
    DRb.thread.join
  end
end

class << Rinda::RingFinger
  def get_service(name, timeout=nil, &block)
    ring  = Rinda::RingFinger.primary
    tuple = ring.read([:name, name, nil, nil], timeout)
    front = tuple[2]
    front.cascade(&block)
  end

  def get_service_as_tuplespace(name, timeout=nil, &block)
    front = get_service(name, timeout)
    ts    = Rinda::TupleSpaceProxy.new(front)
    ts.cascade(&block)
  end
end

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

Rinda::RingServer.start は Ringサーバを起動します。起動したら返ってきません。

Rinda::RingProvider.start はサービスプロバイダを起動します。起動したら返ってきません。1番目の引数でサービスの名前を指定します。2番目の引数でサービスの front オブジェクトを指定できます。指定しない場合は、TupleSpace が new されて使用されます。

Rinda::RingFinger.get_service はサービスプロバイダの名前でサービスのfrontオブジェクトを取得します。

Rinda::RingFinger.get_service_as_tuplespace は get_service のラッパです。取得した front を tuplespace と想定して、それを引数にして TupleSpaceProxy を new して返します。

-----

ring-ex.rb はオリジナルに合わせて rinda サブディレクトリに格納します。

私は、~/lib/ruby/lib/ を $RUBYLIB に設定しているので、~/lib/ruby/lib/rinda/ に ring-ex.rb を置きました。

-----

使ってみます。

それぞれ irb-r rinda/ring-ex をつけて起動します。irb は 4つ使います。

それぞれ、最初に DRb.start_service で dRuby のサービスを起動するのを忘れないようにします。

Ring サーバ起動

$ irb -r rinda/ring-ex
>> DRb.start_service
...
>> Rinda::RingServer.start    # サーバは動き続けるため返ってこない

TupleSpace をサービスするプロバイダを起動。サービス名は test1。

$ irb -r rinda/ring-ex
>> DRb.start_service
...
>> Rinda::RingRingProvider.start(:test1)    # 返ってこない

クライアント#1。test1 の TupleSpace にメッセージを書き込む。

メッセージの形式は [:message, "...(メッセージ)..." ] とします。

$ irb -r rinda/ring-ex
>> DRb.start_service
...
ts = Rinda::RingFinger.get_service_as_tuplespace(:test1) # TupleSpae 取得
...
>> ts.write([:message, "hello"])   # メッセージ書き込み
=> #<Rinda::TupleEntry:0x00000001dc8358>
>> 

クライアント#2。test1 の TupleSpace からメッセージを読み出す。

$ irb -r rinda/ring-ex
>> DRb.start_service
...
>> ts = Rinda::RingFinger.get_service_as_tuplespace(:test1) # TupleSpae 取得
...
>> ts.read([:message, nil])
=> [:message, "hello"]
>> 

できました。サーバは CTRL-C で止めてください。