20190506のRubyに関する記事は16件です。

capistrano3でデプロイ時にslack通知する

趣味でやってるアプリ開発でcapistrano3でデプロイ実行時にslack通知を導入した際の備忘録です。

実現したいこと

自分ひとりでやってたらコンソールのログを見ていたら良いのですが、チーム開発をしてるときにデプロイ状況を確実に共有するために通知を実装しました。
deployに時間がかからないので開始時の通知はしないようにします。

使ったgem

capistrano-slackify

実装方法

Gemfileにslackifyを追加

Gemfile
gem "capistrano-slackify"

インストール

bundle install

Capfileに1行追加してrequrie

Capfile
require "capistrano/slackify"

これで使う準備が整いました。

次に、通知の設定を行います。config/deploy.rbに記述するとすべてのdeployに適用されるので、そこに書きます。

config/deploy.rb
# 通知
set :slack_url, "https://hooks.slack.com/services/XXXXXXXXXXX"
set :slack_channel, "#your_channel"

# 開始時の通知を除いたリスト
set :slack_notify_events, [:finished, :failed]

# 通知内容カスタム
set :slack_fields, ['status', 'stage', 'branch', 'hosts']

# デプロイ成功時
before 'slack:notify_finished', :deploy_success do
  set :slack_emoji,    ':dancers:'
  set :slack_username, "Deploy成功"
end

# デプロイ失敗時
before 'slack:notify_failed', :deploy_failure do
  set :slack_emoji,    ':imp:'
  set :slack_username, 'Deploy失敗'
end

slack_notify_events
通知を行うイベントのリストが入ってるので上書きします。
https://github.com/onthebeach/capistrano-slackify/blob/master/lib/capistrano/tasks/slackify.cap#L86
ここを見ると
開始、成功、失敗の3パターンにデフォルトで通知されます。
開始の通知が必要なかったため、[:finished, :failed]と指定しました。

slack_fields
通知に含めるfieldを指定します。
https://github.com/onthebeach/capistrano-slackify/blob/master/lib/capistrano/tasks/slackify.cap#L65
ここを見るとデフォルトで5つ指定されていますが、revisionは必要性を感じなかったので減らしました。
これで通知内容が1行減るので見た目的にはかなりスッキリします。

最後に

非常に簡単に通知の実装をすることができるので便利でした。
失敗した時のエラー内容まで反映できたらとても素敵だなとコメントをもらったりもしました。デフォルトのイベントにdeploy:log_revisionってのがあるので、そこで使ってる情報をあれこれしたらできるかもしれないですね。

参考

https://www.takedajs.com/entry/2017/11/15/201438

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[メモ]継承とは

この記事は「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」を読んだまとめです。

継承

継承はあるメッセージに応答できなければ、他のオブジェクトにそのメッセージを移譲する動きをする

スーパークラス(抽象クラス)はサブクラス間で共有される振る舞いの共有する格納場所を提供する。サブクラスがそれぞれに特化したものを用意する。いきなりスーパークラスを作るのではなく、サブクラスになり得るクラスが複数ある場合、共通の振る舞いを確認してスーパークラスに昇格する形で継承関係を作るようにする。

  • 共通の振る舞いはスーパークラスに定義して、サブクラスで固有の振る舞いを定義する。以下の例では、サブクラスはdefault_tire_sizeを実装しているが、default_chainは実装していない。タイヤのサイズはサブクラスで定義する必要があるが、チェーンのサイズは共通しているのでスーパークラスで定義されている。(「テンプレートメソッド」パターン)

  • default_tire_sizeが定義されていない時にエラーを発生させることで、default_tire_sizeが定義されていないサブクラスが作られることを防いでいる。
    sparesメソッドはスーパークラスで共通する値を定義して、サブクラスでそれぞれ特化した値をマージしている。

  • オブジェクトのクラスを確認し、どのようなメッセージをそのオブジェクトに送るか決める、自分の分類を保持する変数などを確認して、自身に送るメッセージを変えるこのパターンは、相手が誰だか知っているから、特定のメッセージを送る状態であり依存関係が生まれているので注意が必要。

class Bicycle
  attr_reader:size,:chain,:tire_size

  def initialize(args={})
    @size = args[:size]
    @chain = args[:chain] || default_chain
    @tire_size = args[:tire_size] || default_tire_size
  end

  def spares 
    {tire_size:tire_size,
    chain:chain}
  end

  def default_chain
    '10speed'
  end

  def default_tire_size
    raise NotImplementedError
  end
end


class RoadBike < Bicycle
  attr_reader:tape_color

  def initialize(args)
    @tape_color = args[:tape_color]
    super(args)
  end

  def spares 
    super.merge({tape_color:tape_color})
  end

  def default_tire_size
    '23'
  end
end

class MountainBike < Bicycle
  attr_reader:front_shock,:rear_shock

  def initialize(args)
    @front_shock = args[:front_shock]
    @rear_shock = args[:rear_shock]
    super(args)
  end

  def spares
    super.merge({rear_shock:rear_shock})
  end

  def default_tire_size
    '2.1'
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[メモ] ダックタイピングとは

この記事は「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」を読んだまとめです。

ダックタイピング

ダックタイピングはいかなる特定のクラスとも結びつかないパブリックなインターフェース
「もしもオブジェクトがダックのように鳴き、ダックのように歩くのなら、そのクラスは何であれ、それはダックである」というのが名前の由来。
パブリックインターフェースをクラスから切り離して抽象的にする。
何であるかではなく、何をするかによって定義される抽象的な型をを見つける。隠された抽象的なインターフェースを見つけることがダックタイピングにつながりクラス間の依存を取り除き柔軟な設計につながる。

class Trip
attr_reader:bicycles,:customers,:vehicle

  #この'mechanic'引数はどんなクラスのものでもよい。
  def prepare(mechanic)
    mechanic.prepare_bicycles(bicycles)
  end
end

class Mechanic
  def prepare_bicycles(bicycles)
      bicycles.each{|bicycle|prepare_bicycle(bicycle)}
  end
  def prepare_bicycle(bicycle)
    #...
  end
end

Tripのprepareメソッドはprepare_bicyclesに応答できるオブジェクトに依存している。旅行の準備がメカニックにメッセージを送り自転車を準備するだけであれば問題はないが、準備工程が増えた時にこのままでは以下のようになり、Tripクラスは3つのクラス名と具体的なメソッドメソッドを知ってしまい依存してしまう。
Tripはメカニックにしかメッセージを送らないと想定していると、工程が増えたときに行き詰まってしまう。

class Trip
  attr_reader:bicycles,:customers,:vehicle

  def prepare(preparers)
    preparers.each{|preparer|
      case preparer
      when Mechanic
        preparer.prepare_bicycles(bicycles)
      when TripCoordinator
        preparer.buy_food(customers)
      when Driver
        preparer.gas_up(vehicle)
        preparer.fill_water_tank(vehicle)
      end
    }
  end
end

class TripCoordinator
  def buy_food(customers)
    #...
  end
end

class Driver
  defgas_up(vehicle)
    #...
  end
  def fill_water_tank(vehicle)
    #...
  end
end

依存を取り除くための鍵となるのは、「Tripのprepareメソッドは単一の目的を果たすためにあるので、その引数も単一の目的を共に達成するために渡されてくるということを認識すること」です。
どの引数も同じ理由のためにここに存在し、その理由自体は引数の背後にあるクラスとは関係しません。

MechanicもDriverもTripCoordinatorもそれぞれの役割は異なるが目的は、「旅行の準備をする者」である。この発想からダックタイプを生み出すことができる。
この視点で考えればprepare_trip(旅行の準備をする)振る舞いをするのが、「旅行の準備をする者」と言い換えることができる。

抽象的な「旅行の準備をする者」というインターフェースを想定して、prepare_tripを実装しているものは「旅行の準備をする者」というダックタイプができる。MechanicもDriverもTripCoordinatorも「旅行の準備をする者」として振る舞う必要がある。

