sitateru tech blog: Ruby

sitateru tech blog

シタテルの技術やエンジニアの取り組みを紹介するテックブログです。

ラベル Ruby の投稿を表示しています。 すべての投稿を表示
ラベル Ruby の投稿を表示しています。 すべての投稿を表示

2018年12月17日月曜日

【備忘録】Visual Studio Codeで文字検索範囲が広すぎるときに疑う箇所

12月 17, 2018

こんにちは!
シタテル株式会社エンジニアのいしづかです。

みなさんはコードを書く時、どんなエディタを使ってらっしゃるでしょうか。

現在開発が続いているシタテルコントロールシステムのバックエンドはRailsで書かれています。

開発メンバーはそれぞれ自分が合うエディタを使っています。Ruby Mine使っている人もいますし、Atomもいたような・・・。

わたしはVisual Studio Codeを使っています。え?Vimであらずは人であらず?いや、まぁ、えへへ。

コードを書いているとコード内検索をかけたくなります。その際にはnode_modules配下とかは最初から外しておきたいものです。

今回はVisual Studio Code限定ですが、除外設定でハマったので備忘録としてその内容を書こうと思います。

Visual Studio Codeの除外設定

Visual Studio Codeの除外設定は大きく2つあり、 Files ExcludeSearch Exclude があります。

Files Excludeにパスを登録すると、その配下はエクスプローラー上にすら表示されません。Visual Studio Codeからは完全に管理対象外となり、当然検索にも引っかからなくなります。

Search Excludeにパスを登録すると、エクスプローラー上には表示されますが、検索に引っかからなくなります。

よって、検索の除外対象は「Files Exclude」+「Search Exclude」ということになります。

わたしがSearch Excludeに登録しているのは

の4つです。もうちょっと増やしてもいいかなと思うんですが、必要最低限で運用しています。

設定完了であるはずが、、、アレ、logの中身とか検索対象になってる。。。

パターンの書き方が間違っているのか?
設定する箇所が間違っているのか?

アレコレ試した挙げ句、最終的にたどり着いたのは

このボタンがONになっていないだけでした。。。

デフォルトではONでなにかのときにOFFにしちゃっていたのかもしれませんが、こんなの気づかないよVisual Studio Codeさん。。。

まとめ

何かを検索したとき、ほしいものがほしい分だけパッと手に入るっていいですよね。

検索範囲を適度なものに調整しておくのはコードを書くときのストレスを軽減してくれると思います。

かく言うわたしが、この除外設定が効いていなかった数十分間はストレスフルな状態でした。

他の開発メンバーがどんなチューニングを行っているかちょっと気になるところです。どんなプラグイン入れているかとか聞けたらレポートしたいと思います。

2018年12月11日火曜日

Active Storage移行記:データ移行編

12月 11, 2018

こんにちは、あさのです。

前にActive Storageの記事を書きましたが、今回もそのネタで行こうと思います。

Active Storage移行記:バリデーション編|sitateru tech blog

今回は、Paperclip管理のデータをActive Storage管理に移行したときのお話です。

データ構造

Paperclipではファイルのデータは各モデルのカラムに保存されています。

userクラスのiconというファイルであれば、userテーブルに以下のようなカラムがあります。

  t.string "icon_file_name", comment: "ファイル名"
  t.string "icon_content_type", comment: "ファイルのcontent type"
  t.integer "icon_file_size", comment: "ファイルサイズ"
  t.datetime "icon_updated_at", comment: "ファイル更新日時"

対してActive Storageでは全クラスの全ファイルをまとめて2つのテーブルで管理します。

create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
  t.string "name", null: false
  t.string "record_type", null: false
  t.bigint "record_id", null: false
  t.bigint "blob_id", null: false
  t.datetime "created_at", null: false
  t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
  t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end

create_table "active_storage_blobs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
  t.string "key", null: false
  t.string "filename", null: false
  t.string "content_type"
  t.text "metadata"
  t.bigint "byte_size", null: false
  t.string "checksum", null: false
  t.datetime "created_at", null: false
  t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end

たとえば、

  • userクラスのicon
  • user.id=15
  • ファイル名 prof.jpg

というファイルだと以下のようなレコードになります。
2つのテーブルは blob_id で関連づけられるんですね。

id    name    record_type    record_id    blob_id    created_at
15    icon    User    3    16    2017-12-28 19:23:03

id    key    filename    content_type    metadata    byte_size    checksum    created_at
16    3e0ca647-18e3-43ee-85c4-4de552bac46f    prof.jpg    image/jpeg    {}    44202    isFyYNJIs/6pyptfRUMUlA==    2017-12-28 19:23:03

