プログラマでありたい

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

『Rubyによるクローラー開発技法』を書きました

 勉強会やスライドで紹介していましたが、Ruby×クローラーという題材で、『Rubyによるクローラー開発技法』という本を書かせて頂きました。RubyとEmacsの鬼であるるびきちさんとの共著です。


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

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

この本を書いた理由



 そもそものキッカケは、るびきちさんのエントリーにある通り、SBクリエイティブの編集者さんが、クローラーの作成経験のある人を探していて、私の書いた「オープンソースのRubyのWebクローラー"Anemone"を使ってみる」を読んで打診してくださったというのが始まりです。
 私自身も、Webからデータを収集して分析するということは、趣味として長年やってきました。一方で、本業の方でクローラーというものを作ったことはなく、せいぜい業務効率化でデータ取得をサポートするスクリプトを作る程度です。もっと言えば、Webサイトの運用で、質の悪いクローラーと戦うことの方が多かったです。そんなこともあり、クローラーというある種グレーゾーンに入りやすいものを題材とするので、書いても良いのかという悩みました。また、そもそもクローラーの本の需要は、ニッチな上に寿命が短いのではという懸念もありました。
 いろいろ考えることはありましたが、クローラーやスクレイピングの技術を正しく使えば有用なことは間違いありません。そこで、私なりのクローラーというものを伝えられればと思い挑戦してみました。

本の内容



 6章構成になっています。

  • 1章 10分クローラー作成
  • 2章 クローラー作成の基礎
  • 3章 収集したデータを分析する
  • 4章 高度な利用法
  • 5章 目的別クローラーの作成
  • 6章 クローラーの運用

 2&3章で、データの収集から解析まで一通り取り扱っています。nokogiri&anemoneやxpathといった基本的なライブラリの使い方から、軽く正規表現や形態素解析・自然言語処理の話をしています。4章の部分は、クローラーを拡張していくにはどうするかという観点で書いています。そして、5章は目的別のクローラーということで、実際のサイトを対象にどのようにデータをとってくるのかを具体的に書いています。おなじみの株価や新聞からの情報収集や、iTunes StoreやGoogle Playからのアプリランキング取得など20以上のトピックスがあります。最後の6章は、主にサーバサイドで動かすにはという話です。この辺りは、AWSの各種サービス(SNS,SQS)との連携などにも触れています。

 全般的には、クローラーを初めて作る人を意識して書いています。クローラーとかスクレイピングは、超絶テクニックが有るわけではなく、どちらかと言えば泥臭い作業の連続です。その辺りを、どのように考えながら作るのかを書いたつもりです。

まとめというか感想



 本を書くということは初めての経験でした。想像以上の大変さで、改めて執筆者の凄さというのを認識できました。また、本の冒頭に家族への感謝の気持ちが書かれていることが多く、今までいったい何なんだろうと思っていました。自分で書いてみて初めて解ったのですが、家族の協力がなければ執筆作業はとてもじゃないですが出来ないです。(平日の晩や土日が潰れるので、家事や子育てなどの負担が重くなる)本当に感謝の気持ちで一杯です。
文系上がりのエンジニアとして何者でもなかった自分が、何かには成れたように思えます。

See Also:
RubyでWebスクレイピングの話をしてきました。第1回Webスクレイピング勉強会@東京
Ruby製の構文解析ツール、Nokogiriの使い方 with Xpath
あらためてRuby製のクローラー、"anemone"を調べてみた
オープンソースのRubyのWebクローラー"Anemone"を使ってみる
開発用プロキシ、「CocProxy」が便利
Rubyによるクローラー開発技法の目次


参照:
新刊『Rubyによるクローラー開発技法』2014/08/25発売! | るびきち×Emacs
SBクリエイティブ:Rubyによるクローラー開発技法

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

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

RubyでWebスクレイピングの話をしてきました。第1回Webスクレイピング勉強会@東京

 ちょっと間が空きましたが、第1回Webスクレイピング勉強会@東京に参加して、LT枠でRubyでWebスクレイピングの話をしてきました。



 今まで全く参加したことがないレイヤーの勉強会だったので、新しい発見があり非常に勉強になりました。スクレイピングのAPIであるkimonoimportioなど、全く知らないサービスに出会えました。私は趣味でスクレイピングをしているのですが、本職としてやっている方のノウハウや悩みどころを聞けて参考になりました。

 また、資料中に書いているのですが、現在Rubyでクローラーを作る本を書いています。一応全編書き終えてるので、夏頃に出ればなぁという状況です。そして、東京に異動することになりました。勉強会に参加しやすくなるので、色々な所に顔をだしてみたいと思っています。

 ちなみに第2回Webスクレイピング勉強会@東京にも登壇予定です。ブログやサイトから、ヘッダーやフッター、サイドバーなどを除いて本文だけ抽出するにはどうするのというテーマで、20分ほど話す予定です。


