Lチカ開発ブログ

https://l-chika.com/の開発ブログ

nattoとclassifier-rebornを利用してRubyで文書分類

Pythonの統計・数学ライブラリを利用した文書分類はあるが、Rubyで実現できる方法を模索。

目的

文書(データ) の内容から、いくつかのグループ毎に自動的に分類する

手順

  1. 訓練データを予め作成しておく
  2. 訓練データを形態素解析して、訓練する
  3. 実際のデータを形態素解析して、分類を実行

Gemfile

gem 'natto'
gem 'classifier-reborn'

訓練データ (db/fixtures/train.csv)

内容は、分類, 文章 となる。 - データのバリデーションが多ければ多いほど精度が向上した - 各分類間で訓練データ数に偏りがあるとうまく分類してくれないでの、A,B,C でそれぞれデータ数に偏りがないようにした

"A","ほげほげ..."
"B","ほげほげ..."
"C","ほげほげ..."
"A","ほげほげ..."
"C","ほげほげ..."
...

実装

require 'csv'

namespace :classifier do
  desc 'classify'
  task classify: :environment do

    # 品詞分解して名詞を取得する関数
    mecab = Natto::MeCab.new
    def mecab.filter(text)
      words = []
      parse(text) do |n|
        next unless n.feature.match(/名詞/)
        next if n.feature.match(/(非自立||代名詞)/)
        next if n.surface.match(/\./)
        words << n.surface
      end
      words
    end

    features = ['A', 'B', 'C']
    classifier = ClassifierReborn::Bayes.new(features, auto_categorize: true)

    # 訓練データから学習
    csv = CSV.read('db/fixtures/train.csv')
    csv.each do |data|
      classifier.train(data[0], mecab.filter(data[1]).join(' '))
    end

    # 分類
    result = []
    Product.all.each do |product|
      next if product.description.blank?
      # Product.descriptionカラムの内容から適切なfeatureに分類する
      feature_id = classifier.classify(mecab.filter(product.description).join(' '))
      result << { product_id: product.id, feature_id: feature_id }
    end

    result
  end
end

参考

はじめての自然言語処理

はじめての自然言語処理

実践 機械学習システム

実践 機械学習システム

qiita.com

RubyでMarkdownのImgタグのサイズ指定

Markdownエディタを作成したので、次は表示をつくる。

l-chika.hatenablog.com

前提

以下のGemを利用

実現したい事

