hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

スクレイピング

映画のデータ

映画レビューサイトの実装をする前に、まずは映画レビューサイトに必要な映画のデータを用意する。映画レビューサイトではヘッダー部分からレビューを投稿することができる。

 

レビューを投稿するときにレビューをつける映画を選択する。

 

レビューはユーザーが生成する情報。では、この映画の情報はどこにあるのか?これはあらかじめ、アプリケーションのデータベースに映画の情報を入れておく。

プログラムで映画の情報を取得する

どうやってデータベースに映画の情報を用意するのか。1つ1つ自力でデータベースに映画のタイトルや画像の情報を入れるのは大変すぎるのでプログラムを使って映画の情報を自動で取得する。

そこで使うのがスクレイピングと呼ばれる技術。

スクレイピング

スクレイピングとは

スクレイピングとは、ウェブサイト上のHTMLからある特定のデータを抜き出す処理のことを言う。
外部のサーバーからデータを抽出し、集計をしたりするときに役立る。

 スクレイピング

例えば、以下のようなHTMLのサイトがあった場合

1
2
3
4
5
  <ul>
    <li>TEST1</li>
    <li>TEST2</li>
    <li>TEST3</li>
  </ul>

<ul><li>の中にある「TEST1、TEST2、TEST3」等の値を取り出すことを言う。

 

要点チェック

 

スクレイピングで映画情報を取得する

映画レビューサイトで使用するための映画情報をスクレイピングで取得する。

今回は映画の情報が欲しいので、MovieReviewという映画情報サイトから、スクレイピングで映画作品データを取得して「非公開」で利用します。

データの著作権があるため、外部公開はできないので要注意。

 

スクレイピングしてみる

では実際にスクレイピングでHTML内のデータを取得。スクレイピングにはMechanizeというGemが必要である。

 Mechanize

Mechanizeはスクレイピングを行うためのGem。MechanizeのGemを入れるとMechanizeクラスが使えるようになる。このMechanizeクラスにはスクレイピングをするための様々なメソッドが用意されている。

Mechanizeを入れる。MechanizeはGemなのでGemfileにMechanizeを使用するという記述を行う。

 GemfileにMechanizeを追記

Mechanizeという、スクレイピング用のGemを使用できるようにする。moooviディレクトリのGemfileの最終行に以下のように追記する。

Gemfile
1
2
3
#省略

gem 'mechanize'

 

Gemfileに記述されたGemをインストールするにはbundle installを実行する。以下のコマンドを実行して、先程moooviディレクトリのGemfileに追記したMechanizeをインストールする。

ターミナル
1
2
$ pwd  # moooviのディレクトリにいることを確認する
$ bundle install  # 追加されたGemのインストールをする(少し時間がかかる場合があります)

エラーなくbundle installが成功すると、以下のような「bundle complete!」という表示がされる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//7297bd80999cfbc01aa8c5db68bad459.png

要点チェック

 

Mechanizeクラスを使ってみる

Mechanizeを使ってスクレイピングをするには、まずMechanizeクラスのインスタンスを生成する。

1
  agent = Mechanize.new # Mechanizeクラスのインスタンスを生成

次にMechanizeクラスのインスタンスメソッドgetメソッドを使ってスクレイピングしたいウェブサイトのHTMLを取得する。

 

要点チェック

 

ウェブサイトのHTML情報を取得する

 getメソッド

getメソッドはMechanizeクラスのインスタンスメソッド。get("スクレイピングしたいウェブサイトのURL")と引数にURLの文字列を指定することで、そのURLのウェブサイトのHTMLを取得。

1
2
  agent = Mechanize.new
  page = agent.get("http://review-movie.herokuapp.com/") # MovieReviewのHTMLを取得

ここで取得できたpageは単なるHTMLの文字列ではなくそのウェブサイトのHTMLの情報を持ったMechanize::Pageオブジェクト。

1
2
3
4
  agent = Mechanize.new
  page = agent.get("http://review-movie.herokuapp.com/") # MovieReviewのHTMLを取得
  puts page
  # <Mechanize::Page:0x007fcdda803dd8>

pageがMechanize::Pageオブジェクトであることが確認できた。

 

要点チェック

 

HTML情報から指定のタグ要素の情報を検索

特定のhtmlタグ情報を取得するには searchメソッド を使う。

 searchメソッド

searchメソッドはgetメソッドで取得したページの情報が入ったオブジェクトに対して使用する。これを使うと取得したウェブサイトのHTML情報の中から指定したHTML要素の内容を検索できる。
該当するHTMLのタグ要素が1つでも、返り値は配列の形式で返ってくる。

