Tech-Draft

rubyとかscalaとかjavascriptとか?AndroidやiOSもチマチマと。

(認証つきの)Proxyの背後でPlayframeworkを動かすには

(認証つきの)Proxyの背後でPlayframeworkを動かしたい場合、
http://www.playframework.org/documentation/2.0.1/Installing の手順に従ってPlayframeworkをインストールした後、

$ play new hogehoge

ではなく

$ play -Dhttp.proxyHost=192.168.0.1 -Dhttp.proxyPort=8080 -Dhttp.proxyUser=hoge -Dhttp.proxyPassword=fuga new hogehoge

などのように、playの引数でProxyの設定を付け加えれば良いよ!

Garb 0.9.2が動かなくなった

2012/01/12時点で、Garb 0.9.2が動かなくなりました。

Garbのソースは少し古くてGoogle Analytics Data Export API v2.3を前提にしていますが、どうもこの数日でこのv2.3がshutdownされた模様。

2011/12/5のChangeLogをみると、

This release also announces the deprecation of Version 2.3 and the upcoming shut down of the Account Feed in the Data Export API. In 6 months:

Requests to the version 2.3 JSON output will not be supported by the version 2.4 response and will return 404 Not Found status codes. If you need a JSON response, upgrade to Version 3.0.

http://code.google.com/intl/en/apis/analytics/community/export_changelog.html

とあり、6ヶ月の猶予も無くshutdownされた感じです。


実際Garb0.9.2を動かすと、Google Analytics Data Export APIを用いてProfileリストを取得する部分で下記のようなエラーが発生しています。