![image](http://url.to/image.png = 250x250)

![image](http://url.to/image.png = 250x)

のようなMarkdownを、

<img src="http://url.to/image.png" height="250px" width="250px">
<img src="http://url.to/image.png" height="250px">

と、したい

参考

Resize image in the wiki of GitHub using Markdown - Stack Overflow

実装

Decorator

class HogeDecorator < Draper::Decorator
  ...
  def markdown_content
    markdown = Redcarpet::Markdown.new(CustomRender.new(with_toc_data: true),
                                       autolink: true, space_after_headers: true, tables: true, fenced_code_blocks: true)
    markdown.render(object.content).html_safe
  end
  ...
end
...
class CustomRender < Redcarpet::Render::HTML
  def image(link, title, alt_text)
    if link =~ /(.*)=(\d+)x(\d+|)/
      "<img src='#{$1.strip}' width='#{$2}px' height='#{$3}px' title='#{title}' alt='#{alt_text}'>"
    else
      "<img src='#{link}' title='#{title}' alt='#{alt_text}'>"
    end
  end
end

view

   <div>
      <%= @hoge.markdown_content %>
   </div>

参考

こんなアプローチもあった

ruby on rails - Markdown external image links with Redcarpet - Stack Overflow

Easier Image Markup With Redcarpet

RailsでsimplemdeをつかってMarkdownエディタをつくる

RailsMarkdownのエディタを実装するために、simplemdeを利用する。

github.com

前提

l-chika.hatenablog.com

インストール

npm でフロントのライブラリは管理しているので、npm install でインストール

$ npm install simplemde --save

実装

app/assets/javascripts/application.js

//= require simplemde/dist/simplemde.min.js

app/assets/stylesheets/application.scss

@import 'simplemde/dist/simplemde.min.css';

form

なぜかスペルチェックでエラーが出力されていたので無効にする

<%= form_for(hoge) do |f| %>

  <div class="form-group">
    <%= f.text_area :foo, class: 'form-control', size: '20x15' %>
  </div>

  <div class='row justify-content-md-center'>
    <%= f.submit('投稿', class: 'btn btn-primary btn-lg') %>
  </div>
<% end %>

<script>
  var simplemde = new SimpleMDE({ element: document.getElementById('hoge_foo'), spellChecker: false});
</script>

スクリーンショット

f:id:l-chika:20170308001805p:plain

include_all_helpersについて

include_all_helpersについてのメモ。

以下にあるとおりController名と同名のhelperしか読み込まないように設定をすると、ちょっと厄介なことがある。 qiita.com

HelperをもつGemを利用する場合。例えば、font-awesome-railsgithub.com

課題

  1. include_all_helpers でhelperの読み込みに制限をかける
  2. font-awesome-rails no fa_icon ヘルパーを利用
  3. エラーが発生

config/application.rb

module Hoge
  class Application < Rails::Application
    ...
    config.action_controller.include_all_helpers = false
  end
end

view

<%= fa_icon 'envelope-o' %>

エラー

NoMethodError - undefined methodfa_icon' for …` が発生

解決

application_helperfont-awesome-railsinclude をすれば良い。

app/helpers/application_helper.rb

module ApplicationHelper
  include FontAwesome::Rails::IconHelper
  ...

ただGemのHelperを利用する度に、このを設定していくのは面倒なので、ひとまず include_all_helpers = false はやめる。

ActiveModelのベストプラクティスを考える

ActiveModelとは

ActiveModelとは こちら にあるように Modelと直接紐付けないような、フロントのフォームに密接に関わるバリデーションを実装する場合に便利な Module。

課題

ActiveModel を利用シーンの一つとして、一つフォームで複数のモデルを扱うことがある。

その際によく迷うことがある。

  • モデルのバリデーションが ActiveModelApplicationRecord のモデルとで重複する
  • データの作成と編集で ActiveModelintialize にどうやって、何を渡すか(ここらへん情報が検索してもあまり見つからなかった)

解決方法

そこで、とりあえず考えた解決方法

  • ActiveModel に対象のモデル群を内包する
  • find, save, valid? を実装
  • ActiveModel のバリデーションはフォーム固有のものだけを実装し、モデルで実装されているバリデーションはモデルに任せる
  • Controllercreate,update はフォームの内容を, edit の場合は保存されている内容からフォームを作成できるようにする
  • save 時に valid? を実行し、内包するモデルのバリデーションエラーとフォームのバリデーションエラーを全てフォームのエラーに追加する

以下イメージ。実際の動作確認はしてないです

class HogeController < ApplicationController
  der new
    @hoge_form = HogeForm.new()
  end
   
  der create
    @hoge_form = HogeForm.new(params[:hoge_form])
    if @hoge_form.save
        ...
    else
        ...
    end
  end
   
  def edit
    @hoge_form = HogeForm.find(parmas['id'])
  end
  
  def update
    ...
  end
...
 
calss HogeForm
   include ActiveModel
    
   attr_accessor :hoge_a, :foo_b
   
   validates :hoge_a, presence: true
   validates :foo_b, presence: true
    
   class << self
      def find(id)
        hoge = Hoge.find id
        new(hoge: hoge.attributes, foo: hoge.foo.attributes)
      end
   end
 
   def intialize(params = {})
     @hoge = Hoge.new(params[:hoge])
     @foo = Foo.new(params[:foo])
   end
 
  def valid?(context = nil)
    super
     
    @hoge.valid?
    @hoge.errors.each do |k, v|
      errros.add(k, v)
    end
    
    @foo.valid?
    @foo.errors.each do |k, v|
    ...
  end
   
  def save
     return unless valid?
     
     return false unless @hoge.save
     @foo.save
  end
end

関連のおすすめ本

Ruby on Rails 5 超入門

Ruby on Rails 5 超入門

Mechanizeでリンクをスクレイピング

Mechanize を利用したときのメモ。

Mechanize を試した理由

  • ちょっと前は anemone を利用していたが、最近はメンテしてないみたい。
  • 内部でNokogiri を利用しているので、学習・導入コストが個人的に低かった。

よく利用する関数と戻り値の型をメモ

> mechanize = Mechanize.new
> search_page = mechanize.get(url)

> search_page.class
=> Mechanize::Page
> search_page.links.first.class
=> Mechanize::Page::Link
> search_page.links.second.uri
=> #<URI::Generic /a/b.html>
> search_page.links.second.resolved_uri
=> #<URI::HTTP http://hoge.jp/a/b.html>


> div = search_page.search('.hoge')
> div.class
=> Nokogiri::XML::NodeSet
> div.first.class
=> Nokogiri::XML::Element

利用例

links 関数でドキュメント内の a タグを簡単に取得できる。

Mechanizeをラッパーしたリンク取集クラス

class Scraper
  attr_accessor :mechanize

  def initialize
    @mechanize = Mechanize.new
  end

  def scraping_links(url:, links_pattern:, sleep: 1)
    result_links = {}

    loop do
      Rails.logger.debug(url)
      search_page = mechanize.get(url)
      target_links = search_page.links.select { |link| link.uri.to_s =~ links_pattern }
      links = target_links.map { |link| [link.resolved_uri.to_s, link] }.to_h
      result_links.merge!(links)

      next_link = block_given? ? yield(search_page) : nil
      break if next_link.blank?
      url = next_link.resolved_uri.to_s
      sleep(sleep)
    end

    result_links
  end

  def search_document(url)
    search_page = mechanize.get(url)
    yield(search_page)
  end
end

呼び出し例

# http://hoge.jp ページ内の /foo/ のリンクを全て取得したい場合
links = Scraper.new.scraping_links(url: 'http://hoge.jp', links_pattern: /foo/)

# http://hoge.jp ページ内の /foo/ のリンクをページングして全て取得したい場合。この場合は「次へ」のリンクがなくなるまでクロール
links = Scraper.new.scraping_links(url: 'https://hoge.co.jp/foos/?page=1',
                                  links_pattern: /foos\/[A-Z]/) do |search_page|
             search_page.links.find { |link| link.text == '次へ' }
            end

おすすめ本

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

データを集める技術 最速で作るスクレイピング&クローラー (Informatics&IDEA)

データを集める技術 最速で作るスクレイピング&クローラー (Informatics&IDEA)

PythonによるWebスクレイピング

PythonによるWebスクレイピング

ransackのinをカンマ区切りでする

やりたいこと

ransack を利用して、 in の検索をカンマ区切りで指定できるようにする。

手順

Controller

class HogeController < ApplicationController
...
  def index
    @q = Hoge.ransack(ransack_queries)
    @hoges = @q.result.page(params[:page])
  end

...

  def ransack_queries
    queries = params[:q]
    if queries.present? && queries[:id_in].present?
      # ページネーション等で2回目のアクセスが来た場合には既にarrayになっているので return。
      return queries if queries[:id_in].is_a?(Array)

      queries[:id_in] = queries[:id_in].split(',').map(&:strip)
    end
    queries
  end
...

View

= search_form_for(@q) do |f|
...
  = f.search_field(:id_in, class: 'form-control', value: params[:q].try(:fetch, :id_in, nil).try(:join, ','))

参考

319ring.net

関連する書籍

Ruby on Rails 5 超入門

Ruby on Rails 5 超入門