class Trip
  attr_reader:bicycles,:customers,:vehicle

  def prepare(preparers)
    preparers.each{|preparer|
      preparer.preparer_trip(self)
  end
end

class Mechanic
  def prepare_trip(trip)
    trip.bicycles.each{|bicycle|
      prepare_bicycle(bicycle)}
    end
  end

class TripCoordinator
  def prepare_trip(trip)
    buy_food(trip.customers)
  end
end

設計をどこまで抽象的にするかはトレードオフ。ifとクラス名で分岐させて具体的にコーディングすれば必要な時間は少なく済むだろうしぱっとはわかりやすい。しかし今後の拡張、変更にコストがかかる。抽象化は手間はかかるが今後の拡張、変更は容易になる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on railsを学習してる者たちよ、SQLを学べ!

SQLを学ぼうと思ったきっかけ

Railsで自作アプリを作っているときにN+1問題に直面した。

includesやleft_joinとかを学習してるときに、SQLの動きをちゃんとわかってないとやばくね?って思った。

Railsはあくまでフレームワークだ。本来SQL直書きするところを簡単にしてくれている。

なのでSQLをある程度わかっていたら、もし違うプログラミング言語で組むことになったとき、フレームワークであれば関連づけて覚えやすいし、直書きなら普通に書ける。

実際にSQLを勉強してみたら意外とすんなり頭に入ってくれた。何も怖がることはなかったので勉強しておくとあとあと絶対いいはず!

この記事では、結合とグループ化について書いていく。

テーブルの結合

Usersテーブル
+----+--------+
| id | name   |
+----+--------+
|  1 | user1  |
|  2 | user2  |
+----+------- +

Articlesテーブル
+----+--------+-------+---------+
| id | title  | body  | user_id |
+----+--------+-------+---------+
|  1 | title1 | body1 |       1 |
|  2 | title2 | body2 |       1 |
|  3 | title3 | body3 |       2 |
|  4 | title4 | body4 |         |
+----+--------+-------+---------+

上記の2つのテーブルがあるとします。

INNER JOIN(内部結合)

articleのuser_idとusersのidを紐づけている。

select articles.*, users.name 
from articles inner join users 
on articles.user_id = users.id;

出力結果

+----+--------+-------+---------+-------+
| id | title  | body  | user_id | name  |
+----+--------+-------+---------+-------+
|  1 | title1 | body1 |       1 | user1 |
|  2 | title2 | body2 |       1 | user1 |
|  3 | title3 | body3 |       2 | user2 |
+----+--------+-------+---------+-------+

articles.user_idとusers.idが一致しているデータだけを出力している。

なので、Articlesテーブルのid = 4のデータはuser_idを持っていないので出力されていない。

LEFT JOIN(外部結合)

LEFT OUTER JOINLEFT JOINと同じ意味。

SQL

select articles.*, users.name 
from articles left join users 
on articles.user_id = users.id;

出力結果

+----+--------+-------+---------+-------+
| id | title  | body  | user_id | name  |
+----+--------+-------+---------+-------+
|  1 | title1 | body1 |       1 | user1 |
|  2 | title2 | body2 |       1 | user1 |
|  3 | title3 | body3 |       2 | user2 |
|  4 | title4 | body4 |         |       |
+----+--------+-------+---------+-------+

articlesテーブルとuserテーブルを並べるようなイメージ。

左のテーブル(articlesテーブル)を基準に結合している。

articlesテーブルはwhereの条件にあてはまる全件が出力され、それに紐づくusersのデータが出力されている。

ちなみにwhere句の条件なしの表現は where 1 = 1

RIGHT JOIN(外部結合)

LEFT JOINの逆なので省略

グループ化

Teamsテーブル
+----+-------+-------+
| id |  team | point |
+----+-------+-------+
|  1 | teamA |   3   |
|  2 | teamB |   2   |
|  3 | teamA |   5   |
|  4 | teamB |   4   |
|  5 | teamC |  10   |
|  6 | teamC |  20   |
+----+-------+-------+

上記のテーブルがあるとします。

Group by

SQL

select team, sum(point) from teams group by team;

出力結果

+----+-------+-------+
| id |  team | point |
+----+-------+-------+
|  1 | teamA |   8   |
|  2 | teamB |   6   |
|  3 | teamC |  30   |
+----+-------+-------+

teamカラムを基準にpointを合計している。
今回扱ったのはsum()合計やけど他にも色々あるで。

例:

  • COUNT(team) 
select team, sum(point) from teams group by team;
レコードのカウント数
+----+-------+-------+
| id |  team | COUNT |
+----+-------+-------+
|  1 | teamA |   2   |
|  2 | teamB |   2   |
|  3 | teamC |   2   |
+----+-------+-------+
  • avg(point)では平均pointが出力される。

where句の位置

SQL

select team, sum(point) from teams where team != "teamB" group by team;

出力結果

+----+-------+-------+
| id |  team | point |
+----+-------+-------+
|  1 | teamA |   8   |
|  2 | teamC |  30   |
+----+-------+-------+

Having句

group byした結果をさらに条件をつける。

SQL

select team, sum(point) from teams where team != "teamB" 
group by team having sum(point) > 10;

出力結果

+----+-------+-------+
| id |  team | point |
+----+-------+-------+
|  1 | teamC |  30   |
+----+-------+-------+

何をしているかというと、

team != "teamB"の条件でグループ化

グループ化した結果をさらにsum(point) > 10の条件で検索

Havingはグループ化した後のwhere句みたいなかんじ。

余計わかりにくいかな。

おわり

他にもいろいろあるけど、とりあえず今回は結合とグループ化までで。

これがわかっただけでもRailsで走っているSQLだいたい読めると思う。

記事書いてるときたまたま見つけたサイト。

https://www.1keydata.com/jp/sql/sql-intersect.php

じゃ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsを学習してる者たちよ、SQLを学べ!

SQLを学ぼうと思ったきっかけ

Railsで自作アプリを作っているときにN+1問題に直面した。

includesやleft_joinとかを学習してるときに、SQLの動きをちゃんとわかってないとやばくね?って思った。

Railsはあくまでフレームワークだ。本来SQL直書きするところを簡単にしてくれている。

なのでSQLをある程度わかっていたら、もし違うプログラミング言語で組むことになったとき、フレームワークであれば関連づけて覚えやすいし、直書きなら普通に書ける。

実際にSQLを勉強してみたら意外とすんなり頭に入ってくれた。何も怖がることはなかったので勉強しておくとあとあと絶対いいはず!

この記事では、結合とグループ化について書いていく。

テーブルの結合

Usersテーブル
+----+--------+
| id | name   |
+----+--------+
|  1 | user1  |
|  2 | user2  |
+----+------- +

Articlesテーブル
+----+--------+-------+---------+
| id | title  | body  | user_id |
+----+--------+-------+---------+
|  1 | title1 | body1 |       1 |
|  2 | title2 | body2 |       1 |
|  3 | title3 | body3 |       2 |
|  4 | title4 | body4 |         |
+----+--------+-------+---------+

上記の2つのテーブルがあるとします。

INNER JOIN(内部結合)

articleのuser_idとusersのidを紐づけている。

select articles.*, users.name 
from articles inner join users 
on articles.user_id = users.id;

出力結果

+----+--------+-------+---------+-------+
| id | title  | body  | user_id | name  |
+----+--------+-------+---------+-------+
|  1 | title1 | body1 |       1 | user1 |
|  2 | title2 | body2 |       1 | user1 |
|  3 | title3 | body3 |       2 | user2 |
+----+--------+-------+---------+-------+

articles.user_idとusers.idが一致しているデータだけを出力している。

なので、Articlesテーブルのid = 4のデータはuser_idを持っていないので出力されていない。

LEFT JOIN(外部結合)

LEFT OUTER JOINLEFT JOINと同じ意味。

SQL

select articles.*, users.name 
from articles left join users 
on articles.user_id = users.id;

出力結果

+----+--------+-------+---------+-------+
| id | title  | body  | user_id | name  |
+----+--------+-------+---------+-------+
|  1 | title1 | body1 |       1 | user1 |
|  2 | title2 | body2 |       1 | user1 |
|  3 | title3 | body3 |       2 | user2 |
|  4 | title4 | body4 |         |       |
+----+--------+-------+---------+-------+

articlesテーブルとuserテーブルを並べるようなイメージ。

左のテーブル(articlesテーブル)を基準に結合している。

articlesテーブルはwhereの条件にあてはまる全件が出力され、それに紐づくusersのデータが出力されている。

ちなみにwhere句の条件なしの表現は where 1 = 1

RIGHT JOIN(外部結合)

LEFT JOINの逆なので省略

グループ化

Teamsテーブル
+----+-------+-------+
| id |  team | point |
+----+-------+-------+
|  1 | teamA |   3   |
|  2 | teamB |   2   |
|  3 | teamA |   5   |
|  4 | teamB |   4   |
|  5 | teamC |  10   |
|  6 | teamC |  20   |
+----+-------+-------+

上記のテーブルがあるとします。

Group by

SQL

select team, sum(point) from teams group by team;

出力結果

+----+-------+-------+
| id |  team | point |
+----+-------+-------+
|  1 | teamA |   8   |
|  2 | teamB |   6   |
|  3 | teamC |  30   |
+----+-------+-------+

teamカラムを基準にpointを合計している。
今回扱ったのはsum()合計やけど他にも色々あるで。

例:

  • COUNT(team) 
select team, sum(point) from teams group by team;
レコードのカウント数
+----+-------+-------+
| id |  team | COUNT |
+----+-------+-------+
|  1 | teamA |   2   |
|  2 | teamB |   2   |
|  3 | teamC |   2   |
+----+-------+-------+
  • avg(point)では平均pointが出力される。

where句の位置

SQL

select team, sum(point) from teams where team != "teamB" group by team;

出力結果

+----+-------+-------+
| id |  team | point |
+----+-------+-------+
|  1 | teamA |   8   |
|  2 | teamC |  30   |
+----+-------+-------+

Having句

group byした結果をさらに条件をつける。

SQL

select team, sum(point) from teams where team != "teamB" 
group by team having sum(point) > 10;

出力結果

+----+-------+-------+
| id |  team | point |
+----+-------+-------+
|  1 | teamC |  30   |
+----+-------+-------+

何をしているかというと、

team != "teamB"の条件でグループ化

グループ化した結果をさらにsum(point) > 10の条件で検索

Havingはグループ化した後のwhere句みたいなかんじ。

余計わかりにくいかな。

おわり

他にもいろいろあるけど、とりあえず今回は結合とグループ化までで。

これがわかっただけでもRailsで走っているSQLだいたい読めると思う。

記事書いてるときたまたま見つけたサイト。

https://www.1keydata.com/jp/sql/sql-intersect.php

じゃ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2年目の営業マンが3ヶ月プログラミングを勉強して、RubyでAmazonのランキングを表示してくれるSlack botを作ってみた

自己紹介

普段は渋谷のフィンテックベンチャーでセールスマネージャーをしており、プログラミングとは無縁の仕事をしています。ビジネスサイドが主戦場なので、物を作れる人に対する大きな憧れがあったのと、日々の業務の中でも、自分自身のシステムに対する理解の浅さから、エンジニアに迷惑をかけてしまうことがありました。また、技術的知識不足から、競合とのコンペで負けてしまい、悔しい思いをしたことがあったので、プログラミングの勉強をすることにしました。

はじめに

2月からRubyの勉強を始め、今回の10連休で簡易的なサービスを作れればと思い、UIの設計が不要なSlack botを作ることにしました。

写真のように、botにメンションを飛ばすと、botが反応し、カテゴリを選ぶと、AmazonのTOP10を表示してくれます。

スクリーンショット 2019-05-06 18.59.19.png

詳細のコードはGithubにて共有しております。rubyのバージョンは2.3.0 で作っています。

開発の大まかな流れ

  • 必要なデータをAmazonから取得する
  • Slackとの連携
  • Slack内でのUIの作り込み

Amazonから必要なデータをスクレイピング

今回はAmazonが提供しているAPIではなく、スクレイピングでデータを取得しています。

require 'mechanize'

今回は mechanize というgemを使ってスクレイピングを進めていきます。

# Amazon.comの対象カテゴリのランキングをスクレイピングで取得する
def get_amazon_ranking(category)
    agent = Mechanize.new
    agent.user_agent_alias = "Windows Mozilla" #書かないとエラーが起こるおまじない的なもの。
    page = agent.get("#{get_amazon_ranking_url(category)}")

ここでは、変数categoryのデータを引っ張ってくる指示を飛ばしています。

# カテゴリー指定
def get_amazon_ranking_url(category)
    if category.include?('ビジネス・経済')
        url = 'https://www.amazon.co.jp/gp/new-releases/books/466282'
    else
        exit
    end

    return url
end

カテゴリー指定は上記の様な形で行い、今後、入力したいカテゴリーを増やす際は、上記にカテゴリー名とURLを入力し、if文を作るだけで追加出来ます。

データをタイトルをkeyに、URLをvalueにしてハッシュへ変換していきます。

# XMLを配列にする(Titles)
def convert_array_from_xml_titles(xml_titles)
    titles = []
    xml_titles.each_with_index do |xml_title, i|
        titles <<  "#{i + 1}#{xml_title.inner_text.gsub(/\r\n|\r|\n|\s|\t/, "")}"
        break 1 if i == 9
    end
    return titles
end
# XMLを配列にする(URLs)
def convert_array_from_xml_urls(xml_urls)
    urls = []
    xml_urls.each_with_index do |xml_url, i|
        # レビューのURLもClass名が同じなので、取得しない
        # 不要なクエリパラメータを削除する
        url = xml_url.get_attribute('href').match(/dp\/[a-zA-Z0-9]+/).to_s

        # レビューを除いた際に、空白が入ってしまうので削除する
        if url != ''
            urls << 'https://www.amazon.co.jp/' + url
        end
        break 1 if i == 9
    end
    return urls
end

それぞれ、convert_array_from_xml という形で変数を定義し、配列を用意します。Urlsの部分では、本に対するレビューのURLも同じClass名の中に入っていたため、不要なクエリパラメータを削除するための処理を行っています。

スクリーンショット 2019-05-05 10.30.51.png
(タイトルのURLと本のレビューのURLが a-link-normal という同一名義のClassに囲われてる)

タイトル、URLともに、10個取り出せた時点でbreakする設定をしています。ここで回したデータを、TitlesとUrlsに返し、指定したクラスからデータを抜き出します。

# 必要な項目だけを抜き出す&XMLを配列にする
    titles = convert_array_from_xml_titles(page.search('.p13n-sc-line-clamp-2'))
    urls   = convert_array_from_xml_urls(page.search('.a-col-left .a-link-normal'))

    rankings = {}
    titles.zip(urls) do |title, url| 
        rankings[title] = url
    end 

    return rankings
end

ここで更にrankingsという連想配列を用意し、.zipメソッド を活用して、TitleとURLがSlackに交互に出力される指示を出します。

Slackとの連携

まず、slack-ruby-client というgemをインストールし、ひな形をコピペします。

require 'slack-ruby-client'

  Slack.configure do |conf|
    conf.token = 'xoxb-*****************' # トークンは後ほど取得します。
  end

  # RTM Clientのインスタンス生成
  client = Slack::RealTime::Client.new

  # Slackに接続できたときの処理
  client.on :hello do
    puts 'connected!'
    client.message channel: 'your_channel_id', text: 'connected!'
  end

  # ユーザからのメッセージを検知したときの処理
  client.on :message do |data|
    if data['text'].include?('こんにちは')
      client.message channel: data['channel'], text: "Hi!"
    end
    if data['text'].include?('かしこい') || data['text'].include?('えらい')
      client.message channel: data['channel'], text: "Thank you!"
    end
    if data['text'].include?('おやすみ')
      client.message channel: data['channel'], text: "Good night"
    end
  end

  # Bot start
  client.start!

次に、ブラウザ上のSlackで Bots から、botを作成します。
https://xxxxxxxxxxx.slack.com/apps/search?q=bots

slack_bot_1.png

slack_bot_2.png

client.message channel: 'your_channel_id', text: 'connected!'

上記の部分を今回表示したい自分のSlackのチャンネルに変える作業を行います。

xoxb-***************** の部分に、bot作成後のアクセストークンを貼ると、繋込みがされます。

Slack内でのUIの作り込み

Slack内での挙動に関しては、@レイワーくん とスラックで呼び出すと、「僕は現在のAmazonランキング10選を教えることができるよ!★をつけてカテゴリを選んでください!」→「カテゴリー一覧」という流れで、反応するインターフェイスにしました。カテゴリー名をユーザーが入力したかどうかを検出するため、各カテゴリーの頭に★マークを入れ、★があるかどうかで、カテゴリー名と紐づけたデータを引っ張ってくる処理をif文で書いています。

# ユーザからのメッセージを検知したときの処理
client.on :message do |data|
    if data['text'].include?('レイワーくん') || data['text'].include?('<@UJA1HUXEG>')
        client.message channel: data['channel'], 
        # 追加していく!
        text: "僕は現在のAmazonランキング10選を教えることができるよ!\n★をつけてカテゴリを選んでください!\n```★ビジネス・経済 ★コンピュータ・IT ★科学・テクノロジー  ★エンターテイメント ★歴史・地理  ★教育・学参・受験 \n★文学・評論 ★社会・政治 ★家電&カメラ ★ホーム&キッチン ★ホビー ★パソコン・周辺機器 ★ゲーム ★おもちゃ```"
    end

    # FIXME
    # メッセージの中に★があれば、カテゴリとしてみな
    if data['text'].include?('★')
        rankings = get_amazon_ranking(data['text'])

        rankings.each{|title, url|
            client.message channel: data['channel'], text: "#{title} \n#{url}"
        }
    end
end

完成!
スクリーンショット 2019-05-06 18.59.19.png

現在、Herokuへのアップロードも進めております。詳細はまた別記事で書いていこうと思います。

作り終えての感想

簡単に作れるかと思いきや、XMLの変換や、スクレイピングでノイズを除去する作業で、思った以上に苦戦しました。一方、エラーを自力で解決出来たときの快感や、作ったものを人に見せて褒めてもらえたときの喜びは、これまでにあまり経験したことのない感動でした。未知の世界で自分の非力さを実感しながら格闘出来たことも非常に価値のある経験でした。

制作過程で死にそうな自分を何度も助けてくださった@IZUMIRU0313さんには感謝しかないです。早くエンジニアの先輩方とも肩を並べて、共闘できるくらいの戦闘力を身に着けていけるよう精進していきます。

サービスに関して、質問があれば、@matsukazu1995g1までご連絡ください!

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsのポリモーフィック関連の挙動確認

はじめに

公式ドキュメントのポリモーフィック関連の項目を見た際に、挙動がイメージしにくかったので、ポリモーフィック関連を持つモデルを作成しつつ、DBのデータやコンソールで動きを確認します。

環境

  • OS : Ubuntu 17.04
  • Ruby : 2.6.3
  • Rails: 5.2.3
  • MySQL: 5.7.20

ポリモーフィズム(多様性)

ポリモーフィックと聞くと、オブジェクト指向プログラミングで出てくるポリモーフィズムを思い出す人が多いはずです。

ポリモーフィズムとは、プログラミング言語の持つ性質の一つで、ある関数やメソッドなどが、引数や返り値の数やデータ型などの異なる複数の実装を持ち、呼び出し時に使い分けるようにできること。
(略)
オブジェクト指向プログラミング言語では親クラスから派生(継承)した子クラスがメソッドの内容を上書き(オーバーライド)したり、インターフェースで定義されたメソッドを実装することによりこれを実現している。

オブジェクト指向プログラミングの、ポリモーフィズムでは、継承やインターフェース1を使って同名のメソッドを複数のクラスで定義します。それによって同名のメソッドでも、インスタンスごとに振る舞いを変える(多様性2が生まれる)というものです。

ただ、ActiveRecordのポリモーフィック関連は、継承やインターフェース1を用いるのではなく、ActiveRecordで定義されている関連付けの機能を使用します。

(用途は異なりますが継承を用いるシングルテーブル継承 (STI)というものもあります)

ポリモーフィック関連を持つモデルの作成

Railsガイドを元に、ポリモーフィック関連付けのモデルを作成します。

作成するモデルは、Picture/Employee/Productの3つです。
ここでは、モデルPictureと複数のモデルEmployee/Productとの関連を、imageableという関連1つで表現します。

$ bin/rails generate model Product name:string
Running via Spring preloader in process 21448
      invoke  active_record
      create    db/migrate/20190504040556_create_products.rb
      create    app/models/product.rb
      invoke    test_unit
      create      test/models/product_test.rb
      create      test/fixtures/products.yml

$ bin/rails generate model Employee name:string
# 省略

$ bin/rails generate model Picture name:string
# 省略

PictureのMigrationファイルを修正します。

class CreatePictures < ActiveRecord::Migration[5.2]
  def change
    create_table :pictures do |t|
      t.string  :name
      t.integer :imageable_id
      t.string  :imageable_type
      t.timestamps
    end

    add_index :pictures, [:imageable_type, :imageable_id]
  end
end

作成した、それぞれのモデルに関連を記載します。

# app/models/picture.rb
class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end

# app/models/employee.rb
class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end

# app/models/product.rb
class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

マイグレーションを実行します。

$ bin/rails db:migrate
== 20190504040556 CreateProducts: migrating ===================================
-- create_table(:products)
   -> 0.0588s
== 20190504040556 CreateProducts: migrated (0.0590s) ==========================

== 20190504040742 CreateEmployees: migrating ==================================
-- create_table(:employees)
   -> 0.0648s
== 20190504040742 CreateEmployees: migrated (0.0650s) =========================

== 20190504041111 CreatePictures: migrating ===================================
-- create_table(:pictures)
   -> 0.0630s
-- add_index(:pictures, [:imageable_type, :imageable_id])
   -> 0.0678s
== 20190504041111 CreatePictures: migrated (0.1311s) ==========================

MySQLのデータを確認

DBeaver をつかってデータを確認します。

Screenshot from 2019-05-04 13-34-04.png

Screenshot from 2019-05-04 13-34-24.png

Screenshot from 2019-05-04 13-34-51.png

ポリモーフィック関連を実現するために、テーブルpicturesに、imageable_id/imageable_typeというカラムが追加されてます。
imageableという単語は、モデルEmployee/Productで定義されていました。

has_many :pictures, as: :imageable

説明をすると、「複数のpictureを持ち、その関連はimageableとする」といったところでしょうか。

モデルの作成

動きを確認するために、モデルを作成します。

$ bin/rails c

> product = Product.create(name: '商品1')
=> #<Product id: 1, name: "商品1", created_at: "2019-05-04 04:41:12", updated_at: "2019-05-04 04:41:12">

> employee = Employee.create(name: '田中一郎')
=> #<Employee id: 1, name: "田中一郎", created_at: "2019-05-04 04:42:23", updated_at: "2019-05-04 04:42:23">

> product.pictures
=> #<ActiveRecord::Associations::CollectionProxy []>

> employee.pictures
=> #<ActiveRecord::Associations::CollectionProxy []>

ここはhas_many の動きです。まだPictureを作成していないのでデータがヒットしません。

product.pictures.create(name: '商品1-1.jpg')
=> #<Picture id: 1, name: "商品1-1.jpg", imageable_id: 1, imageable_type: "Product", created_at: "2019-05-04 04:54:46", updated_at: "2019-05-04 04:54:46">

employee.pictures.create(name: '田中一郎_1.jpg')
=> #<Picture id: 2, name: "田中一郎_1.jpg", imageable_id: 1, imageable_type: "Employee", created_at: "2019-05-04 04:59:24", updated_at: "2019-05-04 04:59:24">

それぞれのProduct/Employeeで、picturesを作成したところ、こちらで指定していないのにもかかわらずimageable_idとimageable_typeに値が入っています。

今度はPictureから、Product/Employeeの関連を見てみましょう。

> pictures = Picture.all
  Picture Load (0.9ms)  SELECT  `pictures`.* FROM `pictures` LIMIT 11
=> #<ActiveRecord::Relation [#<Picture id: 1, name: "商品1-1.jpg", imageable_id: 1, imageable_type: "Product", created_at: "2019-05-04 04:54:46", updated_at: "2019-05-04 04:54:46">, #<Picture id: 2, name: "田中一郎_1.jpg", imageable_id: 1, imageable_type: "Employee", created_at: "2019-05-04 04:59:24", updated_at: "2019-05-04 04:59:24">]>

> pictures.first.imageable
  Picture Load (0.8ms)  SELECT  `pictures`.* FROM `pictures` ORDER BY `pictures`.'id' ASC LIMIT 1
  Product Load (1.1ms)  SELECT  `products`.* FROM `products` WHERE `products`.'id' = 1 LIMIT 1
=> #<Product id: 1, name: "商品1", created_at: "2019-05-04 04:41:12", updated_at: "2019-05-04 04:41:12">

> pictures.last.imageable
  Picture Load (0.8ms)  SELECT  `pictures`.* FROM `pictures` ORDER BY `pictures`.'id' DESC LIMIT 1
  Employee Load (1.2ms)  SELECT  `employees`.* FROM `employees` WHERE `employees`.'id' = 1 LIMIT 1
=> #<Employee id: 1, name: "田中一郎", created_at: "2019-05-04 04:42:23", updated_at: "2019-05-04 04:42:23">

(省略していた、SQL文のログも記載してます)

Pictureでは、Product/Employeeの値を参照する際に、Product/Employeeで定義していたimageableを使用して参照することができます。

ログから察するに、1回目のSQLでテーブルpicturesのデータを取得、2回目のSQLでpicturesのimageable_id/imageable_typeの値を使用して、検索するテーブルと、id値を決めているようです。

MySQLのデータを確認

Screenshot from 2019-05-04 13-53-18.png

Screenshot from 2019-05-04 13-53-38.png

Screenshot from 2019-05-04 14-05-43.png

コンソールで確認したように、それぞれのテーブルにデータが格納されています。

注意する点としてはpicturesのimageable_id/imageable_typeはRails(アプリ側)だと、ポリモーフィック関連を提供するカラムとして認識されますが、DB側にはポリモーフィック関連を表現する機能がないため「インデックスが張られているカラム」という認識しかありません。

例えば、アプリ側でEmployeeもしくはProductのデータを削除した際にdependentを使い、Pictureのimageable_id/imageable_typeのデータに手を加え関連の整合性を保つことができます。しかし、DB側でemployeeもしくはproductsを削除しても外部キー制約などを設定できない(このカラムでは外部キーとなるテーブルが明確でない)ため、picturesとの関連の整合性が取れなくなります。

まとめ

  • ActiveRecordのポリモーフィック関連は、ポリモーフィズムと目的は同様
  • しかし、オブジェクト指向プログラミングなどと手法が異なる
  • x_id/x_typeといったカラムを追加して、対象のオブジェクト(テーブル)名と主キーのidを格納する
  • 1つの関連で、複数のオブジェクトの関連を表す事ができる
  • ポリモーフィック関連はActiveRecordの機能であり、DBはポリモーフィックの整合性を担保しない

  1. ここでいうインターフェースは、Javaの実装におけるインターフェースを想定しているような気がします。 

  2. オブジェクト指向プログラミングでよく例に挙げられるものは、厳密には「ポリモーフィズムの部分型付け」を指すようです。Wikipedia ポリモーフィズム  

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails チュートリアル 第10章 学習内容のメモ

第10章ユーザーの更新・表示・削除

10.1 ユーザーを更新する

  • ユーザー名やメールアドレスを編集するときに、毎回パスワードを入力しなくて良い仕様にする。
  • パスワードがnilでも更新できるように、userモデルのバリデーションを追加する必要がある。

validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

  • allow_nil: trueの追加したとしても、has_secure_passwordによって、新規ユーザ登録時に存在性の検証が行われる(もともとhas_secure_passwordとpresence: trueという存在性を検証する2つのバリデーションが存在していた)

10.2 認可

  • 認証=サイトのユーザーを識別すること
  • 認可=ユーザーが実行可能な操作を管理すること
  • 第8章で認証の機能は構築したが、認可の機能は未実装
    ⇨どのユーザでもあらゆるアクションにアクセスできる。URLを直接編集し、別のユーザの設定変更が可能となっている
  • ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御する
  • 具体的には、before_action(何らかの処理が実行される直前に特定のメソッドを実行する)に以下のlogged_in_userメソッドを追加する。
app/controllers/users_controller.rb
    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby Gold 模擬試験にたまに出てくるもの

Ruby Gold勉強3

こんにちは。
前回に続いて、Ruby Goldの学習を続けております。

今は順次模擬試験を受けながら、勉強をしているので、詰まった問題・分からなかった問題について、整理していきたいと思います。
(今回の内容は、前回に比べて優先度は低めだと思われます。前回のブロックの取り扱いや、前々回のメソッド探索の方が優先度は高いので、悪しからず。)

なお、今回記載させていただくのはCTC教育サービスの模擬問題を一部変更して使わせていただいております。

特異メソッド

以下のコードの説明として正しいものを選択してください。

 1: obj1 = Object.new
 2: def obj1.study
 3:  puts "Ruby Study!"
 4: end
 5: obj1.study
 6: Object.new.study

正解:6行目でエラーが発生する。


解説

Rubyでは 特定のオブジェクトにだけ メソッドを定義することができる。
これを特異メソッドと呼び、
def <オブジェクト名>.<新たに定義するメソッド名>という風に定義する。

今回の場合obj1というオブジェクトにだけ、特異メソッドを定義しているため、6行目で新たにオブジェクトクラスのインスタンスを生成しても、studyというメソッドは使えないため、エラーになる。


requireloadの違い

以下のようなfile1.rbとfile2.rbがあります。file2.rbを実行した結果は?

 [file1.rb]
 $var += 1


 [file2.rb]
 $var = 0
 require "./file1.rb" ⬅︎カレントディレクトリを指定して実行
 require "./file1"  ⬅︎ファイルの拡張子を省略しても補完される。
 puts $var

正解:1

解説


requireは外部のファイルを読み込みます。
requireは次のような特徴があるため、良くライブラリの読み込みに使われます。

  • 上の問題のように何度同じファイルを実行しても、一度しか実行されない。
  • ファイルの拡張子などを省力しても、自動的に補完する。

一方で同じように外部のファイルを読み込むメソッドとして、loadメソッドがあります。
こちらは、次のような特徴があルため、設定情報の読み込み用などに使われます。

  • 同じファイルを実行する場合は、実行された回数だけ実行される。
  • ファイルの拡張子は補完されないため、省略できない。

loadで上の問題を読み込むと次のようになりますので、動きの違いを押さえておきましょう。

 [file1.rb]
 $var += 1


 [file2.rb]
 $var = 0
 load "./file1.rb" 
 load "./file1.rb"
 puts $var => 2

undef_metdhod と remove_method の違い

以下の2つのコードの実行結果の出力として正しいものは?(問40からそのまま抜粋させていただいております)

[コード1]
 class Foo
  def foo
   puts "foo"
  end
 end
 class Bar < Foo
  def foo
   puts "bar"
  end
 end
 class Bar
  undef_method :foo
 end
 Bar.new.foo


 [コード2]
 class Foo
  def foo
   puts "foo"
  end
 end
 class Bar < Foo
  def foo
   puts "bar"
  end
 end
 class Bar
  remove_method :foo
 end
 Bar.new.foo

正解
コード1:エラーになる
コード2:foo(ちなみにpryでそのまま実行するとnilが返る。.rbファイルを作成して実行する)


解説

undef_methodメソッドは、クラスやモジュールのメソッドを未定義にして、呼び出せなくします。
親クラスのメソッドをサブクラスで未定義にした場合は、undef_methodを呼び出したクラスとそのサブクラスでメソッドを呼び出せなくなります。ただし、親クラスのメソッドには影響しません。

ということで、[コード1]でBarクラスでundefメソッドを実行しているため、親クラスのfooメソッドも呼び出せません。

remove_methodはクラスやモジュールからメソッドを削除します。
undef_methodメソッドとは違い、削除できるのはそのクラス・モジュールで定義されたメソッドだけです。親クラスのメソッドは指定できません。また、削除したメソッドと同名のメソッドが親クラスにある場合は、親クラスのメソッドが呼び出されるようになります。

というわけで、[コード2]ではBarクラスのfooメソッドは呼び出せなくなっていますが、親クラスのFooクラスのfooメソッドは呼び出せるため、fooが出力されます。


と、今回は模擬試験を見ながらちょっと詰まってしまった問題をまとめました。
本当は添付ライブラリの問題も分からないものがあるのですが、出題数の割には勉強する範囲が広いため、優先度を低くして、模擬試験に出たものを中心に見ていこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】混同しやすいredirect_toとrenderの動きと引数

はじめに

Qiita初投稿なので最初に少しだけ。

1か月ほど前にプログラミングの独学をし始め、現在はHTMLとCSSの基礎を終えて簡単なLPやサイトであれば模写してjQueryで軽く動きを付けられるくらいのレベルです。

恥ずかしながら現在ProgateでRuby on Railsを学習中です。
このレベルでQiitaに記事を書くのは大変申し訳なく思いますが、理解するのに少し時間がかかった点について、頭の中にあるふんわりとした概念を言語化して理解を定着させるためにも文章としてまとめたいと思います。

自身のインプットの質を高めるためのアウトプットですが、これからプログラミングを独学される方は多いと思うので、そういった方たちに向けても書いていきます。

また、まだまだプログラミング入門者ゆえに記述内容が間違っているかもしれないので、そういった場合はご指摘いただけると幸いです。

今回躓いた点

僕が今回躓いたのは、redirect_torenderの引数がどちらともURLだと間違って理解していたためにエラーを発生してしまい少し躓きました。

redirect_toは引数に"URL"を指定する*のに対して、renderは"フォルダ名/ファイル名"を指定します。

qiita.rb
redirect_to("/posts/index")  #「/posts/index」というURLを指定している
render("posts/index")  #「postsフォルダ内にあるindexファイル」を直接指定している

ちなみに、render("/posts/index")でも動作します。URLとして記述したのに動作していたことが今回の間違った理解につながっていました。

躓いたことによって、それぞれの引数が全く別のものを指していることを理解すると同時に、この二つのメソッドの動きについてもしっかりと理解できました。

今回は各メソッドの内側の動きについて書いていきます。

redirect_toとrenderは表面上の動きは似ているが内部の動きは大きく異なる

先ほども述べましたが、redirect_torenderは表面上の動きは確かに似ていますが、表示するまでの内部の動作が大きく異なります。

どういう事かというと、例えば、引数をこんな感じで取る場合を考えてみましょう。

qiita.rb
# どちらとも表面上は「edit.html.erb」というビューを表示
redirect_to("/posts/index")
render("posts/index")

両者ともに表面上は「edit.html.erb」というビューを表示させます。(URL:/posts/indexのルーティングがindexアクションになっている場合。基本的にはこうなっていると思います。)

しかし、表面上は「edit.html.erb」を表示するだけですが、その内部の動きは全く異なります。

Railsの表示の仕組み

内部の動きの違いについて説明する前に、Railsにおける表示の仕組みについて軽く押さえたいと思います。
Railsでは、ブラウザからHTMLファイルを要求された際に以下の順番でHTMLをブラウザに渡します。
①ルーティング
②コントローラのアクション
③ビュー

ルーティングは、指定されたURLからどのコントローラどのアクションで処理をするのかを決定する対応表のような役割を持っています。
そして、ルーティングによって指定されたコントローラのアクションはコントローラと同じ名前のフォルダからアクション名と同じビュー(HTMLファイル)をブラウザに返してます。

redirect_toメソッドの実際の動き

表示までの大まかな流れが分かったところで話を戻します。
redirect_to("/posts/index")render("posts/index")は両者ともに表面上は「edit.html.erb」というビューを表示させますが、その内部の動きは大きく異なります。

redirect_toは指定したURLに転送するメソッドです。

URLを指定するので、先ほど説明した「①ルーティングを通って→②アクションを決定して→③ビューを表示する」という流れでビューを表示し、ブラウザからURLを指定された時と同じ動きをします。

renderメソッドの実際の動き

一方、renderルーティングやアクションを経由せずに直接ビューを表示することができます。

ルーティングとアクションを経由せず直接ビューを表示するので、renderメソッドと同じアクション内で定義した@変数をビューで使える点がredirect_toとの大きな違いです。

まとめ

  • redirect_toの引数は「"URL"」でrenderの引数は「"フォルダ名/ファイル名"」
  • redirect_toメソッドは引数でURLを指定するのでルーティングからアクションを通してビューを表示する
  • renderメソッドは引数で直接ビューを指定して表示するため、ルーティング→アクションを経由しないのでrenderメソッドと同じアクション内で定義した@変数をビューで使うことができる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rbenvをホームディレクトリ以外にインストール

概要

Linuxの場合、rbenvはGithubのREADME含めてホームディレクトリにインストールすることを前提に手順が記載されていることが多いです。
個人的にホームディレクトリ以外にインストールして利用しようと思ったのですが、その際に少し工夫が必要だったため、その方法を記載しています。

環境

  • Amazon Linux 2
  • kernel 4.14.72-73.55.amzn2.x86_64
  • bash 4.2.46
  • git 2.14.5
  • rbenv 1.1.2

rbenvインストール

今回は例として/opt配下にインストールする方向で進めていきます。

# git clone https://github.com/rbenv/rbenv.git /opt/rbenv

ログイン時にPATHの読み込みとrbenvのセットアップを行うように設定を行います。

ログインユーザ全体に反映させたい場合は/etc/profileに記載します。
ただし、直接同ファイルを修正するよりも、/etc/profile.d配下に*.shファイルを作成するほうが好ましいため、今回は/etc/profile.d/rbenv.shを新規作成して記載する方針とします。

その際に、環境変数RBENV_ROOT今回インストールしたrbenvのパスを定義してあげます。

# cat << EOF > /etc/profile.d/rbenv.sh
export RBENV_ROOT=/opt/rbenv
export PATH="\$RBENV_ROOT/bin:\$PATH"
eval "\$(rbenv init -)"
EOF

シェルを再起動して上記設定を読み込ませます。
rbenvのバージョンが表示されれば完了です。

# exec $SHELL -l
# rbenv -v
rbenv 1.1.2-2-g4e92322

次にrubyインストールのためにruby-buildrbenvのプラグインとしてインストールします。

# mkdir -p "$(rbenv root)"/plugins
# git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

ここまでくれば、あとはいつも通りにrubyをインストールすればOKです。
※rubyインストール前に必要なコンパイラなどもインストールしておきます。

# yum -y install bzip2 gcc openssl-devel readline-devel zlib-devel
# rbenv install -l
Available versions:
  1.8.5-p52
  1.8.5-p113
  1.8.5-p114
  1.8.5-p115
  1.8.5-p231
  1.8.6
(中略)
# rbenv install 2.6.3

rbenv local <ruby version>を実行すれば各々のディレクトリでRubyのバージョンを切り替えることができます。

$ pwd
/home/<user>
$ rbenv local 2.6.3
$ rbenv version
2.6.3 (set by /home/<user>/.ruby-version)
$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]
$ 

