はじめに
こんにちは、rubyエンジニアのさもです。
本記事では、rubyで簡単にクローラが作れるよというのを紹介したいと思います。
何が作れるのかな、と思う方は、実行 を見てみてください。
ruby入門者や、rubyを勉強したけど何ができるのと思っている方、ruby=railsと思っている方、クローラをやってみたいけど実際どうすればいいか分からない方へ、rubyでこんなこともできるんだ、クローラってこんな感じでいいんだ、と思っていただけたら幸いです。
今回実装するクローラでは、ruby標準のライブラリしか使いません。外部ライブラリのインストールではまって、本来やりたいことができなくなることを極力避けるためです。
目次
準備
いくつか準備が必要になります。
最終的にそろえるものは、c3.js, c3.css. d3.js.min, ruby, index.html.erbになります。
jsライブラリの準備
今回は、結果をグラフで表示したいので、c3.jsというjsライブラリを使って、ブラウザ上にグラフを表示させたいと思います。
準備としては、c3.js, c3.css, d3.js.min が必要になります。
以前c3.jsの入門記事を書いたので、そちらを参考にしてください。
ダウンロードだけなので一瞬で終わります
普通にダウンロードしてくるだけですが、d3だけバージョンに注意です。d3がv4系だとたぶん動かないです。
rubyのインストール
著者が使用したrubyのバージョンは2.2.3でした。
今回は特にバージョンを気にすることもないと思うので、お好きなバージョンでお試しください
インストールは以下のサイトに丸投げしてしまいます。
htmlファイルの準備
後で実装していくので今は中身が空っぽのファイルを作っておいてください。
ファイル名はindex.html.erbです。
拡張子は、.htmlじゃなくて、.html.erbです。ご注意ください。
rubyファイルの準備
こちらも後で実装していくので空っぽのファイルを作っておいてください。
ファイル名はhatena.rbとか、何でもいいです。
全て準備できたところで、新しくフォルダを作り、c3.js, c3.css, d3.js.min, index.html.erb, hatena.rb を入れておいてください。
実装
hatena.rbの実装
クローラの本体です。
以下のコードをhatena.rbへコピペしてください。
# 1. 必要なライブラリを読み込む require 'erb' require 'open-uri' require 'robotex' # 2. 実行時引数にはてなブログのURLをわたす hatena_url = ARGV[0] # 3. 動的に作成するhtmlファイルのパスを指定 erb_file = "./index.html.erb" html_file = "./index.html" # 4. 最終ページかどうかのフラグ last_page = false # 5. robots.txtを確認する robotex = Robotex.new "Crawler" allow_crawler = robotex.allowed?(hatena_url) dates = [] dates_count = {} # 6. 最終ページに達すると終了する while allow_crawler && !last_page do # 7. urlからページをダウンロード open(hatena_url) do |file| p hatena_url page = file.read # 8. ページ内の記事投稿日一覧を正規表現で取得 page.scan(/<time pubdate datetime=\"(.*?)\" title/).each do |time| # 9. 投稿日の配列と、投稿日と投稿数のハッシュを作成 created_at = Date.parse(time[0]).strftime("%Y-%m-%d") dates << created_at dates_count[created_at] ? dates_count[created_at] += 1 : dates_count[created_at] = 1 end # 10. ページ下段にある次へリンクを正規表現で取得 next_url = page.scan(/<a href=\"(.*?)\" rel=\"next\"/) # 11. 次へリンクが見つかればurlをセット。 # 見つからなければ最終ページへ到達したとみなして、フラグを立てる next_url.size == 0 ? last_page = true : hatena_url = next_url[0][0] # 12. サーバに負荷がかからないように、次のリクエストを投げるまで4秒待つ # 優しい一行 sleep(4) end end # 13. c3.js用にデータを加工する @date_count_values = ['投稿数'] + dates.uniq.sort.map{ |date| dates_count[date] } @duration = ['x'] + dates.uniq.sort # 14. ページを動的に作成 File.open(html_file, "w") do |file| file.puts(ERB.new(File.read(erb_file)).result(binding)) end
解説していきます。とりあえず動かしたいという方は、次のindex.html.erbをコピペして、実行まで進んでください。
1. 必要なライブラリを読み込む
erbはhtmlのコードの中にrubyコードを埋め込み、動的にファイルを生成するためのライブラリです。
html以外でもいけます!すごい!
open-uriはファイルを開くメソッドopenをuriにも対応させたものです。
robotexはrobot.txtを読み、クローラ可能かどうかを判定するのに使います。クローラしないでというページはクローラしません。
2. 実行時引数にはてなブログのURLを渡す
実行時引数にサイトURLの文字列を渡すことで、実行時にクローラ対象を変更できるようにしています。
4. 最終ページかどうかのフラグ
今回作ったクローラは、ページを開く-> 各記事の日付を取得する -> 次のページへ移動する を基本動作としています。
最後のページには次へリンクがないので、last_pageをtrueにし、クローラを終了させます。
5. robots.txtを確認する
Robotexのallowed?メソッドでクローラOKかどうかをみて、OKならクローラを実行させます。
NGならクローラを実行させません。
6. 最終ページに達すると終了する
allow_crawler && !last_page すなわち、クローラOKかつ、最後のページでない限り、クローラを続けます。
7. urlからページをダウンロード
openメソッドはopen-uriによって提供されているメソッドです。
サイトのURLを渡すことで、ダウンロードし、ファイルのように扱えます。
ブロックに具体的な処理を書いて行きます
8. ページ内の記事投稿日一覧を正規表現で取得
このコードの要となる部分です。
scanメソッドに正規表現を渡すことで、文字列全体から一致するものを配列形式で受け取れます。
ブロックの中で、取得したデータをdatesとdates_countへ蓄積していきます
10. ページ下段にある次へリンクを正規表現で取得
ページ内の日付をすべて収集したあとで、次のページへリンクを探します。
見つかればそのリンク先をhatena_url変数へ代入します。
見つからなければ、そのページで最後ということなのでlast_pageフラグを立てます。
12. サーバに負荷がかからないように、次のリクエストを投げるまで4秒待つ
優しさの1行です。
プログラムから高速に連続してアクセスすると、サーバに負荷がかかってしまいます。
そのため少し待機します。
絶対コメントアウトしないでください
13. c3.js用にデータを加工する
収集したデータを並べ替えたり、ユニークにしたり、ラベルをつけたり、c3.jsでグラフを表示するためのデータを作ります。
@date_count_valuesが各日付に何件投稿したのかが、配列形式で入っています。
@durationが投稿した日付の一覧です。
14. ページを動的に作成
作成したデータをindex.html.erbへ埋め込み、index.htmlを動的に作成します。
index.html.erbの実装
クローラの実行結果を表示させるためのhtmlファイルを作成します。
以下のコードをindex.html.erbへコピペしてください。
<html> <head> <link href="./c3.css" rel="stylesheet"> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <script src="./d3.min.js" charset="utf-8"></script> <script src="./c3.js"></script> </head> <body> <div id="chart"></div> <script> c3.generate({ bindto: "#chart", data: { x: 'x', columns: [ <%= @duration %>, <%= @date_count_values %> ], }, axis: { x: { type: 'timeseries', tick: { fit: true, format: '%Y-%m-%d' } } } }); </script> </body> </html>
基本的な書き方は、簡単に綺麗なグラフが書けるc3.jsの紹介 - 子育てしつつ、たまにエンジニア をみていただきたいです。
c3.jsの記事で言及していないところを解説します。
x
x: 'x'
で'x'というラベルのついたデータをx軸にします。という宣言です。
‘x'ラベルは投稿した日付の配列につけています。
<%= ~ %>
<%= ~ %>
~の部分にrubyのコードを埋め込めることができます。
<%= で埋め込んだrubyコードの実行結果をその部分へ置き換えるという意味になります。
<% ~ %>
だけだと、実行されるだけで表示されません。
axis
axisは軸に関するあれこれを設定していく部分です。
type: 'timeseries'
は、x軸が日時であらわされていますよという設定です。
tickはx軸に表示されるラベルやメモリの設定です。
fitをtrueに設定すると、日付に抜けがあってもいい感じに調整してくれます。
formatはx軸に表示されるメモリの表示形式を指定します。'%Y-%m-%d'にすると、年-月-日の形式で表示してくれます
実行
さて、実装も終わったので、そろそろ実行しましょう。
以下のコマンドを、rubyが実行できるプロンプト等で実行します。
ruby hatena.rb はてなブログのURL
はてなブログのURLには例えば、私のブログのURL http://www.uosansatox.biz/ を入れてみてください。
ruby hatena.rb http://www.uosansatox.biz/
しばらく実行経過を見守っていると、作業フォルダにindex.htmlファイルが作成されたと思います。
index.htmlをブラウザを開くと以下のようなグラフが表示されます。
作ってしばらく放置して、ある時突然はまり出した感がお分かりになったでしょうか。
最後に
うまく実行できたでしょうか。日付が詰まりすぎていて少し見にくいですね。
c3.jsには表示するメモリを制限するオプションもあるので、もっと綺麗にできると思います。
その他、プログラムに問題があれば教えてください。
hatena.rbのコメントと、空白行を全て消すと、30行です。
意外と少ない行数でクローラが作れてしまいました。
結果をerb経由で渡して、ブラウザで"でーたびゅじゅあらいぜーしょん"できるのはカッコいいですよね。
また何か面白いクローラができたら記事を書いてみたいと思います。