参照:
第1回Webスクレイピング勉強会@東京 (全3回) - Qiita
2014/06/22 第1回Webスクレイピング勉強会@東京 #東京スクラッパー - Togetterまとめ
第1回Webスクレイピング勉強会@東京に参加してきました - 夜はいよいよ冴えたのだ。

発表者資料
オープンデータのためのスクレイピング
Webスクレイピング勉強会@東京 オープニングトーク (第1版) #東京スクラッパー
ScrapyとPhantomJSを用いたスクレイピングDSL
Webスクレイピングの基礎知識 #東京スクラッパー
CasperJSを使って任意のWebサイトを電子書籍化する方法
渡る世間は自然言語ばかり #東京スクラッパー
オープンデータのためのスクレイピング

See Also:
オープンソースのRubyのWebクローラー"Anemone"を使ってみる - プログラマになりたい
Capybara-DSLのはなし - プログラマになりたい
複数並行可能なRubyのクローラー、「cosmicrawler」を試してみた - プログラマになりたい
開発用プロキシ、「CocProxy」が便利 - プログラマになりたい
iTunesのランキングを毎日自動で取得する その1 - プログラマになりたい
Rubyのtwitterライブラリで、Twitter Streaming APIが扱えるようになっていた - プログラマになりたい


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

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

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

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

開発用プロキシ、「CocProxy」が便利

 CocProxyというツールがあります。名前の通りプロキシサーバーなのですが、用途が開発用です。置換プロキシと言うらしいですが、これがとっても便利です。例えば、クローラーの開発や、HTML,CSS,JavaScriptの修正をする際に、このCocProxyを利用するとダウンロード待ちのイライラが解消され、効率アップ間違いなしです。ローカルのみで完結するのと、Webへのアクセスが必要とするのでは、スピードが数十〜数百倍違います。一回だけでみたら、数百ミリSecの差ですが累積で考えると大きいですよ。

CocProxyとは?



 Ruby製のProxyServerです。かつ標準ライブラリで動くことを目標としているので、Rubyの環境があれば、ダウンロードして直ぐに使えます。また基本的には1ファイルで完結するので、導入も簡単です。Ruby1.9をベースにしていますが、Ruby 2.0でも特に問題なく動くようです。
 動作としては、Webファイルの取得時に、まずローカルのファイルもしくはキャッシュを確認して、存在したらそれを返します。なければWebファイルを取得にいきます。その際、キャッシュとしても保存するので、2回目以降は取得しないようになります。
 ローカルファイルについてですが、filesというディレクトリを作成してファイルをおいておくと、まずそれを見てくれるようになります。つまり上手くやると、オフラインでも開発できるようになります。一致条件については、下記のルールとなります。

#{File.basename(req.path_info)}",
#{req.host}#{req.path_info}",
#{req.host}/#{File.basename(req.path_info)}",
.#{req.path_info}",

具体的にいうと、 http://blog.takuros.net/entry/2014/04/15/070434 にアクセスする場合、最初に070434というファイルを見て、次にhttpなしのFQDN、ドメイン名+ファイル名、絶対パスの順で照合していきます。下のケースですと、無いので取得にいってキャッシュしていますね。

Checking files/070434
Checking files/blog.takuros.net/entry/2014/04/15/070434
Checking files/blog.takuros.net/070434
Checking files/./entry/2014/04/15/070434
Cached: http://blog.takuros.net/entry/2014/04/15/070434

CocProxyのインストールと起動



 インストールは、簡単です。下記のURLからCocProxyをダウンロードするだけです。
http://svn.coderepos.org/share/lang/ruby/cocproxy/proxy.rb

 起動は、proxy.rbをrubyからキックするだけです。
デフォルトで、5432ポートを利用します。

$ ruby proxy.rb 
Use default configuration.
Port : 5432
Dir  : files/
Cache: true
Rules:
    1. #{File.basename(req.path_info)}
    2. #{req.host}#{req.path_info}
    3. #{req.host}/#{File.basename(req.path_info)}
    4. .#{req.path_info}
Checking files//
Checking files/www.yahoo.co.jp/
Checking files/www.yahoo.co.jp//
Checking files/./
Cached: http://www.yahoo.co.jp/
Checking files/070434
Checking files/blog.takuros.net/entry/2014/04/15/070434
Checking files/blog.takuros.net/070434
Checking files/./entry/2014/04/15/070434
Cached: http://blog.takuros.net/entry/2014/04/15/070434

 ちなみに1ファイルでない、完全版は下記のURLからダウンロードできます。
http://svn.coderepos.org/share/lang/ruby/cocproxy/

CocProxyの利用 With Nokogiri



 CocProxyは、各アプリからの設定でProxyとして指定することで利用できます。Nokogiriの場合は、次のような感じです。

require 'nokogiri'
require 'open-uri'