searchメソッドは以下のような使い方をする。

1
   elements = Mechanize::Pageオブジェクト.search('セレクタ')

引数のセレクタとはCSSのようにHTMLのタグの要素名を指定する。'h1''li a'のような指定。また、クラス名でも.hogeのように指定することができる。

では、例としてMovieReviewのページからh2要素のHTMLの情報を取得してみる。

1
2
3
4
5
  agent = Mechanize.new
  page = agent.get("http://review-movie.herokuapp.com/")
  elements = page.search('h2')
 # h2要素を検索
  puts elements

これを実行すると以下のような出力を得られる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<h2 class="entry-title index-entry-title">
<a href="/products/1" title="Single Post">あのひと</a>
            </h2>
<h2 class="entry-title index-entry-title">
<a href="/products/2" title="Single Post">海よりもまだ深く</a>
            </h2>
<h2 class="entry-title index-entry-title">
<a href="/products/3" title="Single Post">ディストラクション・ベイビーズ</a>
            </h2>
#以下略

ちゃんと、MovieReviewのh2要素のHTMLの情報が取得できた。
次はh2要素の下のa要素のHTML情報を取得してみる。

1
2
3
4
  agent = Mechanize.new
  page = agent.get("http://review-movie.herokuapp.com/")
  elements = page.search('h2 a') # h2要素の下のa要素を検索
  puts elements

これを実行すると以下のような出力を得られる。

ターミナル
1
2
3
4
5
<a href="/products/1" title="Single Post">あのひと</a>
<a href="/products/2" title="Single Post">海よりもまだ深く</a>
<a href="/products/3" title="Single Post">ディストラクション・ベイビーズ</a>
<a href="/products/4" title="Single Post">少女椿</a>
# 以下略

このように、該当するHTMLのタグ要素全てが取得される。

例として、searchメソッドの返り値が本当に配列の形式になっていることを確認するために、以下のような例のコードを試してみる。

test_scraping.rb
1
2
3
4
5
6
  require 'mechanize'

  agent = Mechanize.new
  page = agent.get("http://review-movie.herokuapp.com/")
  elements = page.search('h2 a') # h2要素の下のa要素を検索
  puts elements[0]

これを実行すると以下のような出力を得られる。

ターミナル
1
<a href="/products/1" title="Single Post">あのひと</a>

このように、返ってきた値の1番目の要素をとりだすことができるので配列の形式で返ってきていることが分かる。

では、searchメソッドで指定した要素のHTMLからテキストやリンク先を取得する。

 

要点チェック

 

HTML情報からテキストを抜き出す

 inner_textメソッド

searchメソッドで得られたHTML情報のテキストを取得したい場合、inner_textメソッドを使う。

例えば、先ほどのsearchメソッドから取得した'h2 a'要素の一覧を見てみる。

1
2
3
4
 agent = Mechanize.new
  page = agent.get("http://review-movie.herokuapp.com/")
  elements = page.search('h2 a') # h2要素の下のa要素を検索
  puts elements

ターミナルの出力

ターミナル
1
2
3
4
5
<a href="/products/1" title="Single Post">あのひと</a>
<a href="/products/2" title="Single Post">海よりもまだ深く</a>
<a href="/products/3" title="Single Post">ディストラクション・ベイビーズ</a>
<a href="/products/4" title="Single Post">少女椿</a>
# 多すぎるので以下略

これらの要素のテキスト、すなわち「あのひと、海よりもまだ深くディストラクション・ベイビーズ...」が欲しい場合は以下のようにする

1
2
3
4
5
6
7
  agent = Mechanize.new
  page = agent.get("http://review-movie.herokuapp.com/")
  elements = page.search('h2 a') # h2要素の下のa要素を検索

  elements.each do |ele|
    puts ele.inner_text
  end

ターミナルの出力

ターミナル
1
2
3
4
5
6

ちゃんとHTML情報のテキストだけが抜き出された。searchメソッドでは該当する要素のHTML情報がすべて取得できるのでeachメソッドを使って、1つ1つのオブジェクトに対してinner_textメソッドを呼び出す。

 

要点チェック

 

HTML情報から属性の値を抜き出す

 get_attributeメソッド

aタグ要素のHTMLはリンク先のURLを値とする属性hrefを持っている。このようなHTMLの属性の値を取得したい場合、get_attributeメソッドを使う。get_attribute(属性)で指定した属性の値を取得できる。また、.get_attribute(属性)[:属性]と簡略化して書くこともできる。

