はじめに
Ruby Advent Calender 2017、11日目の記事になります。
 今回は以前実施した、Rubyでのスクレイピングについて再度記載したいと思います。
Selenium WebDriverやPhantomJSなどは使用しておりません。
概要
以前、技術評論社さんのデータサイエンティスト養成読本という本を読みました。
 いろいろな技術の紹介があり、非常に参考になりました。
その中でスクレイピングに関するサンプルコードもありました。
 今回はPythonで書かれていたスクレイピングのコードをRubyで書き直した記事になります。
なお、Pythonで書かれたサンプルの方は実際に書籍をご覧になってください。
仕様
技術評論社さんのサイトに行き、直近の記事タイトルとそのリンクを取得してJSONで吐き出します。
ソースコード
実際に書いたソースコードはこちらになります。
 その後、各項目について記載したいと思います。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | require 'uri' require 'open-uri' require 'nokogiri' require 'json' class GihyoCrawler   @@base_url = 'http://gihyo.jp'   @@content_list_url = 'http://gihyo.jp/contentslist'   @@title_xpath = '//*[@id="latestArticle"]/dl/dd/h3/a'   @@next_url_xpath = '//*[@id="latestArticle"]/div[4]/p[2]/a'   @@output_file = 'output.json'   @@page_num = 2   @@sleep_time = 1   def self.run     url = @@content_list_url     File.open(@@output_file, 'w:utf-8') do |f|       f.write("{\n")       @@page_num.times do         dom = read_html(url)         scrape_content_page(dom).each do |title, url|           data = {title: title, url: url}           f.write(JSON.dump(data) + ",\n")         end         url = get_next_url(dom)         break if url.nil?         sleep(@@sleep_time)       end       f.write("}")     end   end   def self.read_html(url)     charset = nil     user_agent = {"User-Agent" => "Gihyo Bot"}     html = open(url, user_agent) do |f|       charset = f.charset       f.read     end     Nokogiri::HTML.parse(html, nil, charset)   rescue StandardError => e     p "Error: #{e}"   end   def self.scrape_content_page(dom)     ret_arr = []     dom.xpath(@@title_xpath).each do |dom_by_xpath|       title = get_title(dom_by_xpath)       url   = get_article_url(dom_by_xpath)       ret_arr << [title, url]     end     ret_arr   end   def self.get_title(dom)     dom.inner_text   end   def self.get_article_url(dom)     url = dom.attribute('href').value     URI.join(@@base_url, url).to_s   end   def self.get_next_url(dom)     next_url = dom.xpath(@@next_url_xpath)[0].attribute('href').value     return URI.join(@@base_url, next_url).to_s   rescue     return nil   end end GihyoCrawler.run | 
ファイル名を指定して
ruby
コマンドで実行できます。
| 1 | $ ruby gihyo_crawler.rb | 
使用したモジュール、Gem
対象ページを取得
open-uriのopenメソッドを使用し、対象URLを開いて取得します。
 その際、オプションとしてUser Agentが渡せるので、クローラーだと分かるような名前をつけてあげます。
 取得したページはNokogiriでHTMLとして解析して返却します。
| 1 2 3 4 5 6 7 8 9 10 11 | def self.read_html(url)   charset = nil   user_agent = {"User-Agent" => "Gihyo Bot"}   html = open(url, user_agent) do |f|     charset = f.charset     f.read   end   Nokogiri::HTML.parse(html, nil, charset) rescue StandardError => e   p "Error: #{e}" end | 
XPATHから目的のものを抜き出す
Google Chromeなどを使用すれば、対象のHTMLタグのXPATHを取得することができるので、そちらから目的のものを取得します。
 XPATHの説明は、下記の方の記事が分かりやすいかと思います。
同じ階層の、例えば
<div>
などは配列で返ってくるので、ループして必要なだけ抽出します。
 記事タイトル、リンク先それぞれ取得の方法が違うので、メソッドを用意してあげます。
