それHpricot(ry

あまりしっかり読んでなくてスルーしていたのですが,

http://wota.jp/ac/?date=20070115#p01

で使われているような特定のページにCSSセレクタをかけるようなケースならまさにHpricotが適任だと思います。

ScrAPIの良いところはむしろ簡単にParser用のクラスを分かりやすく書けるところにあると思うので大量のページをクロールして定型のデータを貯めたりしようとする際にはあのAPIがいいなぁと思います。

ということで上のページと同じことをHpricotでやってみます。

require 'kconv'
  #=> true

require 'open-uri'
  #=> true

require 'hpricot' # 以下の例はversion 0.5以上を想定してます
  #=> true

$KCODE = 'u'
  #=> "u"

maiha = Hpricot.parse(<<-EOT)
<tr>
 <td class="style_td">スライム</td>
 <td class="style_td">基本のスライム</td>
</tr>
<tr>
 <td class="style_td">バブルスライム</td>
 <td class="style_td">緑の溶けてる奴</td>
</tr>
EOT
  #=> #<Hpricot::Doc {elem <tr> "\n " {elem <td class="style_td"> "\343\202\271\343\203\251\343\202\244\343\203\240" </td>} "\n " {elem <td class="style_td"> "\345\237\272\346\234\254\343\201\256\343\202\271\343\203\251\343\202\244\343\203\240" </td>} "\n" </tr>} "\n" {elem <tr> "\n " {elem <td class="style_td"> "\343\203\220\343\203\226\343\203\253\343\202\271\343\203\251\343\202\244\343\203\240" </td>} "\n " {elem <td class="style_td"> "\347\267\221\343\201\256\346\272\266\343\201\221\343\201\246\343\202\213\345\245\264" </td>} "\n" </tr>} "\n">

$KCODEを設定していてもなぜかinspect結果が日本語で表示されないのは何とかならないかなぁと思っています

grepのようにつかう

maiha.search("tr td").map {|e| e.inner_text }
  #=> ["スライム", "基本のスライム", "バブルスライム", "緑の溶けてる奴"]

String#scrapeに比べると冗長ですが属性を返したりとかするときでもほぼ同様にできるのでこれはこれで良いと思います。

キモいのが好きな人は

(maiha/"tr td").map {|e| e.inner_text }

でもOK

CSS3をエンジョイ

maiha.search("tr td:nth-child(0)").map {|e| e.inner_text }
  #=> ["スライム", "バブルスライム"]

ここでのインデックスに0を指定しているのに注意。どうやらHpricotだと数え方が0から始まっていて一つずれているようです。

CSS3のセレクタの仕様によれば,

http://www.w3.org/TR/2005/WD-css3-selectors-20051215/#structural-pseudos

The :nth-child(an+b) pseudo-class notation represents an element that has an+b-1 siblings before it in the document tree, for a given positive integer or zero value of n, and has a parent element. In other words, this matches the bth child of an element after all the children have been split into groups of a elements each. For example, this allows the selectors to address every other row in a table, and could be used to alternate the color of paragraph text in a cycle of four. The a and b values must be zero, negative integers or positive integers. The index of the first child of an element is 1. 

最後に書いてあるように1である場合に最初の要素を返すとなっているのですが,これは作者が意図的にやっていたりするのでしょうか。連絡した方が良いのかもしれません。

朝日COMのRSS代わり

asahi = Hpricot.parse(open('http://www.asahi.com').read.toutf8) ; nil
  #=> nil
asahi.search("#con1 li a:nth-child(0)").map {|e| e.inner_text }
  #=> ["本紙記者が記事盗用 読売新聞HPから", "1億円記載の出納帳廃棄「津島氏に報告」 元会計責任者", "キャディーら格下げ、東武子会社に賠償命令 宇都宮地裁", "味の素、ヤマキと資本・業務提携へ", "買収防衛策廃止、サッポロに提案 スティール"]

リンクを抽出

asahi.search("#con1 li a:nth-child(0)").map {|e| e['href'] }
  #=> ["/national/update/0201/TKY200702010413.html", "/national/update/0201/TKY200702010403.html", "/national/update/0201/TKY200702010402.html", "/business/update/0201/185.html", "/business/update/0201/187.html"]

属性値にはHpricot::Elem#[]あるいは,Hpricot::Elem#attributes(attr_name)をつかって属性値にアクセスできます。

element は親と子の全ての参照を持つため、"{|e| e}" とかやるのは表示が大変なことになるので危険。(= 一度やって体で覚えるのもあり)。実際の用途としては、「リンク名とリンク先を配列に入れる」場合などに便利。

と同様にHpricotも戻り値がHpricot::Elementsの中身とかだと表示が大変なことになるのでirbでこのようにする際は,コマンドの後ろに;nilをつけるか,mapで必要な情報のみ返すようにした方が良いでしょう。

asahi.search("#con1 li a:nth-child(0)").map {|e| [e['href'], e.inner_text ]}
  #=> [["/national/update/0201/TKY200702010413.html", "本紙記者が記事盗用 読売 新聞HPから"], ["/national/update/0201/TKY200702010403.html", "1億円記載の出納帳廃棄「津島氏に報告」 元会計責任者"], ["/national/update/0201/TKY200702010402.html", "キャディーら格下げ、東武子会社に賠償命令 宇都宮地裁"], ["/business/update/0201/185.html", "味の素、ヤマキと資本・業務提携へ"], ["/business/update/0201/187.html", "買収防衛策廃止、サッポロに提案 スティール"]]

あと内部的にHPricotを使っているWWW::Mechanizeクローラーを作ったりする際に非常に便利です。

参考:

日本語によるHpricotの使い方のまとめ
http://mono.kmc.gr.jp/~yhara/w/?HpricotShowcaseJa

HpricotでサポートしているCSSセレクタ
http://code.whytheluckystiff.net/hpricot/wiki/SupportedCssSelectors

日本語によるWWW::Mechanizeの使い方まとめ
http://mono.kmc.gr.jp/~yhara/w/?Ruby-WWW-Mechanize