スクレイピングのためのNokogiri利用メモ

スクレイピングチュートリアルを書いてみた。
参考:http://nokogiri.rubyforge.org/nokogiri/Nokogiri.html
まだまだたくさんのクラスやメソッドがあるが(読んでない)、HTMLのスクレイピングに限定すれば多分これくらいで十分。

2014-02-16追記

なんかたくさんブックマークされていることに気づいたので、サンプルコードのRuby1.9/2対応のアップデート。
Mechanize周りも修正。WWW::Mechanize → Mechanize 等

(0) 前提知識

Ruby、HTML、DOM、CSSセレクタまたはXPath

(1) クラス構造の理解

Nokogiri::HTML::Document < Nokogiri::XML::Document < Nokogiri::XML::Node < Object
Nokogiri::XML::Element < Nokogiri::XML::Node < Object
Nogogiri::XML::NodeSet < Enumerable < Object

(2) HTMLドキュメントオブジェクトを得る

まずは最初に、解析したいページの Nokogiri::HTML::Document オブジェクトを得る。
Nokogiri::HTML::Document は、Nokogiri::XML::Document のさらに、Nokogiri::XML::Node のサブクラスなので、
Node のメソッドが使える。

ログイン処理など、Cookieを使ったページ遷移が必要な場合
Mechanize を利用して、page = agent.get(url) で Mechanize::Page オブジェクトが返り、page.parser またはその alias の page.root で Nokogiri::HTML::Document オブジェクトが返る。ページの文字コードutf-8 以外だとうまく処理できないケースもあるので、id:otn:20090429 参照
単純なページ取得の場合
open-uriを使って、doc = Nokogiri.HTML(open(url)[,url,[jcode] ]) で Nokogiri::HTML::Document オブジェクトが返る。HTMLメソッドの第三引数でページの文字コードを指定する

(3) 基本的な処理パターン

Document 等の Nokogiri::XML::Node または Nokogiri::XML::NodeSet オブジェクトに対して、
CSSセレクタXPathで検索を行い、検索結果として Nokogiri::XML::NodeSet オブジェクトを得る。
NodeSetはArrayのようなもので、eachや[]でNodeのサブクラスのElementを得て、情報を得る。

tds=doc.xpath("//td") # => tdタグの検索(NodeSetオブジェクト)
tds.size              # => tdタグの個数
tds[0]                # => 最初のtdタグ(Elementオブジェクト)
tds[0]["class"]       # => 最初のtdタグのclass名(String)
tds[0].xpath(".//a")  # => さらにその中のaタグを探す(NodeSetオブジェクト)


よく使うと思われるメソッドは、xpathまたはcss、NodeSetからElementを取り出すやeach、Elementの属性値を取る、テキストの取り出しtext、あとは近くのノードをたどるparentやchild・children・previous・nextくらいでしょうか。あ、NodeSetに対するempty?とかsizeも。

(4) Nodeの参照系メソッド

○検索
at("検索")
XPathCSSセレクタで検索し、結果の最初のノード(Element)。無ければnil
css("CSSセレクタ")
引数は複数やArrayも可能。CSSセレクタで検索し、NodeSetを返す。無ければ空のNodeSet
xpath("XPath")
引数は複数やArrayも可能。XPathで検索し、NodeSetを返す。無ければ空のNodeSet
search("検索")、/ "検索"
引数は複数やArrayも可能。XPathCSSセレクタで検索し、NodeSetを返す。無ければ空のNodeSet
○自ノード情報
node_name、name
ノード名(String)
css_path
このノードのCSSセレクタ(String)
path
ノードのXPath(String)
node_type、type
ノードタイプ(Fixnum)
blank?
空白文字のみのテキストノードか?
cdata?
CDATAノードか?
text?
テキストノードか?
commnet?
コメントノードか?
element?、elem?
エレメントノードか?
○属性情報

属性値(String)を返すものと、属性(Attr < Node)を返すものがある。

["属性名"]、get_attribute("属性名")
属性値(String)。無ければnil
key?("属性名")、has_attribute?("属性名")
属性があるか?
keys
属性名(String)の一覧(Array)
values
属性値(String)の一覧(Array)
attributes
属性名(String)と属性オブジェクト(Attr)のハッシュ(Hash)
attribute("属性名")、attribute_nodes
属性オブジェクト(Attr)やそのリスト(Array)
each { |k,v| 。。。}
属性名(String)と属性値(String)でブロック呼び出し
○子ノード情報
child
最初の子ノード(Element)
children
子ノード(Element)のリスト(Array)
content、text、inner_text、to_str
テキスト子孫ノードの内容をつなぎ合わせたもの(String)。IEFirefoxJavaScriptのinnerTEXT、textContent相当
inner_html
子孫ノードのHTMLをつなぎ合わせたもの(String)。IEFirefoxJavaScriptのinnerHTML相当
○兄弟ノード情報
previous_sibling、previous
兄ノード(Element)
next_sibling、next
弟ノード(Element)
○親ノード情報
parent
親ノード(Element)
ancestors
親、祖父・・・ノード(Element)のリスト(Array)
document
そのノードを含むDocumentオブジェクトを得る。
○テキスト化
to_html、to_html("エンコード")
ノード全体をHTMLテキストに(String)。IEJavaScriptのouterHTML相当
to_xhtml、to_xhtml("エンコード")
ノード全体をXHTMLテキストに(String)
write_html_to(io,"エンコード")
ノード全体をHTMLテキスト(String)にして書き出す(io)

(5) NodeSetの参照系メソッド