ターミナル
1
2
3
4
5
<a href="/products/1" title="Single Post">あのひと</a>
<a href="/products/2" title="Single Post">海よりもまだ深く</a>
<a href="/products/3" title="Single Post">ディストラクション・ベイビーズ</a>
<a href="/products/4" title="Single Post">少女椿</a>
# 多すぎるので以下略

この中からhref="リンク先"の「リンク先」を抜き出す。HTML情報のオブジェクトにメソッドget_attribute('href')を使う。

1
2
3
4
5
6
7
  agent = Mechanize.new
  page = agent.get("http://review-movie.herokuapp.com/")
  elements = page.search('h2 a') # h2要素の下のa要素を検索

  elements.each do |ele|
    puts ele.get_attribute('href') # puts ele[:href]としても良い
  end

ターミナルの出力

ターミナル
1
2
3
4
5
/products/1
/products/2
/products/3
/products/4
# 多すぎるので以下略

ちゃんと属性hrefの値だけ抜き出すことができた。
このようにMechanizeクラスを使えばウェブサイト上のHTMLのデータを自由に取得することができる。

 

要点チェック

 

スクレイピングの手順まとめ

Mechanizeを使ったスクレイピングの手順。

  • Mechanizeクラスのインスタンスを生成する
  • Mechanizeクラスのインスタンスメソッドget(情報を取得したいウェブサイトのURL)で、ウェブサイトのHTML情報を取得する
  • 欲しいデータのあるタグ要素をsearchメソッドで指定して取得する
  • 取得したタグ要素のHTML情報にたいしてinner_textメソッド、またはget_attributeメソッドを使って欲しい値を取得する

実際にスクレイピングしてみる

Mechanizeを使って実際にスクレイピングをしてみる。ここでのスクレイピングの練習はRubyファイルを作成して行うので、スクレイピング用のRubyファイルscraping.rbデスクトップに作成する。

 デスクトップにスクレイピング用のRubyファイルscraping.rbを作成

テキストエディタを選択している状態で、

command + Nでファイルを新しく作成。

command + Sでファイルを保存。

③ファイル名を scraping.rb と編集し、保存先にデスクトップを選択する。

 

では作成したRubyファイルscraping.rbテキストエディタで開く。

Mechanizeクラスは外部のクラスなのでそのままでは使えない。よって、Rubyファイルの一番上でMechanizeクラスを使うことを宣言しなければならない。宣言にはrequireを使う。

 scraping.rbの一番上にMechanizeクラスを使うことを宣言する一行を追加する

scraping.rb
1
  require 'mechanize'

これでMechanizeクラスが使えるようになる。Mechanizeクラスのインスタンスを生成する。

scraping.rb
1
2
3
4
  require 'mechanize'

  agent = Mechanize.new
  puts agent # インスタンスを生成できたか確認

 ターミナルで実行

上のような編集ができたら、ターミナルで 以下のようにコマンドを実行する。

ターミナル
1
2
$ cd ~/Desktop # scraping.rbが置かれているデスクトップディレクトリに移動
$ ruby scraping.rb

以下の作業チェックに記載された結果が出力されるか確認する。

 

これでスクレイピングをする準備はできた。

スクレイピング練習

以下のスクレイピング用に用意したウェブページをスクレイピングしてみる。

https://app-mooovi.herokuapp.com/works/initial_scraping

「TEST1, TEST2, TEST3」と書いてあるだけ。この「TEST1, TEST2, TEST3」というテキストを取得する。まずはこのページのHTML構造を確認する。

 実際のHTMLを確認してみる

https://app-mooovi.herokuapp.com/works/initial_scraping

上記のサイトにアクセスして、[command] + [option] + [u]を押してソースコードを表示させる。

スクレイピング

“TEST1”, “TEST2”, “TEST3” は、pタグに囲まれていることがわかる。このようにスクレイピングをするときはスクレイピングしたいウェブページのHTML構造を確認することが大切。

 

では、このウェブページからスクレイピングで「TEST1, TEST2, TEST3」を抜き出すプログラムをscraping.rbに書く。
まずはgetメソッドでウェブページのHTMLの情報を取得する。

 getメソッドを使ってスクレイピング用ウェブページのHTML情報を取得する

scraping.rb
1
2
3
4
  require 'mechanize'

  agent = Mechanize.new
  page = agent.get("https://app-mooovi.herokuapp.com/works/initial_scraping")

HTML構造を確認すると「TEST1, TEST2, TEST3」はそれぞれpタグ要素に囲まれている。searchメソッドでpタグ要素を取得する。

