プログラマでありたい

おっさんになっても、プログラマでありつづけたい

RubyでYahoo! キーフレーズ抽出APIを使ってテキストマイニング

 Rubyでキーワード抽出およびTF-IDFでキーワードの重要度の重み付けが出来んかなと思ってGemを漁ってみました。残念ながら、日本語データに関しては、それらしいのは見つかりませんでした。キーワード抽出については、Mecabで形態素解析後に品詞同士のつながりのパターンを考えれば、割りと簡単に作れます。昔作ったことがあるけど、ソースはどっかに旅立たれていたので、改めて作った上でGitHubに登録しようかなぁと思います。またTF-IDFについては、何と言ってもIDFのデータをどうするかがポイントになります。そのあたりのデータ含めて、PerlのLingua::JA::TFIDFを参考に移植させて貰おうかなぁと考えています。

 一方で、てっとり早く上記の2つを実現する方法もあります。Yahooのキーフレーズ抽出APIを利用すると、キーワード抽出と重要度のスコアリングを同時にやってくれます。ただし、24時間あたり5万リクエストと1リクエストあたりのサイズや、レスポンスが上位20件までしか返さないという制約があります。
 それで充分という場合は、さくっと利用するとよいでしょう。下記は、簡単なサンプルです。Yahooのニュースを抜き出して、重要なキーワードを抜き出すといったイメージです。定期的に流していると、ニュースのトレンドの推移などが解ってくるのではないでしょうか?

require 'open-uri'
require 'rexml/document'
require 'nokogiri'

APPLICATION_ID = ENV["YAHOO_API_KEY"]
BASE_URL   = 'http://jlp.yahooapis.jp/KeyphraseService/V1/extract'

$word_list = Hash::new

def request(text)
  app_id = APPLICATION_ID
  params = "?appid=#{app_id}&output=xml"
  url = "#{BASE_URL}#{params}"+"&sentence="+URI.encode("#{text}")
  response = open(url)
  doc = REXML::Document.new(response).elements['ResultSet/']
  doc.elements.each('Result') {|element|
    text = element.elements["Keyphrase"][0]
    score = element.elements["Score"][0] #キーワードの重要度は、とりあえず切り捨て
    $word_list["#{text}"] = $word_list["#{text}"].nil? ? 1 : $word_list["#{text}"]+1
  }
end

def get_urls(page_url)
  urls = Array.new()
  uri = URI.parse(page_url)
  doc = Nokogiri::HTML(open(page_url))
  doc.xpath("//*[@id='main']//ul[@class='list']//a").each do |anchor|
    url = anchor[:href]
    url = uri.merge(url) if not uri =~ /^http/
    urls << url
  end
  return urls
end

def get_headline_text(page_url)
  text = ""
  doc = Nokogiri::HTML(open(page_url))
  if page_url.to_s.match(/dailynews/)
    text = doc.xpath("//*[@id='detailHeadline']").text
  else
    text = doc.xpath("//*[@id='main']//p[@class='hbody']").text
  end
  return text.gsub(/\n/,"")
end

def get(page_url)
  urls = get_urls(page_url)
  urls.each {|url|
    text = get_headline_text(url)
    #p text
    request(text)
  }
end

page_url = 'http://news.yahoo.co.jp/list/?c=economy'
get(page_url)
$word_list.each{ |key,value|
  p "#{key}=#{value}"
}


 サクッと作っただけなので、悪しからず。



See Also:
これはセンスが良い。Lingua::JA::TFIDF - プログラマになりたい
特徴語抽出のあれこれ - プログラマになりたい
キーワード抽出モジュール Lingua::JA::Summarize - プログラマになりたい


参照:
Lingua::JA::TFIDF - search.cpan.org
テキスト解析:キーフレーズ抽出API - Yahoo!デベロッパーネットワーク

Ruby製のクローラー Anemoneでストレージをファイルに変更する

 シリーズの如く何度かAnemoneの話を書いています。Anemoneは割りと小さなモジュールなので、ソースを読めば直ぐに解ることが多いです。一方で、ドキュメントが充実しているとは言い難いので、少し違うことをしようとすると、ソース嫁という状態になります。今回のエントリーもそんな話の一つです。