データ移行

さて、それぞれのデータ構造がわかったのでマイグレーションをしていきます。

このあたりの記事を参考にさせてもらい、rakeタスクで実装しました。

namespace :import_to_active_storage do
  desc 'Import to ActiveStorage'
  task run_all: :environment do
    models = ActiveRecord::Base.descendants.reject(&:abstract_class?)
    import_data(models)
  end

  private

  def import_data(models)
    conn = ActiveRecord::Base.connection
    models.each do |model|
      attachments = model.column_names.map do |c|
        c =~ /(.+)_file_name$/ && Regexp.last_match(1)
      end.compact

      model.find_each do |instance|
        attachments.each do |attachment|
          puts "Process #{model.name}/#{instance.id}"

          next unless instance.send("#{attachment}?")
          next if conn.select_one("SELECT `id` FROM active_storage_attachments WHERE `record_type` = '#{model.name}' AND `record_id` = #{instance.id}").present?

          puts "Migrate #{instance.send(attachment.to_s)}"

          ActiveRecord::Base.transaction do
            execute_statement(conn, model, instance, attachment)
          end
        end
      end
    end
  end

  def execute_statement(connection, model, instance, attachment)
    active_storage_blob_statement(connection).execute(
      key,
      instance.send("#{attachment}_file_name"),
      instance.send("#{attachment}_content_type"),
      instance.send("#{attachment}_file_size"),
      checksum(instance.send(attachment)),
      instance.updated_at.strftime('%Y-%m-%d %H:%M:%S')
    )

    active_storage_attachment_statement(connection).execute(
      attachment,
      model.name,
      instance.id,
      instance.updated_at.strftime('%Y-%m-%d %H:%M:%S')
    )
  end

  def active_storage_blob_statement(connection)
    connection.raw_connection.prepare(<<-SQL)
      INSERT INTO active_storage_blobs (
        `key`, `filename`, `content_type`, `metadata`, `byte_size`, `checksum`, `created_at`
      ) VALUES (?, ?, ?, '{}', ?, ?, ?)
    SQL
  end

  def active_storage_attachment_statement(connection)
    connection.raw_connection.prepare(<<-SQL)
      INSERT INTO active_storage_attachments (
        `name`, `record_type`, `record_id`, `blob_id`, `created_at`
      ) VALUES (?, ?, ?, LAST_INSERT_ID(), ?)
    SQL
  end

  def key
    SecureRandom.uuid
  end

  def checksum(image)
    uri = URI.parse("https:#{image.url}")
    body = Net::HTTP.get(uri)
    Digest::MD5.base64digest(body)
  end
end

そこそこ長いですが、結局は各モデルの各ファイルごとにPaperclipのカラム情報とファイルのハッシュ値を取り、
Active Storageのテーブルに入れるという繰り返しです。

なお、各モデルの実装をActive Storage用に書き換えた後ではこのタスクは動かないので、
Paperclip時に実行する必要があります。

実際にやったところ数万件のデータがあったために5~6時間かかるという大引っ越しになってしましましたが、
なんとか乗り切りました。


ということで今回はActive Storageのデータ移行について書きました。

まだもう少しActive Storageネタで書けるかもしれません。

それでは。

2018年11月26日月曜日

Active Storage移行記:バリデーション編

11月 26, 2018

こんにちはあさのです。

今回は、Ruby on Railsのファイルアップロード機能をPaperclipからActive Storageに移行したときの話を書こうと思います。

Paperclip

Paperclipはファイルアップロード用のGemです。

https://github.com/thoughtbot/paperclip

比較的導入が簡単であり、アプリケーションにファイルアップロード機能をつけるためによく使われています。

ですが、現在は "Deprecated" とされていて、Rails 5.2から標準機能の1つとなったActive Storageへの移行が推奨されています。

Active Storage

Active StorageはRailsのバージョン5.2から標準搭載されている機能の一つで、Paperclipと同様Active Recordのオブジェクトと紐づけたファイルアップロード機能を提供しています。

https://github.com/rails/rails/tree/master/activestorage

シタテルでもRails 5.2へのアップグレードが落ち着いたところでこの移行を行いました。

そんなわけで、移行にあたっていくつか注意点となったところを振り返っていきたいと思います。

今回はモデルのバリデーションの話です。

Active Storage でのバリデーション

Papaerclipにはバリデーション機能があります。

以下はiconという名前で扱う添付ファイルのバリデートをする例で、モデル内にこう書いておけば保存時に自動でチェックされます。

