Word Cloud + kuromoji + Processing で日本語文章をビジュアライズ

完成品:

Word Cloud

文章中で出現頻度に重みを付け、その重みに応じた大きさで単語群を図示する手法。

Processing で行うには WordCram が手っ取り早く、デザインも良い。

いかにして日本語文章中の単語を抜き出すか

WordCram に何の手も加えていない日本語文章を読み込ませたところで、日本語単語への分かち書き方法を知らないため、不自然な Word Cloud が出来上がる。

そこで、日本語解析器である kuromoji を使い、対象文章を解析し、名詞を読みこませることにした。

下準備

WordCram

さて、まずは外部ライブラリである WordCram を 手元の Processing にインストールする。

  • こちらから Processing 3.0 に対応した WordCram 1.0.0 の zip ファイルをダウンロードし、解凍する。
  • Processing の sketchbook フォルダを開く。(Processing > Preferences > Sketchbook location)

  • そのフォルダ内の libraries に、解凍した WordCram フォルダをコピーする。

基本的な使い方は次の通り:

import wordcram.*;

// Set up the Processing sketch
size(1000, 600);
colorMode(HSB);
background(230);

// Make a wordcram from a random wikipedia page.
new WordCram(this)
  .fromWebPage("https://en.wikipedia.org/wiki/Special:Random")
  .withColors(color(30), color(110),
              color(random(255), 240, 200))
  .sizedByWeight(5, 120)
  .withFont("Copse")
  .drawAll();

kuromoji

kuromoji は Java 用のライブラリなので少し手を加える必要がある。

  • こちらから zip ファイルをダウンロードし解凍する。
  • lib フォルダ内に kuromoji-0.7.7.jar ファイルがあるので、これを kuromoji.jar にリネーム。
  • sketchbook フォルダの librarieskuromoji/librariesを作成し、先ほどの kuromoji.jar ファイルを配置する。

基本的な使い方は次の通り:

import org.atilika.kuromoji.Token;
import org.atilika.kuromoji.Tokenizer;
import java.util.*;

Tokenizer tokenizer = Tokenizer.builder().build();
List<Token> tokens = tokenizer.tokenize("吾輩は猫である。");

for (Token token : tokens) {
    println("PartOfSpeech:" + token.getPartOfSpeech());
    println("surfaceForm:" + token.getSurfaceForm());
    println("baseForm:" + token.getBaseForm());
}

出力:

PartOfSpeech:名詞,代名詞,一般,*
surfaceForm:吾輩
baseForm:吾輩
PartOfSpeech:助詞,係助詞,*,*
surfaceForm:は
baseForm:は
PartOfSpeech:名詞,一般,*,*
surfaceForm:猫
baseForm:猫
PartOfSpeech:助動詞,*,*,*
surfaceForm:で
baseForm:だ
PartOfSpeech:助動詞,*,*,*
surfaceForm:ある
baseForm:ある
PartOfSpeech:記号,句点,*,*
surfaceForm:。
baseForm:。

日本語文章を Word Cloud でビジュアライズ

これら2つのライブラリを活用して、ホッテントリのフィードを解析、Word Cloud を作成する。

String URL = "http://feeds.feedburner.com/hatena/b/hotentry";
XML[] items;
String contents;
PrintWriter output;
String words;

import wordcram.*;
import org.atilika.kuromoji.Token;
import org.atilika.kuromoji.Tokenizer;
import java.util.*;

void setup() {
  size(1000, 600);
  colorMode(HSB);
  background(230);

  XML xml = loadXML(URL);
  items = xml.getChildren("item");
  for(int i = 0; i < items.length; i++){
    contents += items[i].getChild("title").getContent();
    contents += items[i].getChild("description").getContent();
  }

  Tokenizer tokenizer = Tokenizer.builder().build();
  List<Token> tokens = tokenizer.tokenize(contents);
  for (Token token : tokens) {
    String[] partOfSpeech = token.getPartOfSpeech().split(",");
    if (partOfSpeech[0].equals("名詞") && !token.getSurfaceForm().equals("こと")) {
      words += token.getSurfaceForm() + "\n";
    }
  }

  output = createWriter("words.txt");
  output.println(words);
  output.close();

  drawWordCloud();
}

void draw() {

}

void mousePressed() {
  drawWordCloud();
}

