scramble cadenza

技術ネタのガラクタ置き場

まるごと hpricot

イントロ

何かと便利な html parser の説明。
自分用の忘備録も兼ねて逆引きっぽく書いてみるテスト。

主な使用法

  1. Hpricot.parse(Hpricot() でも良い) メソッドの引数に、parse したい html を与える。
  2. search or at メソッドに「検索したい条件」を引数に与えて、必要な情報を抜き出す
doc = Hpricot(html) #=> Hpricot.parse(html) と同じ
doc.search(...) #=> 必要な情報
doc.at(...) #=> 必要な情報

search と at の違い

  1. 返り値のオブジェクト
    • search のときは Hpricot::Elements
    • at のときは Hpricot::Elem
  2. 返してくれる要素の個数
    • search は複数件
      • 条件にマッチした全てを Hpricot::Elements オブジェクトに内包して返してくれる。
    • at は一件のみ
      • 条件にマッチした最初の部分を、Hpricot::Elem オブジェクトにして返す
  3. もし探したい情報で、見つけられなかった場合
    • search のときは Hpricot::Elements
    • at のときは nil
aliases
alias_method :/, :search
alias_method :%, :at

らしいです。

search を使う場合

1. タグ名で探したい

Hpricot::Elements#search の引数に タグ名 を文字列で渡す

html = '<div>hoge</div>'

doc = Hpricot(html)
doc.search('div')
#=> #<Hpricot::Elements[{elem <div> "hoge" </div>}]>

該当するタグが複数件ヒットしたら、どんなに階層が深くても、全部まとめて返します。

ちなみに当てはまるタグが無かったら

doc.search('hoge')
=> #<Hpricot::Elements[]>

のようになります。
「Hpricot::Elements」の表示の後ろが、空配列 なら見つからなかったという意味です。

Hpricot::Elements オブジェクトは enumerable を継承していますので、each が使えるオブジェクトです。
分かりづらければ、ヒットした複数の要素が配列に入って返ってきていると思えばよいかと。

2. ネストされた内部のタグを検索したい

search の引数を、スペースで区切る。
もしくは '>' で区切る。 (こちらのほうがより直感的)

html = <<-EOF
<table>
  <tr>
    <td>
      <div>
        hoge
      </div>
    </td>
  </tr>
</table>
EOF

doc = Hpricot(html)
doc.search('table tr td div')
#=>  #<Hpricot::Elements[{elem <div> "\n   hoge\n  " </div>}]>

# こちらでもよい
doc.search('table > tr > td')
#=>  #<Hpricot::Elements[{elem <div> "\n   hoge\n  " </div>}]>

# この例では div はひとつしか無いので、div だけ取りたいならこれで十分
doc.search('div')
#=>  #<Hpricot::Elements[{elem <div> "\n   hoge\n  " </div>}]>

意味としては

doc.search('table').search('tr').search('td')

ですが、くどい...

3. class で検索したい

search の引数に ".class名" を渡す

html = '<div class="link">hoge</div>'
doc = Hpricot(html)
doc.search('.link')
#=> #<Hpricot::Elements[{elem <div class="link"> "hoge" </div>}]>

タグ名とセットで使うことも可能

html = '<div class="link">hoge</div>'
doc = Hpricot(html)
doc.search('div.link') #=> td タグで、かつ class が link のものを search する。
#=>#<Hpricot::Elements[{elem <div class="link"> "hoge" </div>}]>

4. id で検索したい

search に "#id名" を渡す。

html = '<div id="hoge">Id = hoge の要素</div>'
doc = Hpricot(html)
doc.search('#hoge')
#=>#<Hpricot::Elements[{<div id="hoge"> "Id = hoge の要素"}]>

5. タグの要素内容を取得したい

Hpricot::Elements#inner_html を使う

html = '<div>ここが欲しい</div>'

doc = Hpricot(html)
doc.search('div').inner_html
#=> "ここが欲しい"
aliases
alias_method :html, :inner_html
alias_method :innerHTML, :inner_html

6. 属性値を取得したい

Hpricot::Elements#attr を使う

html = '<div><a href="http://www.example.jp"></a></div>'
doc = Hpricot(html)
doc.search('div > a').attr('href')
#=> "http://www.example.jp"
aliases
alias_method :set, :attr

7. タグを含めた html を取得したい

Hpricot::Elements#to_html を使う
search メソッドを使ってヒットした部分まるごとを返してくれる

html = <<-EOF
<table>
  <tr>
    <td>
      <div>
        hoge
      </div>
    </td>
  </tr>
</table>
EOF

doc = Hpricot(html)
doc.search('td').to_html
#=> => "<td>\n      <div>\n        hoge\n      </div>\n    </td>"

html のdomを見やすくするために、改行スペースを入れたから、結果にもそれが出ています。

