スクレイピングのためのNokogiri利用メモ
スクレイピングのチュートリアルを書いてみた。
参考:http://nokogiri.rubyforge.org/nokogiri/Nokogiri.html
まだまだたくさんのクラスやメソッドがあるが(読んでない)、HTMLのスクレイピングに限定すれば多分これくらいで十分。
2014-02-16追記
なんかたくさんブックマークされていることに気づいたので、サンプルコードのRuby1.9/2対応のアップデート。
Mechanize周りも修正。WWW::Mechanize → Mechanize 等
(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 のメソッドが使える。
(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の参照系メソッド
○検索
○自ノード情報
○属性情報
属性値(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)。IEやFirefoxのJavaScriptのinnerTEXT、textContent相当
- inner_html
- 子孫ノードのHTMLをつなぎ合わせたもの(String)。IEやFirefoxのJavaScriptのinnerHTML相当
○兄弟ノード情報
- previous_sibling、previous
- 兄ノード(Element)
- next_sibling、next
- 弟ノード(Element)
○親ノード情報
- parent
- 親ノード(Element)
- ancestors
- 親、祖父・・・ノード(Element)のリスト(Array)
- document
- そのノードを含むDocumentオブジェクトを得る。
(5) NodeSetの参照系メソッド
○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の更新系メソッド
○自ノード処理
○属性書き換え
- ["属性名"]="値"、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
- すべてのエレメントをそれぞれの親から削除