scraping.rb
1
2
3
4
5
6
  require 'mechanize'

  agent = Mechanize.new
  page = agent.get("https://app-mooovi.herokuapp.com/works/initial_scraping")
  elements = page.search('p')
  puts elements

ここまで書けたら、下記のコマンドでscraping.rbを実行。

1
  $ ruby scraping.rb #scraping.rbを実行

ターミナルには以下のように表示される。

1
2
3
<p>TEST1</p>
<p>TEST2</p>
<p>TEST3</p>

ちゃんとすべてのpタグ要素が取得できている。あとはinner_textメソッドでテキストを取得する。

scraping.rb
1
2
3
4
5
6
7
8
9
  require 'mechanize'

  agent = Mechanize.new
  page = agent.get("https://app-mooovi.herokuapp.com/works/initial_scraping")
  elements = page.search('p')

  elements.each do |ele|
    puts ele.inner_text
  end

pタグ要素は複数あるのでeachメソッドを使ってオブジェクト一つ一つにたいしてinner_textメソッドを使う。このプログラムを実行すると以下のようにターミナルに表示される。

1
2
3
TEST1
TEST2
TEST3

テキスト「TEST1, TEST2, TEST3」が取得できた。

 

 

MovieReviewからのスクレイピング

MovieReviewから映画のデータを取得する。ページへは以下のリンクから遷移できる。

http://review-movie.herokuapp.com/

このページには20件の映画情報が載っている。まずはこのページにある20件の映画情報をスクレイピングで取得する。

作品名、作品画像のURLを取得

映画の情報と言っても作品名、監督名、公開日など様々なものがあるが、とりあえず今回は最低限の情報として「作品名」「作品画像のURL」を取得する。

映画情報のスクレイピング用のRubyファイルを作る

映画の「作品名」と「作品画像のURL」を取得するスクレイピングのプログラムを書くためのRubyファイルを作成。作品名のスクレイピング用のRubyファイルscraping_title.rb 、作品画像のスクレイピング用のRubyファイルscraping_image.rb 、をデスクトップに作成。

 

ウェブサイトのHTML構造を確認

スクレイピングするウェブサイトのHTML構造を調べる。

 スクレイピングするページにウェブブラウザでアクセスして、HTMLを確認

http://review-movie.herokuapp.com/

 作品名のHTML構造を確認

開発者ツールを使えば、HTMLが見やすい

  1. 作品名や作品画像を右クリック
  2. [要素の検証]をクリックして開発者ツールを立ち上げる
  3. HTMLが見やすく整形されて表示される 

  

作品名を取得①

開発者ツールを利用して作品名がある要素のHTML構造を確認。

 Chromeの開発者ツールを使って下記のようなHTML構造を確認

HTMLを確認してみると、作品名はどこに存在するかというのがわかると。

https://tech-master.s3.amazonaws.com/uploads/curriculums//6477b658f8fde2a7fe3a13311560bf52.png

あとは、このHTML構造にたいしてスクレイピングのプログラムを書くだけ。作業ファイルはscraping_title.rb

 問題4:MovieReviewに表示されている映画20件の「作品名」を取得する

作業ファイル:scraping_title.rb
ヒント:開発者ツールで確認したHTMLのタグ要素をsearchメソッドの引数にする

 

作品画像のURLを取得

画像に対して開発者ツールの要素の検証をして、HTMLの構造を確認する。作業ファイルはscraping_image.rb

 

では、作品名と同じように表示されている20件の映画の作品画像URLをスクレイピングで取得。

 問題5:MovieReviewに表示されている映画20件の「作品画像のURL」を取得する

作業ファイル:scraping_image.rb
ヒント:取得するのはテキストでなく属性の値

作品画像のURLを取得②

試しに取得できた画像を開いてみる。
画像が小さい。あとカバー写真というよりは映画のワンシーンのような画像。これでは表示させるとき少し物足りない。

そこでちゃんとした作品画像を取り直す。MovieReviewでどれかの映画タイトルをクリックしてみる。その映画の個別ページに移動した。そこに良さそうな画像がある。

 

個別ページの作品画像のURLを取得

まずMovieReviewに表示されている映画ならどれでもいいので個別ページに移動する。

移動できたらそのページの作品画像URLを取得してみる。作業ファイルはscraping_image.rb。先ほど書いたソースコードは使わないので消してしまう。

 問題6:映画の個別ページに表示されている映画の「作品画像のURL」を取得する