aliases
alias_method :to_s, :to_html

8. (子要素がある状態で)子要素全てのテキストを取得したい

Hpricot::Elements#inner_text を使う

html = <<-EOF
<table>
  <tr>
    <td>ルフィ</td>
    <td>ゾロ</td>
    <td>サンジ</td>
  </tr>
</table>
EOF

doc = Hpricot(html)
doc.search('table').inner_text
#=> "\n  \n   ルフィ\n   ゾロ\n   ナミ\n  \n"
aliases
alias_method :text, :inner_text

at を使う場合

以下は at で検索した時にのみ使える。

9. 何かで検索して、マッチした最初の要素を取得したい

Hpricot::Elements#at を使う search と同じように使える。だが、返り値は Hpricot::Elem オブジェクトであることに注意。

html = <<-EOF
<div>
  <span>ルフィ</span>
  <span>ゾロ</span>
  <span>サンジ</span>
</div>
EOF

doc = Hpricot(html)
doc.at('span').inner_html
=> "ルフィ"

10. (指定した要素の)親要素 を取得したい

Hpricot::Elem#parent を使う

html = <<-EOF
<table>
  <tr>
    <td>ルフィ</td>
    <td>ゾロ</td>
    <td>サンジ</td>
  </tr>
  <tr>
    <td>クロコダイル</td>
    <td>Mr. 1</td>
    <td>Mr. 2</td>
  </tr>
</div>
EOF

doc = Hpricot(html)
doc.at('tr').parent
#=>  {elem <table> "\n  " ...} (Hash っぽく見えるけど、ちゃんとHpricot::Elem クラスのインスタンス)

11. (指定した要素の)子要素を取得したい

Hpricot::Elem#containers を使う

返り値は Array
似たようなメソッドに Hpricot::Elem#children があるが、こちらはテキストを含んだ要素を返すメソッド。したがって、タグ間に改行などが入ると、その改行を含めた配列を返してくれる。

html = <<-EOF
<table>
  <tr>
    <td>ルフィ</td>
    <td>ゾロ</td>
    <td>サンジ</td>
 </tr>
 <tr>
    <td>クロコダイル</td>
    <td>Mr. 1</td>
    <td>Mr. 2</td>
 </tr>
</div>
EOF

doc = Hpricot(html)

# containers
doc.at('tr').containers
#=> [{elem <td> ..., {elem <td> "ルフィ", {elem <td>}] (Array であることに注意)

# children
doc.at('tr').children
#=> ["\n    ", {elem <td> "ルフィ" </td>}, "\n] (改行っぽいテキスト(Hpricot::Textのオブジェクト)が先頭に入っていることに注意)

12. タグ名を取得したい

Hpricot::Elem#name を使う

html = <<-EOF
<table>
  <tr>
    <td>ルフィ</td>
    <td>ゾロ</td>
    <td>サンジ</td>
 </tr>
 <tr>
    <td>クロコダイル</td>
    <td>Mr. 1</td>
    <td>Mr. 2</td>
 </tr>
</div>
EOF

doc = Hpricot(html)

# 親要素のタグ名
doc.at('tr').parent.name
#=> "table"

# 子要素のタグ名
doc.at('tr').containers.first.name
#=> "td"

13. (同じ階層の)隣の要素を取得したい

Hpricot::Elem#next_sibling
Hpricot::Elem#previous_sibling を使う

似たようなメソッドに Hpricot::Elem#previous_node, Hpricot::Elem#next_node があるが、こちらはテキストを含んだ要素を返します。したがってタグ間に改行などが入ると、それを含めて「次の要素」を返してくれます。
Hpricot::Elem#children ベースでの「次の要素」と考えれば良い。

html = <<-EOF
<table>
  <tr>
    <td>ルフィ</td>
    <td>ゾロ</td>
    <td>サンジ</td>
 </tr> ....★
 <tr>
    <td>クロコダイル</td>
    <td>Mr. 1</td>
    <td>Mr. 2</td>
 </tr>
</div>
EOF

doc = Hpricot(html)

# next_sibling
# 最初の tr タグの次のタグを取得
doc.at('tr').next_sibling.inner_html
#=> "\n    <td>クロコダイル</td>\n    <td>Mr. 1</td>\n    <td>Mr. 2</td>\n "

# next_node
# ★の改行が返ってくる
doc.at('tr').next_node
#=>  "\n " (改行に見えるけど、Hpricot::Text オブジェクト)

# 改行の次は、tr 要素
 doc.at('tr').next_node.next_node
#=> {elem <tr> "\n  <td>クロコダイル....} (Hpricot::Elem オブジェクト)

まとめ

疲れた

参考

Module: Hpricot — Documentation for hpricot/hpricot (master)
Route 477(2007-02-05)