補足

rbenvコマンドは環境変数RBENV_ROOTを明示的に指定していないと現在ログインしているユーザのホームディレクトリ配下をrbenvのrootディレクトリと定義するようです。
https://github.com/rbenv/rbenv/blob/master/libexec/rbenv

rbenv(抜粋)
if [ -z "${RBENV_ROOT}" ]; then
  RBENV_ROOT="${HOME}/.rbenv"
else
  RBENV_ROOT="${RBENV_ROOT%/}"
fi
export RBENV_ROOT

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

N+1の予感がしたらincludesを追加?

こんにちは!
入社したてのころ、右も左もわからずにコーディングをしていました。
そんな中で、僕もよく悩まされたN+1についての対策について簡単にまとめてみました。
N+1は簡単に防げてパフォーマンスをあげることができます。
すぐできて、効果大なのでぜひ実践してみてください!

そもそもN+1とは

SQLクエリが 「データ量N + 1回 」走ってしまい、取得するデータが多くなるにつれて(Nの回数が増えるにつれて)パフォーマンスを低下させてしまう問題です。

N+1問題 / Eager Loading とは
(引用させていただきました)

→簡単にいうとデータ取得の際、余計にSQLを発行してしまいパフォーマンスを下げてしまうことです。

テーブルの定義