作業ファイル:scraping_image.rb
ヒント:リンクは各自開いた映画の個別ページのものを使う 

 

20件分すべての映画の個別ページから作品画像のURLを取得

20件すべての映画の個別ページから作品画像のURLを取得。もちろんすべてプログラムで自動で取得。

以下のような流れでスクレイピングする。

  1. MovieReviewで表示されている映画の個別ページのリンクを取得する
  2. 取得した個別ページのリンク1つ1つに対してスクレイピングをする

以下のようにスクレイピングのプログラムを書く。

scraping_image.rb
10
11
12
13
14
15
16
17
18
19
  require 'mechanize'

  links = [] # 個別ページのリンクを保存する配列
  agent = Mechanize.new
  current_page = agent.get("http://review-movie.herokuapp.com/")
  # 個別ページのリンクを取得

  links.each do |link|
    # 個別ページから作品画像のURLを取得
  end

 問題7:MovieReviewにある20件の映画すべての個別ページから作品画像のURLを取得する

作業ファイル:scraping_image.rb
ヒント1:映画で表示されている映画の個別ページのリンクを取得する
ヒント2:取得した個別ページのリンク1つ1つに対してスクレイピングをする 

データベースに映画のデータを保存

これで映画レビューサイトで必要な映画の情報「作品名」と「作品画像」がスクレイピングで取得できた。あとは得たこの情報をデータベースに入れるだけ。 ここからは映画レビューアプリケーションのディレクトmoooviで作業する。

映画情報のモデルを作成

映画の情報を扱うモデルとしてProductモデルを作成する。このProductモデルに対応するproductsテーブルは以下のカラムを持っている。

カラム名 情報
title String 作品名
image_url Text 作品画像のURL

 

Productモデルを作成し、上記のカラムを持ったテーブルの作成する。

 問題8:映画レビューアプリケーションmoooviに指定したカラムを持つproductテーブルを作成しましょう

作業ディレクトリ:mooovi
ヒント:特になし 

 

映画情報をデータベースに保存するメソッドを書く

では、今まで書いてきたスクレイピングのメソッドを利用して「MovieReview」から映画情報を取得し、データベースに保存するメソッドを書く。

先ほどまではDesktopにRubyファイルを作って実行していますが、今回は取得した情報をデータベースへ保存する必要があるのでmoooviディレクトリの中にファイルを作成し、ソースコードを書いていく。

 

mooovi > app > models 内にscraping.rbというファイルを作成。

この時、 テキストエディタ上から直接ファイルを作成。

①「mooovi」>「app」>「models」内にscraping.rbを作成。

②「scraping.rb」というファイル名で保存する

その後、以下のようなディレクトリ構造になっていることを確認する。

  •  app
    •  models
      •  scraping.rb

記述するメソッドと実行方法

 スクレイピングのためのコードを記述する

先ほど作成したscraping.rbというファイルにスクレイピング関連のメソッドを記述していく。

scraping.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Scraping
  def self.movie_urls
    puts 'get movies link URL'
    # ここに処理を書く
  end

  def self.get_product(link)
    puts 'get movie information'
    # ここに処理を書く
  end
end

クラスメソッドmovie_urlsget_productがある。この中に具体的な処理を書いていく。

このクラスメソッドmovie_urlsは、rails cをしている状態で実行することができる。

まずrails cコマンドを打ち、クラスメソッドmovie_urlsを実行してみる。

ターミナル
1
2
3
4
$ rails c #コンソールの立ち上げ
pry(main)> Scraping.movie_urls #Scrapingクラスのクラスメソッド movie_urls の実行
get movies link URL
=> nil

"get movies link URL"と表示されればクラスメソッドmovie_urlsは正常に実行できている。

クラスメソッドmovie_urls

scraping.rbに記述されているScrapingクラスのクラスメソッドmovie_urlsは表示されている20件分の映画の個別ページのリンクURLを取得して、そのリンクをクラスメソッドget_productへ渡す処理をする。

scraping.rb
1
2
3
4
  def self.movie_urls
    # 映画の個別ページのURLを取得
    # get_product(link)を呼び出す
  end

クラスメソッドget_product

scraping.rbに記述されているScrapingクラスのクラスメソッドget_productは引数として渡された個別ページのリンクURLを使って「作品名」と「作品画像のURL」をスクレイピングし、それらをproductsテーブルに保存する処理を書く。

scraping.rb
1
2
3
4
  def self.get_product(link)
    # 「作品名」と「作品画像のURL」をスクレイピング
    # スクレイピングした「作品名」と「作品画像のURL」をProductsテーブルに保存
  end

 puts 'get movies link URL'puts 'get movie information'は消して良い。