見てのとおりですが、ファイル形式は[jpg, jpeg, gif, png]のどれか、ファイルサイズが10MB以下である必要があります。

  validates_attachment :icon,
                       content_type: {
                         content_type: [
                           'image/jpg',
                           'image/jpeg',
                           'image/gif',
                           'image/png'
                         ]
                       },
                       size: {
                         less_than_or_equal_to: 10.megabytes,
                         message: I18n.t('errors.messages.file_too_large')
                       }

しかしActive Storageにはこのようなバリデーション機能はありません。

じゃあどうするんだ!という話ですが、素直にActive Recordのカスタムバリデーションを使いましょう。
これで同じようなことができます。

  validate :validate_icon

  def validate_icon
    return unless icon.attached?
    if icon.blob.byte_size > 10.megabytes
      icon.purge
      errors.add(:icon, I18n.t('errors.messages.file_too_large')
    elsif !image?
      icon.purge
      errors.add(:icon, I18n.t('errors.messages.file_type_not_image'))
    end
  end

  def image?
    %w[image/jpg image/jpeg image/gif image/png].include?(icon.blob.content_type)
  end

ちなみに、

  • attached? はファイルが存在するかどうか
  • blob.byte_size はファイルサイズ
  • blob.content_type はファイルタイプ

を取得しています。


ということでActive Storageのバリデーションについて書いてみました。

Paperclipからの移行はいろいろと作業があったので他のトピックについてもまた書いていきたいと思います。

それでは。

2018年11月8日木曜日

Ruby+Nokogiri 5分で作るスクレイピングツール

11月 08, 2018
こんにちは、シタテルエンジニアの工です!
マーケティングチームからページャー付きのWEBサイト全ページのhtmlが欲しいと言われましたので、簡単なRuby+Nokogiriスクレイピングコードを書きました。
今回は、fromAnaviさんのページから、弊社渋谷オフィス周辺のアルバイト情報のhtmlをGetしてみます!

Nokogiriをインストール

gem install nokogiri

コーディング

scraping.rb
require "open-uri"
require "nokogiri"

file = File.open("froma.txt", "w")

(1..117).each do |page|
  charset = nil

  doc = Nokogiri::HTML(open("https://www.froma.com/baito/jobList/?s_area_cd=1i2002&s_area_cd=1i2004&sort_cd=&edition_cd=1&shrt_indx_cd=1002&st=08&page=#{page}"))
  file.puts(doc.xpath("//div[@class='main']").to_html)
end

file.close

それでは取得してみます 🚀

$ ruby scraping.rb
出力結果
froma.txt
<div class="main">
<ol class="breadcrumbs" itemprop="breadcrumb">
<li><a href="/index.html">全国のアルバイト/バイト</a></li>
<li><a href="/P01/">関東</a></li>
<li>渋谷駅周辺、原宿/表参道の仕事情報を探す</li>
</ol>
<!-- インクルードエリア11(編集用上エリア) -->
<!-- /インクルードエリア11(編集用上エリア) --><h1 class="hd-basic-1">「いろいろな条件」のアルバイト/求人情報</h1>
<table class="scp scp--job-list">
                <colgroup>
                    <col style="width:96px;">
                    <col style="width:131px;">
                    <col style="width:96px;">
                    <col style="width:406px;">
                </colgroup>
    <tr>
                    <th>勤務地</th>
                    <td colspan="3" class="scp-area">
    <div class="scp-area-main">
                            <ul class="scp-area-main__txt">
            <li>渋谷区 &gt; 渋谷駅周辺、原宿/表参道</li>
            </ul>
                            <div class="scp-area-main__btn"><a href="/baito/ajax/jobSearchCond/doAreaCond?st=08&amp;sort_cd=&amp;shrt_indx_cd=1002&amp;s_area_cd=1i2002&amp;s_area_cd=1i2004&amp;edition_cd=1" class="btn-panel--main jsc-modal-trigger-area" onclick="sendSC_ListLimitPanel('1000');">エリアを選ぶ</a></div>
                        </div>
                        <div class="scp-area-sub">
                            <div class="btn"><a href="/baito/ajax/jobSearchCond/doAlongRailCond?st=08&amp;sort_cd=&amp;shrt_indx_cd=1002&amp;s_area_cd=1i2002&amp;s_area_cd=1i2004&amp;edition_cd=1" class="btn-panel--sub jsc-modal-trigger-rail" onclick="sendSC_ListLimitPanel('2000');">沿線・駅を選ぶ</a></div>
                        </div>
        </td>

(長いので割愛)
無事に取得できました 🍻
ハードコーディングだらけですが、即興ということで今回はこここまで。
RubyとNokogiriを使ったシンプルなスクレイピングを紹介しました。