例えば配列展開でBook(本)からAuthor(著者)の名前を出力したい場合(以下ER図&作成データになります)。
スクリーンショット 2019-05-05 20.13.02.png

念の為コードも

Author

class Author < ApplicationRecord
  has_many :books
end

Book

class Book < ApplicationRecord
  belongs_to :author
end

データの用意

author(諫山さん)が6冊の本(book)のリレーションを持っています。

irb(main):004:0> Author.all # 全てのAuthorレコード
  Author Load (0.5ms)  SELECT "authors".* FROM "authors"
+----+--------+-------------------------+-------------------------+
| id | name   | created_at              | updated_at              |
+----+--------+-------------------------+-------------------------+
| 1  | 諫山創 | 2019-05-05 10:44:51 UTC | 2019-05-05 10:44:51 UTC |
+----+--------+-------------------------+-------------------------+
irb(main):003:0> Book.all # 全てのBookレコード
  Book Load (1.3ms)  SELECT "books".* FROM "books"
+----+---------+-----------+-------------------------+-------------------------+
| id | title   | author_id | created_at              | updated_at              |
+----+---------+-----------+-------------------------+-------------------------+
| 1  | 進撃の1 | 1         | 2019-05-05 10:46:17 UTC | 2019-05-05 10:46:17 UTC |
| 2  | 進撃の2 | 1         | 2019-05-05 10:46:25 UTC | 2019-05-05 10:46:25 UTC |
| 3  | 進撃の3 | 1         | 2019-05-05 10:46:28 UTC | 2019-05-05 10:46:28 UTC |
| 4  | 進撃の4 | 1         | 2019-05-05 10:46:31 UTC | 2019-05-05 10:46:31 UTC |
| 5  | 進撃の5 | 1         | 2019-05-05 10:46:35 UTC | 2019-05-05 10:46:35 UTC |
| 6  | 進撃の6 | 1         | 2019-05-05 10:46:38 UTC | 2019-05-05 10:46:38 UTC |
+----+---------+-----------+-------------------------+-------------------------+
irb(main):004:0> Author.first.books # 諫山さんが6冊の本(book)のリレーションを保持
  Author Load (1.7ms)  SELECT  "authors".* FROM "authors" ORDER BY "authors"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Book Load (0.2ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ?  [["author_id", 1]]
+----+---------+-----------+-------------------------+-------------------------+
| id | title   | author_id | created_at              | updated_at              |
+----+---------+-----------+-------------------------+-------------------------+
| 1  | 進撃の1 | 1         | 2019-05-05 10:46:17 UTC | 2019-05-05 10:46:17 UTC |
| 2  | 進撃の2 | 1         | 2019-05-05 10:46:25 UTC | 2019-05-05 10:46:25 UTC |
| 3  | 進撃の3 | 1         | 2019-05-05 10:46:28 UTC | 2019-05-05 10:46:28 UTC |
| 4  | 進撃の4 | 1         | 2019-05-05 10:46:31 UTC | 2019-05-05 10:46:31 UTC |
| 5  | 進撃の5 | 1         | 2019-05-05 10:46:35 UTC | 2019-05-05 10:46:35 UTC |
| 6  | 進撃の6 | 1         | 2019-05-05 10:46:38 UTC | 2019-05-05 10:46:38 UTC |
+----+---------+-----------+-------------------------+-------------------------+

コンソールで実行

配列展開でBookから親モデルのAuthorのnameを呼び出すとBookのデータ数分(6個)SQLを発行してしまいます。
→つまり5回もSQLが無駄に発行されてしまうのです。

books = Book.all
> Book Load (0.2ms)  SELECT "books".* FROM "books"
irb(main):037:0* books.each do |book|
irb(main):038:1*  book.author.name
irb(main):039:1> end
  Author Load (0.6ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
irb(main):040:1>

スクリーンショット 2019-05-05 20.25.32.png

この無駄なクエリを防ぐにはincludesを追加するのがもっとも簡単です

includes追加

  books = Book.all.includes(:author) #追加
  Book Load (2.1ms)  SELECT "books".* FROM "books"
  Author Load (0.2ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 1]]

お気づきでしょうか、includes追加により関連レコード(author)も一緒に取得されています。
→つまり、Bookと一緒に紐づいたAuthorモデルのレコードも取得し変数に代入していることになります。

Before

books = Book.all
> Book Load (0.2ms)  SELECT "books".* FROM "books"

books = Bookモデルの全てのデータ

After
スクリーンショット 2019-05-05 20.33.31.png
books = Bookモデルの全てのデータとそれらにひもづくAuthorデータ

この状態でもう一度booksを展開してみましょう!

irb(main):051:0* books.each do |book|
irb(main):052:1*   book.author.name
irb(main):053:1> end
irb(main):054:0>

今度は展開のたびにAuthorを取得していません。
SQLの発行をおさえてパフォーマンス低下を防ぐことができましたね。

includesで色々なリレーションを取得する

先ほどはN:1(Book:Author)でのパターンでしたが、実際はもっと複雑な利用パターンが多いと思います。
そんな時に利用できる書き方をご紹介します。

N:1 = Book:Author

  books = Book.all.includes(:author)

こちらは先ほどのパターンでしたね

N:1:1 = Book:Author:Profile

では、Authorのプロフィール情報を保存するAuthors::Profileがあった場合

  books = Book.all.includes(author: :profile)
   Book Load (1.6ms)  SELECT  "books".* FROM "books" LIMIT ?  [["LIMIT", 11]]
  Author Load (0.4ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 1]]
  Authors::Profile Load (0.4ms)  SELECT "authors_profiles".* FROM "authors_profiles" WHERE "authors_profiles"."author_id" = ?  [["author_id", 1]]

