Hatena::Grouprubyist

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

Ruby ロゴ (C) Ruby Association LLC

2011年07月18日(月)

Hpricot の拡張(実験)

| 23:59 | Hpricot の拡張(実験) - Going My Ruby Way を含むブックマーク はてなブックマーク - Hpricot の拡張(実験) - Going My Ruby Way Hpricot の拡張(実験) - Going My Ruby Way のブックマークコメント

(2011/07/23 Obsoleted by Hpricot::Doc を NodeTree に変換する - Going My Ruby Way - Rubyist)

--------------------------

Hpricot を拡張してみました。実験です。

retrieve というメソッドを追加してます。

require 'open-uri'
require 'hpricot-ex'

uri     = ARGV.shift
source  = uri ? open(uri) : ARGF
doc     = Hpricot(source)
tree    = doc.retrieve

tree はドキュメントのノードツリーを表現しています。

ノードツリーは以下のようなものです。

  • テキストノードは String で表現します。
  • エレメントノードは Array で表現します。
  • エレメントノードの 1番目の配列要素は名前(タグ)です。String です。
  • エレメントノードの 2番目の配列要素はアトリビュートです。Hash です。
  • エレメントノードの 3番目の配列要素はノードリストです。 Array です。
  • ノードリストは0個以上のノードを持ちます。
  • root 要素(エレメントノード)を格納するノードリスト(Array) がドキュメントノードに相当します(上記の tree です)。
  • 現仕様では、XML宣言、ノーテーション、コメント、PI は無視してます。

エレメントノード(Array) のイメージ

  [tag, {}, []]

ArrayHashString でノードツリー構造を表現しているので、そのまま、YAMLJSON でシリアライズしたり、dRuby のタプルスペースに投げ込めたりできるんじゃないかと思ってます。(おいおいやっていきます)

-----

Apache2 のデフォルトの index.html ("It works!" とでるアレです)を指定して pp tree すると以下のような出力になります。

(スクリプト)

require 'open-uri'
require 'hpricot-ex'
require 'pp'

doc  = Hpricot(open('http://localhost/'))
tree = doc.retrieve
pp tree

(出力)

[["html",
  {},
  [["body",
    {},
    [["h1", {}, ["It works!"]],
     ["p", {}, ["This is the default web page for this server."]],
     ["p",
      {},
      ["The web server software is running but no content has been added, yet."]]]]]]]

puts "#{tree}" とすると HTML形式で出力します。

pp tree
の代わりに
puts "#{tree}"
<html><body><h1>It works!</h1><p>This is the default web page for this server.</p><p>The web server software is running but no content has been added, yet.</p></body></html>

実装を簡略化しているため、ノーテーション、コメント、PI 等と空白文字だけのテキストは tree への変換時に省いています。

----

以下、実装です。Ruby 1.9.x で動作します。

hpricot-ex.rb

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

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

module Hpricot
  module Retrievable
    def retrieve(list=[])
      list.extend(NodeTree::NodeList).tap do |l|
        (children||[]).each do |c|
          c.touch(l) if c.respond_to?(:touch)
        end
      end
    end
  end

  class Doc
    include Retrievable
  end

  class Elem
    include Retrievable

    def touch(list)
        tag       = name
        attrs     = Hash[ attributes.to_hash.map{|a| a.map(&:toutf8)} ]
        children  = []

        list << [tag, attrs, children]

        retrieve(list.last.children)
    end
  end

  class Text
    def touch(list)
      list << name.toutf8 unless name.blank?
    end
  end
end

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

  uri     = ARGV.shift
  source  = uri ? open(uri) : ARGF
  doc     = Hpricot(source)
  tree    = doc.retrieve

  NodeTree.view(tree)
end

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

nodelist.rb

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

class NodeTree
  module NodeList
    def at(*)
      (c = super).kind_of?(Array) ? c.extend(ElemNode) : c
    end

    def first ; at( 0); end
    def last  ; at(-1); end

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

    def to_s
      map(&:to_s).join
    end
  end

  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

  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:

string.rb (使っているのは String#blank? だけ(のはず))

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

class String
  def remove(s)
    sub(s, '')
  end

  alias - remove

  def indent(n, s=' ')
    (s * n) + self
  end

  alias >> indent

  def wrap(w)
    w1 = w.at(0) || ''
    w2 = w.at(1) || w1 || ''

    w1 + self + w2
  end

  alias ** wrap

  def q
    wrap("'")
  end

  def qq
    wrap('"')
  end

  def at(n)
    self[n,1]
  end

  def blank?
    !!match(/^\s*$/)
  end

  def ucc(sep=/[^[:alnum:]]+/)
    split(sep).map{|s| s.capitalize }.join
  end

  def lcc(sep=/[^[:alnum:]]+/)
    ucc(sep).sub(/./){ $&.downcase }
  end
end

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

エレメントノードの [n] は n が Integer の場合は子要素の指定、Integer 以外の場合はアトリビュートの指定になります。

p elem        # => ['p', {'id'=>'ch01'}, ["hello", "world"]] の場合

elem['id']    # => "ch01"
elem[1]       # => "world" 

エレメントノードの [n,m,..] の場合、2個目以降の引数は子要素に渡されます。ノードリストも同様です。

p elem        # => ['div',{},[['p',{},["hello","world"]]]] の場合

elem[0, 1]    # => "world"
elem[0][1]    # => "world" (同じ)
ノードツリー tree の場合
tree[0]       # たいてい html要素
tree[0, 0]    # たいてい head 要素
tree[0, 1]    # たいてい body 要素

ノードツリー上の各ノードの位置は、祖先ノードの位置情報の並びで

[0, 1, 1, 0, 2, 1, 4, 0, 2]

のように表わせます。(SNMPMIB の OID のような感じです)

NodeTree.view はノードの位置情報を表示します。

NodeTree.view(tree)

hpricot-ex.rb を単体で実行すると引数の URI のドキュメントを読んで NodeTree.view で出力します。Apache2 のデフォルト index.html の場合、以下のようになります。

$ ./hpricot-ex.rb  http://localhost/
[0] >>> ["html", {}]
[0, 0] >>> ["body", {}]
[0, 0, 0] >>> ["h1", {}]
[0, 0, 0, 0] >>> "It works!"
[0, 0, 1] >>> ["p", {}]
[0, 0, 1, 0] >>> "This is the default web page for this server."
[0, 0, 2] >>> ["p", {}]
[0, 0, 2, 0] >>> "The web server software is running but no content has been added, yet."

行の左の位置情報を tree に与えるとそのノードを取得できます。

tree[0, 0, 2, 0]  #=> "The web server software is running but no content has been added, yet."

-----

実装を簡単にするため、仕様を以下のように倒しています。

  • テキストノードとアトリビュートは UTF-8 に変換
  • 「無視できるホワイトスペース」と思われる空白のみのテキストは無視

UTF-8 変換と空白テキスト無視を行なわない場合、以下のように hpricot-ex.rb を修正します。

(修正前)
  class Elem
    include Retrievable

    def touch(list)
        tag       = name
        attrs     = Hash[ attributes.to_hash.map{|a| a.map(&:toutf8)} ]
        children  = []
         :
(修正後)
  class Elem
    include Retrievable

    def touch(list)
        tag       = name
        attrs     = attributes.to_hash
        children  = []
         :
(修正前)
  class Text
    def touch(list)
      list << name.toutf8 unless name.blank?
    end
  end
(修正後)
  class Text
    def touch(list)
      list << name
    end
  end