のこぎりのつかいかた
December 13, 2015
日本語も上手で有名なtenderlove(たこ焼き仮面はやめたのか?)がつくった Nokogiri 。いろいろなものが、この Nokogiri に依存しているが、自分にとっては、bundle install すると止まる奴という印象が強い。
DOM の構造を解析して、欲しい部分を抜き取るためには、なくてはならない存在だが、こののこぎりこつが必要。
使い方の流れを、かなり大雑把に言うと、
- HTMLをパースして、
- 欲しいものを抜いて、
- 抜いたものを出す、
- さらに、欲しいものを抜いて、
- そして、それを出す、(の繰り返し)
- 欲しいものを抜いて、
「抜いた範囲内で、さらに抜きたい」というのにはまった。(方法は後ほど)
HTMLのパース
require 'nokogiri'
require 'open-uri'
# 既にhtmlを取得していて、それをパースさせてもいいし、
html = open("http://www.lambda-consulting.jp/").read
doc = Nokogiri::HTML.parse(html)
# URLで取得されるものを直接パースさせてもいい
doc = Nokogiri::HTML(open("http://www.lambda-consulting.jp/"))
docが、パースされたオブジェクトになる。
欲しいものを抜く
欲しいものを特定する方法は2つあり、さらに、見つかった結果を「すべて取得する」か、「最初のひとつだけ」にするかでメソッドがわかれる。
欲しいものを特定する方法 その1 XPath
- xpath : 指定したxpathに合致する全て(の要素の集まり : NodeSetという = Nodeのコレクション)
- at_xpath : 指定したxpathに合致する最初のひとつ(の要素 : Nodeという)
doc.xpath('//h2') # => 全てのh2タグのNodeSetオブジェクト
doc.at_xpath('//h2') # => 最初に見つかったh2タグのNodeオブジェクト
欲しいものを特定する方法 その2 CSS
こちらは、jQuery とかに近い。なので、こちらの方が使いやすいと思いきや、なぜか自分では、XPathばかり使っている。でも、それは、自分のCSSのクラス設計がいまいちだからだろう。
- css : 指定したcssに合致する全て(の要素の集まり : NodeSetという = Nodeのコレクション)
- at_css : 指定したcssに合致する最初のひとつ(の要素 : Nodeという)
doc.css('.hoge') # => 全てのhogeクラスのNodeSetオブジェクト
doc.at_css('.hoge') # => 最初に見つかったhogeクラスのNodeオブジェクト
欲しいものを特定する方法 その3 XPath or CSS
XPathでもCSSでもどっちでもいいよというのもある。
- search : 指定したxpathかcssに合致する全て(の要素の集まり : NodeSetという = Nodeのコレクション)
- at : 指定したxpathかcssに合致する最初のひとつ(の要素 : Nodeという)
抜いたものを出す
出し方によって、次の3つにわかれる。(言葉よりも、例を見たほうがわかりやすい)
- to_html : タグを含めて返す (to_sでも同じ)
- inner_html : タグの内側の中身を返す
- inner_text : タグの内側のテキストだけを返す(Nodeが対象の場合は、content、to_strでも同じ)
doc = Nokogiri::HTML.parse("<h2 class='hoge'>ABCD<span>efg</span>HIJ</h2><h2 class='hoge'>1234<span>567</span>890</h2>")
doc.to_html
=> "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\"
\"http://www.w3.org/TR/REC-html40/loose.dtd\">\n<html><body>\n
<h2 class=\"hoge\">ABCD<span>efg</span>HIJ</h2>\n
<h2 class=\"hoge\">1234<span>567</span>890</h2>\n
</body></html>\n"
# NodeSetオブジェクトの場合
doc.xpath('//h2').to_html
=> "<h2 class=\"hoge\">ABCD<span>efg</span>HIJ</h2>\n<h2 class=\"hoge\">1234<span>567</span>890</h2>"
doc.xpath('//h2').to_s
=> "<h2 class=\"hoge\">ABCD<span>efg</span>HIJ</h2>\n<h2 class=\"hoge\">1234<span>567</span>890</h2>"
doc.xpath('//h2').inner_html
=> "ABCD<span>efg</span>HIJ1234<span>567</span>890"
doc.xpath('//h2').inner_text
=> "ABCDefgHIJ1234567890"
# Nodeオブジェクトの場合
doc.at_xpath('//h2').to_html
=> "<h2 class=\"hoge\">ABCD<span>efg</span>HIJ</h2>\n"
doc.at_xpath('//h2').to_s
=> "<h2 class=\"hoge\">ABCD<span>efg</span>HIJ</h2>\n"
doc.at_xpath('//h2').inner_html
=> "ABCD<span>efg</span>HIJ"
doc.at_xpath('//h2').inner_text
=> "ABCDefgHIJ"
doc.at_xpath('//h2').content
=> "ABCDefgHIJ"
doc.at_xpath('//h2').to_str
=> "ABCDefgHIJ"
linkタグに含まれるhrefのような属性値を取得したい場合
doc = Nokogiri::HTML.parse("<h2 class='hoge'>ABCD<span>efg</span>HIJ</h2><h2 class='hoge'>1234<span>567</span>890</h2>")
doc.at_xpath('//h2').get_attribute('class')
=> "hoge"
doc.at_xpath('//h2')[:class]
=> "hoge"
[重要] 抜いた範囲内で、さらに抜きたい
Nokogiriというより、XPathを理解していなかったので、はまってしまった。。
そもそもXPathでは、
- //h2 : ドキュメント内のすべてのh2要素
- ./h2 : 現在のコンテキスト内でのすべてh2要素
という意味になる。よって:
doc = Nokogiri::HTML.parse("<h2 class='hoge'>ABCD<span>efg</span>HIJ</h2><h2 class='hoge'>1234<span>567</span>890</h2>")
# 2つ目のh2要素を抜く
second_h2 = doc.xpath(//h2[2])
second_h2.to_html
=> <h2 class='hoge'>1234<span>567</span>890</h2>
# 2つ目のh2要素内のspanのつもりが、、、(xpath //は、ドキュメント全体の意味になる)
second_h2.at_xpath('//span').to_html
=> "<span>efg</span>"
# 「このコンテンツ内で」としたければ、xpathを./としなければならない
second_h2.at_xpath('./span').to_html
=> "<span>567</span>"