N:1:1:1 = Book : Author : Profile : ProfileImage

さらにProfileに1つのプロフィール写真Authors::ProfileImageひもづく場合

  books = Book.all.includes(author: [profile: :profile_image])
  Book Load (0.5ms)  SELECT  "books".* FROM "books" LIMIT ?  [["LIMIT", 11]]
  Author Load (0.1ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 1]]
  Authors::Profile Load (0.2ms)  SELECT "authors_profiles".* FROM "authors_profiles" WHERE "authors_profiles"."author_id" = ?  [["author_id", 1]]
  Authors::ProfileImage Load (0.1ms)  SELECT "authors_profile_images".* FROM "authors_profile_images" WHERE "authors_profile_images"."id" = ?  [["id", nil]]

逆に1:N(Author:Book)なら?
これはリレーションを使い関連データを全て取得できますね。

  > Author.first.books
  Author Load (0.3ms)  SELECT  "authors".* FROM "authors" ORDER BY "authors"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Book Load (0.3ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ?  [["author_id", 1]]

N+1の予感とタイトルにありますが、余計にクエリを投げるケースは配列展開が多いと思います。
慣れていない方はeachやmap, selectなど配列展開のメソッドを使う際にN+1が起きていないかぜひ意識してみてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

APIサーバのレスポンスがスキーマ上のレスポンス定義と一致するかのテスト

committeecommittee-railsが(わからなかった|動かなかった)ので、WEB+DB PRESS Vol.108に記載されている方法でやってみた。

ざっくり言うと

oas_parserでswagger.jsonをパースして、json_schemaでバリデーションを行う

gem

gem 'oas_parser'
gem 'json_schema'

swagger.json

{
  "swagger": "2.0",
  "info": {
    "title": "API V1",
    "version": "v1"
  },
  "servers": [
    {
      "url": "http://localhost:3000/api-docs",
      "description": "development server"
    }
  ],
  "basePath": "/api/v1",
  "paths": {
    "/areas/{id}": {
      "get": {
        "summary": "Retrieves a area",
        "tags": [
          "Areas"
        ],
        "description": "Retrieves a specific area by id",
        "operationId": "getArea",
        "security": [
          {
            "apiKey": [

            ]
          }
        ],
        "produces": [
          "application/json"
        ],
        "parameters": [
          {
            "name":        "id",
            "in":          "path",
            "type":        "integer",
            "description": "エリアID",
            "required":    true
          }
        ],
        "responses": {
          "200": {
            "description": "success",
            "schema": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "name"
              ]
            }
          }
        }
      }
    },
    "securityDefinitions": {
    "apiKey": {
      "type": "apiKey",
      "name": "Authorization",
      "in": "header"
    }
  }
}