doc = Nokogiri::HTML(open('http://www.yahoo.co.jp', :proxy => 'http://localhost:5432'))

puts doc.title # => Yahoo! JAPAN

 動作はこの通りです。2回目は、キャッシュを参照しているのが解ります。

Checking files//
Checking files/www.yahoo.co.jp/
Checking files/www.yahoo.co.jp//
Checking files/./
From Cache: http://www.yahoo.co.jp/
Checking files//
Checking files/www.yahoo.co.jp/
Checking files/www.yahoo.co.jp//
Checking files/./
From Cache: http://www.yahoo.co.jp/

 Nokogiriなどを使ってスクレイピングする場合は、なかなか一度で思い通りのデータを取得することは難しいです。何度も試行錯誤すると、当然ながら何度もWebサイトにアクセスすることになります。迷惑掛けていないか心苦しく思うことが多々あります。そんな際に、CocProxyがあれば思う存分試行錯誤できます。

改善点



 ローカルファイル参照とキャッシュ参照の仕組みの二つがあるのは面白いです。一方で自分が使ってて思うのは、わざわざローカルファイルを置くのは面倒臭いというのがあります。初回取得時にキャッシュではなく、ローカルにファイルを置く機構があれば便利かなと思います。また、ローカルファイルの取得もURLのハッシュ値で取得するだけで充分です。また、ユーザーエージェントが取得先の動作に影響する場合があるので、考慮が必要な場合があります。その辺りは作者の方と利用想定が違うと思うので、自分用に改造して使っています。その辺り、次回くらいで紹介します。

Ruby製のクローラー AnemoneでストレージをSQLite3, MongoDBに変更する

 Anemoneのストレージを変更するシリーズ、ついでにSQLite3とMongoDBも書いておきます。

AnemoneでSQLite3を利用する



 AnemoneでSQLite3を利用するには、オプションで:storage => Anemone::Storage::SQLite3()と指定するだけです。引数を渡さなければ、anemone.dbという形式のsqlite3ファイルが作成されます。プログラムとしては、次のような形ですね。

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

opts = {
  :storage => Anemone::Storage::SQLite3(),
  :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("//title/text()").to_s if page.doc
  end
end

 SQLite3を使った場合の嬉しいところは、2回目以降の起動でも既読スキップが有効になる点です。これは、初期化処理でPStoreと違いファイルの削除なども行わず、テーブル作成時も既存テーブルがあればちゃんと残している点です。

      def initialize(file)
        @db = ::SQLite3::Database.new(file)
        create_schema
      end

      def create_schema
        @db.execute_batch <<SQL
          create table if not exists anemone_storage (
            id INTEGER PRIMARY KEY ASC,
            key TEXT,
            data BLOB
          );
          create index  if not exists anemone_key_idx on anemone_storage (key);
SQL
      end

 一方で、既読スキップはURLのみで判断しています。訪問後、N日の場合は再取得といった処理はできません。何故なら、前回取得時の日時情報を持っていないからです。

      def has_key?(url)
        !!@db.get_first_value('SELECT id FROM anemone_storage WHERE key = ?', url.to_s)
      end

 この辺りは、少し改善したいですね。

AnemoneでMongoDBを利用する



 AnemoneでMongoDBを利用する場合も、オプションで:storage => Anemone::Storage::MongoDB()と指定するだけです。前提として、ローカルにMongoDBのデーモンが起動されている必要があります。

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

opts = {
  :storage => Anemone::Storage::MongoDB(),
  :depth_limit => 0
}

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

 MongoDBで利用するDB名やCollection名も変更可能ですが、DB名の変更の方法は微妙です。mongoのGemを呼び元でrequireして、DBを作成の上で渡す必要があります。

require 'anemone'
require 'mongo'

opts = {
  :storage => Anemone::Storage::MongoDB(Mongo::Connection.new.db('crawler'),"documents")
}

 AnemoneのStorageクラスで、下記のような処理をしている為です。

    def self.MongoDB(mongo_db = nil, collection_name = 'pages')
      require 'anemone/storage/mongodb'
      mongo_db ||= Mongo::Connection.new.db('anemone')
      raise "First argument must be an instance of Mongo::DB" unless mongo_db.is_a?(Mongo::DB)
      self::MongoDB.new(mongo_db, collection_name)
    end

 こんな感じの方が、使い易いのではと思います。

def self.MongoDB(db_name = nil, collection_name = 'pages')
  db_name ||= "anemone
  mongo_db = Mongo::Connection.new.db(db_name)

 ちなみに、AnemoneのMongoDBは、初期化処理でCollectionを削除しています。その為、2回目以降の処理で既読スキップは出来ません。(必要であれば、コメントアウトするなり対処しましょう。)

      def initialize(mongo_db, collection_name)
        @db = mongo_db
        @collection = @db[collection_name]
        @collection.remove
        @collection.create_index 'url'
      end

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製のクローラー 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選

あらためて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の運用例