void drawWordCloud() {
  println("creating a new word clound");
  background(230);
  new WordCram(this)
    .fromTextFile("./words.txt")
    .withColors(color(30), color(110),
                color(random(255), 240, 200))
    .sizedByWeight(5, 120)
    .withFont("Copse")
    .drawAll();
}

結果はこんな感じ:

WordCram にかける前にある程度意味のない頻出単語をフィルタしたほうが良い。今回は Processing での正規表現がわからなかったので、!token.getSurfaceForm().equals("こと") のように kuromoji で弾くようにしている。

Rails5 API + devise でユーザーの認証と作成機能を実装した API を作成する

2017/04/26 curl部分に間違いがあったので修正、ついでにログインの必要な動作を追記

目標

Rails v5.0.0 から追加されたapiオプションを使い、ユーザーの作成と認証機能を実装したベーシックな Rails API を作る

rails new

まずはプロジェクトを作成します

$ rails new devise-api --api --skip-bundle

Gemfile に次の gem を追加し, bundle install

gem 'devise'
gem 'active_model_serializers'

devise

devise を立ち上げます

$ rails generate devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml

User モデルを作成します

$ rails generate devise User
      invoke  active_record
      create    db/migrate/20160710134334_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users

token 認証

access_token カラムをユーザーテーブルに追加します

$ rails generate migration add_access_token_to_user
class AddAccessTokenToUser < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :access_token, :string
  end
end

サンプルとしてトークンの生成にユーザーのidとdeviseが生成するトークン自身を使用します。 devise モジュールの説明についてはこちら

# app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  # :recoverable, :rememberable, :trackable
  devise :database_authenticatable, :registerable, :validatable

  after_create :update_access_token!

  validates :email, presence: true

  def update_access_token!
    self.access_token = "#{self.id}:#{Devise.friendly_token}"
    save
  end

end

ユーザー認証のロジックは application_controller に配置されます。 具体的には、ヘッダーのAuthorizationにてトークンを受け取って、データベースのアクセストークンと照合しています。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include AbstractController::Translation

  before_action :authenticate_user_from_token!

  respond_to :json

  ##
  # User Authentication
  # Authenticates the user with OAuth2 Resource Owner Password Credentials
  def authenticate_user_from_token!
    auth_token = request.headers['Authorization']

    if auth_token
      authenticate_with_auth_token auth_token
    else
      authenticate_error
    end
  end

  private

  def authenticate_with_auth_token auth_token
    unless auth_token.include?(':')
      authenticate_error
      return
    end

    user_id = auth_token.split(':').first
    user = User.where(id: user_id).first

    if user && Devise.secure_compare(user.access_token, auth_token)
      # User can access
      sign_in user, store: false
    else
      authenticate_error
    end
  end

  ##
  # Authentication Failure
  # Renders a 401 error
  def authenticate_error
    render json: { error: t('devise.failure.unauthenticated') }, status: 401
  end
end

sessions_controller に対して login ルートを割り当てます

# config/routes.rb
Rails.application.routes.draw do
  devise_for :user, only: []

  namespace :v1, defaults: { format: :json } do
    resource :login, only: [:create], controller: :sessions
  end
end

sessions_controller は次のようにログインリクエストを処理します。 skip_before_action :authenticate_user_from_token!によって、認証処理をスキップします。

# app/controllers/v1/sessions_controller.rb
module V1
  class SessionsController < ApplicationController
    skip_before_action :authenticate_user_from_token!

    # POST /v1/login
    def create
      @user = User.find_for_database_authentication(email: params[:email])
      return invalid_email unless @user

      if @user.valid_password?(params[:password])
        sign_in :user, @user
        render json: @user, serializer: SessionSerializer, root: nil
      else
        invalid_password
      end
    end

    private

    def invalid_email
      warden.custom_failure!
      render json: { error: t('invalid_email') }
    end

    def invalid_password
      warden.custom_failure!
      render json: { error: t('invalid_password') }
    end
  end
end

session_serializer.rb によって、オブジェクトを整形することができます

# app/serializers/v1/session_serializer.rb
module V1
  class SessionSerializer < ActiveModel::Serializer

    attributes :email, :token_type, :user_id, :access_token

    def user_id
      object.id
    end

    def token_type
      'Bearer'
    end

  end
end

ユーザーのサインアップ

次にユーザーの作成プロセスを実装します