映画情報をデータベースに保存

今まで書いたスクレイピングRubyファイル、scraping_title.rbscraping_image.rbを用いて映画の「作品名」と「作品画像のURL」を取得し、productsテーブルにその情報を保存する処理をscraping.rbに書く。

 reload!

コンソール起動中にコードを修正・追記した際、コンソールはリアルタイムで変更内容を反映してくれない。
その際は通常、一度exitして再度 rails c コマンドを打ちコンソールを再起動するという手順を踏むが、 reload! コマンドを実行すると毎回exitせずともコンソールがコードの変更内容を読み込んでくれるので大変便利。

今後コンソールで何かを実行する前には reload! コマンドの実行を忘れないようにする。
以下では、先ほど実行したScrapingクラスのクラスメソッド movie_urls を例にとって実際の動作を示している。

ターミナル
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  [1] pry(main)> Scraping.movie_urls
  get movies link URL
  #=> nil

  ## movie_urlsメソッド内の文字列「get movies link URL」を「リロードのテストです」に変更

  #reload!をかけずに再度実行した場合は変更内容が反映されない
  [2] pry(main)> Scraping.movie_urls
  get movies link URL
  #=> nil

  #reload!をかけてから再度実行すると編集内容が反映される
  [3] pry(main)> reload!
  Reloading...
  #=> true

  [4] pry(main)> Scraping.movie_urls
  # リロードのテストです
  #=> nil

 問題9:ProductsテーブルにMovieReviewから取得した映画20件の情報を保存する

scraping.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    class Scraping
      def self.movie_urls
        #①linksという配列の空枠を作る
        #②Mechanizeクラスのインスタンスを生成する
        #③映画の全体ページのURLを取得
        #④全体ページから映画20件の個別URLのタグを取得
        #⑤個別URLのタグからhref要素を取り出し、links配列に格納する
        #⑥get_productを実行する際にリンクを引数として渡す
      end

      def self.get_product(link)
        #⑦Mechanizeクラスのインスタンスを生成する
        #⑧映画の個別ページのURLを取得
        #⑨inner_textメソッドを利用し映画のタイトルを取得
        #①⓪image_urlがあるsrc要素のみを取り出す
        #①①newメソッド、saveメソッドを使い、 スクレイピングした「映画タイトル」と「作品画像のURL」をproductsテーブルに保存
      end
    end

 

さらに映画の情報を充実させる

スクレイピングのメソッドを拡張してより充実した映画の情報を取得できるようする。

同じ映画の情報はデータベースに新規で保存できないように

いまスクレイピングのメソッドmovie_urlsを2回実行すると同じ映画が2つずつ保存されてしまう。つまり、メソッドが実行されるたびに同じ映画がデータベース上に増えていってしまう。これではまずいので同じタイトルの映画情報はデータベースに保存しないように注意する。

 first_or_initializeメソッド

whereメソッドとともに使うことで、whereで検索した条件のレコードがあればそのレコードのインスタンスを返し、なければ新しくインスタンスを作るメソッド。

  • ニックネームが"Shinbo"のユーザーがすでに保存されていれば取得し、保存されていなければ生成
1
  user = User.where(nickname: "Shinbo").first_or_initialize

【例-1】

  • すでにニックネームが"Shinbo"のユーザーが保存されている場合
1
2
  user = User.where(nickname: "Shinbo").first_or_initialize
  => #<User id: 1, nickname: "Shinbo">

すでにデータベースのUsersテーブルにニックネームが"Shinbo"のレコードが保存されている場合、そのレコードのインスタンスが取得される。

【例-2】

  • まだニックネームが"Shinbo"のユーザーが保存されていない場合
1
2
3
4
  user = User.where(nickname: "Shinbo").first_or_initialize
  => #<User id: nil, nickname: "Shinbo">
  p user.nickname
  => "Shinbo" # "Shinbo"がnicknameカラムに代入されている

ニックネームが"Shinbo"のレコードが無い場合は新しくUserモデルのインスタンスが生成されます。生成されたインスタンス検索で使われた"Shinbo"がnicknameに代入された状態となっている。

 first_or_initializeではnewメソッドと同様、インスタンスを生成しただけでデータベースに保存されていない。saveメソッドでデータベースに保存するのを忘れないように。

1
2
  user = User.where(nickname: "Shinbo").first_or_initialize
  user.save

 問題10:同じ映画の情報がデータベースにすでに保存されていたら、新規にインスタンスを作らずに既に保存されているインスタンスを更新するようにする。