デフォルトストレージを変更する



 Anemoneのデフォルトストレージのデフォルトオプションは、nilです。この場合、どういう動作をするかというと、Anemone::Storage.Hashが指定されます。つまりメモリーです。当然ながらクロール量に比例してメモリの使用量が増えます。そうすると、当然動いている端末もしくはサーバのメモリーを圧迫していくという結果になります。
 当然望ましくないので、本格的に使用する場合はストレージを変更した方が良いでしょう。Anemoneはストレージのオプションが豊富で、PStore(ファイル)やMongoDB,Sqlite3などを始めとして、TokyoCabinet,KyotoCabinet,Redisなどが使えます。mongoなどについては、使ってみたよという事例がちょくちょく出ているのですが、一番単純なPStore(ファイル)についての情報は見たことがないです。そんなこんなで、ちょこっと書いてみます。

AnemoneでPStoreを利用する



 使い方は簡単です。起動時のオプションの中の:storegeで、Anemone::Storage.PStoreを指定するだけです。簡単ですね。しかし、それで起動すると、以下のようなエラーが発生します。

pstore.rb:11:in `initialize': wrong number of arguments (0 for 1) (ArgumentError)
        from C:/Ruby200/lib/ruby/gems/2.0.0/gems/anemone-0.7.2/lib/anemone/stora
ge.rb:13:in `new'
        from C:/Ruby200/lib/ruby/gems/2.0.0/gems/anemone-0.7.2/lib/anemone/stora
ge.rb:13:in `PStore'
        from anemone-pstore.rb:7:in `<main>'

 端的にいうと、引数が足りませんよということです。どこに保存するかの情報がないということでしょう。しかし、オプションでは、ファイル等の指定するところがありません。どうしたら良いのでしょうか?pstore.rbを見てみると、initializeでファイルを引数としています。

pstore.rb

      def initialize(file)
        File.delete(file) if File.exists?(file)
        @store = ::PStore.new(file)
        @keys = {}
      end

 親クラスのpage_store.rbを見ると、次のようになっています。オプション変数をそのまま渡しているっぽいですね。

    def initialize(storage = {})
      @storage = storage
    end

 呼び元のcore.rbは、どうしているかというと、次の通りです。

storage = Anemone::Storage::Base.new(@opts[:storage] || Anemone::Storage.Hash)

 ということで、次のような感じで保存先のファイルと一緒にオプションで指定したら良いです。それだけです。

storage => Anemone
:Storage.PStore('file.txt')


 下記が、それを踏まえたソースです。この場合、ファイルは実行したディレクトリに作成されます。ちなみにファイル名は.txtにしていますが、Mashal.dmp形式のバイナリになっている為、殆ど読めません。

require 'anemone'
 
urls = []
urls.push("http://www.yahoo.co.jp")

opts = {
  :storage => Anemone::Storage.PStore('file.txt'),
  :obey_robots_txt => true,
  :depth_limit => 0
}

Anemone.crawl(urls, opts) do |anemone|
  anemone.on_every_page do |page|
    puts page.url
    p page.doc.xpath("/head/title/text()").to_s if page.doc
  end
end

 一方でストレージに保存しているものの、それを再利用するかというと別の話です。PStoreのイニシャライザを見ての通り、既存のファイルがあれば消してから作成しています。このあたりを何とかしたいのであれば、ライブラリに手を加えるしかないですね。訪問済みのページは訪れないとか、一定期間経過した場合のみ訪れるとか。

Ruby製の構文解析ツール、Nokogiriの使い方 with Xpath

f:id:dkfj:20140415021125p:plain

 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でダイアグラム図を作ってみると下記のように壮大なものになります。

f:id:dkfj:20140413193256p:plain

 正直、どこから見ていけばよいのか途方に暮れると思います。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の運用例

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Spidering hacks―ウェブ情報ラクラク取得テクニック101選

Spidering hacks―ウェブ情報ラクラク取得テクニック101選

バッドデータハンドブック ―データにまつわる問題への19の処方箋

