データが無いけどデータサイエンスごっこしたくて、Ruby+Capybaraでスクレイピングしてみた話

# 動機
試したいアルゴリズムや便利そうなAPIがあって
手軽にデータサイエンスごっこできそうな風潮があるのに、手元にデータが無い。
疑似データの生成はもうやったし、なんかもうちょっと実際にあるもので行きたい。
おっ！そう言えばこの箱、ウェブにつながっていたな？

ちょうどスクレイピングに詳しい方が参加するもくもく会が催されるということで、
喜び勇んで参加させていただき、スクレイピングの勉強を始めた話。

# スクレイピングってなんだ
取得したHTMLなどから、必要なデータを抽出する。

## 取得？大丈夫なのか？
Webスクレイピングの注意事項一覧
http://qiita.com/nezuq/items/c5e827e1827e7cb29011
や
Webスクレイピングの法律周りの話をしよう！
http://qiita.com/nezuq/items/3cc9772118ad112c18dc
データサイエンスごっこということで解析目的で適度にやる。
アクセスも1秒以上間隔を開けてのんびりやる。

とは言えややこしいことになるのはよくないので、
今回のコードには対象としたURLは書いていない。

# 実装
## 環境の準備
＠arao99 さんの https://github.com/arao99/scraping
を参考にさせていただき、
JSで生成されるコンテンツに詰まってCapybaraまで手を出した。

macOS Sierra 10.12.1
rbenv（ruby 2.2.5）
bundler
Nokogiri/Capybara+PhantomJS
MySQL
Sequel Pro
今回の対象はとある馬関連の掲示板からコメントを拾ってみる。
（対象URLは消してあります。）

$ mysql.server start

## ちょっとだけ説明がついたコード
* 生のコード（対象URLは消してあります。）

https://github.com/EnsekiTT/scraping_uma
ここでは、データベースの作成については省いた。
構造についてはGithubをご覧いただけたらと思う。

### 馬の名前とIDをSQLに突っ込むスクリプト
``` uma.rb
require './database_uma.rb'
require 'open-uri'
require 'nokogiri'
require 'date'
class Uma < ActiveRecord::Base
end

def get_uma_list(url)
  puts url
  # アクセスするところ
  html = open(url){|f|
    f.read
  }

  # Nokogiriでパースする
  doc = Nokogiri::HTML.parse(html, nil, 'euc-jp')
  umas = doc.css('.db_data_list')

  # Databaseに入れる
  umas.css('tr').each{|uma|
    # 実際にパースするところ
    name = uma.css('a').text
    uri = uma.css('a').first[:href]
    # uriにUmaのIDが含まれているので、正規表現で引っこ抜く
    id_str = uri.match(/\/horse\/(.+)\//)
    uma_id = id_str[1]
    rate = uma.css('strong').text.to_f

    # UmaのIDと名前と詳細URLとレートを突っ込んでみた。
    line = {
      :uma_id => uma_id,
      :name => name,
      :uri => uri,
      :rate => rate,
    }
    # 被ったUma_idがなければデータ生成
    if Uma.find_by(uma_id: uma_id) == nil
      Uma.create(line)
    end
  }
  # 再アクセス待ち
  sleep 1
end

# ランキングサイトからuma_idとURLを取得するループ
range = 1..100
range.each{|page|
  # URLの生成
  url = "とても詳しいUmaのページのUmaランキング" + page.to_s
  get_uma_list(url=url)
}
```
MySQLにドバドバとデータが入ってきたら成功。

### 掲示板にアクセスしてJSで発行されるコメントを拾いSQLに突っ込むスクリプト
``` uma_comment.rb
require './database_uma.rb'
require 'open-uri'
require 'nokogiri'
require 'date'
require 'bundler/setup'
require 'capybara/poltergeist'
Bundler.require

class Comment < ActiveRecord::Base
end
class Uma < ActiveRecord::Base
end

def get_comment_uri(uma_id)
  # コメントページの基本的なURL（uma_idごとに掲示板があった）
  url = "とても詳しい馬のページの掲示板" + uma_id

  # Capybaraのセットアップ
  # JSでデータを受信して、クライアントサイドで掲示板を生成していたので
  # Open-uriではなくCapybaraで訪問することに
  Capybara.register_driver :poltergeist do |app|
    Capybara::Poltergeist::Driver.new(app, {:js_errors => false, :timeout => 1000 })
  end
  session = Capybara::Session.new(:poltergeist)

  # このページではGET QUERYで掲示板のページ数を管理していたので、
  # コメントページを何ページ分まで取るか指定する。
  comment_range = 1..15
  comment_range.each{|page|
    # コメントページのURLを生成する
    comment_url = url + '&page=' + page.to_s
    puts comment_url

    # Capybaraのセッションでサイトに訪れる
    session.visit comment_url
    if session.status_code == 200
      # 各コメントごとにデータベースに突っ込む
      session.find('#Comment_List').all('li.border_bottom').each do |single_com|
        id_str = single_com.find('div:nth-child(1) > div:nth-child(1)').text
        id = id_str.match(/\[(.+)\]/)
        comment_id = uma_id.to_s + id[1].to_s
        comment = single_com.find('.comment').text
        # UmaのIDとコメントのIDとコメントを入れてみた。
        # 他のメタ情報もあってもいいけど、後はベクトル化しか無いので今回はこれで
        line = {
          :uma_id => uma_id,
          :comment_id => comment_id,
          :comment => comment,
        }

        # コメントIDでかぶってないことを確認したらデータを追加
        if Comment.find_by(comment_id: comment_id) == nil
          Comment.create(line)
        end
      end
    end
    # 再アクセス待ち
    sleep 1
  }
  # 再アクセス待ち（どちらかでいいと思うけどあるに越したことはない。）
  sleep 1
end

# さっきデータベースに突っ込んだUmaのuma_idを拾っては、コメントを取得するループ
uma_list = Uma.all
uma_list.each{|uma|
  get_comment_uri(uma_id=uma.uma_id)
}
```
こっちもMySQLにドバドバとデータが入ってきたら成功。

### SQLにため込んだコメントをテキストで書き出すスクリプト
テキストで書き出すのは後でJUMAN++にかけてみようと思っているからで、それについては別の記事で。（ちからつきた。

``` uma2txt.rb
require './database_uma.rb'

class Comment < ActiveRecord::Base
end

file = File.open('./uma.txt', 'a')

coms = Comment.all
coms.each{|com|
  file.puts com.comment
}
file.close
```

の3本建てで、順番に実行。
以上！少し遊べるデータセットができました。

クリエイティブ・コモンズ 表示-継承ライセンス