プログラマでありたい

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

ヤフー「つぶやき感情分析」で、衆議院議員の人気度を見る

 ネットを見てると、面白いサービスが出ていました。

 ヤフー株式会社は12日、ソーシャルメディア上の投稿を検索できるYahoo!検索の「リアルタイム検索」において、「つぶやき感情分析」の正式版を公開した。

 つぶやき感情分析は、Twitter上の投稿を分析し、検索したキーワードについてユーザーがどのような感情を持っているかを、「ポジティブ」「ネガティブ」の割合でグラフ表示する機能。2013年9月にベータ版として提供を開始し、今回、正式版として公開した。

ヤフー「つぶやき感情分析」、Twitter上の感情を「ポジ/ネガ」で判定 -INTERNET Watch

 Twitterの情報をもとに、感情分析をするというサービスです。簡単にいうと、入力されたキーワードに対して、世間は好きか嫌いかを出すというサービスです。こういったことをやりたいなぁと思って、Twitterのデータを大量に収集したり、そのデータを分析したりしたことがあります。
 こういった感情分析があれば何が出来るのか、いろいろ妄想がはかどります。今回は旬なテーマとして、衆議院議員に対する世間の反応をみてみましょう。

「つぶやき感情分析」を使ってやったこと



 

  • 衆議院サイトの議員一覧から、全議員の名前を収集する
  • 議員一人一人に対して、Capybara+Seleniumを使って「つぶやき感情分析」をする
  • 結果を一覧化する

その結果は、次の通りです。

名前 ポジティブ(%) ネガティブ(%) 備考
逢沢一郎 0 0  
青木愛 35 7  
青柳陽一郎 0 0  
青山周平 61 0 同名のオートレーサーがいる
赤枝恒雄 0 0  
赤澤亮正 0 0  
赤羽一嘉 0 0  
あかま二郎 0 0  
赤松広隆 0 0  
赤嶺政賢 0 0  
秋葉賢也 0 0  
秋元司 0 0  
秋本真利 0 0  
浅尾慶一郎 0 0  
安住淳 0 0  
麻生太郎 10 14  
足立康史 0 0  
穴見陽一 0 0  
阿部寿一 0 0  
安倍晋三 0 22  
あべ俊子 0 0  
阿部知子 0 26  
甘利明 0 32  
荒井聰 0 0  
安藤裕 0 0  
池田道孝 - -  
池田佳隆 0 0  
伊佐進一 0 0  
井坂信彦 0 0  
石井啓一 0 0  
石川昭政 0 0  
石崎徹 0 0  
石関貴史 0 0  
石田祝稔 0 0  
石田真敏 0 0  
石破茂 0 28  
石原慎太郎 34 9  
石原伸晃 0 18  
石原宏高 0 0  
泉原保二 - -  
泉健太 0 0  
井出庸生 0 0  
伊藤信太郎 0 0  
伊藤忠彦 0 0  
伊藤達也 0 0  
伊東信久 0 0  
伊東良孝 0 0  
伊藤渉 0 0  
稲田朋美 0 3  
稲津久 0 0  
井野俊郎 0 0  
井上信治 0 0  
井上貴博 0 0  
井上英孝 0 0  
井上義久 0 0  
井林辰憲 0 0  
伊吹文明 0 0  
今井雅人 0 62  
今枝宗一郎 0 0  
今津寛 0 0  
今村洋史 0 0  
今村雅弘 0 0  
岩田和親 0 0  
岩永裕貴 0 0  
岩屋毅 0 0  
上杉光弘 0 0  
上田勇 0 0  
上西小百合 0 0  
うえの賢一郎 - -  
上野ひろし 0 0  
浮島智子 0 0  
生方幸夫 0 0  
浦野靖人 0 0  
漆原良夫 0 0  
江崎鐵磨 - -  
江田憲司 0 28  
江田康幸 0 0  
枝野幸男 3 17  
江渡聡徳 0 0  
衛藤征士郎 0 0  
江藤拓 0 6  
遠藤敬 0 0  
遠藤利明 0 0  
大岡敏孝 0 0  
大串博志 0 0  
大串正樹 0 0  
大口善徳 0 0  
大久保三代 0 0  
大熊利昭 0 0  
大島敦 0 0  
大島理森 0 0  
太田昭宏 0 0  
大塚高司 0 0  
大塚拓 0 0  
大西健介 0 0  
大西英男 0 0  
大野敬太郎 0 0  
大畠章宏 0 0  
大見正 0 0  
岡田克也 0 27  
岡本三成 0 0  
小川淳也 0 0  
奥野信亮 0 0  
奥野総一郎 0 0  
小熊慎司 0 0  
小倉將信 0 0  
小此木八郎 0 0  
小里泰弘 0 0  
小沢一郎 1 33  
小沢鋭仁 0 18  
小田原潔 0 0  
越智隆雄 0 0  
鬼木誠 0 0  
小野寺五典 31 0  
小渕優子 3 40  