/usr/local/lib/ruby/gems/1.9.1/gems/garb-0.9.2/lib/garb/request/data.rb:39:in `send_request': "{\"error\":{\"errors\":[{\"domain\":\"global\",\"reason\":\"unsupportedOutputFormat\",\"message\":\"Unsupported Output Format\",\"locationType\":\"parameter\",\"location\":\"alt\"}],\"code\":400,\"message\":\"Unsupported Output Format\"}}" (Garb::Request::Data::ClientError)
	from /usr/local/lib/ruby/gems/1.9.1/gems/garb-0.9.2/lib/garb/management/feed.rb:22:in `response'
	from /usr/local/lib/ruby/gems/1.9.1/gems/garb-0.9.2/lib/garb/management/feed.rb:13:in `parsed_response'
	from /usr/local/lib/ruby/gems/1.9.1/gems/garb-0.9.2/lib/garb/management/feed.rb:18:in `entries'
	from /usr/local/lib/ruby/gems/1.9.1/gems/garb-0.9.2/lib/garb/management/profile.rb:12:in `all'

このエラーは https://www.google.com/analytics/feeds/datasources/ga/accounts/~all/webproperties/~all/profiles?alt=json というリクエストを投げた時にraiseされているので、ChangeLogにあるようにJSONフォーマットによる出力がshutdownされたことが直接原因のようです。

うーん、v3対応を書くかなぁ・・・

Gruffで折れ線グラフを描く際のTIPS

rubyには面白いGemがたくさんありますが、その一つにグラフを描くためのライブラリ
Gruff Graphs for Ruby | Ruby on Rails for Newbies
があります。
このGemを用いれば折れ線グラフや円グラフなどを簡単に描くことができますが、少し微妙な部分があります。それを回避するMonkeyPatchを紹介します。

検証したバージョン

OS
Mac OS X Lion 10.7.2
imagemagick
6.7.1
ruby
1.9.2p290
rmagick
2.13.1
gruff
0.3.6

インストール

imagemagickやrmagickを事前にインストールする必要があるため、少し手間がかかります。本家サイトや下記サイトなどを参考に、インストールしてください。
noanoa 日々の日記 : Mac OS X Lion に Ruby グラフ描画用 Gruff をインストールする
Gruffでグラフを書いてみよう - まめ畑
Gruffでグラフ!格好いいグラフを簡単に生成 - OneRingToFind

とりあえず描く

とりあえず、下記のようにすれば折れ線グラフが描けます。日本語を出すためには、FONT指定が必須です。

#!/usr/bin/env ruby
# -*- encoding: utf-8 -*-

require 'rubygems'
require 'gruff'

g = Gruff::Line.new
g.font = "/Library/Fonts/Osaka.ttf"

g.labels = {0=>'01/01', 3=>'01/04', 6=>'01/07'}

g.data("ほげ", [1,2,3,4,5,4,3,2,1])
g.data("foooooooooooooooooooooooooooooooooooooooooooooo",  [9,9,9,8,8,8,7,7,7])
g.data("bar",  [3,2,1,3,2,1,3,2,1])

g.write('hoge.png')

f:id:n_matsui:20120101214223p:plain

y軸の最小値・最大値・増分を指定する

マニュアル等に従えば、minimum_value, maximum_value, y_axis_incrementを指定すればy軸の最小値・最大値・増分を指定できることになっています。
が、現バージョンではエラーになります。

#!/usr/bin/env ruby
# -*- encoding: utf-8 -*-

require 'rubygems'
require 'gruff'

g = Gruff::Line.new
g.font = "/Library/Fonts/Osaka.ttf"

g.minimum_value = 0
g.maximum_value = 15
g.y_axis_increment = 5

g.labels = {0=>'01/01', 3=>'01/04', 6=>'01/07'}

g.data("ほげ", [1,2,3,4,5,4,3,2,1])
g.data("foooooooooooooooooooooooooooooooooooooooooooooo",  [9,9,9,8,8,8,7,7,7])
g.data("bar",  [3,2,1,3,2,1,3,2,1])

g.write('hoge.png')
/Users/cube/.rvm/gems/ruby-1.9.2-p290@rails_3.1.1/gems/gruff-0.3.6/lib/gruff/line.rb:112:in `normalize': wrong number of arguments (1 for 0) (ArgumentError)
	from /Users/cube/.rvm/gems/ruby-1.9.2-p290@rails_3.1.1/gems/gruff-0.3.6/lib/gruff/base.rb:687:in `draw_line_markers'
	from /Users/cube/.rvm/gems/ruby-1.9.2-p290@rails_3.1.1/gems/gruff-0.3.6/lib/gruff/base.rb:536:in `setup_drawing'
	from /Users/cube/.rvm/gems/ruby-1.9.2-p290@rails_3.1.1/gems/gruff-0.3.6/lib/gruff/base.rb:508:in `draw'
	from /Users/cube/.rvm/gems/ruby-1.9.2-p290@rails_3.1.1/gems/gruff-0.3.6/lib/gruff/line.rb:53:in `draw'
	from /Users/cube/.rvm/gems/ruby-1.9.2-p290@rails_3.1.1/gems/gruff-0.3.6/lib/gruff/base.rb:487:in `write'
	from ./grufftest.rb:20:in `<main>'

エラーメッセージは「line.rbにはひとつの引数を取れるnormalizeメソッドなんか無いぜ!」って言ってますが、line.rbのソースを見ると確かに引数をひとつ取れるnormalizeメソッドは存在しません。
下記の引数のないnormalizeメソッドのみが定義されています。

  def normalize
    @maximum_value = [@maximum_value.to_f, @baseline_value.to_f].max
    super
    @norm_baseline = (@baseline_value.to_f / @maximum_value.to_f) if @baseline_value
  end

Lineクラスの親クラスであるBaseクラスを確認すると、def normalize(force=false)という引数有りメソッドが定義されていますが、このメソッドはprotectedです。

つーことで、サクっとline.rbにMonkey Patchを当てることにします。GemのソースをForkして修正してローカルインストールして・・・ ってやっても良いですが、小さい修正ならばrubyのオープンクラス特性を利用してMonkey Patchを当てたほうが早かったりします。

# ./monkey_patch/gruff/line.rb
class Gruff::Line
  def normalize(force=false)
    @maximum_value = [@maximum_value.to_f, @baseline_value.to_f].max
    super(force)
    @norm_baseline = (@baseline_value.to_f / @maximum_value.to_f) if @baseline_value
  end
