20191028のRubyに関する記事は15件です。

Rails5.2 + Vue.js + Dockerでプロジェクトを作成

はじめに

Railsを使っていてVueも一通り学んだのでRailsとVueを使えるように環境構築してみようと思い勉強してみたことをアウトプット。

Rubyのバージョンは2.5.3
Railsのバージョンは5.2.3
DBはMysqlを使用

Railsのプロジェクトを作成するディレクトリを事前に作成しておく
$ mkdir <任意のフォルダ名>
フォルダを作成したら作成したディレクトリに移動しておく

Docker用のファイルの作成

Dockerのコンテナを作成するためのファイルを作成

Dockerfile
docker-compose.yml
Gemfile
Gemfile.lock
の4つのファイルを作成する。

Dockerfile
FROM ruby:2.5.3

RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && apt-get update && \
    apt-get install -y nodejs --no-install-recommends && rm -rf /var/lib/apt/lists/*

RUN apt-get update -qq && apt-get install -y build-essential libpq-dev

RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && apt-get install -y yarn

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
    apt-get install nodejs

RUN yarn add node-sass

RUN mkdir /app
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install
COPY . /app

docker-compose.yml
version: '3'
services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db
    tty: true
    stdin_open: true
  db:
    image: mysql:5.7
    volumes:
      - db-volume:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
volumes:
  db-volume:
Gemfile
source 'https://rubygems.org'
ruby '2.5.3'

Gemfile.lock
はbuildした後に自動的に記述されるので空にしておく。(ファイルは必要)

この4つのファイルを作成し、先ほど作成したディレクトリに置く。

Railsプロジェクトの作成

次にRailsのプロジェクトを作成するため次のコマンドを実行
$ docker-compose run web rails new . --force --database=mysql
(rails newの . は現在いるディレクトリにそのままプロジェクトを作成するというもの)

プロジェクトの作成が終わったらdocker-compose psでコンテナが作成されているか確認。
次にRailsプロジェクトのdatabase.ymlを編集します。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: db

passwordとhostの部分をdocker-compose.ymlの記述と合わせます。

$ docker-compose upコマンドでコンテナを立ち上げ、localhost:3000に接続し、RailsのHello World画面が表示されれば無事成功です!

Vue.jsの導入

vue.jsをRailsで使用できるようにするためにまずはWebpackerの導入が必要。Gemfileに以下を追加します。

Gemfile
gem 'webpacker'

記述したら

$ docker-compose run web rails webpacker:installを実行

自分の場合、Gemfileの記述を
gem 'webpacker', github: 'rails/webpacker'
とするとyarnエラーが出てインストールができませんでした。

インストールが完了したらいろいろRailsのプロジェクトにファイルが追加されると思います。

次にVueをインストールしていきます。
$docker-compose run web rails webpacker:install:vue

これでVue.jsの導入が完了しました。

vue.jsファイルのビルド

次にVue.js関連のコンポーネントをJavascriptにコンパイルするため
$ docker-compose run web bin/webpackコマンドを実行します。

RailsのViewsファイルに
<%=javascript_pack_tag 'hello_vue'%>と記述し問題なく表示されていれば成功です!

参考
dockerでrails+vueの環境を作ったので解説
Rails5.2 + Docker環境にVue.js (Webpacker) を導入する

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

Rails: validates: 指定した値の時だけ複数カラムでuniquenessを制約を掛ける方法備忘録

ケース

一つのfugaに対して紐付く複数のhogeの内、is_piyoがfalseであれば幾らでも登録できるが、trueのものは一つしか登録できないようにしたい

コード

app/models/user.rb
class Hoge < ApplicationRecord
  belongs_to :fuga

  validates :is_piyo,
            inclusion: { in: [true, false] },
            uniqueness: { scope: :fuga_id,
                          conditions: -> { where(is_piyo: true) }}
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rubyの予約語を使っちゃったカラム名をfactory_botでテストしたい

例えば class というカラムを作ってしまっけれど factory_bot でテストしたいと思ったら…

factory :reservation do
  add_attribute(:class) { 'A組' }
end

add_attribute

"Getting Started" に書いてある!
https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#method-name--reserved-word-attributes

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

【rails】かんたんログイン機能導入方法

現在railsでポートフォリを作成中ですが、よりスムーズに使ってほしいためにこの機能を導入したくなったので作りました。
若干手抜き感が否めないですが、一応使えます。

これはdivise導入済みで進めていきます。

まずはテストユーザーで新規登録します。

name:テストユーザー
email:test@example.com
password:testtest

ログインを簡単にする機能なのでまずは登録ないと始まりません。

view側を編集

もともとviews/devise/sessions/new.html.slimというファイルがありますが中身を真似て、
views/devise/sessions/_testuser.html.slimというファイルを作ります。

_testuser.html.slim
    h2
      | かんたんログイン
    = form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
      .field
        =f.hidden_field :email ,value: "test@example.com"
        =f.hidden_field :password ,value: "testtest"
      .actions
        = f.submit "かんたんログイン"

このように書いてviews/devise/registrations/new.html.slim

new.html.slim
.......
= render 'devise/sessions/testuser'

これで新規登録画面にかんたんログインボタンが表示されるようになります。

編集ボタンを隠す

私の場合はviews/users/show.html.slimにユーザー詳細画面がありますがテストユーザーのみ編集ボタンをを隠したいと思います

show.html.slim
.....
 - unless @user.email == "test@example.com"
  = link_to "edit",edit_user_registration_path

これではurlに直接打ち込むとアクセスできてしまうのが注意です。

deviseに対応したコントローラーを導入

$ rails g devise:controllers users

ルーティングの編集

userのコントローラーを作ったので、そこにアクセスするようにルーティングを編集します。

routes.rb
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

コントローラー内を編集

テストユーザーを編集、削除されてほしくないので制限する。

registrations_controller.rb
......
before_action :forbid_test_user, {only: [:edit,:update,:destroy]}
......

  private
  def forbid_test_user
      if @user.email == "test@example.com"
        flash[:notice] = "テストユーザーのため変更できません"
        redirect_to root_path
      end
  end
.......

これによりかんたんログイン機能ができるようになります。
参考:【学習アウトプット2】離脱率を下げる!かんたんログイン機能の実装

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

#Ruby で特定の桁数の、ランダムな数字を生成する例。ゼロ埋めバージョン。

8桁の場合

sprintf('%08d', rand(99999999))
=> "15419306"
sprintf('%08d', rand(99999999))
=> "06627530"
sprintf('%08d', rand(99999999))
=> "75424850"

別解

8.times.map { rand(9) }.join
=> "08628150"
8.times.map { rand(9) }.join
=> "88810560"
8.times.map { rand(9) }.join
=> "31853611"

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2633

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

Could not update RVM rvmのバージョンアップの対処法

rvmのアップデートができない

WSLの環境でRVMのバージョンアップを行おうとした所、
以下のように表示がされ、アップデートができませんでした。

Could not update RVM, please report to https://github.com/rvm/rvm/issues

解決方法

以下のコマンドを入力することで、
無事に最新版にアップデートできるようになりました。

$ rvmsudo rvm cleanup all

以下のissueを参考にしました

https://github.com/rvm/rvm/issues/4345

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

RubyのCSV.tableはヘッダーを小文字に変換するので注意

注意というか自分が多少ハマったのでメモしておきます。

次のようなカラムのテーブルデータのcsvを読み込もうとして、 CSV.table を使って読み込もうとしました。

CampaignId,CampaignName
1000,example1
1001,example2

すると以下のように自動で小文字に変換されてしまっていました。これによって一部のキャンペーン情報が読み取れずにエラーが発生してしまっていました。

require 'csv'

table = CSV.table("report.csv")
p row[0][:CampaignId]
# => nil

p row[0][:campaignid]
# => 1000

実際にドキュメントを読むと、 header_converters: :symbolが設定されており、

CSV.read( path, { headers:           true,
                  converters:        :numeric,
                  header_converters: :symbol }.merge(options) )

HeaderConvertsの挙動は以下のようなものらしいです。

ヘッダの文字列を小文字に変換してから、空白文字列 (\s) をアンダースコアに 置換し、非英数字 (\W) を削除します。最後に String#to_sym を呼び出します。

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

【第2回】Ruby + Seleniumを使ってスクレイピングしてみる

@cosmeの商品レビューをスクレイピングしてみる

第1回の前置きが長くなってしまいましたが、今回からコーディングしていきたいと思います。

想定仕様

1.プロダクトIDが入ったURLを受け取る
2.上記で指定した商品の各レビューページに設置されている「続きを読む」リンクを踏み、全文レビューページに遷移
3.レビュー全文を取得する
4.同ページにある「次へ」リンクを踏み、次のレビュー全文ページに飛ぶ
5.3&4を任意で指定したレビュー取得件数分繰り返す
6.完了したらchromeを閉じる

こんな感じの仕様ができればと思います。Seleniumを使うことで、「4.同ページにある「次へ」リンクを踏み、次のレビュー全文ページに飛ぶ」という仕様ができるのが本当にデカいですね。これなら各レビューIDを指定する必要もないので、100件でも1000件でも楽々取得できるようになります。

Let's ruby

今回使用するライブラリはこんな感じです

#自動ブラウジング
require 'selenium-webdriver'
#HTMLをパースする
require 'nokogiri'
#指定したURLを開く&レビュー本文を取得
require 'open-uri'
#既存のxlsxフォーマットの書き込み
require 'rubyXL'

nokogiriopen-uriはスクレイピングではほぼ必須のライブラリ。
今回はxlsxファイルへの「書き込み」のみ(読み込みはしない)ですのでrubyXLを使用します。

Selenium起動&全文レビューページに遷移

#Seleniumを立てる(今回はchromeを使います)
driver = Selenium::WebDriver.for :chrome

#商品ページのURLを変数input_urlで受け取る
input_url = "https://www.cosme.net/product/product_id/10163439/top"

#input_urlで受け取ったページに移動する
driver.navigate.to("#{input_url}")

#「続きを読む」リンクを踏み、全文レビューページに遷移する
driver.find_element(:partial_link_text,'続きを読む').click

#カウンター(後のwhile文で使います)
count = 0

#取得レビュー件数を変数limitで受け取る
limit = 50

#商品名を変数nameで受け取る(任意の商品名を設定する。出力するxlsxのファイル名になります。)
name = "チェリー本"

#変数reviewsの空配列を置いておく(取得した各レビューは、変数reviewsの配列の要素として代入されます。)
reviews = []

driver = Selenium::WebDriver.for :chrome
今回はchromeで行いたいと思います。firefox, IE, Safariなどにも対応しているようです。

limit = 50
取得レビュー件数はここで指定します。今回はwhile構文を使った繰り返し処理を行いますが、その繰り返しの回数=limitの値になります。

reviews = []
取得した各レビューは配列の要素としてreviewsに追加できるようにします。

while文による繰り返し処理

while count < limit.to_i
  #現在のページのURLを変数current_urlで取得
  current_url = driver.current_url

  #UTF-8に変換(@cosmeの文字コードはShift_JISなので)
  doc = Nokogiri::HTML.parse(open(current_url, "r:Shift_JIS:UTF-8").read)

  #レビューを指定&本文のみを変数nに代入
  n = doc.css("p.read").text

  #変数reviewsに配列要素としてnを代入する
  reviews << n

  #カウンターを追加
  count+=1

  #Dos対策(次のレビューページに遷移するまで1秒以上空ける)
  sleep 1

  #ページ内の「次へ」を選択&ページ遷移
  driver.find_element(:partial_link_text,'次へ').click
end

while count < limit.to_i
今回はwhile文を使ってみました。構文内にカウンターを設置し一周ごとにcount+=1になるようにしてます。limitで指定した取得レビュー件数を上回るまで繰り返し処理を実行する、という仕組みです。

doc = Nokogiri::HTML.parse(open(current_url, "r:Shift_JIS:UTF-8").read)
なんとよくよくみたら@cosmeはShift-JISだったことが判明。UTF-8に変換しておきます。

<meta http-equiv="Content-Type" content="text/html; charset=shift_jis">

sleep 1
次のHTTPリクエストまで1秒以上空けます。

取得したレビューをEXCELに書き込む

#フォーマットのxlsxを選択
workbook = RubyXL::Parser.parse("@cosme_format.xlsx")

#シートを指定&シート名を変更
worksheet_a = workbook[0]
worksheet_a.sheet_name ="@cosmeレビュー収集"

#指定したセルに各レビューを書き出す
p = 1
reviews.each do |re|
  worksheet_a.add_cell(p,2,"#{re}")
  p+=1
end

#書き込みが完了したらブラウザを終了する
driver.quit

#保存する(序盤で任意で設定したnameの値がここに代入されます。)
workbook.write("@cosmeレビュー収集結果_#{name}.xlsx")

workbook = RubyXL::Parser.parse("@cosme_format.xlsx")
エクセルを操作できるrubyのライブラリは色々ありますが、今回は読み込みはせず書き込みだけなのでrubyXLを使ってみました。

アウトプット例

モザイクかけましたがこんな感じです。
image.png

まとめ

sleep 1で次のリクエストまで1秒開けているものの、あまりにも膨大な量を続けてリクエストすることはあまり良くないので、使用には十分注意する。長時間連続して回し続けないこと。
・特殊絵文字?などがレビュー本文に含まれている場合、エラーが起きて処理が止まってしまうので例外処理を用いて止まらないようにする。

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

【第1回】Ruby + Seleniumを使ってスクレイピングしてみる

@cosmeの商品レビューをスクレイピングしてみる

ゴール

@cosmeの各商品のレビューを自動収集し.xlsxに書き出す。
(余力があれば各レビューの品詞解析&ポジネガ判別もできればよいかも)

条件

Ruby 2.4.6
Ruby on Rails 5.2.6

なぜSeleniumを使うのか

これは@cosmeのページ構成が大きな理由です。他のECサイト(Amazon, LOHACO)と@cosmeのHTMLを比較してみます。

Amazon

Amazonでは1ページに最大10レビューが表示される仕組みになっています。
各レビューは全文が<span class>に入っているので、スクレイピングの際はこのクラスを指定すれば、レビュー本文だけをを丸ごと抜き出すことができます。(LOHACOも同様の仕組みです。)

@cosme

@cosmeの商品レビューは、同一URLに全文が表示されません。
各レビューの最後にある「続きを読む」リンクを踏むと、そのレビュー全文ページに遷移します。(言うまでもなく、URLも変わります。)
なので、@cosmeの商品レビューのスクレイピングをする時には、
①スクレイピングする商品レビューページの「続きを読む」のリンク先に遷移
②レビュー全文をスクレイピングする
のフローを経る必要があります。

まとめますと、もし100件のレビューを取得するとしたら、
・Amazon
 1ページ(1URL)10レビュー × 10ページ分
@cosme
 1ページ(1URL)1レビュー × 100ページ分   のスクレイピングが必要になるわけです。大変ですねこれは・・・。

加えて、AmazonであればレビューページのURLに規則性があるので、
例:1ページ目 amazon.co.jp/商品名/商品IDとか/review/page=1
  2ページ目 amazon.co.jp/商品名/商品IDとか/review/page=2
とまあこんな感じに規則性があるので、もし8ページ分取りたければ1ページごとに最後のページ数に+1してやるだけで済みます。

しかし@cosmeの全文レビューページURLには規則性が何もありません。レビューごとに固有のreview_idなるものが割り当てられているので、これを直接指定しないことにはどうにもこうにもならないってことらしいです。
例:全文レビューページのURL 
  cosme.net/product/product_id/商品ID/review/レビューID(アットランダムな整数9桁)

はいこれは困りますね・・・100件分のレビューIDなんて知らんし打ち込むのめんどくさいしなんかいい方法ないかなーと困っていたところ思いついたのが「Selenium」ってわけです。(前置き長くてすみません笑笑)

Seleniumでできること 

ひとことで言えば「自動でブラウジング」ができます。今回のケースだと、ブラウザ起動して、ECサイトに飛んで、商品名入力して、検索一覧から商品選択して、レビューページ飛んで、「続きを読む」クリックして、、、、、、、とかいうのをぜーんぶ自動でやってくれます。賢いですね。(×100回)
他にも、テキストボックスに自動入力したり、スクショとったり、けっこう色々できちゃいます。
https://www.seleniumqref.com/index.html 
(↑↑有志による??リファレンスがあったのでこちらに載せさせて頂きます。)

想定仕様

・任意の商品URL&取得レビュー件数を指定
・xlsxのフォーマットをあらかじめ用意し、取得したレビューを上書きする

長くなったので、第二回に続けます。
https://qiita.com/sasaken33/items/9f07b8ff677bf49fa2ba

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

Rails6 のちょい足しな新機能を試す100(不正なURLパラメータのリクエスト編)

はじめに

Rails 6 に追加された新機能を試す第100段。 今回は、 不正なURLパラメータのリクエスト編です。
Rails 6 では、 不正なパラメータのリクエストの場合でもエラーにならずに、Railsのエラー画面を表示するようになりました。

Ruby 2.6.5, Rails 6.0.0 で確認しました。

$ rails --version
Rails 6.0.0

今回は、Userの CRUD を作って確認します。

Rails プロジェクトを作る

$ rails new rails_sandbox
cd rails_sandbox

User の CRUD を作る

$ bin/rails g scaffold User name

データベースのマイグレーションを実行する

$ bin/rails db:create db:migrate

不正なパラメータのURLにアクセスする

ここで、 rails server を実行して、不正なパラメータつきの存在しないURLにブラウザからアクセスしてみます。
今回は、 http://localhost:3000/foo?x=1&x[y]=2 にアクセスしてみます

エラー画面が表示されます。

error1.png

このときパラメータの情報は画面には表示されません。

試しに不正ではないパラメータを使い http://localhost:3000/foo?x=1 にアクセスしてみます。

このときは、パラメータの情報が画面に表示されます。

error2.png

Rails 5 では

Rails 5.2.3 では、 http://localhost:3000/foo?x=1&x[y]=2 にアクセスしたときに Rails のエラー画面は表示されません。
error3.png

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try100_malformed_url

参考情報

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

遷移元のコントローラーとアクションを取得して、条件分岐をする

~同じコントローラーのアクションを使って今いるページからのリンク先を変える方法~

起きていたこと

1.新規登録の時(修正前)

/signup/cards

cards.png
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

/card/show

show.png

2.カード情報を追加登録の時(修正前)

/card/new

new.png
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

/card/show

show.png

"1.新規登録"の時の遷移先を/signup/finishにしたいが
問題のコントローラーを確認すると/signup/finishに飛んでない!!**

card_controller.rb(修正前)
  def pay 
    Payjp.api_key = ENV["PAYJP_ACCESS_KEY"]
          [省略]
      if @card.save//保存できたら
        redirect_to action: "show"//カード情報を表示する
      else//保存できなかったら
        redirect_to action: "pay"
      end
    end
  end

やりたいこと

/signup/finishを設定してない遷移元のコントローラーとアクションを指定して条件分岐できないだろうか?
=>会員登録の時とユーザー設定ページのそれぞれでcardコントローラーでnewを使用したした時で分けたい
理想形.png

結論:request.referrerを使用する

request.referrerの例
遷移元のコントローラーを取得
Rails.application.routes.recognize_path(request.referrer)
 {:controller=>"users", :action=>"index"}

こちらで遷移元の URL が取得できる
request.referrer
 "http://localhost:3000/users"

=========================================

request.referrerを実装する

card_controller.rb(修正後)
  def pay 
    Payjp.api_key = ENV["PAYJP![cards.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487723/8eae145d-f132-acff-0c61-a44229d99489.png)
_ACCESS_KEY"]
          [省略]
       path = Rails.application.routes.recognize_path(request.referer)
        if path[:controller] == "card" && path[:action] == "new" //もしも、"card"コントローラでnewを行ったら、
          redirect_to action: "show" //showを行う
        else //違ったらfinishへ飛ばす
          redirect_to finish_signup_index_path
        end
          [省略]

修正した結果

1.新規登録の時(修正版)

/signup/cards

cards.png
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

/signup/finish

finish.png
falseで読み込まれるため/signup/finishへ飛ぶ

実行したところ
        else //違ったらfinishへ飛ばす
          redirect_to finish_signup_index_path

2.カード情報を追加登録の時

trueのためこちらの結果は修正前と変わらない

参考記事

https://qiita.com/sayama0402/items/ffe96ff76148231a0c22
http://kawahiro.hatenablog.jp/entry/2014/03/21/164449

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

nkf&KAKASIと編集距離で"それっぽい"見出し語を検索する

ご挨拶

バイトルなどでおなじみのディップ株式会社に新卒で入社しました @raitehu です.

この記事は何?

今年の春にわたしたち新卒3人に与えられた研修課題は「いい感じに社内用語を検索できるシステム」を作ることでした.
そのシステムの内部で実装した表記ゆれ・誤字脱字があっても"それっぽい"言葉を検索する機能についてご紹介します.

作ったもの

  1. ユーザがSlackで「ask hoge」と発言する
  2. あらかじめ登録してある見出し語と解説をSlack botが発言する(部分一致)
  3. もし2.で一致する語が見つからなかった場合は"それっぽい"語を3つほどサジェスチョンする

というアプリです.
実際にはタグ機能や登録・編集・削除といった機能もありますが,
本記事では3.の"それっぽい"語を探す部分をピックアップしてお話します.

表記ゆれ・誤字脱字への対応

完全一致・部分一致では検索できないものが多すぎる

たとえばQiitaという見出し語が登録されていたとして,
qiitaキータという検索ワードもヒットさせたい...どうすれば...
かといってヒットさせたい全ての単語を一つの解説文に登録させなければいけないのも微妙ですよね.

STEP1 大文字・小文字・半角・全角の差異を統一する

Qiita,qiita,Qiita,qiita,きーた,キータ,キータといった表記ゆれをできるだけ統一して検索する必要がありそうです.
そのためにタイトルにもあるnkf(*1)とKAKASIを利用しました.

1-1 英数字の標準化

nkfによって英数字は半角小文字を標準として統一しておきます.
これは以下のコードで実現できます.

nkf_sample.rb
require 'nkf'

def half_width_word(word)
  NKF.nkf("-m0Z1 -W -w", word).downcase
end

これによって,Qiita,qiita,Qiita,qiitaは[qiita]に統一できます.

1-2 日本語の標準化

一方できーた,キータ,キータといった日本語も統一しておきたいですね.
欲を言うと[qiita]を検索するメソッドと同じメソッドで検索を行いたいです.
そこで日本語->ローマ字変換プログラムであるKAKASI(*2)を利用しました.

きーた,キータ,キータはnkfによって全角に直し,KAKASIによって半角英数字小文字化します.
上記のコードnkf_sample.rbにKAKASIも足したものが次になります.

furigana_generater.rb
require 'kakasi'
require 'nkf'

def generate_romaji(word)
  Kakasi.kakasi('-Ja -Ha -Ka', NKF.nkf("-m0Z1 -W -w", word)).downcase
end

これによって,きーた,キータ,キータは[ki-ta]に統一されました.
このようにして登録段階でフリガナ(実際には英数字)を生成し,見出し語・解説と共にフリガナをDBに登録しておきます.
これで検索段階でも検索ワードのフリガナを生成してフリガナどうしで近いものを探せば見つけられそうだと思いませんか?
(ちなみにKAKASIは独自の辞書を内部にもっているため,漢字も無理やりローマ字に変換されます)

STEP2 "それっぽさ"を測る

2-1 文字列の距離

ここでいう"それっぽさ"とは語句の意味の近さではなくて,文字列の近さ=編集距離です.
例を出すと,
[ラーメンと中華そば],[ごはんとライス]は意味的な近さはありますが,文字列的にはまったく近くなく,
[ラーメンとソーメン],[ごはんとごばん(碁盤)]は意味的には全く近くないですが,文字列的にはかなり近いです.
雰囲気はつかめましたでしょうか?

ラーメン<->ソーメンはを一文字変えるだけで変換可能,
ごはん<->ごばんはを一文字変えるだけで変換可能です.
編集操作として追加,編集,削除を考えた文字列同士の距離をレーベンシュタイン距離というそうで,
本サービスもレーベンシュタイン距離を文字列の"それっぽさ"の指標としました.
実装は動的計画法(DP)を用いた,シンプルなものにしました.
(もっと効率的な導出法もあるみたいです(*3))

calc_levenshtein.rb
def calc_levenshtein_distance(word_1, word_2)
  n = word_1.length
  m = word_2.length
  dp_table = Array.new(n + 1){ Array.new(m + 1, 0) }

  (0...n+1).each do |i|
    dp_table[i][0] = i
  end
  (0...m+1).each do |j|
    dp_table[0][j] = j
  end

  (1...n + 1).each do |i|
    (1...m + 1).each do |j|
      word_1[i - 1] == word_2[j - 1] ? cost = 0 : cost = 1
      surrounding_cells = [
                            (dp_table[i - 1][j] + 1),
                            (dp_table[i][j - 1] + 1),
                            (dp_table[i - 1][j - 1] + cost)
                          ]
      dp_table[i][j] = surrounding_cells.min
    end
  end
  dp_table[n][m]
end

これに2つのフリガナを入れてあげることで,その2つのフリガナ間の編集距離が分かります.
たとえば[qiita][ki-ta]の距離はqki-の2文字を入れ替えれば変換可能ですので距離2となります.

2-2 文字列の距離を標準化する

順調そうですが,ここでの問題が1つ.
1万文字がんばって打った中で1文字だけの打ち間違いと2文字打った中での1文字の打ち間違いが同じレベルのミスとみなされてしまうわけです.
割合でいえば0.01%のミスと50%のミスが同等,これはちょっと感覚とズレてますね.
そこで標準化として,2つの文字列のうち長い方の長さで割ってあげるという1工夫をしておきます.

calc_normalized_levenshtein.rb
def calc_normalized_levenshtein_distance(word_1, word_2)
  longest_word_length = [ word_1.length, word_2.length] .max
  calc_levenshtein_distance(word_1, word_2).to_f / longest_word_length
end

STEP3 すべての見出し語のフリガナ中,距離最小のものを探す

もうあと一息です.
検索ワードとすべての見出し語の距離が出せるので,距離が小さい順に並べれば"それっぽい"ものを好きな数だけ抽出できます.
以下のサンプルでは一つの解説文に対して複数の見出し語が存在する.
つまり,HeadWordモデル(見出し語)はArticleモデル(解説文)と1対多の関係です.
([Qiita]の解説文に対して見出し語が[qiita],[キータ]など複数いれたかったので...)

なので同一のarticle_idに紐づいた見出し語が複数の編集距離をもちますが,
同一の解説文をいくつも出しても意味がないので複数の見出し語をもつものについては最小のものを採用しています.

また,閾値を設定してあまりにかけ離れた言葉を除外しています.
例では0.5を閾値にしているため,[qiita]に対して
[qita(0.2)],[kita(0.4)],[ki-ta(0.4)],[kiita(0.2)]などはヒットしますが,
[qiiiiiiiita(0.55)],[qixxx(0.6)]などは除外されます.

tolerant.rb
def search_tolerant(search_word)
  # 検索ワードをローマ字化する
  search_word_romaji = generate_romaji(search_word)
  # head_wordsはid, 見出し語(headword), フリガナ(romaji), article_id(解説文のid)をもつ
  head_words = HeadWord.all
  # 何も登録されていないときは空配列を返す
  return [] if head_words.blank?

  # distance_recirds = {article_id, 編集距離(float型)}のハッシュが格納される配列、格納順序はhead_wordsのid昇順
  # 全見出し語のromajiと検索ワードのフリガナの標準レーベンシュタイン距離を求めていく
  distance_records = []
  head_words.each do |head_word|
    calculated_distance = calc_normalized_levenshtein_distance(head_word.romaji, search_word_romaji)
    distance_records.push({
                            id: head_word.article_id, 
                            distance: calculated_distance
                          })
  end

  # 同一article_idの中で最小の編集距離のものだけに抽出する
  # 閾値を[0.5]とし,これをこえる距離のもの=半分以上間違ってるものは除外する
  # 結果的に空っぽの配列が帰ってくることもある
  results = distance_records.group_by{ |record| record[:id] }.map do |_, values|
    minimum = values.map{|value| value[:distance]}.min
    {:id => values[0][:id], :distance => minimum} if minimum <= 0.5
  end

  # 閾値を超えるものはnilで記録されるので配列中のnilを消す
  results.compact!

  # 編集距離順にならべかえる
  results.sort_by! { |result| result[:distance] }

  # article_idだけのint配列で返す
  results.map{|result| result[:id]}
end

さいごに

正直なところ社内用語検索が目的なので,検索対象が多くない前提で作っています.
そのため検索速度面では改善の余地がかなりあると思うのでゆくゆくはそのあたりを効率化できたらと思っています.

*1 【 nkf 】コマンド――文字コードと改行コードを変換する
*2 Ruby on Railsで漢字/ひらがな/カタカナをローマ字に変換したい
*3 編集距離(レーベンシュタイン距離)を理解し、実装する

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

【69日目】「オブジェクト指向でなぜつくるのか」を読んで学んだこと 1

パーフェクトPHPで学習を進めているのですが、5章のクラスとオブジェクトからどうも進捗が芳しくない。
サンプルコードを読んでも理解できないことが多くなった気がします。
PHPの文法の知識がないのもありますが、オブジェクト指向の理解ができておらず、クラス、インスタンス、オブジェクトと言った用語に翻弄されている気がしたため、オブジェクト指向を学ぶことにしようと思いました。

そこで学習書籍に選んだのがこちら
オブジェクト指向でなぜつくるのか
軽く手にとってみたところ、ネットでよく見かける説明とは異なる説明の仕方がされていたため選択。
3日程度で読んでみて、学んだことをまとめていこうと思います。

今日の記事では時間の都合もあり、かけるところまで書いて次回に続きます。

1 歴史から理解するオブジェクト指向

 本書は「なぜ」と「なに」に注目した解説書でした。
 私がこれまで触れてきたオブジェクト指向についての教材は、Railsアプリを作成するための動画教材中で「インスタンス変数」とかを使う際にちょっと説明で出てくるもの程度でしたから、「how」に重きが置かれていました。なので、「インスタンス変数はクラス内で『@変数名 = new.Class(引数)』とすれば生成できる」ということは知っていますが、インスタンス変数が何かはあまりわかりません。
 オブジェクト指向では変数も関数も全てオブジェクト = モノとして扱える、と聞いたことはありますがどういう意味かはあまりわかりませんでした。
 本書では、歴史的な経緯から何を求めてオブジェクト指向が開発されたのかを説明してくれます。
 つまり、オブジェクト指向で実現したい目標を理解した上で、コードを見ることができるようになるのです。
 ※ただし、私は実際のコードを見た経験が少ないせいもあり、本書を読んでもポリモーフィズムが具体的にどう実装されるのか、実際のコードのイメージがあまり沸いていません。PHPでいうabstractクラスのことでしょうか。

1.1 オブジェクト指向以前のプログラミング言語の課題

 プログラミング言語は最初「プログラミング難易度の易化 = 生産性の向上」のために開発されました。
 当時はハードが貧弱だったためプログラムは短命で、改修のたび一から作り直していたようで、簡単に作れるようになることが求められました。また初期の言語は非常に難しく、平易に表現できることが求められました。
 しかしながら、ハードが進化しプログラムの寿命が延びると、プログラムをメンテナンスして使い続けるようになります。すると、言語には「保守性」が求められるようになりました。

 その流れを確認して、オブジェクト指向が解決したい課題について確認します。

1.1.1 機械語

 最初のコンピュータは全て2進法の機械語で命令していました。非常に可読性が低く、専門的な知識が必要で難しい作業です。
 今でもコンピュータは機械語しか読めないので、どんなコードもコンパイルされて最後は機械語になるわけですが、当時はコンパイルが存在しません。
 この難易度の高さがこの時代の課題です。

1.1.2 アセンブリ言語

 アセンブリというのが最初のプログラミング言語だそうです。
 機械語から、多少人間が読める記号に置き換えて記述できる言語に進化しました。同時にアセンブラというプログラムに読ませて機械語に翻訳する「コンパイル」という技術が生まれました。

1.1.3 高級言語 FORTRANやCOBOL

 アセンブリ言語はあくまでコンピュータが理解できる命令である機械語を、そのまま人間のわかる記号に置き換えただけです。
 高級言語ではさらにその命令を命令のままではなく、人間が読みやすい形にまとめました。
 例えば、アセンブリ以前では、Xというデータを送れ、Yというデータを送れ、XとYを合計しろ、合計値Zを送れ、と命令ごとに1行ずつ記載していました。高級言語ではこれを「X + Y = Z」と書くことができわかりやすいです。
 このように高級言語までの進化では、そもそも「プログラムを平易にする」ものでした。

1.1.4 構造化言語 C系列

 ここで初めて「構造をわかりやすくする」ことで、より正しく動作し保守性を高めた言語が生まれます。
 大きな改良点は①GOTOレスプログラミングと、②サブルーチン(関数)の独立性強化です。
 具体的には、GOTO文をやめ、「順次進行、条件分岐、繰り返し」の3構造のみで記述しながら、グローバル変数をできるだけ使わないように、ローカル変数と引数の受け渡しでプログラミングするということです。グローバル変数は全てのサブルーチンからアクセスできるので、一度変更するたび、全てサブルーチンへの影響を調べる必要があるのです。
 しかしながら、アプリケーションの規模は巨大化していく一方で、グローバル変数は未だ現存しており、数千のサブルーチンをもつアプリケーションなどにおいては依然として負担となっていましたし、コードの再利用という面でも、サブルーチンまでしか共通部品を作れなかったので、巨大なアプリではまだまだ手書き部分が多く労力がかかっていたのでした。

1.2 次世代言語に求められたもの

 もうお分かりだと思いますが、次世代言語とはオブジェクト指向言語のことです。
 そして、求められたものとは
 
 (1) 【保守性】グローバル変数対策
     →プログラム全体に影響するため、変更するたびに全ロジックの確認が必要
   (2) 【再利用性】サブルーチンよりも大規模なコードの再利用

 でした。
 オブジェクト指向では「クラス」「ポリモーフィング」、「承継」という3つの要素によって実現されています。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

今日はだいぶ遅くなってしまったので、中途半端ではありますがここまでとします。
明日以降で上記の3つの目的をどのように解決したのか確認していきます。

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

【68日目】「オブジェクト指向でなぜつくるのか」を読んで学んだこと 1

パーフェクトPHPで学習を進めているのですが、5章のクラスとオブジェクトからどうも進捗が芳しくない。
サンプルコードを読んでも理解できないことが多くなった気がします。
PHPの文法の知識がないのもありますが、オブジェクト指向の理解ができておらず、クラス、インスタンス、オブジェクトと言った用語に翻弄されている気がしたため、オブジェクト指向を学ぶことにしようと思いました。

そこで学習書籍に選んだのがこちら
オブジェクト指向でなぜつくるのか
軽く手にとってみたところ、ネットでよく見かける説明とは異なる説明の仕方がされていたため選択。
3日程度で読んでみて、学んだことをまとめていこうと思います。

今日の記事では時間の都合もあり、かけるところまで書いて次回に続きます。

1 歴史から理解するオブジェクト指向

 本書は「なぜ」と「なに」に注目した解説書でした。
 私がこれまで触れてきたオブジェクト指向についての教材は、Railsアプリを作成するための動画教材中で「インスタンス変数」とかを使う際にちょっと説明で出てくるもの程度でしたから、「how」に重きが置かれていました。なので、「インスタンス変数はクラス内で『@変数名 = new.Class(引数)』とすれば生成できる」ということは知っていますが、インスタンス変数が何かはあまりわかりません。
 オブジェクト指向では変数も関数も全てオブジェクト = モノとして扱える、と聞いたことはありますがどういう意味かはあまりわかりませんでした。
 本書では、歴史的な経緯から何を求めてオブジェクト指向が開発されたのかを説明してくれます。
 つまり、オブジェクト指向で実現したい目標を理解した上で、コードを見ることができるようになるのです。
 ※ただし、私は実際のコードを見た経験が少ないせいもあり、本書を読んでもポリモーフィズムが具体的にどう実装されるのか、実際のコードのイメージがあまり沸いていません。PHPでいうabstractクラスのことでしょうか。

1.1 オブジェクト指向以前のプログラミング言語の課題

 プログラミング言語は最初「プログラミング難易度の易化 = 生産性の向上」のために開発されました。
 当時はハードが貧弱だったためプログラムは短命で、改修のたび一から作り直していたようで、簡単に作れるようになることが求められました。また初期の言語は非常に難しく、平易に表現できることが求められました。
 しかしながら、ハードが進化しプログラムの寿命が延びると、プログラムをメンテナンスして使い続けるようになります。すると、言語には「保守性」が求められるようになりました。

 その流れを確認して、オブジェクト指向が解決したい課題について確認します。

1.1.1 機械語

 最初のコンピュータは全て2進法の機械語で命令していました。非常に可読性が低く、専門的な知識が必要で難しい作業です。
 今でもコンピュータは機械語しか読めないので、どんなコードもコンパイルされて最後は機械語になるわけですが、当時はコンパイルが存在しません。
 この難易度の高さがこの時代の課題です。

1.1.2 アセンブリ言語

 アセンブリというのが最初のプログラミング言語だそうです。
 機械語から、多少人間が読める記号に置き換えて記述できる言語に進化しました。同時にアセンブラというプログラムに読ませて機械語に翻訳する「コンパイル」という技術が生まれました。

1.1.3 高級言語 FORTRANやCOBOL

 アセンブリ言語はあくまでコンピュータが理解できる命令である機械語を、そのまま人間のわかる記号に置き換えただけです。
 高級言語ではさらにその命令を命令のままではなく、人間が読みやすい形にまとめました。
 例えば、アセンブリ以前では、Xというデータを送れ、Yというデータを送れ、XとYを合計しろ、合計値Zを送れ、と命令ごとに1行ずつ記載していました。高級言語ではこれを「X + Y = Z」と書くことができわかりやすいです。
 このように高級言語までの進化では、そもそも「プログラムを平易にする」ものでした。

1.1.4 構造化言語 C系列

 ここで初めて「構造をわかりやすくする」ことで、より正しく動作し保守性を高めた言語が生まれます。
 大きな改良点は①GOTOレスプログラミングと、②サブルーチン(関数)の独立性強化です。
 具体的には、GOTO文をやめ、「順次進行、条件分岐、繰り返し」の3構造のみで記述しながら、グローバル変数をできるだけ使わないように、ローカル変数と引数の受け渡しでプログラミングするということです。グローバル変数は全てのサブルーチンからアクセスできるので、一度変更するたび、全てサブルーチンへの影響を調べる必要があるのです。
 しかしながら、アプリケーションの規模は巨大化していく一方で、グローバル変数は未だ現存しており、数千のサブルーチンをもつアプリケーションなどにおいては依然として負担となっていましたし、コードの再利用という面でも、サブルーチンまでしか共通部品を作れなかったので、巨大なアプリではまだまだ手書き部分が多く労力がかかっていたのでした。

1.2 次世代言語に求められたもの

 もうお分かりだと思いますが、次世代言語とはオブジェクト指向言語のことです。
 そして、求められたものとは
 
 (1) 【保守性】グローバル変数対策
     →プログラム全体に影響するため、変更するたびに全ロジックの確認が必要
   (2) 【再利用性】サブルーチンよりも大規模なコードの再利用

 でした。
 オブジェクト指向では「クラス」「ポリモーフィング」、「承継」という3つの要素によって実現されています。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

今日はだいぶ遅くなってしまったので、中途半端ではありますがここまでとします。
明日以降で上記の3つの目的をどのように解決したのか確認していきます。

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

RSpec導入方法

環境

  • Mac OS
  • Ruby 2.6.3
  • Rails 5.2.3
  • MySQL 8.0

RSpecについて

TDD(テスト駆動開発)で使用される、
Rubyの代表的なテスティングフレームワーク。

TDD(テスト駆動開発)とは

  1. 先にテストコードを書き実行、テストを失敗させる(レッド)
  2. テストコードが成功するようにコードを書く(グリーン)
  3. テストコードを実行し、テストを成功させる(リファクタリング)

を繰り返し、ある程度のところでリファクタリングをする
というように開発を進めていく手法。

  • バグを開発段階で発見、修正できる
  • テストを繰り返すことで実装するシステムへの理解が深まる
  • 有効なリファクタリングかどうかの確認がしやすい

などのメリットがある。

Capybaraについて

WebアプリケーションのE2Eテスト
(単体テストとは違い、システム全体を実稼働時に近い状況で動かしつつテストする手法)
用のテストフレームワーク。

FactoryBotについて

テスト用のデータの作成をサポートするgem
テスト用のデータを簡単に作成し、テストから呼び出して使用できる。

【1】 gemをインストールする

Gemfile
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

Gemfileの上記の部分に

gem 'rspec-rails', '~> 3.7'
gem 'factory_bot_rails', '~> 4.11'

を追加して下記のようにする。

Gemfile
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails', '~> 3.7'
  gem 'factory_bot_rails', '~> 4.11'
end

その後bundle installを実行する。

$ bundle

【2】 RSpecの設定ファイルを作成する

$ rails g rspec:install

上記のコマンドを実行しRSpecの設定ファイルを作成する。

その後、元々あったtestディレクトリ(Minitestで使うもの)を削除する。

$ rm -r ./test

【3】 Capybaraを使う準備をする

spec_helper.rb
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
  # rspec-expectations config goes here. You can use an alternate
  # assertion/expectation library such as wrong or the stdlib/minitest
  # assertions if you prefer.

作成されたspecディレクトリにある
上記のようなspec_helper.rbに対して必要なコードを加え、下記のようにする。

spec_helper.rb
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration

require 'capybara/rspec'

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
  # rspec-expectations config goes here. You can use an alternate
  # assertion/expectation library such as wrong or the stdlib/minitest
  # assertions if you prefer.

感じたこと

RSpecはどうやって導入するんだっけ?
と忘れてしまう事もあるので自分用のメモとしてさらっとまとめました!(笑)
まだまだ勉強中の身なのでしっかり学習していきます!!

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