作業ファイル:app/models/scraping.rb
ヒント:productインスタンスを生成しているコードを書き換える。
scraping.rb
1
    product = Product.where(検索されるカラム名: 検索する値).first_or_initialize

MovieReviewですべてのページから映画の情報を取得できるようにする

いまのスクレイピングのメソッドはMovieReviewの1ページ目の20件の映画しか取得していない。そこでこれから、2ページ目以降の映画の情報もすべて取得するスクレイピングを書いていく。

MovieReviewはページネーションを利用している。各ページの下に次のページへのリンクがある。これを使えば次のページのURLリンクをたどることができる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//309e3a32a5bc7c2dbf95d348f5642744.png

次のページへのリンク先がなくなったときにスクレイピングを終了させる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//5ab28ff6b8579f8cebb230aedfb77bd2.png

 問題11:MovieReviewの全てのページにある映画のタイトル、映画の画像を取得している。

作業ファイル:app/models/scraping.rb
scraping.rb
 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
 class Scraping
  def self.movie_urls
    agent = Mechanize.new
    links = []

    # パスの部分を変数で定義(はじめは、空にしておきます)
    next_url = ""

    while true do

      current_page = agent.get("http://review-movie.herokuapp.com/" + next_url)
      elements = current_page.search(".entry-title a")
      elements.each do |ele|
        links << ele.get_attribute('href')
      end

      # 「次へ」を表すタグを取得
      next_link = ???

      # next_linkがなかったらwhile文を抜ける
      unless ???
        ???
      end

      # そのタグからhref属性の値を取得
      next_url = ???


    end

    links.each do |link|
      get_product('http://review-movie.herokuapp.com' + link)
    end
  end
end

Productsテーブルに「監督名」、「あらすじ」、「公開日」のカラムを追加

映画の情報は「監督名」や「あらすじ」、「公開日」など様々なものがある。スクレイピングするならこれらの情報もまとめて取得しておきたいところ。

カラム名 情報
director string 監督名
detail text あらすじ
open_date string 公開日

 

 rails generate migrationコマンド

テーブルにカラムを追加したり、削除するようなテーブルの構造を変えたいときもマイグレーションファイルを作成。作成にはrails generate migrationコマンドを使用する。このコマンドはマイグレーションファイルを新しく作るためのもの。

ターミナル
1
2
  $ bundle exec rails g migration AddRateToProducts
  # => マイグレーションファイルの作成

rails generate migrationに続けてクラス名を指定する。このコマンドで指定したクラス名のマイグレーションファイルが作成される。また generate は g と省略することができる。「db > migrate」に20141027115020_add_rate_to_products.rbというファイルが作成された。中身は以下のようになっている。

20141027115020_add_rate_to_products.rb
1
2
3
4
5
  class AddRateToProducts < ActiveRecord::Migration[5.2]
  def change

  end
end

メソッドchangeの中にテーブル構造の変更を書く。

例えば、BooksテーブルInteger型のカラムpriceを追加するためには以下の様なマイグレーションファイルを作成する必要がある。

20141027115020_add_price_to_books.rb
1
2
3
4
5
  class AddPriceToBooks < ActiveRecord::Migration[5.2]
  def change
    add_column :books, :price, :integer
  end
end

 add_column

add_columnマイグレーションファイルのchangeメソッド内に書くとカラムの追加ができる。記述方法は以下。

1
  add_column :テーブル名, :カラム名, :カラムの型

複数のカラムを追加する場合は以下のよう改行をして続けて記述する。

1
2
  add_column :テーブル名, :カラム名, :カラムの型
  add_column :テーブル名, :カラム名, :カラムの型

 remove_column

マイグレーションファイルのchangeメソッドではカラムを追加だけでなく削除もできる。remove_columnマイグレーションファイルのchangeメソッド内に書くとカラムの削除ができる。記述方法は以下。

1
  remove_column :テーブル名, :カラム名, :カラムの型

マイグレーションファイルに書いたテーブルの変更内容をテーブルに反映させるにはターミナルでrake db:migrateコマンドを実行する。

マイグレーションの実行

ターミナル
1
2
3
4
$ bundle exec rake db:migrate
== AddPriceToBooks: migrating ==========================
-- add_column(:books, :price, :integer)
   -> 0.0500s

上の例ではマイグレーションファイルAddPriceToBooksが実行され、booksテーブル内にinteger型のpriceカラムが追加された。