# app/controllers/v1/users_controller.rb
module V1
  class UsersController < ApplicationController
    skip_before_action :authenticate_user_from_token!, only: [:create]

    # POST
    # Create an user
    def create
      @user = User.new user_params

      if @user.save!
        render json: @user, serializer: V1::SessionSerializer, root: nil
      else
        render json: { error: t('user_create_error') }, status: :unprocessable_entity
      end
    end

    private

    def user_params
      params.require(:user).permit(:email, :password)
    end
  end
end
# config/routes.rb
Rails.application.routes.draw do
  devise_for :user, only: []

  namespace :v1, defaults: { format: :json } do
    resource :login, only: [:create], controller: :sessions
    resource :users, only: [:create]
  end
end

動作確認

データベースを作成、マイグレーション(rake db:create db:migrate)して、サーバーを立ち上げ(rails server)ます。 コンソールからユーザーを作成、ログインをテストします。

まずはPOST /v1/users: パラメーターをjson形式で送ります。

$ curl localhost:3000/v1/users --data '{"user": {"email": "user@example.com", "password": "mypass"}}' -v -H "Accept: application/json" -H "Content-type: application/json"

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> POST /v1/users HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.51.0
> Accept: application/json
> Content-type: application/json
> Content-Length: 61
> 
* upload completely sent off: 61 out of 61 bytes
< HTTP/1.1 200 OK
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< Content-Type: application/json; charset=utf-8
< ETag: W/"fc7ab698e44a0b4687a7d41dc95e8c7a"
< Cache-Control: max-age=0, private, must-revalidate
< X-Request-Id: 85aa1e62-ac17-4074-b593-7ad45e40e166
< X-Runtime: 0.192480
< Transfer-Encoding: chunked
< 
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact

{"email":"user@example.com","token_type":"Bearer","user_id":1,"access_token":"1:ZsQVFPi8qvc_zLe2Zjtj"}

ユーザーが作成できたので、次にPOST /v1/login

$ curl localhost:3000/v1/login --data 'email=user@example.com&password=mypass'
{"email":"user@example.com","token_type":"Bearer","user_id":1,"access_token":"1:ZsQVFPi8qvc_zLe2Zjtj"}

ちゃんとaccess_tokenが返ってくることを確認できました。

認証が必要なメソッドの作成

access_tokenを用いて認証しなければ、使うことの出来ないメソッドを作ってみます。

Userコントローラーにindexメソッドを追加します:

module V1
  class UsersController < ApplicationController
    skip_before_action :authenticate_user_from_token!, only: [:create]

    def index
      render json: User.all, each_serializer: V1::UserSerializer
    end

    # POST
    # Create an user
    def create
      @user = User.new user_params

      if @user.save!
        render json: @user, serializer: V1::SessionSerializer, root: nil
      else
        render json: { error: t('user_create_error') }, status: :unprocessable_entity
      end
    end

    private

    def user_params
      params.require(:user).permit(:email, :password)
    end
  end
end

このとき、skip_before_action :authenticate_user_from_token!には何も追加しないことで、indexメソッドが認証を必要とするメソッドとなります。逆に認証を必要としないメソッドを作成したい場合は追加するようにしましょう。

ルートも忘れずに設定しましょう:

    resources :users, only: [:index, :create]

user_serializer.rbを追加します:

# app/serializers/v1/user_serializer.rb
module V1
  class UserSerializer < ActiveModel::Serializer
    attributes :id, :email, :created_at, :updated_at
  end
end

ではここで、コマンドからGET /v1/usersを行ってみましょう:

$ curl localhost:3000/v1/users
{"error":"You need to sign in or sign up before continuing."

ちゃんと、application_controller内のAuthenticate Failureが返ってきています。

次に認証を行ってみましょう。

まずはログインしてaccess_tokenを取得:

curl localhost:3000/v1/login --data 'email=user@example.com&password=mypass'
{"email":"user@example.com","token_type":"Bearer","user_id":1,"access_token":"1:ZsQVFPi8qvc_zLe2Zjtj"}

そのaccess_tokenをヘッダーのAuthorizationに入れ、リクエストを送る:

curl localhost:3000/v1/users -v -H "Authorization: 1:ZsQVFPi8qvc_zLe2Zjtj"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /v1/users HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.51.0
> Accept: */*
> Authorization: 1:ZsQVFPi8qvc_zLe2Zjtj
> 
< HTTP/1.1 200 OK
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< Content-Type: application/json; charset=utf-8
< ETag: W/"6e18487e56deb9bae7d6fa2bebffc7af"
< Cache-Control: max-age=0, private, must-revalidate
< X-Request-Id: 1f7e86b8-ff99-45be-a818-69c34605ac62
< X-Runtime: 0.006412
< Transfer-Encoding: chunked
< 
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact
[{"id":1,"email":"user@example.com","created_at":"2017-04-26T01:44:16.104Z","updated_at":"2017-04-26T01:44:16.104Z"}]

このようにユーザーの一覧がリストになって返ってきていれば正しい動作です。 実際にフロントエンドからリクエストを行う際にはユーザーの作成やログイン後に、access_tokenをローカルストレージ等に保存しておくのが良いでしょう。

CORS

フロントエンドのアプリケーションをこの API に対しリクエストを送る形で作成するのですが、通常の設定では別ドメインからのリクエストは拒否されてしまいます。

これを解決するには rack-cors を使ってクロスドメイン通信を許可します。

gem 'rack-cors'

設定を config/initializers/cors.rb に追加します:

config.middleware.insert_before 'Rack::Runtime', 'Rack::Cors' do
  allow do
    origins '*'
    resource '*',
             headers: :any,
             methods: [:get, :put, :post, :patch, :delete, :options]
  end
end

この設定のままではすべてのドメインからのリクエストを許可しているので、セキュリティ面は脆弱です

まとめ

難しいトークン認証も、deviseを用いれば結構簡単に実装できますね。不明点やなにか間違っていること等ありましたら、コメントいただければと思います。

Rails5: データベースがrakeタスクからdropできない時の対処法

問題点

Rails5になってrakeタスクのdrop動作に大きな変更があったらしく, Rails4からアップデートして初めての rake db:drop がエラーを吐いてしまう.

$ bundle exec rake db:drop
Mysql2::Error: Error dropping database (can't rmdir './db_name', errno: 66): DROP DATABASE IF EXISTS `db_name`
Couldn't drop database 'db_name'

mysqlから直接ドロップも出来ない

mysql> DROP DATABASE db_name;
ERROR 1010 (HY000): Error dropping database (can't rmdir './db_name', errno: 66)

解決策: データベースのファイルを根こそぎ削除する

まずはデータベースの入っているディレクトリを調べる:

mysql> select @@datadir;
+-----------------------+
| @@datadir             |
+-----------------------+
| /usr/local/var/mysql/ |
+-----------------------+
1 row in set (0.00 sec)

あるいは:

$ mysql -e "select @@datadir"
+-----------------------+
| @@datadir             |
+-----------------------+
| /usr/local/var/mysql/ |
+-----------------------+

データベースディレクトリに移動して, 該当ファイルを削除する:

$ cd /usr/local/val/mysql/
$ sudo rm -rf db_name

あとはデータベースを作りなおせば良い:

rake db:create

hatena-blog-and-fotolife v4.2.0 をリリース: エントリーリストとエントリーの取得ができるようになりました

hatena-blog-and-fotolife v4.2.0 をリリースしました。

以下の機能を追加しています。

エントリーリストの取得

コマンド: ⌥⌘I index

エントリーリストからエントリーの取得

get

Atomエディタからはてなブログエントリーの削除ができるようになりました

かねてより開発していたAtomパッケージ hatena-blog-entry-postをアップデートし、エントリーの削除機能を追加しました。

デモ

delete

このパッケージを使って、はてなブログにエントリーを投稿すると、次のようなコメントがファイルに挿入されます。

<!--
{"id":"6653812171401174041","title":"Atomエディタからはてなブログエントリーの削除ができるようになりました","categories":["Atom","hatena-blog-entry-post"],"draft":true}
-->

このコメントが存在する場合に限り、エントリーの更新/削除が可能になります。

また、更新/削除機能の追加に伴い、パッケージをhatena-blog-entry-postからhatena-blog-and-fotolifeへ変更しました。そのままアップデートしてお使いいただけます。

Atomパッケージのリネーム

Atomパッケージのリネームの仕方は、次の通りです:

$ apm publish --rename {新しい名前}

これでスムーズにパッケージのリネームが完了します。

https://atom.io/packages/hatena-blog-entry-post にアクセスしても、 https://atom.io/packages/hatena-blog-and-fotolife へとリダイレクトされますし、パッケージのアップデートもユーザーには影響なさそうです。

なにかあれば気軽にissueへどうぞ。