end

Gemをrequireした後にこのMonkey Patchをrequireし、Gruff::Lineクラスにnormalize(force=true)メソッドを追加します。

・・・
require 'rubygems'
require 'gruff'
require './monkey_patch/gruff/line'
・・・

f:id:n_matsui:20120101221500p:plain

凡例を左揃えにする

gruffは凡例に表示する文字列の長さと描画領域の横幅を考慮して、凡例をオーバーラップしてくれます。これはこれですばらしいのですが、文字列の長さが(この例のように)大幅に異なる場合、なんだか気持ち悪い見た目になります。ついでに、このオーバーラップ計算を強制的にキャンセルしてみます。

凡例はGruff::Baseクラスのdraw_legendメソッドで描画されます。デフォルトのこのメソッドをundefし書き換えることにします。

# ./monkey_patch/gruff/base.rb
module Gruff
  class Base
    undef draw_legend
    def draw_legend
      return if @hide_legend

      @legend_labels = @data.collect {|item| item[DATA_LABEL_INDEX] }

      legend_square_width = @legend_box_size # small square with color of this item

      # May fix legend drawing problem at small sizes
      @d.font = @font if @font
      @d.pointsize = @legend_font_size

      label_widths = [[]] # Used to calculate line wrap
      @legend_labels.each do |label|
        metrics = @d.get_type_metrics(@base_image, label.to_s)
        label_width = metrics.width + legend_square_width * 2.7
        label_widths.last.push label_width

        if sum(label_widths.last) > (@raw_columns * 0.9)
          label_widths.push [label_widths.last.pop]
        end
      end

      current_x_offset =  50
      current_y_offset =  @hide_title ?
      @top_margin + title_margin :
        @top_margin + title_margin + @title_caps_height

      @legend_labels.each_with_index do |legend_label, index|

        # Draw label
        @d.fill = @font_color
        @d.font = @font if @font
        @d.pointsize = scale_fontsize(@legend_font_size)
        @d.stroke('transparent')
        @d.font_weight = NormalWeight
        @d.gravity = WestGravity
        @d = @d.annotate_scaled( @base_image,
                                 @raw_columns, 1.0,
                                 current_x_offset + (legend_square_width * 1.7), current_y_offset,
                                 legend_label.to_s, @scale)

        # Now draw box with color of this dataset
        @d = @d.stroke('transparent')
        @d = @d.fill @data[index][DATA_COLOR_INDEX]
        @d = @d.rectangle(current_x_offset,
                          current_y_offset - legend_square_width / 2.0,
                          current_x_offset + legend_square_width,
                          current_y_offset + legend_square_width / 2.0)

        @d.pointsize = @legend_font_size
        metrics = @d.get_type_metrics(@base_image, legend_label.to_s)
        current_string_offset = metrics.width + (legend_square_width * 2.7)

        line_height = [@legend_caps_height, legend_square_width].max + legend_margin
        current_y_offset += line_height
        @graph_top += line_height
        @graph_height = @graph_bottom - @graph_top
      end
      @color_index = 0
    end
  end
end

こいつをrequireすると

・・・
require 'rubygems'
require 'gruff'
require './monkey_patch/gruff/base'
require './monkey_patch/gruff/line'
・・・

凡例が左揃えになります。

f:id:n_matsui:20120101222548p:plain

3DモデルがアニメーションするARをOpenGL ESで作るには

という記事を@ITに寄稿しました。

http://www.atmarkit.co.jp/fsmart/articles/armobile04/01.html

このシリーズは私のチームメンバーが持ち回りで書いているのですが、段々マニアックになっていくという・・・

NodeとMeCabを用いたtweet連想ゲーム

JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース)の17日目です。
ちょっと目先を変えて、MeCab形態素解析Wikipedia見出し辞書を利用したtweet連想ゲームについて書きます。

はじめに

このMeCab形態素解析を用いたtweet連想ゲームはもともと、1日目を書かれている@hakoberaさんが主催されているNode塾講義その3 Node.js を使ったコードをもくもく書く会で作ったものです。せっかくなのでリファクタリングして公開しますデス。

