RubyでHTMLやXMLをパースする構文解析ツールの定番は、Nokogiriです。スクレイピングする際の必需品で、なくてはならないモジュールの1つです。ただ色々なことが出来る反面、どこから取りかかれば良いのか解り難い部分もあります。自習を兼ねて、Nokogiri概要と主要な機能を紹介してみます。
Nokogiriとは何か?
ReademeによるとNokogiriとは、「HTMLとXMLとSAXとXSLTとReaderのパーサー」で、特徴としては、XPathとCSS3セレクター経由で探索する機能を持つことのようです。他にもHTMLやXMLのビルダーの機能を持っていますが、HTMLとXMLのパーサー(構文解析器)と覚えておけばよいでしょう。
Nokogiriのクラス構造
Nokogiriは、なかなか巨大なライブラリです。10以上のモジュールと70以上のクラスで構成されていて、yardでダイアグラム図を作ってみると下記のように壮大なものになります。
正直、どこから見ていけばよいのか途方に暮れると思います。Nokogoriを使う前に、次のことだけ覚えてください。何となく頭に入りやすくなると思います。
3つのクラスをおさえる
Nokogiriを理解する上では、Nokogiri::XML::Nodeのメソッドと挙動を覚えるのが大事です。ダイアグラム図を見れば解るように、Nokogiri::XML::Nodeを継承しているオブジェクトが多数あります。また、Nokogiri::HTML::Document < Nokogiri::XML::Documentの関係から解るように、HTMLモジュールもXMLモジュールを継承しているものが多数あります。
Nokogiri::HTML::Document < Nokogiri::XML::Document < Nokogiri::XML::Node
挙動が解らなければ、Nokogiri::XML::Nodeのソースをじっくり読みとくと良いです。あとは、Nokogiri::XML::DocumentとNokogiri::HTML::Documentの2つです。この3つのクラスをおさえておくと、Nokogiriが出来ることが大体把握できます。
http://nokogiri.org/Nokogiri/XML/Node.html
http://nokogiri.org/Nokogiri/XML/Document.html
http://nokogiri.org/Nokogiri/HTML/Document.html
NokogiriでHTMLを解析する
一番基本的なHTMLの解析は以下の通りです。open-uriはrubyの組み込みライブラリであるKernel#openを再定義したもので、ファイルと同様の操作でhttp/ftpにアクセスすることができます。Nokogiriと一緒に利用すると便利です
require 'nokogiri' require 'open-uri' doc = Nokogiri::HTML(open('http://www.yahoo.co.jp'))
Nokogiri::HTMLもしくはNokogiri::HTML#parseを呼び出すと、内部的にNokogiri::HTML::Document#parseメソッドを実行します。返り値としては、Nokogiri::HTML::Documentを返します。下記と同じです。
doc = Nokogiri::HTML.parse(open('http://www.yahoo.co.jp'))
生成したNokogiri::HTML::Documentから、特定のタグを抽出するには下記のとおりです。
require 'nokogiri' require 'open-uri' doc = Nokogiri::HTML(open('http://www.yahoo.co.jp')) title = doc.xpath('/html/head/title') objects = doc.xpath('//a')
NodeとNodeSet
XPathやcssの検索結果は、Nokogiri::XML::NodeSetを返します。NodeSetは、Nokogiri::XML::Nodeのリストです。NodeSetのメソッドの1つであるNodeSet#inner_text()は、リスト内の全てのNodeのinner_textを返します。textは、inner_textのエイリアスです。HTMLのTitleタグのようにドキュメント中に1つしかないタグの場合は、下記のように全て同じ結果を返します。また、HTML::Documentには、Titleタグを抜き出す特別のメソッドがあります。
require 'nokogiri' require 'open-uri' doc = Nokogiri::HTML(open('http://www.yahoo.co.jp')) puts doc.title # => Yahoo! JAPAN nodesets = doc.xpath('//title') puts nodesets.text # => Yahoo! JAPAN puts nodesets.inner_text # => Yahoo! JAPAN puts nodesets.first.inner_text # => Yahoo! JAPAN nodesets.each{|nodeset| puts nodeset.content() # => Yahoo! JAPAN puts nodeset.text # => Yahoo! JAPAN puts nodeset.inner_text # => Yahoo! JAPAN }
複数の要素がヒットする場合、結果は違ってきます
nodesets = doc.xpath('//a') puts nodesets.inner_text nodesets.each{|nodeset| puts nodeset.inner_text # => Yahoo! JAPAN }
NodeとNodeSetの検索メソッド
NodeとNodeSetには、様々な検索方法があります。検索に関しては、同じメソッドが利用可能です。下記の例は、全て同じ結果を返します。
puts doc%'//title' puts doc/'//title' puts doc.at('//title') # => 検索にヒットした最初のノードを返す puts doc.at_xpath('//title') # => xpathの検索にヒットした最初のノードを返す puts doc.at_css('title') # => cssの検索にヒットした最初のノードを返す puts doc.css('title') # => cssで検索。NodeSetを返す puts doc.css('title')[0] # => cssで検索。NodeSetから最初のノードを返す puts doc.search('title') # => xpathかcssで検索。NodeSetを返す puts doc.search('title')[0] # => xpathかcssで検索。NodeSetから最初のノードを返す puts doc.xpath('//title') # => xpathで検索。NodeSetを返す puts doc.xpath('//title')[0] # => xpathで検索。NodeSetから最初のノードを返す puts doc.xpath('//title').first # => xpathで検索。NodeSetから最初のノードを返す
NodeとNodeSetの参照メソッド
NodeとNodeSetは、参照に関してほぼ同じメソッドが使えます。一部NodeSetでは使えないメソッドがあります。また、同一の結果を返すメソッドの多くはエイリアスとして設定されているものです。
#Nodeの参照 #HTMLタグ含む puts doc.at('//title').to_html puts doc.at('//title').to_xhtml puts doc.at('//title').to_xml puts doc.at('//title').to_s #HTMLタグで囲まれた文字列 puts doc.at('//title').text puts doc.at('//title').inner_html puts doc.at('//title').inner_text puts doc.at('//title').text puts doc.at('//title').to_str #属性値の取得 puts doc.at('//a').[]('href') puts doc.at('//a').attribute('href') puts doc.at('//a').get_attribute('href') #NodeSetの参照 #HTMLタグ含む puts doc.xpath('//title').to_html puts doc.xpath('//title').to_xhtml puts doc.xpath('//title').to_xml puts doc.xpath('//title').to_s #HTMLタグで囲まれた文字列 puts doc.xpath('//title').text puts doc.xpath('//title').inner_html puts doc.xpath('//title').inner_text puts doc.xpath('//title').text #puts doc.xpath('//title').to_str
検索方法いろいろ
NokogiriというかXPathの検索方法です。idやclassなどの属性値で検索することが多いですが、実は属性値であれば、なんでも使えます。属性値検索の場合は、[]で指定します。@部分が属性値の名前です。
require 'nokogiri' require 'open-uri' doc = Nokogiri::HTML(open('http://www.hatena.ne.jp/')) #class指定でh2タグを検索 puts doc.xpath("//h2[@class='title']") #id指定でdivタグを検索 puts doc.xpath("//div[@id='copyright']") #カスタムの属性値でdivタグを検索 puts doc.xpath("//div[@data-component-term='tweet']") #id指定で全てのタグを検索 puts doc.xpath("//*[@id='copyright']") #絞込検索 puts doc.xpath("//div[@id='copyright']//ul") #リンク先のURLを抜き出す doc.xpath('//a').each do |item| puts item[:href] end
NodeSetなのかElementなのか
検索結果から属性等を参照しようとして、次のようなエラーが出る場合があります。
`[]': no implicit conversion of String into Integer (TypeError)
殆どの場合は、Elementを取得したと思っていたのに、NodeSetを取得していたというパターンが多いでしょう。よくある間違いとしては、一意の要素をとってきても、その下にタグがあってNodeSetになっている場合です。
つまったら、取得したデータがNodeSetなのかElementなのかを確認しましょう。classで確認できます。
doc.xpath("//div[@data-component-term='tweet']").each { |tweet| puts tweet.class #=> Nokogiri::XML::NodeSet puts tweet[0].class #=> Nokogiri::XML::Element
NokogiriでXMLを解析する
Nokogiriは、HTMLのみならずXMLも解析できます。XMLは構造もシンプルなので、解析はHTMLよりも簡単に出来ると思います。しかし、一点だけ注意が必要です。Nokogiriを使ってXPathで検索する場合、そのXMLが名前空間がある場合は必ず指定する必要があるという点です。指定しないと、全く検索にヒットしません。
下記の例は、はてなのホットエントリーのRSSフィードを抽出する例です。RSS1.0なので名前空間を持ちます。
require 'nokogiri' require 'open-uri' url = 'http://feeds.feedburner.com/hatena/b/hotentry' xml = open(url).read doc = Nokogiri::XML(xml) namespaces = { "rss" => "http://purl.org/rss/1.0/", #デフォルト名前空間 "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "content" => "http://purl.org/rss/1.0/modules/content/", "dc" => "http://purl.org/dc/elements/1.1/", "feedburner" => "http://rssnamespace.org/feedburner/ext/1.0" } #channel channel = doc.xpath('//rss:channel', namespaces) #Xpathでtitleを検索 puts channel.xpath('rss:title', namespaces) puts channel.xpath('feedburner:info', namespaces) lis = channel.xpath('//rdf:li', namespaces) lis.each {|li| puts li.attribute("resource") }
NokogiriとHpricot
Ruby製のHTML/XMLパーサーとしては、hpricotも選択肢の1つでした。しかしながら、今では公式に「Hpricot is over.」と宣言され、Nokogiriを使いましょうとなっています。Nokogiriとhpricotは、APIなど共通点が多いのですが、一点だけ大きな違いがあります。Nokogiriは内部の処理をUTF-8で行うに対して、hpricotは与えられた文字コードをそのまま扱い変換をしません。その為、文字コード絡みのトラブルを避ける為に、今でもhpricotを利用している人もいるようです。
XPathとCSS3セレクタのどちらを使うか
Nokogiriのノードの探索は、主にXPathかCSS3セレクターを使う方法があります。両方共覚える必要はありません。どちらかの使い方を覚えれば充分です。CSSの方が得意であれば、CSS3セレクターを使いましょう。どちらも得意でないのであれば、比較的サンプルが多いXPathを使えば良いのでしょう。デザイナー系であればCSSの方が得意という人も多いでしょう。その場合は、CSSを使えばよいと思います。
Nokogiriの解析の実装
NokogiriのXML,HTMLのパースは、基本的にはLibXML2に依存しています。内部のソースを見ていると、LibXML2に文句を言いつつ足りない機能を補完しようとしているのが解ります。Nokogiriとは極論すると、LibXML2のRubyのラッパーモジュールです。それを頭に入れておくと、HTMLでもXMLでも同じ考え方で解析しているのが解ります。
Nokogiriの各メソッドのエイリアス
Nokogiriのちょっと取っ付きにくい理由として、同じ処理を様々なパターンで記述できるところにあります。色々なサンプル見ても、多種多様で初めての人は面食らいます。その一因となっているのが、メソッドのエイリアスの多さ。基本的には、自分が馴染んだ名前で使うのがよいでしょう。
$ grep -R alias * css/parser_extras.rb: alias :cache_on? :cache_on css/parser_extras.rb: alias :set_cache :cache_on= css/tokenizer.rb: alias :scan :scan_str xml/attr.rb: alias :value :content xml/attr.rb: alias :to_s :content xml/attr.rb: alias :content= :value= xml/document.rb: alias :to_xml :serialize xml/document.rb: alias :clone :dup xml/document.rb: alias :<< :add_child xml/document_fragment.rb: alias :serialize :to_s xml/node/save_options.rb: alias :to_i :options xml/node.rb: alias :/ :search xml/node.rb: alias :% :at xml/node.rb: alias :next :next_sibling xml/node.rb: alias :previous :previous_sibling xml/node.rb: alias :next= :add_next_sibling xml/node.rb: alias :previous= :add_previous_sibling xml/node.rb: alias :remove :unlink xml/node.rb: alias :get_attribute :[] xml/node.rb: alias :attr :[] xml/node.rb: alias :set_attribute :[]= xml/node.rb: alias :text :content xml/node.rb: alias :inner_text :content xml/node.rb: alias :has_attribute? :key? xml/node.rb: alias :name :node_name xml/node.rb: alias :name= :node_name= xml/node.rb: alias :type :node_type xml/node.rb: alias :to_str :text xml/node.rb: alias :clone :dup xml/node.rb: alias :elements :element_children xml/node.rb: alias :delete :remove_attribute xml/node.rb: alias :elem? :element? xml/node.rb: alias :add_namespace :add_namespace_definition xml/node_set.rb: alias :<< :push xml/node_set.rb: alias :remove :unlink xml/node_set.rb: alias :/ :search xml/node_set.rb: alias :% :at xml/node_set.rb: alias :set :attr xml/node_set.rb: alias :attribute :attr xml/node_set.rb: alias :text :inner_text xml/node_set.rb: alias :size :length xml/node_set.rb: alias :to_ary :to_a xml/node_set.rb: alias :+ :| xml/parse_options.rb: alias :to_i :options xml/reader.rb: alias :self_closing? :empty_element? xml/sax/push_parser.rb: alias :<< :write
まとめ
イマイチまとまりませんでしたが、Nokogiriの使い方について自分なりに理解が深まったような気がします。Ruby使うのであれば、使いこなしたいライブラリの上位に入ると思います。少し時間を掛けてドキュメントやソースを読むことで理解は格段にあがるので、ぜひ一度お試しあれ!!
追記:
そんで、どうやってXPathを抽出するのという話を書きました
FireFoxやChromeを使って任意のノードのXPathを簡単に抽出する方法について - プログラマになりたい
追記2:
この辺りの話をまとめた、「Rubyによるクローラー開発技法」という本を出しています。
See Also:
Ruby製のクローラー Anemoneの文字化け対策
あらためてRuby製のクローラー、"anemone"を調べてみた
オープンソースのRubyのWebクローラー"Anemone"を使ってみる
JavaScriptにも対応出来るruby製のクローラー、Masqueを試してみる
複数並行可能なRubyのクローラー、「cosmicrawler」を試してみた
takuros/anemone · GitHub
参照:
sparklemotion/nokogiri · GitHub
Tutorials - Nokogiri 鋸

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例
- 作者: るびきち,佐々木拓郎
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2014/08/25
- メディア: 大型本
- この商品を含むブログ (6件) を見る

Spidering hacks―ウェブ情報ラクラク取得テクニック101選
- 作者: Kevin Hemenway,Tara Calishain,村上雅章
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2004/05
- メディア: 単行本
- 購入: 52人 クリック: 904回
- この商品を含むブログ (104件) を見る

バッドデータハンドブック ―データにまつわる問題への19の処方箋
- 作者: Q. Ethan McCallum,磯蘭水(監訳),笹井崇司
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/09/26
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (9件) を見る