【4回目(後編)】Railsに挫折中の人が、Ruby/Sinatraから再入門してみた(全7回)

5/11より毎週土曜日、株式会社X-HACK主催の勉強会、
【全7回】Ruby / Vue.js「ゼロから」ウェブサービスを作る【初心者向け | 個別指導あり】
に参加しています。
学んだことをこちらでアウトプットしていきます!

今回は4回目!後編です!!
後編:スクレイピングデータをDBに保存〜procとyieldについて!

前編はこちら!(掲示板を作ってみよう!〜関数・モジュール化まで!

間違っている所などあれば教えていただけると嬉しいです!

スクレイピングしたデータをDBに保存しよう

前編で作成した「day4-sinatra」をそのまま使用します!

⭐︎テーブルを作ろう

スクレイピングしたデータを保存するためのテーブルを作ります。
ここは、前編の復習です!

・「create_scraping.rb」を作成

【ターミナル】

$ cd day4-sinatra
$ touch create_scraping.rb

・「create_scraping.rb」を編集
前回作成した「Mydatabase」モジュールも使用しています!便利!

【day4-sinatra/create_scraping.rb】

require 'pg'
require 'dotenv/load'
require './mydatabase'

sql = "CREATE TABLE \"scraping_titles\" (
   \"id\" serial,
   \"title\" text,
   PRIMARY KEY (\"id\")
);"
Mydatabase.exec(sql)

・「create_scraping.rb」を実行し、「scraping_titles」テーブルを作成

【ターミナル】

$ bundle exec ruby create_scraping.rb 

⭐︎スクレイピングしよう!

まずはQiitaにて、「ruby」でキーワード検索+「ストック数順」で表示された記事のタイトル(画像赤枠内)をスクレイピング!

・"Nokogiri"というgemを導入
Nokogiriとは?
→Rubyのスクレイピングでよく使われるGem。
 XpathやCSSセレクタを使って要素の抽出を行うことができる。
gem "nokogiri" を追記。

【day4-sinatra/Gemfile】

gem "nokogiri"

・スクレイピング処理を行うscraping.rbファイルを作成

【ターミナル】

$ touch scraping.rb

・要素を抽出(Google Chromeデベロッパーツール[F12]を使用)

・スクレイピング処理を記述
 先程コピーした要素を貼り付ける!

【day4-sinatra/scraping.rb】

require 'nokogiri'
require 'open-uri' # URLを読み込むための標準ライブラリ

# scrapingモジュールを作成
module Myscraping
 def self.load_url_data_qiita_ruby(url)
   charset = nil

   # htmlの読み込み
   html = open(url) do |f|
   sleep(2)  # サイトに負荷をかけすぎると怒られるので、間隔を設定
     charset = f.charset
     f.read
   end

  # htmlをパース(解析)してオブジェクトを生成
   doc = Nokogiri::HTML.parse(html, nil, charset)

  # class内のデータを全て読み込む
  # コピーした要素を貼り付ける
   doc.css('.searchResult_main').each do |node|
     # 更にタイトルのテキストデータを読み込む
     puts node.css('.searchResult_itemTitle a').inner_html
   end
 end
end

# QiitaのURLを渡す
Myscraping.load_url_data_qiita_ruby('https://qiita.com/search?q=ruby&sort=stock')

・実行するとタイトルが抽出される!

【ターミナル】

$  ruby scraping.rb 
Markdown記法 チートシート
ペアプログラミングして気がついた新人プログラマの成長を阻害する悪習
非デザイナーエンジニアが一人でWebサービスを作るときに便利なツール32選
プログラミングでよく使う英単語のまとめ【随時更新】
【まとめ】これ知らないプログラマって損してんなって思う汎用的なツール 100超
新人プログラマに知っておいてもらいたい人類がオブジェクト指向を手に入れるまでの軌跡
もう保守されない画面遷移図は嫌なので、UI Flow図を簡単にマークダウンぽく書くエディタ作った
翻訳: WebAPI 設計のベストプラクティス
Pythonを書き始める前に見るべきTips
エンジニアの情報収集法まとめ

・DBに登録する処理を追記

【day4-sinatra/scraping.rb】

require 'nokogiri'
require 'open-uri'
require './mydatabase.rb'

module Myscraping
 def self.load_url_data_qiita_ruby(url)
   charset = nil
   html = open(url) do |f|
     sleep(2)
     charset = f.charset
     f.read
   end
  
   doc = Nokogiri::HTML.parse(html, nil, charset)
   doc.css('.searchResult_main').each do |node|
     # 1.抽出したデータを変数に格納
     title = node.css('.searchResult_itemTitle a').inner_html
   # 2.SQL文に埋め込む
     sql = "INSERT INTO scraping_titles (title) VALUES ('#{title}');"
   # 3.DB実行
     Mydatabase.exec(sql)
   end
 end
end

# QiitaのURLを渡す
Myscraping.load_url_data_qiita_ruby('https://qiita.com/search?q=ruby&sort=stock')

無事スクレイピングしたデータをDBに登録できました!!\(^o^)/

Proc、yieldを使ってみよう!

Proc、yieldについての説明をするために、
まずスターバックスさんの商品情報をスクレイピングしてみます。

・scraping.rbに下記を追加します!
→手順は先程と同じです

【day4-sinatra/scraping.rb】

module Myscraping

 (省略)

 # スタバ用の関数を定義
 def self.load_url_data_starbucks_beans(url)
   charset = nil

   html = open(url) do |f|
   sleep(2)
     charset = f.charset
     f.read
   end

   doc = Nokogiri::HTML.parse(html, nil, charset)
   doc.css('.recommend').each do |node|
     puts node.css('.productName').inner_html
     # sql = 'insert into (〜省略)'
     # Mydatabase.exec(sql)
   end
 end
end

# スタバ用の関数を呼び出す
Myscraping.load_url_data_starbucks_beans('https://www.starbucks.co.jp/beans/')

・scraping.rbを実行!

【ターミナル】
$ bundle exec ruby scraping.rb 
スターバックス カティ カティ ブレンド
スターバックス® コールドブリュー コーヒー ピッチャーパック
スターバックス ヴィア® & スターバックス オリガミ® サマーギフトアソート

(省略)

無事データを取得できました\(^o^)/

でも、疑問が湧いてきませんか?
新たなスクレイピングする度に、関数作り直さないといけないの??

実は現在も、2つのサイトからスクレイピングするために、2つの関数を作成しています。

1. Qiita(ruby検索)用の関数  → load_url_data_qiita_ruby(url)
2. スタバ用の関数       → load_url_data_starbucks_beans(url)

これスクレイピングの度に、関数作るのやっぱり面倒ですよね。。。

これを1つの関数でまとめてしまうのが、そう!Procです!

⭐︎Procを使って書き換えてみよう!

今回はProcの威力を知ってほしいので、まず完成コードから!

【day4-sinatra/scraping.rb】

require 'nokogiri'
require 'open-uri'
require './mydatabase.rb'

module Myscraping
 # スクレイピングのための関数
 def self.load_url_data(url, pattern, &block)
   charset = nil
   html = open(url) do |f|
     charset = f.charset
     f.read
   end
   doc = Nokogiri::HTML.parse(html, nil, charset)
   doc.css(pattern).each do |node|
   # 「ブロック」を呼び出す
     block.call(node)
   end
 end
end

# Qiitaのページをスクレイピング
Myscraping.load_url_data('https://qiita.com/search?q=ruby', '.searchResult') do |node|
 title = node.css('.searchResult_itemTitle a').inner_html
 sql = "INSERT INTO scraping_titles (title) VALUES ('#{title}');"
 Mydatabase.exec(sql)
end

# スタバのページをスクレイピング
Myscraping.load_url_data('https://www.starbucks.co.jp/beans/', '.recommend') do |node|
 product_name = node.css('.productName').inner_html
 sql = "INSERT INTO (~省略);"
 Mydatabase.exec(sql)
end

先程2つあった関数が1つにまとまっています!まずは、便利そう!
というのがわかってもらえたと思います!
それでは、少しずつ紐解いていきます。

⭐︎load_url_data関数の引数(url, pattern, &block)について

引数①url   → スクレイピングするページのURLが渡されます。
引数②pattern → スクレイピングする範囲のセレクタ名、要素名が渡されます。
引数③&block → ここが今回の肝!「ブロック」が渡されています!

■「ブロック」とは?
ざっくりいうと、do~end(もしくは{~})で囲われたカタマリのこと

そのため、&blockが引数で渡されるということは、do~endの中の処理が引数に渡されている事になります!
今回で言うと、下記画像の赤枠③の処理がボコッと渡されます!

⭐︎block.callについて

引数「&block」の「&」って?
「&」を付けることで、実際に引数にブロックが渡ってきた際に、Procオブジェクトに変換しています。
また、Procオブジェクトは、callで呼び出すことが出来るため「block.call」で呼び出すことができます!

結果、今回は「block.call(node)」→上記画像の赤枠③が呼び出される処理となります!

⭐︎yieldについて

引数が「&block」1つの場合のみ、
1. 引数「&block」省略
2. 「block.call」 → 「yield」

にできる!
(省略しすぎて、よく分からなくなりそうな気もするが。)

・yield使えば、こう書ける!

#メソッド定義
def give_me_block  # 1. ( &block )いらない
 yield    # 2. block.callをやめてyieldに変更
end

#実行
give_me_block do
 p "Hello, block!"
end
=> "Hello, block!"

⭐︎Procオブジェクトは、変数に格納できる!

・load_url_data関数を呼ぶ処理をこう書くこともできる!
ブロックをオブジェクト化すれば変数に代入することもできるので、load_url_dataの引数をスッキリ書くこともできます!

# 1. 「ブロック」をprocオブジェクト化して、変数「starbucks」に格納
starbucks = proc do |node|
 product_name = node.css('.productName').inner_html
 sql = "INSERT INTO scraping_titles (title) VALUES ('#{product_name}');"
 Mydatabase.exec(sql)
end

# 2.「&starbucks」を引数として設定(procオブジェクトを引数とする場合は、「&」が必須みたい)
Myscraping.load_url_data('https://www.starbucks.co.jp/beans/', '.recommend', &starbucks)

また、この書き方のメリットは、
メソッドを後々の利用時に柔軟に拡張出来る
状態を持った関数(クロージャ)としての機能が得られる
あるみたいです!
ここらへんドヤ顔で使い分けられるとかっこいいですね〜!

今回はこれで以上です!\(^o^)/
Procについては、以下を参考にしました!ありがとうございました!https://qiita.com/kidach1/items/15cfee9ec66804c3afd2

Procのあたり特に複雑で、頭から火吹いていましたが、まとめている間になんとか理解できてきました!
(めちゃくちゃ時間かかりましたが・・・汗)
ここ違う、分かりにくい、また質問など、お待ちしています\(^o^)/

いいなと思ったら応援しよう!