そもそもMeCabって?

MeCab京都大学情報学研究科−日本電信電話株式会社コミュニケーション科学基礎研究所 共同研究ユニットプロジェクトを通じて開発されたオープンソース 形態素解析エンジンです. 言語, 辞書,コーパスに依存しない汎用的な設計を 基本方針としています. パラメータの推定に Conditional Random Fields (CRF) を用 いており, ChaSenが採用している 隠れマルコフモデルに比べ性能が向上しています。また、平均的に ChaSen, Juman, KAKASIより高速に動作します. ちなみに和布蕪(めかぶ)は, 作者の好物です.

http://mecab.sourceforge.net/

だそうです。自然言語形態素に分解して、各語の原型や品詞、活用形などを教えてくれます。
今回は「出発単語 → 単語をtwitter searchで検索 → 得られた本文を形態素解析 → 名詞を抽出し、ランダムに次の検策語を決定 → 単語をtwitter searchで検索・・・」という処理の形態素解析部分でMeCabを使います。

MeCabIPA辞書をインストール

最新バージョンをutf-8で使いたいので、http://mecab.sourceforge.net/#installあたりを参考に、現時点の最新版0.98をソースからインストールします。

$ wget http://sourceforge.net/projects/mecab/files/mecab/0.98/mecab-0.98.tar.gz
$ tar zxfv mecab-0.98.tar.gz
$ cd mecab-0.98/
$ ./configure --with-charset=utf8
$ make
$ sudo make install
$ sudo ldconfig
$ wget http://sourceforge.net/projects/mecab/files/mecab-ipadic/2.7.0-20070801/mecab-ipadic-2.7.0-20070801.tar.gz
$ tar zxfv mecab-ipadic-2.7.0-20070801.tar.gz
$ cd mecab-ipadic-2.7.0-20070801/
$ make
$ sudo make install

nodeのMeCabバインディングのインストール

探してみたらやっぱり、 Node用MeCabバインディング あと非同期版つくろうとして失敗した話 - mizchi logがありました。ありがとう、偉大な先達たち!
ただ紹介されているコードのままだと、いくつか問題があるため、index.jsに修正を加えました。実際のソースコードは、nmatsui/node-mecab - GitHubから取得してください。

  • requireするディレクトリの問題で0.6系のnodeで動かない => 0.4系の「build/default」が存在しない場合、 0.6系の「build/Release」からrequireする
  • MeCabインスタンス化する際にオプションが指定できない => オプションをつけたMeCabインスタンスを生成できる公開メソッドを追加する
$ git diff
diff --git a/index.js b/index.js
index f00c7ed..639f5a9 100644
--- a/index.js
+++ b/index.js
@@ -1,7 +1,24 @@
-var MeCab = new require('./build/default/mecab');
+var MeCab
+try {
+  MeCab = new require('./build/default/mecab');
+}
+catch (e) {
+  if (e.message.match('Cannot find module')) {
+    MeCab = new require('./build/Release/mecab');
+  }
+  else {
+    throw e;
+  }
+}
+
 exports.MeCab = MeCab;
 
 var nomal = new MeCab.Tagger();