SchemaオブジェクトとJSON Schemaライブラリを用いてJSONのバリデーションを行う

  • spec/rails_helper.rb
RSpec.configure do |config|
  config.include ControllerSpecsHelper

  config.before :example, type: :controller do
    spec        = OasParser::Definition.resolve(Rails.root.join('swagger', 'v1', 'swagger.json'))
    schema_data = spec.path_by_path(schema_path).endpoint_by_method(schema_method).response_by_code(code.to_s).raw["schema"]
    @schema     = JsonSchema.parse!(schema_data) if schema_data
  end
  • spec/support/controller_specs_helper.rb
module ControllerSpecsHelper
  def expect_to_conform_schema(response)
    expect {
      @schema.validate!(JSON.parse(response.body))
    }.not_to raise_error
  end
end

テスト

require 'rails_helper'

RSpec.describe Api::V1::AreasController, type: :controller do
  render_views

  describe 'GET #show' do
    let(:schema_path)   { '/areas/{id}' }
    let(:schema_method) { 'get' }
    let(:code)          { 200 }

    it 'conform json schema' do
      expect_to_conform_schema response
    end

  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Day004 webエンジニアへの道 - rails tutorialを始める -

こんにちは。
webエンジニアを目指すtomoです。

エンジニアの勉強をしている日々の学びを記録しています。