バッドデータハンドブック ―データにまつわる問題への19の処方箋

Ruby製のクローラー Anemoneの文字化け対策

 何度も取り上げていますが、Ruby製のクローラーであるAnemoneについてです。もう2年ほどメンテナンスされていないものの、Rubyの中のクローラー・フレームワークとしては未だに一番の完成度です。しかし、残念ながら幾つかの問題点があります。その中で日本語を扱う我々にとっては一番大きな問題は、文字化けです。

Anemoneの文字化けの原因



 Anemoneの文字化けの原因は、ずばりUTF-8以外の考慮が何もされていないためです。Anemoneが利用するHTMLパーサーであるNokogiriは、もともと内部的な文字コードをUTF-8として扱います。UTF-8以外の文字コードを扱う場合は、文字コードを指定して渡す必要があります。それにもかかわらず、AnemoneがHTMLをパースする時は、次のような実装になっています。

#
# Nokogiri document for the HTML body
#
def doc
  return @doc if @doc
  @doc = Nokogiri::HTML(@body) if @body && html? rescue nil
end

 この実装ではUTF-8やASCII以外の場合は、文字化けが発生します。

小手先のAnemoneの文字化け対策



 対策の1つとしては、AnemoneがパースしたNokogiriドキュメントを使わない方法があります。
取得先のページの文字コードをみて、String#encodeを使って明示的に変換します。

require 'anemone'

urls = []
urls.push("http://www.amazon.co.jp/gp/bestsellers/books/466282/")

Anemone.crawl(urls, :depth_limit => 0) do |anemone|
  anemone.on_every_page do |page|
    doc = Nokogiri::HTML.parse(page.body.encode("utf-8","shift_jis"))
    puts doc.xpath("//title").text
  end
end

 実行すると、この通り文字化けせずに結果を取得できます。

$ ruby anemone-convert.rb 
Amazon.co.jp ベストセラー: ビジネス・経済 の中で最も人気のある商品です

抜本的なAnemoneの文字化け対策



 一方で、折角Anemone内部でNokogiriドキュメントを作っているのに、それを切り捨てて新たなNokogoriドキュメントを作るのは無駄以外の何者でもありません。また、複数のサイトをクローリングする際は、決め打ちの文字コードの対処はできません。抜本的な対策としては、Anemone内部の問題を解決するのが良いでしょう。

 AnemoneがNokogiriドキュメントを生成しているのは、Anemone::Pageクラスのdocメソッドで実体はlib/anemone/paage.rbにあります。問題は、変換の際の文字コードに何を指定すれば良いかという点です。Anemoneが扱うのは、HTMLです。なので、HTML中からcharsetを抜き出し、それをセットすることにしましょう。幸い、Anemoneにはcontent_typeというメソッドがあり、HTMLヘッダーからcontent-typeを抜き出してくれています。ここからcharsetを抜き出せば大丈夫です。

#
# Nokogiri document for the HTML body
#
def doc
  return @doc if @doc
  if @body && html?
if charset == 'utf-8' || charset.nil?
  body = @body
else
  body = @body.encode("UTF-8", charset, :invalid => :replace, :undef => :replace) rescue nil
end
@doc = Nokogiri::HTML(body) if body
  end
end

#
# The content-type returned by the HTTP request for this page
#
def content_type
  headers['content-type'].first
end