+
+exports.options = function(opt) {
+  nomal = new MeCab.Tagger(opt);
+}
+
 exports.parse = function(text) {
   var buf, i, row, _i, _len;
   row = nomal.parse(text).split("\n");

実験実験!

$ git clone git://github.com/nmatsui/node-mecab.git
$ cd node-mecab/
$ npm install .
$ npm link
$ cd ../tweet-associate/
$ npm link mecab
$ node
> mecab = require('mecab');
{ MeCab: { Tagger: [Function: Tagger] },
  parse: [Function] }
> console.log(mecab.parse('すもももももももものうち'));
[ [ 'すもも', '名詞', '一般', '*', '*', '*', '*', 'すもも', 'スモモ', 'スモモ' ],
  [ 'も', '助詞', '係助詞', '*', '*', '*', '*', 'も', 'モ', 'モ' ],
  [ 'もも', '名詞', '一般', '*', '*', '*', '*', 'もも', 'モモ', 'モモ' ],
  [ 'も', '助詞', '係助詞', '*', '*', '*', '*', 'も', 'モ', 'モ' ],
  [ 'もも', '名詞', '一般', '*', '*', '*', '*', 'もも', 'モモ', 'モモ' ],
  [ 'の', '助詞', '連体化', '*', '*', '*', '*', 'の', 'ノ', 'ノ' ],
  [ 'うち', '名詞', '非自立', '副詞可能', '*', '*', '*', 'うち', 'ウチ', 'ウチ' ] ]
undefined
> 

おけ!

tweet連想ゲーム 初版

ということでMeCabのNodeバインドを用い、とある単語を含むtweetを検索し、その本文に含まれる単語から次のtweetを探し出すシンプルなコンソールアプリを書いてみました。
ただあまりにヘンな単語で検索しても楽しくないので、次の単語を連想する際に

  • 検索する単語の品詞は名詞のみ
  • 前回検索した単語と同じ単語は使わない
  • 1文字の単語は使わない
  • アルファベット、数字、記号のみの単語は使わない

という縛りを入れています。

#!/usr/bin/env nod
var http   = require('http');
var qs     = require('querystring');
var colors = require('colors');
var argv   = require('optimist').argv;
var mecab  = require('mecab');

var initialWord = argv.w || "javascript";

function findNoun(text, searchWord) {
  var nouns = mecab.parse(text).filter(function(morpheme) {
                  return typeof morpheme[0] != 'undefined' &&
                         morpheme[1] == '名詞'   &&
                         morpheme[0].length >= 2 &&
                         morpheme[0] != searchWord &&
                         !/^[!-~]+$/.test(morpheme[0])
                }).map(function(morpheme) {
                  return morpheme[0];
                });
  return nouns[Math.floor(Math.random()*nouns.length)];
}

function tweetAssociate(searchWord) {
  http.get({
    host: "search.twitter.com",
    port: 80,
    path: "/search.json?" + qs.stringify({
      q   : searchWord,
      rpp : "10",
      lang: "ja"
    })
  }, function(response) {
    var body = "";
    response.on('data', function(data) {
      body += data;
    });
    response.on('end', function() {
      var results = JSON.parse(body).results;
      if (results.length == 0) {
        console.log(searchWord + "のようなマニアックな単語はダメ!");
        setTimeout(function() { tweetAssociate(initialWord) }, 1000);
      }
      else {
        var tweet = results[Math.floor(Math.random()*results.length)];
        var nextWord = findNoun(tweet.text, searchWord);
        console.log(searchWord.bold.red + " => " + nextWord.bold.blue  + "\n");
        console.log("@" + tweet.from_user);
        console.log(tweet.text.replace(searchWord, searchWord.bold.red)
                              .replace(nextWord,   nextWord.bold.blue));
        console.log("====");
        setTimeout(function() { tweetAssociate(nextWord) }, 5000);
      }
    });
  });
}

tweetAssociate(initialWord);
$ node ./tweet-associate. -w 連想

って感じで起動すると、"連想"という単語から出発してtweet連想ゲームが勝手に進行します。

tweet連想ゲーム Wikipedia見出し辞書追加版

上記の初版でも動きますが、連想する単語はかなり短いものばかりです。固有名詞はほとんど抽出できませんし、複合語もうまく連想できません。そもそもMeCabの辞書に載っていないのですから、そりゃ無理ってものです。
そこでMeCab: 単語の追加方法MeCab の辞書構造と汎用テキスト変換ツールとしての利用を参考に、Wikipediaの見出し語をMeCabユーザー辞書として追加しましょう。このテキストファイルには、12/17時点で125万語ほどの見出し語が収集されています。

$ mkdir dic
$ cd dic
$ wget http://dumps.wikimedia.org/jawiki/latest/jawiki-latest-all-titles-in-ns0.gz
$ gunzip jawiki-latest-all-titles-in-ns0.gz

取得したWikipedia見出し語を、MeCab辞書として取り込める形式へ変換します。全て一般名詞扱いのため、活用などなく簡単です。
ただし重要なのは、語のコストです。なるべく長いキーワードを優先したいので、長いキーワードほど小さな値になるようにします。

#!/usr/bin/env ruby
# -*- encoding: utf-8 -*-
ARGF.each do |word|
  word.chomp!
  if word.length >= 2 && 
     /^[!-~]+$/.match(word).nil? &&
     /^.*(,|").*$/.match(word).nil?
    cost = [-36000, -400 * word.length**1.5].max.to_i
    puts "#{word},0,0,#{cost},名詞,一般,*,*,*,*,#{word},*,*"
  end
end

このスクリプトでWikipedia見出し語辞書の元ネタを作り、MeCabが認識できるバイナリ形式へコンパイルします。このWikipedia見出し語辞書を有効にしてmecabを実行してみると・・・

$ ./conv.rb jawiki-latest-all-titles-in-ns0 > wikipedia.csv
$ /usr/local/libexec/mecab/mecab-dict-index -d /usr/local/lib/mecab/dic/ipadic -u wikipedia.dic -f utf-8 -t utf-8 wikipedia.csv
$ mecab -u wikipedia.dic
すもももももももものうち
すもももももも	名詞,一般,*,*,*,*,すもももももも,*,*
もも	名詞,一般,*,*,*,*,もも,*,*
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

と、ノーマルなIPA辞書とは違いすももももも - Wikipediaを名詞として認識するようになりました。

nodeから追加辞書を指定して実行する場合、optionsメソッドを使います。

$ node 
> mecab = require('mecab');
{ MeCab: { Tagger: [Function: Tagger] },
  options: [Function],
  parse: [Function] }
> mecab.options("-u dic/wikipedia.dic");
undefined
> console.log(mecab.parse('すもももももももものうち'));
[ [ 'すもももももも', '名詞', '一般', '*', '*', '*', '*', 'すもももももも', '*', '*' ],
  [ 'もも', '名詞', '一般', '*', '*', '*', '*', 'もも', '*', '*' ],
  [ 'の', '助詞', '連体化', '*', '*', '*', '*', 'の', 'ノ', 'ノ' ],
  [ 'うち', '名詞', '非自立', '副詞可能', '*', '*', '*', 'うち', 'ウチ', 'ウチ' ] ]
undefined
> 

最後に、コンソールアプリを修正します。といっても、コマンドラインからMeCab用のオプションを取れるようにするだけです。

$ diff -u tweet-associate.js tweet-associate2.js 
--- tweet-associate.js	2011-12-17 13:09:32.166163004 +0900
+++ tweet-associate2.js	2011-12-17 15:47:59.896162998 +0900
@@ -6,7 +6,9 @@
 var mecab  = require('mecab');
 
 var initialWord = argv.w || "javascript";
-
+if (argv.u) {
+  mecab.options("-u " + argv.u);
+}
 function findNoun(text, searchWord) {
   var nouns = mecab.parse(text).filter(function(morpheme) {
                   return typeof morpheme[0] != "undefined" &&

では、実行してみましょう。なんとなく、長い複合語もとれるようになったと思います。

$ node ./tweet-associate2.js -w ゲーム -u `pwd`/dic/wikipedia.dic

最後に

MeCab楽しいですね!
今回は次の単語を連想する際に名詞をランダムに選んでいましたが、元の検索語とのレーベンシュタイン距離や単語長を元に「イイカンジの単語」を選ぶようにするともっと面白いかもしれません。が、力尽きました・・・orz

明日は、砂波幹雄さんです!

Kinect + Node で似非ストリーミング配信

KinectとNode.jsを用いた似非ストリーミング配信

のブログを、ウチの会社で公開している技術ブログ Tech-Sketch - http://tech-sketch.jp/ に書いてます。

そうか、Node 0.4.11 の時代だったのか・・・
続きを実験する時間がナカナカ取れないなぁ