実装について



 チョロチョロっと書いたので、手抜きの極みです。Gistを貼り付けておくので、マサカリ歓迎です。

衆議院議員一覧から名前を抜き出して、ヤフー「つぶやき感情分析」で好悪を調べる


 実装に際して、名簿を集めるのは単一ページに対してNokogiriを使ってスクレイピングするだけなので苦労はなかったです。しかし、「つぶやき感情分析」の検索結果を取得するのが悩みどころです。結果を画像で表示していて、その画像はJavaScriptで取得して描画しているようです。ちょっとその辺りを解析するのが面倒くさかったので、Seleniumで画面キャプチャしてその結果を手で起こしました。ビバ手作業!!ちなみにNokogiriやSeleniumを使ってスクレイピングする方法は、Rubyによるクローラー開発技法に書いている模様ですw

感想



 取得対象者が少ないなぁと思ったら、取得ページが分割されていて"あ行"しか取っていませんでした。面倒くさいので、そのまま放置しています。今回は半自動でしたが、全て手作業でするより10倍くらいは早かったと思います。全部自動化とかすると逆に大変な場合もあるので、ケースバイケースで使い分けるのもありなのではないでしょうか?
 ちなみに結果を眺めてたのですが、「つぶやき感情分析」はネガティブに傾きすぎる傾向があります。政治絡みの単語そのものがネガティブな得点与えられているのかなと思います。今度、別分野で試してみます。(結果取得の部分も、暇見つけて自動化考えてみます。)
 また、あまり呟かれていない、阿部知子さんとか小沢鋭仁さんなどもネガティブな評価が高くなっています。恐らく苗字と名前でもそれぞれ評価をして、総合的に判断するといった処理をしているのではないでしょうか。(それで、何故評価が下がるかは、リストの前後をみてください)
 誤解のないように言っておくと、この呟きの評価と選挙の当落は全く相関はないと思います。マイナスの投票が出来ない分、より嫌われている方が存在感が大きさを示している可能性があります。ここで大事になるのは、一定の基準で毎日の推移が見えることによって、何らかの指標に使えるという可能性があるということです。

See Also:
RubyでWebスクレイピングの話をしてきました。第1回Webスクレイピング勉強会@東京
「第2回Webスクレイピング勉強会@東京」に参加&発表してきました
『Rubyによるクローラー開発技法』を書きました
Rubyによるクローラー開発技法の目次

参照:
Yahoo!検索(リアルタイム)
ヤフー「つぶやき感情分析」、Twitter上の感情を「ポジ/ネガ」で判定 -INTERNET Watch

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選