私自身の頭の整理と今後迷った時のリファレンスも兼ねて書き連ねていきますが、同じようにエンジニア転職を考えている方の参考にもなればと考えています。
また、「ここ間違ってる!こっちが正しい!」といったご指摘もあれば頂けると嬉しいです?


[1.3.2 rails server]のブラウザ表示でつまづく

railsチュートリアルの文章に「クラウドIDEの場合は、[Share] を開いて、開きたいアプリケーションのアドレスをクリックします(図 1.11)。]と記載があり、文章に従って進めていたのですがうまく表示されませんでした。

調べた結果、文章ではなく図1.11と図1.12に従うことでブラウザを正しく表示することができました。

正しくは「クラウドIDEの場合は、[Preview]から[Preview running application]を開いて、アドレスバーの右横にあるブラウザ展開アイコンをクリックする。」のようです。
*[Share]から開く方法があれば教えてください!

[参考]
Cloud9上でのRuby on rails サーバ起動/ページ表示方法(Ruby on rails Tutorial 1.3.2)

★メモ

*後日調べること & 気づいたこと
  • チュートリアルとWikipediaでMVCモデルの概念図が異なるので、他のサイトではどのように説明されているか調べる。

さいごに

5月5,6日は勉強外の予定があり進捗少ないかもしれませんが、毎日少しでもコードを書いて進めたいと思います。


twitterもやっているので、宜しければフォローお願いします!
@tomo_tech_

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【10日間でポートフォリオ作成に挑戦】9日目:フロントエンドの実装〜各種機能の修正

概要

今回は、2019年のGW期間(10日間)を全て費やして取り組むポートフォリオの製作過程を取りまとめた内容を投稿させて頂きます。(投稿は毎日行う予定)

全体通した取り組みの詳細については、前回までの記事をご参照ください。

【10日間でポートフォリオ作成に挑戦】1日目:要件定義〜記事投稿のCRUD
【10日間でポートフォリオ作成に挑戦】2日目:アクセス制限〜コメントのCRUD機能
【10日間でポートフォリオ作成に挑戦】3日目:ページネーション~CKEditorの導入
【10日間でポートフォリオ作成に挑戦】4日目:テーブル分割〜CKEditorのフォームへの反映
【10日間でポートフォリオ作成に挑戦】5日目:CKEditorへ画像アップロード機能を追加
【10日間でポートフォリオ作成に挑戦】6日目:テストコードの実装
【10日間でポートフォリオ作成に挑戦】7日目:検索機能〜いいね機能の実装
【10日間でポートフォリオ作成に挑戦】8日目:記事ストック機能〜ユーザーフォロー機能の実装

今日一日の作業内容

ここからは、今日1日で取り組んだ作業内容をご説明します。

フロントエンドの実装

先日、サーバーサイドのの実装が概ね完了したので、全く未着手だったフロントエンドの実装を行って行きます。
と言っても、私自身、実務でフロントの実装は全く経験していないのと、残り二日という短い期間なので、手早く実装できる手段を採用します。

そこで利用するのが、前回紹介したCSSフレームワークのMaterializeCSSです。
本当は、主流のBootstrapを利用したかったのですが、全く触った事が無いので、今回は諦めました。

使い方としては、公式のドキュメントから、使いたいパーツやデザインを探してきて、そのデザインを適用させる為のclassを、任意の箇所に記述するだけで、実装が完了します。
なので、全くCSSを触らなくても、WEBサイトの体裁を整える事ができます。

↓公式ドキュメント

Screen Shot 2019-05-06 at 1.23.33.png

↓実装したコード

= f.submit value: t('common.button.submit'), class: 'waves-effect waves-light btn orange'

↓結果
Screen Shot 2019-05-06 at 1.24.16.png

そうして、

↓記事の一覧表示

61f4b02b0ef38083298f9dc935485986.gif

↓記事の作成ページ

6c71ee68a3ea04585e7db5ac002943d5.gif

↓検索機能

30b232fc80076504327daaadcf0f1f9f.gif

↓ログインページ

fd1a1f08010b99a2020da7db27f224f2.gif

↓マイページ

e526903fd6e94b9bb1eae5e0d3d64768.gif

即席なので、かなり粗だらけですが、ゆくゆくはCSSフレームワークは使わずに仕上げていきたいと考えています。Vue.jsも使ってみたいですし!

今日の失敗

ここからは今日の失敗をまとめていきます

ページネーションの表示数をベタ打ち

記事の一覧を表示する箇所ではページネーションを導入していたのですが、1ページの表示件数を指定する記述は、各コードにそれぞれ記述していました。

記事の一覧は、下記のコードで表した通り、「通常の一覧表示・検索結果の一覧・ストックの一覧・自身の投稿記事の一覧」の4箇所が該当します。

controllers/posts_controller.rb
def index
  @posts = Post.page(params[:page]).per(10).order(id: "DESC").includes(:user)
end

def search
  @search = Post.ransack(params[:q])
  @posts = @search.result.page(params[:page]).per(10)
  @keyword = params[:q][:title_cont]
end
controllers/users_controller.rb
def show
  @posts = Post.where(user_id: params[:id]).page(params[:page]).per(10).order(id: "DESC")
  @user = User.find(params[:id])
end

def post_stocks
  post_stocks = current_user.post_stocks.pluck(:post_id)
  @posts = Post.where(id: post_stocks).page(params[:page]).per(10).order(id: "DESC")
end

流石に4箇所全てにベタ打ちは冗長なので、変数に置き換える事にしました。
今回は、コントローラーが別れているので、application_controllerに変数を定義しています。

controllers/application_controller.rb
PER = 10

これで、もし表示件数を変更する必要が出て来ても、application_controllerの記述だけ変更すれば良いので、メンテナンス性は高まります。

画像アップロード機能の実装方法に無理がある

念のため、振り返りでER図を再掲します。

Screen Shot 2019-05-06 at 3.57.13.png