#
# The charset returned by the content-type request for this page
#
def charset
  matcher = content_type.match(/charset=[\"]?([a-zA-Z\_\-\d]*)[\"]?/)
  matcher[1].downcase if matcher
end

 実装している途中で気がついたのですが、anemoneのPull Requestの中にほぼ同じ実装がありました。totothinkという方によるForkです。
totothink/anemone · GitHub
 
 私の方では、少し実装を変えています。content_typeのマッチの部分が「_」が含まれていなかった為にShift_JISに対応出来かなったので追加しています。また、もともとのHTMLのbodyが格納されている@body変数も、変換することなく元のままで残しております。その方が、後々使いやすいと考えたからです。

追記 2014/04/07
 content_typeだけで大丈夫なのというご指摘ありました。その通りだと思います。が、必要に応じて、変更していけば良いと思います。(MetaタグとかLangとか優先順位をつけて判定)ちなみに@bodyに入っている文字列をString#encodingで判定しようとしたのですが、必ずUS_ASCIIと判定されるので使えません。

Fork版のGitHubからのインストール



 GitHubにForkして、修正版を置いています。興味あれば使ってください。

git clone https://github.com/takuros/anemone.git
cd anemone
gem build anemone.gemspec
gem install anemone-0.7.2.gem

まとめ



 Pythonには、ScrapyというCrawlerのフレームワークがあります。今でも開発は活発です。RubyのCrawlerフレームワークとして網羅的な機能を持つのはAnemoneしかありません。Anemoneは残念ながら、もう開発されていない模様です。Pull Requestも幾つもあるけど、取り込まれる様子はありません。Forkで何か新しいの作っても良いのかもしれませんね。


See Also:
あらためてRuby製のクローラー、"anemone"を調べてみた
オープンソースのRubyのWebクローラー"Anemone"を使ってみる
JavaScriptにも対応出来るruby製のクローラー、Masqueを試してみる
複数並行可能なRubyのクローラー、「cosmicrawler」を試してみた
takuros/anemone · GitHub


参照:
chriskite/anemone · GitHub
anemone RDoc


Rubyによるクローラー開発技法

Rubyによるクローラー開発技法

Spidering hacks―ウェブ情報ラクラク取得テクニック101選

Spidering hacks―ウェブ情報ラクラク取得テクニック101選

Ruby2.0の文字エンコーディングの簡単なまとめ。KconvとM17N

 浦島太郎状態になっていたので、Ruby周りの文字コードの扱いについて改めて勉強しなおしました。簡単にいうとRuby 1.9.1以降の多言語化仕様(M17N)では、文字コードをどう扱えばよいのかという点です。

Rubyの文字コード変換ライブラリ


 Ruby 1.9.1の多言語仕様化以前は、文字コードの変換には文字コード変換ライブラリを使用する方法が一般的でした。主な文字コード変換ライブラリは、次のようなものがあります。

  • Kconv
  • NKF
  • Iconv
  • Uconv

 この中で、KconvはNKFのラッパーであり、NKFはnkf(Network Kanji code conversion Filte)をRubyから使うためのモジュールです。つまり実装としては、KconvとNKFの根の部分は同じです。またIconvは、UNIX95のiconv()関数のラッパーです。なお、IconvはRuby 1.9から非推奨、Ruby 2.0から標準添付ライブラリから削除されています。

Kconvの利用


 Kconvを読み込むと、Stringクラスに次のメソッドが追加されます。

String#iseuc String#isjis String#issjis String#isutf8 String#kconv String#toeuc String#tojis String#tolocale String#tosjis String#toutf16 String#toutf32 String#toutf8

使い方としては、下記のとおりで手軽に使えて便利です。

require 'kconv'

str = '日本語の文字コード'     # 何らかの文字コードの文字列
puts str.toeuc   # => ECUの文字コードに変換
puts str.tosjis  # => Shift_JISの文字コードに変換
puts str.toutf8  # => UTF-8の文字コードに変換

 一方で、Kconvについては幾つかの問題点があります。1つは、先述のとおりnkfライブラリを利用する為に、プラットフォーム依存が強いという点です。2つ目は、nkfライブラリの名前(Network Kanji code conversion Filte)が示すように、多言語化対応ではなく日本語対応のモジュールということです。この辺りの改善のために、Ruby 1.9からはM17Nを盛り込んだ仕様になっています。

追記:2014/04/08
 nkfライブラリのプラットフォーム依存の話は、間違いでした。nkfのソース自体がRubyにインポートされて、ビルドされている模様です。モジュールを確認するとその通りでした。@yugui さんご指摘ありがとうございます。

f:id:dkfj:20140408120732p:plain

$ tree nkf/
nkf/
├── Makefile
├── depend
├── extconf.h
├── extconf.rb
├── lib
│   └── kconv.rb
├── mkmf.log
├── nkf-utf8
│   ├── config.h
│   ├── nkf.c
│   ├── nkf.h
│   ├── utf8tbl.c
│   └── utf8tbl.h
├── nkf.c
└── nkf.o

M17N対応



 Ruby 1.9.1からは、M17N(Multilingualization)で多言語化の対応が根本的に変わっています。 Rubyの多言語化は、CSI(Code Set Independent)という方式を採用しています。これは他の言語の主流であるUCS Normalizationとは全く別の方式です。UCS Normalizationは、内部コードを特定の文字コード(多くの場合は、UTF)とし、入出力の際に別の文字コードが来た場合は変換して利用します。
 これに対して、Rubyが採用したCSI方式は内部コードを持ちません。どの文字コードでも、そのエンコーディングに従い扱います。そのために、プログラム自身にエンコーディングを教える必要があります。Ruby 1.9でコード中にマルチバイト文字が出てきた場合に、Magic Commentが必要になったのはこのためです。

# coding: UTF-8

str = '日本語の文字コード'     # 何らかの文字コードの文字列
puts str.encode("Shift_JIS") # => 暗黙的にUFT8から、Shift_JISに変換する
puts str.encode("Shift_JIS","UTF-8") # => 明示的にUFT8から、Shift_JISに変換する

 このString#encodeメソッドの実装は、class Encodingです。これは内部に文字コードの変換表を持っている訳ではなく、文字コードの取扱ルールを定義しているようです。またNKFのように日本語対応のみでなく、多言語対応です。

まとめ



 ということで互換性の問題がなければ、String#encodeメソッドを利用しましょう。またちゃんと、Magic Commentを書きましょうね。(Ruby 2.0ではデフォルトがUTF-8になっているので、ほぼ不要とも言えますが。)


参照:
module NKF
library kconv
Rubyist Magazine - Ruby M17N の設計と実装
class String
class Encoding::Converter
多言語化
Rubyist Magazine - 標準添付ライブラリ紹介 【第 3 回】 Kconv/NKF/Iconv
404 Blog Not Found:ruby|perl - 文字コードのちょっと高度な判定


プログラマのための文字コード技術入門 (WEB+DB PRESS plus) (WEB+DB PRESS plusシリーズ)

プログラマのための文字コード技術入門 (WEB+DB PRESS plus) (WEB+DB PRESS plusシリーズ)

Capybara-DSLのはなし

 ちょっとCapybaraについて、整理する必要があったのでこちらで簡単にまとめておきます。Capybaraは、Githubのスタートページに使い方が丁寧に書いているので、そちらを参照したら大抵のことが解るようになっています。

What is Capybara



 Capybaraは、Webアプリケーションのインテグレーション・テストを補助する為のライブラリです。Capybaraが提供する本質的な機能としては、DSLとDriverの2点のみです。DSLとはドメイン固有言語で、特定の問題に特化したコンピュータ言語です。Capybaraはテスティングフレームワークを操作する命令を、それぞれのフレームワークに依存しない形で提供します。つまり、テスティングフレームワークであるCucumberやRSpec,Test::Unitなどを透過的に利用できます。次にドライバーです。Webアプリケーションのインテグレーション・テストには、ブラウザもしくはそれに類するものが必要です。その為に、Seleniumのようにブラウザを操作するフレームワークや、WebKitのようなブラウザのレンダリングエンジンを直接使う方法があります。Capybaraは、それらをドライバとして扱うことが出来ます。

CapybaraのDSL



 Capybaraには、下記の機能をDSLとして提供しています。要は、CucumberであろうがRSpecであろうが、同じように扱えるところがメリットです。一方で、CucumberからRSpecに乗り換えるという要件もあまりないので、どう考えるべきなんでしょうね。

画面遷移機能

 Capybaraの画面遷移は、GETメソッドのみです。POSTについては、フォーム入力機能を利用します。

メソッド名 機能
visit GETでページ遷移します。カレントのパスは、current_pathで取得できます。
リンク&クリック機能

Capybaraは、一通りのフォームの入力機能が用意されています。

メソッド名 機能
click_link('id-of-link') ID指定でリンクを押します
click_link('Link Text') リンクのテキスト名で押します
click_button('Save') ボタン名で名前を押します
click_on('Link Text') リンクかボタンどちらかをクリックします
click_on('Button Value') ボタンの値指定で押します
フォーム入力機能

Capybaraは、一通りのフォームの入力機能が用意されています。

メソッド名 機能
fill_in('First Name', :with => 'John') フォームテキストを埋めます
fill_in('Password', :with => 'Seekrit') パスワードフォームを埋めます
fill_in('Description', :with => 'Really Long Text...') TextAreaを埋めます
choose('A Radio Button') ラジオボタンを選択します。
check('A Checkbox') チェックボタンを選択します。
uncheck('A Checkbox') チェックボタンの選択を外します
attach_file('Image', '/path/to/image.jpg') 画像を添付します
select('Option', :from => 'Select Box') セレクトボックスを選択します
クエリー機能(確認機能)

Capybaraではクエリー機能を利用して、テストを行います。

メソッド名 機能
page.has_selector?('table tr') エレメントの存在確認をします
page.has_selector?(:xpath, '//table/tr') XPath指定で、エレメントの存在確認をします
page.has_xpath?('//table/tr') XPath指定で、エレメントの存在確認をします
page.has_css?('table tr.foo') CSSで、エレメントの存在確認をします
page.has_content?('foo') 文字列の存在確認をします
検索機能

検索機能としては、find_field,find_button,find,allの4つがあります。

メソッド名 機能
find_field('First Name').value フィールド名指定で検索し、値を表示します
find_link('Hello').visible? フィールド名指定で検索し、表示有無を確認します
find_button('Send').click ボタンを検索し、クリックします
find(:xpath, "//table/tr").click XPath指定で検索し、クリックします
find("#overlay").find("h1").click 入れ子で検索し、クリックします
all('a').each { |a| a[:href] } 全ての要素からaタグを抽出し、hrefを表示します
スコープ機能

 スコープ機能を利用することで、特定のエレメント下のみ操作できます。ループ処理等で便利です。

within("li#employee") do
  fill_in 'Name', :with => 'Jimmy'
end

within(:xpath, "//li[@id='employee']") do
  fill_in 'Name', :with => 'Jimmy'
end
スクリプティング機能(JavaScriptのサポート)

 CapybaraはJavaScriptをサポートしています。ただし、使用中のDriverがJavaScriptをサポートしていることが前提です。(O.K. Selenium,WebKit,Poltergeist NG RackTest)

メソッド名 機能
page.execute_script("$('body').empty()") JavaScriptを実行します
デバッグ機能

 デバッグ用に幾つかの機能が提供されています。テストのエビデンスにも活用出来るのでは。(テストの再現性を確保したら、そもそもエビデンス不要ですがね。)

メソッド名 機能
save_and_open_page スクリーンショットを取得します
print page.html ページの印刷します
page.save_screenshot('screenshot.png') スクリーンショットを取得します

CapybaraのDriver



 Capybaraには、Driverとして幾つかのブラウザシミュレータやブラウザエンジンを扱うことが出来ます。使い分けとしては、JavaScriptを利用しないものについては、RackTestのような軽量のものを利用することが考えられます。また、開発段階は、Seleniumを使って実際の動きを見て、退行テストの段階になったときはWebKitを使い軽量化するなども考えられます。下記のように、簡単にDriverを変更できます。

Capybara.default_driver = :selenium
Driver名 特徴
RackTest デフォルトのDriverです。JavaScriptはサポートしていません。また、外部のURLへのアクセスも出来ません。
capybara-mechanize RackTestと同様にJavaScriptはサポートしていません。しかし、外部のURLへのアクセス可能です。
Selenium Capybaraは、Selenium 2.0(WebDriver)をサポートしています。またJavaScriptをOnにした場合は、指定しないと自動的にSeleniumを利用します。
Capybara-webkit headlessを利用して画面なしでテストします。レンダリングエンジンとしては、WebKitを利用しJavaScriptのテストが可能です。Xvfbが必要です。
Poltergeist headlessを利用して画面なしでテストします。PhantomJSが動作しJavaScriptのテストが可能です。Xvfbが不要です。


 サーバにXvfbのインストールは、割と面倒くさいです。継続的インテグレーションの為にサーバサイドでテストを実行したい場合は、Poltergeistがよいかもしれません。本格的にやるのであれば、Seleniumを利用して、複数のブラウザに対してテストを実行するといったこともよいですね。

まとめ



 Capybaraの役割が解れば、なかなか便利です。私の場合は、SeleniumやCucumber,RSpecを使ってからCapybaraを使いだしました。いきなりCapybaraの方が学習コストが低くてよいかもしれません。一方で、ハマった時によくわからないというのもあるかもしれません。何はともあれ、一度お試しください。Enjoy!!


See Also:
Web画面の自動テストの導入に失敗する理由とその対策
Selenium2.0 WebDriverで複数ブラウザのUIテスト もう一度、Selenium再入門
JenkinsとSelenium WebDriverでUI層のテストも自動化&永続化する


参照:
jnicklas/capybara

あらためてRuby製のクローラー、"anemone"を調べてみた

 3年ほど前に、Ruby製のクローラー"anemone"を紹介しました。その当時から完成度が高く、Rubyでクローラーを使う場合はanemoneを利用してきました。最近、他に新しくて良いのがないか調べましたが、機能面の網羅性という意味でanemoneを超えるものは見つけられませんでした。そこで改めてanemoneのソースを読んでみたところ、クローラーが必要とする機能を必要最小限で実装され、やはり中々良い出来です。冬休みの宿題ではないですが、勉強の意味を兼ねてソースを追っていくことにします。

Anemoneが利用しているライブラリ一覧



 anemoneが利用しているライブラリは、4種類に分類できます。

  • Ruby標準or一般的なライブラリ
  • データ取得で利用しているライブラリ
  • データ解析で利用しているライブラリ
  • データ保存で利用しているライブラリ

この分類別に構造をみるとわかりやすいので、順番に追っていきます。

Ruby標準or一般的なライブラリ


require 'rubygems'
require 'delegate'
require 'forwardable'
require 'optparse'
require 'thread'

 delegateとforwardableは、メソッドの委譲を行うruby標準のライブラリです。optparseは、コマンドラインのオプションを取り扱うためのライブラリです。threadは並行プログラミングを行う為のライブラリです。特筆すべきところは、余りありません。唯一気になったのは、delegateとforwardableの併用についてです。anemoneでは、cookie_storeの実装の部分のみdelegateを使い、データ保存の部分で各ストレージ(kyoto_cabinet,pstore,tokyo_cabinet)についてはforwardableを使っています。この2つのモジュールの選択のポイントについては、よく解っていません。ストレージ機能の実装時期が2年ほど後ということもあり、流儀が変わった可能性もあります。もしくは、移譲について明示的に指定するかどうかの所で、移譲するメソッド数による判断の可能性もあります。

データ取得機能の構造


require 'net/https'
require 'webrick/cookie'
require 'robotex'

 データ取得機能については、core.rbとhttp.rbで実装されています。データ取得の為のライブラリとしては、通信部分には標準のnet/httpsを利用しています。cookieの取扱は、webrick/cookieを利用しています。名前から解るように、Webサーバー用フレームワークのWebrickを利用してCookieの処理を行っているのですね。そして、robotexです。このライブラリは、anemoneの作者であるChris Kiteによるライブラリです。robots.txtの判定を別モジュールとして外出しにしています。この部分は、自分でクローラーを作成する場合にも利用出来ます。使い方は、anemoneでは次のようになっていました。


デフォルトの設定。robots.txtに従わないようになっています。

    # don't obey the robots exclusion protocol
    :obey_robots_txt => false,


引数でrobots.txtに従うように設定した場合、変数@robotsを作成しています。

    @robots = Robotex.new(@opts[:user_agent]) if @opts[:obey_robots_txt]


Robotexモジュールの使い方は、次の通りです。robots.txtに従う場合、Robotexモジュールのallowdメソッドでリンク先を取得可能かの確認をしています。(再度オプションの:obey_robots_txtを見に行っているのは、微妙な気がします。)

  def allowed(link)
    @opts[:obey_robots_txt] ? @robots.allowed?(link) : true
  rescue
    false
  end


このallowdメソッドが、実際使われている所です。visit_linkメソッドでAnd条件で訪問可能か確認しています。

    def visit_link?(link, from_page = nil)
      !@pages.has_page?(link) &&
      !skip_link?(link) &&
      !skip_query_string?(link) &&
      allowed(link) &&
      !too_deep?(from_page)
    end


 この実装であれば、同一サイトでも都度robots.txtを確認するような気がします。念の為、Robotexモジュールの実装も確認してみます。結論的には、一度確認したサイトについては、robots.txtの再取得をしないような作りになっています。一安心です。

  def allowed?(uri, user_agent)
    return true unless @parsed
   〜略〜
  end

データ解析機能の構造


require 'nokogiri'
require 'ostruct'

 データ解析機能については、page.rbで実装されています。そして殆どの処理の実態は、nokogiriでのパースです。その為、anemoneの利用元の方で、nokogiriを使って自由に加工出来ます。
ex)利用例

Anemone.crawl("http://www.example.com/") do |anemone|
    anemone.on_every_page do |page|
      title = page.doc.xpath("//head/title/text()").first.to_s if page.doc
      puts title
    end
end


 HTML解析の中のリンクの検索については、aタグ中のhrefを検索しているだけのようです。FormやJavaScript等で飛び先を指定しているのは、取れません。クローリングで迷惑を掛けることを防止するには、このaタグのみ取得する実装が賢明だと思います。

  def links
    return @links unless @links.nil?
    @links = []
    return @links if !doc

    doc.search("//a[@href]").each do |a|
      u = a['href']
      next if u.nil? or u.empty?
      abs = to_absolute(u) rescue next
      @links << abs if in_domain?(abs)
    end
    @links.uniq!
    @links
  end

データ保存機能の構造


require 'kyotocabinet'
require 'mongo'
require 'tokyocabinet'
require 'pstore'
require 'redis'
require 'sqlite3'

 anemoneは、取得したデータの保存先の選択肢が豊富です。初期は、sqlite3のようにRDBMSや、pstoreなどの標準のファイルオブジェクトのみでした。いまでは、tokyocabinet/kyotocabinet・redisのようなキーバリューストアや、mongoDBのようなNoSQLにも対応するようになっています。履歴を見ていると、利用者からのPullRequestがマージされている模様です。
 構造的には、anemone/storageにストレージごとの実装を追加するという形になっています。割と簡単に追加できそうなので、試しにAmazon S3を利用したタイプでも作ってみようと思います。

まとめ



 ざっとanemoneの構造を確認してみました。ページ取得の部分は、coreの部分と密な結合になっているようです。反対にページ解析やデータ保存については、比較的疎結合になっています。もともと、ページ取得部分だけ取り替え可能を知りたくて調べました。http.rbを呼び出している部分を置き換えれば出来なくもなさそうですが、それよりもページ解析機能やデータ保存機能を移植する方が楽そうです。
 anemoneは比較的小さなライブラリですが、色々な要素があります。ソースを順番に読んでいくと、中々勉強になりました。興味がある方は、時間があるときに一度読んでみてはいかがでしょうか? enjoy!!

PR
anemoneの解説を含めて、Rubyによるクローラー開発の本を書きました。
クローラーの概念から実際の構築・運用手順を網羅しています。


See Also:
オープンソースのRubyのWebクローラー"Anemone"を使ってみる
JavaScriptにも対応出来るruby製のクローラー、Masqueを試してみる
複数並行可能なRubyのクローラー、「cosmicrawler」を試してみた


参照:
chriskite/anemone · GitHub
Rubyist Magazine - 標準添付ライブラリ紹介 【第 6 回】 委譲
anemone RDoc
PythonとかScrapyとか使ってクローリングやスクレイピングするノウハウを公開してみる! - orangain flavor


Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例