このようにテーブルの変更を行うときは必ずマイグレーションファイルを作成。また、分からなくなった際はrails generate migrationコマンドを振り返る。


 Sequel Proのようなデータベースを操作できるアプリケーションを使うとそのアプリケーションからカラムの追加や削除ができる。しかし、アプリケーションから操作してしまうと複数人で開発している場合、開発者ごとにデータベースのテーブル構造が異なるという問題が起きてしまう。そのため、テーブル構造を変えるときは必ずマイグレーションファイルを作成し、マイグレーションを実行することでテーブル構造を操作するようにする。


 作業内容

  • Productsテーブルに以下のカラムを追加する
カラム名 情報
director string 監督名
detail text あらすじ
open_date string 公開日

 編集するファイル

 ヒント

 Sequel Proでカラムが追加されたか確認

きちんと追加されていれば、新たに3つのカラムがproductsテーブルの中に作成されている。

https://tech-master.s3.amazonaws.com/uploads/curriculums//74e07bab0cb02c0788714a1ce0ee6f2e.png

 

映画の個別ページから「監督名」、「あらすじ」、「公開日」を取得してデータベースに保存

カラムを追加したら、それらのデータを実際にデータベースに保存したい。MovieReviewで映画の個別ページには「監督名」、「あらすじ」、「公開日」が載っています。これらをスクレイピングで取得し、Productsテーブルに保存する。

これらの値を取得する際に注意しなくてはならないことは、作品によっては監督名等が記載されていない場合があるということ。この時、searchメソッドあるいはatメソッドで取得したタグがnilになっている恐れがある。これに対してget_attributeメソッドinner_textメソッドを使うとNoMethodErrorとなってしまう。これを回避する必要がある。

さて、すでに取得してある映画情報にカラムを追加しなくてはいならない。first_or_initializeメソッドを使えば条件に当てはまるレコードがテーブルに保存されていればそのレコードのインスタンスを取得できる。

取得したインスタンスのカラムを更新するにはインスタンス.カラム名 = 値という記述方法を使う。

1
2
3
4
5
  product = Product.where(条件).first_or_initialize
  # カラムの更新
  product.director = 監督名
  # 中略
  product.save

最後にsaveするのを忘れないようにする。

 作業内容

  • 映画の個別ページから「監督名」、「あらすじ」、「公開日」を取得する
  • 取得した「監督名」、「あらすじ」、「公開日」の情報をインスタンスに更新する
  • 実際にデータベースに保存する

 編集するファイル

  • app/models/scraping.rb
  • ターミナル

 ヒント

  • 映画の個別ページで作品画像のURLを取得するときに、一緒にこれらの情報も取得
  • カラムの更新には「インスタンス.カラム名 = 値」の式を使う
  • コードが書けたらコンソールでScraping.movie_urlsを実行

Sequel Proのproductsテーブルを確認して、データが保存されているか確認する

きちんと情報が保存されていれば、以下のように表示される。

https://tech-master.s3.amazonaws.com/uploads/curriculums//90ed69c1744e34c197fdcb6fab696d81.png

 

以下が完成の一例。他の記述の方法もありますので参考まで。

scraping.rb
 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
 class Scraping
  def self.movie_urls
    agent = Mechanize.new
    links = []
    next_url = ""

    while true
      current_page = agent.get("http://review-movie.herokuapp.com/" + next_url)
      elements = current_page.search('.entry-title a')
      elements.each do |ele|
        links << ele.get_attribute('href')
      end

      next_link = current_page.at('.pagination .next a')
      break unless next_link
      next_url = next_link.get_attribute('href')

    end
    links.each do |link|
      get_product('http://review-movie.herokuapp.com' + link)
    end
  end

  def self.get_product(link)
    agent = Mechanize.new
    page = agent.get(link)
    title = page.at('.entry-title').inner_text if page.at('.entry-title')

    image_url = page.at('.entry-content img')[:src] if page.at('.entry-content img')
    director = page.at('.director span').inner_text if page.at('.director span')
    detail = page.at('.entry-content p').inner_text if page.at('.entry-content p')
    open_date = page.at('.date span').inner_text if page.at('.date span')

    product = Product.where(title: title).first_or_initialize
    product.image_url = image_url
    product.director = director
    product.detail = detail
    product.open_date = open_date
    product.save
  end
end

ここでコンソールでScraping.movie_urlsを実行すると下の画像のようにデータを全て表示することなく途中で止まるということがあるが、「q」を押すと入力が可能になる。エラーではない。

https://tech-master.s3.amazonaws.com/uploads/curriculums//1905013f75dfa971182b6a91d919fa97.png