| 1 2 3 4 5 6 7 8 9 | def self.scrape_content_page(dom)   ret_arr = []   dom.xpath(@@title_xpath).each do |dom_by_xpath|     title = get_title(dom_by_xpath)     url   = get_article_url(dom_by_xpath)     ret_arr << [title, url]   end   ret_arr end | 
次のページのリンクを取得する
はじめにクラス変数として、
@@page_num = 2
と与えています。
 技術評論社さんの記事ページはページングされており、過去の記事を見るためには次のページへ行く必要があります。
このとき、次のページボタンのXPATHを指定し、そのDOMのURLを取得すれば、同様の処理を繰り返すことで次のページの記事も取得できます。
 今回のサンプルでは、何ページ目まで取得するか、という意味で
@@page_num = 2
が与えられています。
 もしそれ以上過去の記事がなければ、リンクが取得できないはずなので、その場合は
break
します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | def self.run   # 省略     @@page_num.times do       dom = read_html(url)       scrape_content_page(dom).each do |title, url|         data = {"title": title, "url": url}         f.write(JSON.dump(data) + ",\n")       end       # 次のページへのリンクを取得       url = get_next_url(dom)       # もし取得できなければループを抜ける       break if url.nil?       # 連続してアクセスすると迷惑なので、次のループに入る前に一拍入れる       sleep(@@sleep_time)     end   # 省略 end | 
他のサイトの記事でも試してみる
このスクレイピングのコードですが、書籍で紹介されていた内容でかなり整っておりました。
 このコードを継承してしまえば、他のサイトだってだいたい取得できそうだったので試してみます。
継承する際、継承元の
GihyoCrawler.run
をコメントアウトか消しておかないと2回実行されてしまうので気をつけてください。
 (私だけかもしれません。。なんで2回実行されているんだ、とドツボにハマっていました。。)
本ブログ
手前味噌ですが、本ブログにも記事一覧ページがありますので、ここから取得してみます。
 確認しながら進めると、クラス変数の値を変えるだけでいけました。
| 1 2 3 4 5 6 7 8 9 10 | require_relative './gihyo_crawler' class ReEnginesCrawler < GihyoCrawler   @@base_url = 'https://re-engines.com/'   @@content_list_url = 'https://re-engines.com/'   @@title_xpath = '//*[@class="kanren-t"]/a'   @@next_url_xpath = '//*[@id="contentInner"]/div/article/div[1]/div[2]/a[4]' end ReEnginesCrawler.run | 
Qiita
Qiitaも取ってみます。
 Qiitaはログインしていないといわゆる新着ページにいけないので、ログインしていなくても確認できる、「Rubyの記事で最近いいねされた記事」(現在は、404 Not Foundになってしまっています)のページから取得します。
| 1 2 3 4 5 6 7 8 9 10 | require_relative './gihyo_crawler' class QiitaCrawler < GihyoCrawler   @@base_url = 'https://qiita.com'   @@content_list_url = 'https://qiita.com/tags/Ruby/likes'   @@title_xpath = '//div[@class="ItemLink__title"]/a'   @@next_url_xpath = '//*[@id="main"]/div/div/div[1]/section/div[2]/ul/li[8]/a' end QiitaCrawler.run | 
はてなブックマーク
はてなブックマークの新着からも取得してみます。
 はてなブックマークは新着記事の先頭が大きめの表示なので、ちょっとメソッドを変更しないといけないかな、と思いましたが、同じXPATHで取得できますね。
「次へ」のようなリンクがないので、ループ回数は1回にしています。
| 1 2 3 4 5 6 7 8 9 10 | require_relative './gihyo_crawler' class HatenaCrawler < GihyoCrawler   @@base_url = 'http://b.hatena.ne.jp/'   @@content_list_url = 'http://b.hatena.ne.jp/entrylist?sort=hot'   @@title_xpath = '//h3[@class="hb-entry-link-container"]/a'   @@page_num = 1 end HatenaCrawler.run | 
さいごに
スクレイピングの勉強がてら書き換えてみました。
 サンプル自体が非常に良くできていたので、色々なブログをスクレイピングする際もかなりスピーディにできます。
 やはり基底クラスは重要ですね。
Railsで素早くWebページを作成することは違うので、Ruby自体の勉強にオススメです。
 
  
 
 
 
 
