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を使う前に、次のことだけ覚えてください。何となく頭に入りやすくなると思います。
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
nodesets = doc.xpath('//title')
puts nodesets.text
puts nodesets.inner_text
puts nodesets.first.inner_text
nodesets.each{|nodeset|
puts nodeset.content()
puts nodeset.text
puts nodeset.inner_text
}
複数の要素がヒットする場合、結果は違ってきます
nodesets = doc.xpath('//a')
puts nodesets.inner_text
nodesets.each{|nodeset|
puts nodeset.inner_text
}
NodeとNodeSetの検索メソッド
NodeとNodeSetには、様々な検索方法があります。検索に関しては、同じメソッドが利用可能です。下記の例は、全て同じ結果を返します。
puts doc%'//title'
puts doc/'//title'
puts doc.at('//title')
puts doc.at_xpath('//title')
puts doc.at_css('title')
puts doc.css('title')
puts doc.css('title')[0]
puts doc.search('title')
puts doc.search('title')[0]
puts doc.xpath('//title')
puts doc.xpath('//title')[0]
puts doc.xpath('//title').first
NodeとNodeSetの参照メソッド
NodeとNodeSetは、参照に関してほぼ同じメソッドが使えます。一部NodeSetでは使えないメソッドがあります。また、同一の結果を返すメソッドの多くはエイリアスとして設定されているものです。
puts doc.at('//title').to_html
puts doc.at('//title').to_xhtml
puts doc.at('//title').to_xml
puts doc.at('//title').to_s
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')
puts doc.xpath('//title').to_html
puts doc.xpath('//title').to_xhtml
puts doc.xpath('//title').to_xml
puts doc.xpath('//title').to_s
puts doc.xpath('//title').text
puts doc.xpath('//title').inner_html
puts doc.xpath('//title').inner_text
puts doc.xpath('//title').text
検索方法いろいろ
NokogiriというかXPathの検索方法です。idやclassなどの属性値で検索することが多いですが、実は属性値であれば、なんでも使えます。属性値検索の場合は、[]で指定します。@部分が属性値の名前です。
require 'nokogiri'
require 'open-uri'
doc = Nokogiri::HTML(open('http://www.hatena.ne.jp/'))
puts doc.xpath("//h2[@class='title']")
puts doc.xpath("//div[@id='copyright']")
puts doc.xpath("//div[@data-component-term='tweet']")
puts doc.xpath("//*[@id='copyright']")
puts doc.xpath("//div[@id='copyright']//ul")
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
puts tweet[0].class
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 = doc.xpath('//rss:channel', namespaces)
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