○検索
at("検索")
css("CSSセレクタ")
xpath("XPath")
search("検索")、/ "検索"
○Enumerator、Arrayもどき系
length、size
[添え字]
empty?
first
last
each { |x| 。。。}
push(node)、<< node
to_a、to_ary
○テキスト系
inner_text、text
すべてのエレメントに適用してつなぎ合わせ
inner_html
すべてのエレメントに適用してつなぎ合わせ
to_html(*arg)
すべてのエレメントに適用してつなぎ合わせ
to_xhtml(*arg)
すべてのエレメントに適用してつなぎ合わせ

(6) サンプル:各地の今日の天気

XPashとCSSを混ぜてみた。
Windowsで動かしたので、出力はSJIS。-Kは念のためeuc-jpに。

#!/usr/bin/ruby -Ke
require "rubygems"
require "nokogiri"
require "open-uri"
require "kconv"

doc = Nokogiri.HTML(open("http://weather.asahi.com"))

doc.search("//table[@class='font12' and @bgcolor]//tr[position()>1]").each do |tr|
	place   = tr.search("td[1]").text
	weather = tr.search("td[2] > img").map{|img| img["alt"]}.join("|")
	puts "#{place}\t#{weather}".tosjis
end
2014-02-16追記

エンコードをRuby1.9or2らしく書くとこんな感じか。

#!/usr/bin/ruby
require "rubygems"
require "nokogiri"
require "open-uri"

Encoding.default_external = "Windows-31J"

doc = Nokogiri.HTML(open("http://weather.asahi.com","r:euc-jp"),nil,"euc-jp")

doc.search("//table[@class='font12' and @bgcolor]//tr[position()>1]").each do |tr|
	place   = tr.search("td[1]").text
	weather = tr.search("td[2] > img").map{|img| img["alt"]}.join("|")
	puts "#{place}\t#{weather}"
end

(7) サンプル:マイミク最新日記

Windowsで動かしたので、出力はSJIS。-Kは念のためutf-8に。

2014-02-16修正

ページ内容(HTML)が変わっていたので、現状の物に対応。

#!/usr/bin/ruby -Ku
MAIL="foo@example.jp"
PASS="password"

require "rubygems"
require "mechanize"
require "kconv"

agent = Mechanize.new
agent.user_agent_alias = "Windows IE 9"

agent.get("http://mixi.jp")
agent.page.form("login_form").field("email").value=MAIL
agent.page.form("login_form").field("password").value=PASS
agent.page.form("login_form").submit

agent.get("http://mixi.jp/home.pl")
agent.page.root.search("ul.homeFeedList > li.diary").each do |node|
	puts sprintf("%s\t\%s\t%s", node.search("li.date")[0].text,
		node.search("p.name>a")[0].text, node.search("p.title")[0].text).tosjis
end
2014-02-16追記

エンコードをRuby1.9or2らしく書くとこんな感じか。

#!/usr/bin/ruby
MAIL="foo@example.jp"
PASS="password"

require "rubygems"
require "mechanize"

Encoding.default_external = "Windows-31J"

agent = Mechanize.new
agent.user_agent_alias = "Windows IE 9"

agent.get("http://mixi.jp")
agent.page.form("login_form").field("email").value=MAIL
agent.page.form("login_form").field("password").value=PASS
agent.page.form("login_form").submit

agent.get("http://mixi.jp/home.pl")
agent.page.root.search("ul.homeFeedList > li.diary").each do |node|
agent.page.root.search("ul.homeFeedList > li.diary").each do |node|
	printf "%s\t\%s\t%s\n", node.search("li.date")[0].text,
		node.search("p.name>a")[0].text,
		node.search("p.title")[0].text
end

(以下おまけ)

(8) Nodeの更新系メソッド

○自ノード処理
node_name="名前"、name="名前"
ノード名の差し替え
dup
ノードのコピーを作る。dup(0)だと子ノードはコピーしない
unlink、remove
ノードの削除
replace(node)
ノードを置き換え
swap("テキスト")
ノードをテキストのHTMLから作ったノードと置き換えて元のノードを返す(Element)
○属性書き換え
["属性名"]="値"、set_attribute("属性名","値")
属性値をセット
delete("属性名")、remove_attribute("属性名")
属性の削除。属性値(String)を返す。無ければnil
○子ノードの処理
add_child(node)、<< node
nodeを子ノードとして追加(self)
content="テキスト"
子孫ノードすべてをテキストノードで置き換え
inner_html="文字列"
子孫ノードすべてを文字列をHTMLタグとして置き換え
○兄弟ノードの処理
add_previous_sibling(node)
nodeを兄ノードとして追加(self)
add_next_sibling(node)
nodeを弟ノードとして追加(self)
before("テキスト")
テキストを兄テキストノードとして追加(self)
after("テキスト")
テキストを弟テキストノードとして追加(self)
○親ノードの処理
parent=node
親ノードから切り離して別ノードの最後の子ノードに
○その他
encode_special_chars("文字列")
< > & " をエンコードする
fragment("文字列")
文字列をHTMLタグとして DocumentFragmentオブジェクトを作る

(9) NodeSetの更新系メソッド

add_class("クラス名")
すべてのエレメントにクラス追加
remove_class("クラス名")
すべてのエレメントからクラス削除
attr("属性名","属性値",&blk)、set("属性名","属性値",&blk)
すべてのエレメントに属性セット
remove_attr("属性名")
すべてのエレメントから属性削除
after("テキスト")
最後のエレメントに弟としてテキストノード追加
before("テキスト")
最初のエレメントに兄としてテキストノード追加
dup
ノードセットの複製
unlink、remove
すべてのエレメントをそれぞれの親から削除