Rubyのtwitterライブラリで、Twitter Streaming APIが扱えるようになっていた

 久々に@sferikによるTwitterのAPIを使ってみると、いつの間にかTwitter Streaming APIも取得できるようになっていました。Twitter Streaming APIは、APIの中でも異色のもので、ひたすらパブリック・タイムラインを取得するといったものです。4年ほど前に出た当初は、かなり話題になって色々な人がタイムラインを取得して分析していました。かく言う私も、AWSのEC2上で動かして、1年ほどTwitterの呟きを取得して遊んでいました。ちょうど4年前はワールドカップがあり、日本の試合がある度にTweet量が爆発して、プログラムも爆発していました。細かい数字は忘れましたが、無料で使える数%に絞ったAPIのうち日本語だけに絞っても、月数千万件レベルでデータがあったと思います。
 そんなこんなのTwitter Streaming APIですが、当時はサードパーティ製のライブラリもなく自前でNet::HTTPやEventMachineを組み合わせて取得しました。それが、ライブラリ1つで簡単に出来るようになっているんですね。ありがたい限りです。

Twetter Streaming APIのサンプルコード



 一番簡単なTwetter Streaming APIのサンプルコードです。APIキーを取得して、環境変数にセットしておけば使えます。

require 'twitter'

config = {
  :consumer_key => ENV['TWITTER_API_KEY'],
  :consumer_secret => ENV['TWITTER_API_SECRET'],
  :access_token => ENV['TWITTER_ACCESS_TOKEN'],
  :access_token_secret => ENV['TWITTER_ACCESS_TOKEN_SECRET']
}
client = Twitter::Streaming::Client.new(config)
client.sample do |tweet|
  if tweet.is_a?(Twitter::Tweet)
    #日本語の呟きだけ表示
    puts tweet.text if tweet.lang == "ja"
  end
end

Twetter Streaming APIのデモ



まとめ



 これだけでは面白くないので、次はAWS Kinesisと組み合わせてみようかと思います。当時も、突発的に大量のデータが飛んできた時の処理能力などで困っていました。今だとKinesisとバックエンドのサーバを組み合わせたら何でもできそうですね。


See Also:
RubyでTwitter Streaming APIを使ってみる
Twitter Streaming APIのJSONの構造

参照:
sferik/twitter · GitHub

RubyのHTTPS通信で、OpenSSL::SSL::SSLErrorが出たら?

 Mac OSのバージョンアップする度に出しているような気がするのが、RubyのHTTPS通信でのエラー。ルート証明書が見つからなくてエラーがでます。

