カテゴリー: BackEnd

【Ruby Advent Calender 2017】Rubyでスクレイピングをしてみる【11日目】

はじめに

Ruby Advent Calender 2017、11日目の記事になります。
今回は以前実施した、Rubyでのスクレイピングについて再度記載したいと思います。

Selenium WebDriverPhantomJSなどは使用しておりません。

概要

以前、技術評論社さんのデータサイエンティスト養成読本という本を読みました。
いろいろな技術の紹介があり、非常に参考になりました。

その中でスクレイピングに関するサンプルコードもありました。
今回はPythonで書かれていたスクレイピングのコードをRubyで書き直した記事になります。

なお、Pythonで書かれたサンプルの方は実際に書籍をご覧になってください。

仕様

技術評論社さんのサイトに行き、直近の記事タイトルとそのリンクを取得してJSONで吐き出します。

ソースコード

実際に書いたソースコードはこちらになります。
その後、各項目について記載したいと思います。

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コマンドで実行できます。

$ ruby gihyo_crawler.rb

使用したモジュール、Gem

対象ページを取得

open-uriのopenメソッドを使用し、対象URLを開いて取得します。
その際、オプションとしてUser Agentが渡せるので、クローラーだと分かるような名前をつけてあげます。
取得したページはNokogiriでHTMLとして解析して返却します。

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>などは配列で返ってくるので、ループして必要なだけ抽出します。
記事タイトル、リンク先それぞれ取得の方法が違うので、メソッドを用意してあげます。

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します。

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回実行されているんだ、とドツボにハマっていました。。)

本ブログ

手前味噌ですが、本ブログにも記事一覧ページがありますので、ここから取得してみます。
確認しながら進めると、クラス変数の値を変えるだけでいけました。

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になってしまっています)のページから取得します。

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回にしています。

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自体の勉強にオススメです。

naoki85

シェア
執筆者:
naoki85
タグ: RubyDialogflow

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前