注目して頂きたいのが、記事に添付した画像を、どの様に管理しているか?です。
ER図では、PostDescriptionと関連付けさせたimageテーブルで管理する様にしています。

そして、今回の記事の編集はCKEditorを利用していますが、CKEditorは、画像アップロードの操作をした時点で、画像を保存します。

↓この時点

5c1119e716b18c51ea2becac9789f71f.gif

つまり、記事の新規作成の場合、記事の作成より先に、画像がDBに保存されます。
なので、画像保存時に、記事との関連付けを行う事が出来ません(まだ存在しないデータと関連付けは出来ない)

CKEditorは、アップロードした画像のパスも含めて、入力した情報をHTML形式に変換して保存してくれるので、関連付けしていなくとも、編集や詳細表示は問題なく出来ます。

支障をきたすのが、記事の一覧表示で、画像を表示させる場合です。
関連付けしていないので、PostからもImageからも、互いにどのデータと紐づいているか判別出来ません。

それを解消させる為に、下記の様な手段を取りました。

1:postにimage_idカラムを新たに追加し、モデルには下記のアソシエーションを記述

models/post.rb
has_one :image, dependent: :destroy

2:画像保存後に、保存されたレコードのIDをセッションで保持

controllers/images_controller.rb
def create
  image = current_user.description_images.build(
      image: params[:upload],
      image_relation: params[:image_relation]
  )
  if image.save
    render json: {
        url: image.image[:standard].url,
        uploaded: true
    }
     #画像のDBへの保存が完了したタイミングで、IDをセッションに保持
     # 複数枚画像を投稿した際は、最初の画像を登録する様にnilガードで記述
    session[:image_id] ||= image.id
  else
    render json: {
        error: {
            message: image.errors.full_messages
        },
        uploaded: false
    }
  end
end

3:記事保存時に、セッションに保持していたImageのIDも、外部キーとして一緒に保存する

controllers/posts_controller.rb
def create
  @post = current_user.posts.build(post_params)
  @post[:image_id] = session[:image_id]
  if @post.save
    session[:image_id] = nil
    redirect_to post_path(@post), notice: t('common.message.post_create')
  else
    render :new
  end
end

こうする事で、あとは下記のコードで、記事一覧の中で画像も表示させる事が可能になります。

views/posts/index.html.haml
.nav-wrapper.container
  %h5.header.orange-text
    = t('common.header.post_index')
  .row
    -# 要素の数だけ繰り返し
    = render @posts
views/posts/_post.html.haml
= link_to post_path(post) do
  .col.s6
    .post.card
      .card-image
        - if post.image_id.present?
          -# この記述で対象のpostと関連付いた画像を表示させる
          -# (:standard)は任意で変更できる様にした画像リサイズのオプション
          = image_tag post.image.image_url(:standard)
        - else
          = image_tag 'no_image.png'
      .card-title
        = post.title
      .card-user
        = "投稿者:#{post.user.name}"

もっと良い方法がRailsやCKEdtorにはあるのかもしれませんが、あまり調査に時間を掛ける余裕も無かったため、一旦この方法で実装しました。

これについては、追い追い調査して、効果的な方法を探りたいと考えています。

明日の予定

  • AWSへのデプロイ
  • HTTPS化
  • フロントの調整

AWSへのデプロイが1ヶ月ぶりの作業になるのと、HTTPS化が初の試みなので、本当に明日1日だけで完成できるのか?かなり不安が残ります。

いずれにしても、この10日間の開発で終わりにするのではなく、今後も継続して開発は続けて行き、機能を更にブラッシュアップさせて行こうと考えています。

※追記:10日目を投稿しました
【10日間でポートフォリオ作成に挑戦】10日目:AWSでのデプロイ

おまけ

最後になりますが、現在、私は下記の目標を立てて学習に取り組んでいます。

  • 3年間で「10,000時間」をプログラミングに費やす
  • その間、毎日ブログの投稿を行う

Twitterでは、その過程で学んだ事などを発信しています。
もし宜しければフォローしてみてください。

Twitter:@ryoutaku_jo
ブログ:りょうたくのWEBエンジニア日記

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Selenium】Courseraのシラバスをパース| Ruby

はじめに

  • みなさんCoursera受講してますか?少しブームは過ぎましたが、GWにCourseraの機械学習を受講してみようと思ったところ、シラバスの一覧が取得できないことに気がつきました。勉強計画を立てる際に、一覧を取得したい派なのでSeleniumを用いて取得し、CSVに出力します。
  • 言語はRubyです。

環境構築

  • 事前にchromeのwebdriverをインストールして下さい(インストール方法は以前Qiitaで紹介されたものを挙げます: https://qiita.com/y-agatsuma/items/ea2c9845ee0a931d5c9c)
  • Rubyでは、installしたgemを記したGemfileファイル(Pythonでのpip installした一覧が書いてあるファイル)があるので、本リポジトリをGithubからcloneするとすぐに使用できるようになっています
  • リポジトリをcloneした後は、ターミナル上で$ bundle install --path .bundleと入力して下さい。その後.bundleに色々インストールされます。(Github: https://github.com/Beluuuuuuga/class_title_parser)

実行方法

  • ターミナル上で本ディレクトリに移動し、次のコマンドを入力します。$ bundle exec ruby coursera_parser.rb
  • その後inputでURLを入力します。(例では機械学習)https://www.coursera.org/learn/machine-learning?

情報取得情報

  • URLのトップページ
    top.png

  • 取得情報の終了までの時間と授業の内容
    time_title.png

パーサーについて

  • Seleniumの使用方法などは色々紹介されているので割愛します。
  • 授業を表すユニークなキーをデータベースに保存し、そこから全てのシラバスを取得することも考えましたが、ここでは目的の授業のURLを入力してシラバスの情報を取得します。
  • sleep 1が入っていますが、これはクローラーではなくパーサーで負荷が重くなることはそれほどないと思いますが、連続で実行してしまう方がいるかもしれないので念のため入れています。
  • chromeはヘッドレス(プログラム起動時に画面を出さない)を適用していますが、確認したい方はプログラムのコメントアウトを外して下さい。
require "selenium-webdriver"
require 'csv'

sleep 1
def chrome_setup
  ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36"
  args = ["--headless", "--no-sandbox", "--disable-gpu", "--user-agent=#{ua}", "window-size=1280x800"]
  #args = ["--no-sandbox", "--disable-gpu", "--user-agent=#{ua}", "window-size=1280x800"]
  caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {args: args})
  session = Selenium::WebDriver.for :chrome, desired_capabilities: caps
  # session.manage.timeouts.implicit_wait = 30
  session
end

url = gets.chop
#url = "https://www.coursera.org/learn/machine-learning?" #sample_url1 機械学習
#url = "https://www.coursera.org/learn/language-processing?" #sample_url2 自然言語処理

time_ary = []
content_ary = []
session = chrome_setup
wait = Selenium::WebDriver::Wait.new(:timeout => 100)
session.navigate.to url
title = session.find_element(:css, '.H2_1pmnvep-o_O-weightNormal_s9jwp5-o_O-fontHeadline_1uu0gyz.max-text-width-xl.m-b-1s').text
session.find_element(:css, '.Button_1w8tm98-o_O-default_s8ym6d-o_O-md_1jvotax.m-t-1.d-block.m-x-auto').click
#wait.until {session.find_element(:css, '').displayed?}
session.find_elements(:css, '.Row_nvwp6p.SyllabusWeek.m-b-3').each do |ele|
  time_ary << ele.find_element(:css, '.Strong_gjs17i-o_O-weightBold_uvlhiv-o_O-fontBody_56f0wi.text-secondary').text.slice!(0)
  content_ary << ele.find_element(:css, '.H2_1pmnvep-o_O-weightBold_uvlhiv-o_O-bold_1byw3y2.m-b-2 ').text
end

csv_header = ["hour","content"]
CSV.open("#{title}.csv", "w") do |csv|
  csv << csv_header
  time_ary.zip(content_ary).each do |ele|
    csv << ele
  end
end

session.close

技術的に難しかったこと

  • はじめはどこかにjsonファイルがまとまっているのでそれをまとめて取ればいいので簡単だと考えていました。しかし、上のシラバスの画像のように、全11週あるうちの第4週までが、最初の画面に存在しており、その後[詳細]ボタンをクリックすることでJavaScriptが起動してjsonファイルが読み込まれ残りの週が表示される機能になっていました。
  • Seleniumに関する記事はPythonが中心だったので、参照資料が少なかったです。Pythonで使用できる方法が、Rubyで使用できないこともありました。

出力結果

  • コマンド実行後、CSVファイルが作られます。CSVのタイトルは自動でtitleタグをパースするようになっています。CSVファイルはCourseraの機械学習のシラバスです。
hour,content
2,Introduction
3,Linear Regression with Multiple Variables
2,Logistic Regression
5,Neural Networks: Representation
5,Neural Networks: Learning
5,Advice for Applying Machine Learning
5,Support Vector Machines
1,Unsupervised Learning
2,Anomaly Detection
1,Large Scale Machine Learning
1,Application Example: Photo OCR

おわりに

  • Seleniumはハードル高いようでしたが、案外やってみると簡単で驚きました。
  • Courseraでエンジニアリング力高めていきましょう!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む