/.rvm/rubies/ruby-2.0.0-p353/lib/ruby/2.0.0/net/http.rb:918:in `connect': SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed (OpenSSL::SSL::SSLError)


 そんな場合は、まずはRubyの慌てず騒がずNet::HTTPがどこに証明書を探しにいっているかを確認します。

$ ruby -ropenssl -e "p OpenSSL::X509::DEFAULT_CERT_FILE"
"/etc/openssl/cert.pem"


 指し示された場所に、あるのでしょうか? 

$ ls /etc/openssl/cert.pem
ls: /etc/openssl/cert.pem: No such file or directory

 ないですね。OSをバージョンアップすると、何故か消えるっぽいです。


 フォルダ作って、証明書をダウンロードします。

$ sudo mkdir /etc/openssl
Password:
$ cd /etc/openssl/
$ sudo wget http://curl.haxx.se/ca/cacert.pem
--2014-05-17 17:10:23--  http://curl.haxx.se/ca/cacert.pem
Resolving curl.haxx.se... 80.67.6.50, 2a00:1a28:1200:9::2
Connecting to curl.haxx.se|80.67.6.50|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 250283 (244K)
Saving to: 'cacert.pem'

100%[======================================>] 250,283      214KB/s   in 1.1s   

2014-05-17 17:10:25 (214 KB/s) - 'cacert.pem' saved [250283/250283]
$ sudo mv cacert.pem cert.pem


 解決!!

OpenSSL::SSL::SSLError



 このエラーは、当然ながらWindowsでも起こりえます。Macの場合と同様に、デフォルトパスの場所にファイルを配置すれば、問題は解決します。それでは、Windows rubyの場合は、デフォルトの証明書のパスは、どこでしょう?RubyInstallerで入れた場合は、次のようになっていました。

>ruby -ropenssl -e "p OpenSSL::X509::DEFAULT_CERT_FILE"
"C:/Users/Luis/Code/luislavena/knap-build/var/knapsack/software/x86-windows/openssl/1.0.0l/ssl/cert.pem"

 ダメだ。このフォルダは作れない。誰だよ、Luisって!!そういう場合は、環境変数でしています。

>set SSL_CERT_FILE=C:\Ruby200\cacert.pem

 解決!!

RubyでAmazon Product Advertising APIを再び使ってみる

 ちょっと必要になって、再びAmazonのProduct Advertising APIを触ってみました。このAPIは、Amazon謹製のAPIで、商品検索や個別商品情報の取得が出来ます。元々このAPIの名前は、Amazon アソシエイト Web サービスといって、AWSと呼ばれていました。クラウドの方のAWSが出る前の話ですね。その後、クラウドの方がAWSという名称になり、こちらのAPIの名前がProduct Advertising APIに変わっています。
 ちなみに、Amazon Web サービスというサービス名は、現在もクラウドのAWS単体を指すのではなく、AmazonがAPIを使って提供しているサービス全体を指す言葉のようですね。ややこしいけど、全く必要としない豆知識でした。

Product Advertising APIを操作するRuby Gemを選ぶ



 Product Advertising APIは、過去の名称のものも含めて何度かメジャーバージョンアップしています。そして、古いバージョンのものは、一定期間後に使えなくなり気がついていないアプリ開発者の大絶滅を繰り返しているという歴史があったと思います。そんな理由があるので、古いライブラリが打ち捨てられることも多く、イマイチどのライブラリがメジャーなのか解りづらいところです。
 そんな中でGitHubをサクッと検索してみると、上位にくるのは下記の3つです。

  1. GitHub - jugend/amazon-ecs: Amazon Product Advertising Ruby API
  2. GitHub - completelynovel/amazon-product-advertising-api: A nice rubyish interface to the Amazon Product Advertising API, formerly known as the Associates Web Service and before that the Amazon E-Commerce Service.
  3. GitHub - hakanensari/vacuum: Amazon Product Advertising API client


 1つ目のAmazon ECSについては、5年前に記事を書いた時点から生き残っている古株です。その割には、最近でもメンテナンスされています。2つ目のAmazon Product Advertising APIについては、その名もズバリの名前でええのかよという感じですが、もうメンテナンスされていません。最後のvacuumについては、何でその名前を選んだという名称です。作り自体は、Product Advertising APIの薄いラッパーで手軽で使いやすいです。作られたのも最近で、比較的メンテナンスされています。上手くいけば、ちょっと複雑化しているAmazon ECSの対抗馬になると思いますが、名前からAmazon関係のライブラリと気が付かれることもないので、メジャーになりにくいのではと思います。

 そんな訳で、またAmazon ECSを利用します。インストールは、Gemで一発です。

$ gem install amazon-ecs
Fetching: amazon-ecs-2.2.5.gem (100%)
Successfully installed amazon-ecs-2.2.5
Parsing documentation for amazon-ecs-2.2.5
Installing ri documentation for amazon-ecs-2.2.5
Done installing documentation for amazon-ecs after 0 seconds
1 gem installed

Amazon ECSでProduct Advertising APIを操作する



 Product Advertising APIのサインアップがされているという前提で、アクセス・キーとシークレット・アクセス・キーは取得済みの前提とします。ちなみに、これらのキーは、AWSのマスターキーと同じで、かつProduct Advertising APIはIAMアカウントで操作できません。結構危険なので、両方使う人は別のアカウントを作る方がよいでしょうね。
 さて、下記のプログラムが、Amazon ECSを利用して一覧検索と個別商品取得をした例です。基本的には、item_searchメソッドとitem_lookupを覚えていれば事足ります。あとは、レスポンスをppなりで確認しながら弄れば大丈夫でしょう。

require 'amazon/ecs'
require 'pp'

Amazon::Ecs.options = {
  :associate_tag => 'sampleapp-22', 
  :AWS_access_key_id => ENV['AWS_ACCESS_KEY'], 
  :AWS_secret_key => ENV['AWS_SECRET_ACCESS_KEY']
}

#商品検索
res = Amazon::Ecs.item_search('ruby', :country => 'jp')
res.items.each do |item|
  puts item.get('ItemAttributes/Title')
end

#個別商品の詳細表示
res = Amazon::Ecs.item_lookup('B00JXEFT6Y', :response_group => 'Small, ItemAttributes, Images', :country => 'jp')
pp res


## 商品検索 item_searchメソッド
 商品検索のメソッドの実装は以下のとおりです。思いのほか、シンプルです。まぁ基本的には、Amazon Product Advertising APIに値を送っているだけです。optsに何を渡すかは、APIの方を見ないと解りません。

    # Search amazon items with search terms. Default search index option is 'Books'.
    # For other search type other than keywords, please specify :type => [search type param name].
    def self.item_search(terms, opts = {})
      opts[:operation] = 'ItemSearch'
      opts[:search_index] = opts[:search_index] || 'Books'
      
      type = opts.delete(:type)
      if type 
        opts[type.to_sym] = terms
      else 
        opts[:keywords] = terms
      end
      
      self.send_request(opts)
    end

 まずOperationですが、このメソッドを利用すると勝手にItemSearchが指定されます。このAmazon ECSは、幾つかあるAPIのOperationのうちで、3つだけ実装しています。ItemLookupとItemSearch,BrowseNodeLookupです。APIには、下記の通り9つの機能が幾つかあります。商品検索系とカート系の機能ですね。
http://docs.aws.amazon.com/AWSECommerceService/latest/DG/CHAP_OperationListAlphabetical.html

  • BrowseNodeLookup
  • CartAdd
  • CartClear
  • CartCreate
  • CartGet
  • CartModify
  • ItemLookup
  • ItemSearch
  • SimilarityLookup

 ItemSearchのAPIの仕様は以下のURLのとおりです。
http://docs.aws.amazon.com/AWSECommerceService/latest/DG/ItemSearch.html
オプションの値を、ハッシュ配列に格納して送ることが可能です。例えば、著者指定で検索する例です。

opts = {
  :country => 'jp',
  :author => '北方謙三'
}
res = Amazon::Ecs.item_search('三国志', opts)
res.items.each do |item|
  puts item.get('ItemAttributes/Title')
end


## 商品取得 item_lookupメソッド
 個別商品の取得は、item_lookupメソッドを利用します。商品を一意に識別するために、ASIN(本の場合だと、ISBN)を利用します。
http://docs.aws.amazon.com/AWSECommerceService/latest/DG/ItemLookup.html

# Search an item by ASIN no.
def self.item_lookup(item_id, opts = {})
  opts[:operation] = 'ItemLookup'
  opts[:item_id] = item_id
    
  self.send_request(opts)
end    


## その他のOperationの利用
 Amazon ECSで実装されている以外の、Operationを利用する場合は、send_requestメソッドを利用します。Optionの値を詰め込んで、send_requestメソッドを呼び出します。send_requestメソッドの中身が何をやっているかと、タイムスタンプを付けて、リクエスト用のURLに変換して送ってくれています。まぁ、それだけやってくれれば充分だなぁという作りです。カートなども、これを使えば実装できます。

    # Generic send request to ECS REST service. You have to specify the :operation parameter.
    def self.send_request(opts)
      opts = self.options.merge(opts) if self.options
      
      # Include other required options
      opts[:timestamp] = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")

      request_url = prepare_url(opts)
      log "Request URL: #{request_url}"
      
      res = Net::HTTP.get_response(URI::parse(request_url))
      unless res.kind_of? Net::HTTPSuccess
        raise Amazon::RequestError, "HTTP Response: #{res.code} #{res.message}"
      end
      Response.new(res.body)
    end


## まとめ
 商品の情報をまとめて取得しようと、久々に使ってみました。やはりAmazonはAPIがしっかり設計されているので使いやすいですね。


See Also:
RailsでProduct Advertising APIを扱うAmazon ECSを使う


参照:
http://docs.aws.amazon.com/AWSECommerceService/latest/DG/Welcome.html


開発用プロキシ、「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