株式会社クリアコード > ククログ

ククログ

クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)Rubyに関連した開発を得意としています。

Ohloh profile for kou
最新
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|
タグ:

LDD '10 Winter: メールフィルタの作り方 - Rubyで作るmilter

先日、LOCAL DEVELOPER DAY '10 WinterでRubyでmilterを作る方法について話してきました。どのタイミングでどのmilterプロトコルのコマンドが発行されるかについても説明しているので、Rubyではなく(libmilterを使って)Cでmilterを実装する場合にも参考になる部分があるはずです。むしろ、Rubyとmilterの組み合わせについて話している部分は薄めです。これは、Rubyそのものとmilterの仕組みを理解していればRubyとmilterを組み合わせることは容易だからです。

メールフィルタの作り方 - Rubyで作るmilter

少しgroongaについてもふれています。

それでは、ダイジェストで資料の内容を紹介します。完全版はリンク先を見てください。資料のPDF・ソースもリンク先にあります。

内容

話題

具体的にRubyでmilterを作る話に入る前に、まず、前提となる知識を確認します。

はじめにメールフィルタ、次にメールフィルタの仕組みの1つであるmilterについて簡単に説明します。その後、一度milterから離れてSMTPについて説明します。これはmilterの動作を理解するためにはSMTPの動作も知っておく必要があるからです。SMTPの動作を確認したらそれをふまえてmilterの具体的な動作を説明します。

ここまできたらRubyでmilterを作るための下準備は整っているはずです。実際に1つRubyでmilterを作ってみます。

ゴール

今日のみなさんのゴール

説明の途中にいくつか確認ポイントがあります。それぞれの技術は他の技術をベースになりたっているので、ベースとなっている技術をおさえていくことが、理解してしっくりくるためのコツです。

それぞれの確認ポイントをゴールとして最終的な「Rubyでmilterを作れる」ようになるゴールまでたどりついてください。

メールフィルタ

メールフィルタ

メールシステムとは外部とユーザ間でメールを配信するシステムです。すべてのメールシステムではそのままメールをやりとりするのではなく、メールを配信するまでのあいだに、メールに対してなんらかの処理を実行します。つまり、すべてのメールシステムにはメールフィルタ機能が備わっています。

MTAのプラグインとする方法

MTAにプラグイン

メールシステムでメールフィルタを実現する方法はいくつかありますが、その1つがMTA(メールサーバ)のプラグインとして実現する方法です。この方法のメリットはMTAを変更せずにメールフィルタ機能を変更できることです。milterはこのタイプで動作するメールフィルタです。

メールフィルタのまとめ

メールフィルタのまとめ

メールフィルタはメールシステムが持っている必須機能の1つです。その実現方法としてMTAのプラグインとして実現する方法があり、milterもその方法で実現されているメールフィルタです。

それでは、milterの概要について説明します。

milterについて

milter?

milterの名前の由来は「mail filter」です。milterは汎用的なメールフィルタの仕組みのため、同じメールフィルタを異なるMTAと一緒に使うことができます。

Sendmailを用いているメールシステムではmilterを利用していることが多く、milterをサポートした商用のメールフィルタも多く存在します。最近ではPostfixのmilterサポートがリリース毎に改善されていっているため、Postfixを用いたメールシステムでもmilterを利用するケースが徐々に増えています。

milterシステム

milter関連用語

「milter」は文脈によって異なるものを指すことがあります。そこで、ここでは混乱を避けるために異なる名前で呼ぶことにします。

まず、メールフィルタそのものを「milter」と呼びます。

メールフィルタとMTAは別プロセスで動作するため、プロセス間通信でフィルタ対象のメールやフィルタ結果などをやりとりする必要があります。そのやりとりのきまりを「milterプロトコル」と呼びます。

そして、「milter」と「milterプロトコル」をサポートしたMTAを含んだメールフィルタの仕組み全体を「milterシステム」と呼びます。

「milter」といった場合は「メールフィルタそのもの(ここでいうmilter)」という意味で使う場合と、「メールフィルタの仕組み(ここでいうmilterシステム)」という意味で使われる場合が多いです。「milter」という単語が使われている場合はどちらの意味かを判断できるようになってください。

milterプロトコルはSMTPと密接に関連したプロトコルです。そのため、milterプロトコルについて説明する前に、SMTPについて確認します。

SMTPの概要

簡単?

SMTPは以下の4つのコマンドが基本となるシンプルなプロトコルです。

  • HELO
  • MAIL FROM
  • RCPT TO
  • DATA

まず、「HELO」で接続したSMTPクライアントの情報を伝えます。以下の例ではSMTPサーバ(MTA)からのメッセージは先頭に「<」をつけて示します。SMTPクライアントのメッセージは先頭に「>」をつけて示します。

% telnet localhost smtp
< 220 note-pc.example.com ESMTP Postfix (Ubuntu)
> HELO localhost.example.com
< 250 note-pc.example.com

挨拶が済んだらSMTPセッションのスタートです。1つのSMTPのセッションで複数のメールを送ることができます。「MAIL FROM」、「RCPT TO」、「DATA」で1つのメールを送ります。

まず、「MAIL FROM」で送信者を伝えます。

> MAIL FROM: <kou@example.com>
< 250 2.1.0 Ok

次に、「RCPT TO」で宛先を伝えます。

> RCPT TO: <info@example.com>
< 250 2.1.5 Ok

同じメールを複数の宛先に送ることもできます。その場合は「RCPT TO」を複数回実行します。

最後に「DATA」でメールの内容を伝えます。メールの最後は「.」だけの行になります。

> DATA
< 354 End data with <CR><LF>.<CR><LF>
> Subject: Hello
> From: <kou@example.com>
> To: <info@example.com>
> 
> This is a test mail!
> .
< 250 2.0.0 Ok: queued as 054C624FB

これで、1通のメールを送信できました。続けてメールを送信する場合はまた「MAIL FROM」から始めます。

SMTPセッションを終了する場合は「QUIT」です。

> QUIT
< 221 2.0.0 Bye

これで1つのSMTPセッションが終了しました。

milterプロトコルはSMTPと密接に関わっています。それでは、milterプロトコルの詳細を説明します。

SMTPとmilterプロトコル

SMTPとmilterプロトコル

milterプロトコルにもSMTPと同じようにコマンドがあります。そして、そのコマンドはSMTPのコマンドと対応したものになっています。まずSMTPのコマンドを説明したのはそのためです。

例えば、SMTPで「HELO」というコマンドが実行された場合、「HELO」に対応する「helo」というmilterプロトコルのコマンドが発行されます。このとき、SMTPクライアントが指定したHELOコマンドの引数がmilterに渡されます。

MTAはmilterにコマンドを送った後、milterからの返答があるまでSMTPクライアントには返答しません。つまり、milterが「helo」でrejectを返すことで、SMTPクライアントの「HELO」コマンドへの返答をrejectとすることができます。これにより、MTAがSMTPレベルでできることとほとんど同じことをmilterで実現できます。

milterプロトコルのコマンドとSMTPのコマンドの対応

コマンド: メタ情報

mitlerプロトコルのコマンドはほとんどSMTPのコマンドに対応していますが、milterプロトコルのコマンドの方がより細かくなっています。例と一緒にコマンドの対応を説明します。

SMTPでの最初のコマンドは「HELO」ですが、milterプロトコルでは「helo」よりも前にコマンドが発行されます。それが、SMTPクライアントがSMTPサーバに接続したときに発行される「connect」コマンドです。

「connect」コマンド以外はSMTPのコマンドとmilterプロトコルのコマンドは1対1で対応します。「envfrom」の「env」は「envelope」の略で、「封筒」という意味です。「envfrom」で「差出人」という意味、「envrcpt」で「宛先」という意味です。「rcpt」は「recipient」の略で「受信者」という意味です。

SMTPでは1つのメールを複数の宛先に送信できます。この場合、複数回「RCPT TO」を指定します。STMPで複数回「RCPT TO」が指定されるので、milterプロトコルでも「envrcpt」コマンドが複数回発行されます。

コマンド: DATA

SMTPの「DATA」コマンドはmilterプロトコルではより細かいコマンドに分解されています。

まず、「DATA」コマンド時にはmilterプロトコルの「data」コマンドがすぐに発行されます*1。その後、SMTPクライアントはメール本体を送信しますが、「header」などのイベントはすぐには発生しません。SMTPクライアントがデータの終了を示す「.」のみの行を入力するまでは何も起きません。「.」のみの行が入力されると、MTA側でメール本文をパースして「header」、「eoh」(end of header: ヘッダーの終わり)、「body」、「eom」(end of message: メッセージの終わり)コマンドを発行します。もちろん、ヘッダーもパースしてあるので、MTAは「ヘッダー名」と「ヘッダー値」と分解した状態で情報を渡します。

このようにmilterプロトコルはSMTPと密接に関わっています。milterプロトコルのコマンドがわかれば、自分が必要な機能を持つmilterを実現するためにはどのコマンドを利用すればよいかを考えることができるでしょう。

milterサンプル: メール検索

扱うもの

説明用のサンプルとしてメール検索を実現するmilterを作成します。今回はSubject、From、Toと本文のみを扱うことにします。

メール検索を実現するために、全文検索エンジンとしてgroongaを、milterライブラリとしてmilter managerのRubyバインディングを使います。

groonga: カラム指向データストア

カラム指向

groongaは全文検索のためのインデックス作成機能だけではなく、データストアの機能も持っています。groongaのデータストアはカラム指向で、リレーショナルデータベースとは違い、レコード(行)毎にデータをまとめて持つのではなく、カラム(列)毎にデータをまとめて持っています。

このようにデータを持つと、同じカラムの複数の値へのアクセスを高速に行うことができます。このため、カラムの値を使った集計処理を高速に実行できます。集計処理とは、例えば、SQLでいうGROUP BYのような処理です。

集計処理を用いると絞り込み検索をしやすいユーザーインターフェイスを提供することができます。例えば、ショッピングサイトで商品に複数のタグがついているとします。このとき、同じタグがついている商品が何項目あるかを表示してリンクにします。1つも商品が属していないタグは表示しないようにすれば、ユーザは無駄な絞り込み操作を行わずにすみます。

全商品(123件)
タグ
  スポーツ(58件)← リンクにする
  映画(45件)    ← リンクにする
  食べ物(36件)  ← リンクにする
  旅行(0件)     ← 表示しない

この状態で「スポーツ」をクリックしたとします。

全商品(123件) > スポーツ(58件)
タグ
  スポーツ      ← 選択済みなので表示しない
  映画(26件)  ← リンクにする
  食べ物(0件) ← 表示しない
  旅行(0件)   ← 表示しない

このように、絞り込んだ後にがっかりするような操作を示さないことにより、絞り込み検索をしやすいユーザインターフェイスを作ることができます。がっかりするような操作かどうかを判断するために、同じ値を持つレコードの個数を数える、といった集計処理をしています。

groonga: バイナリパトリシアトライ

パトリシアトライ

groongaはキー管理のためのデータ構造としてハッシュテーブルとバイナリパトリシアトライを採用しています。バイナリパトリシアトライはパトリシアトライの一種です。

ここにB+木とパトリシアトライの説明を書く予定でしたが、もう、だいぶ長くなっているので省略します。また別の機会があれば紹介します。

パトリシアトライを利用すると効率よく最長一致検索を実現できます。これを試してみるためのサンプルアプリケーションを用意しました。

groongaでキーワード検出

リンク先ではキーワードを変えて試すことができます。

最長一致機能を利用してキーワード検出している部分のソースは以下の通りです。

target_text = "..."
keywords = request["keywords"].split

words = Groonga::PatriciaTrie.create(:key_type => "ShortText",
                                     :key_normalize => true)
keywords.each do |keyword|
  words.add(keyword)
end
tagged_text = words.tag_keys(target_text) do |record, word|
  "<span class='keyword'>#{word}</span>"
end

まず、パトリシアトライを作り、キーワードを登録します。Groonga::PatriciaTrieにはtag_keysという便利メソッドがあり、これを使うと「最長一致検索」→「キーワードにタグ付け」をより簡潔に記述することができます。

全体のソースはリンク先にあるソース一式の中に含まれています。

スキーマ

スキーマ: Messages

groongaのRubyバインディングであるRuby/groongaはスキーマ定義のためのDSLを提供しています。

メールを保存するMessagesテーブルにはsubjectfromtobodyカラムを定義しています。今回は簡単のため、宛先は1つのみ扱うことにしています。

スキーマ: Terms

次に、高速に全文検索を行うために索引を作成します。Termsテーブルのキーに単語(ここではbigramを利用しているので1文字か2文字の文字列)、カラムにその単語が出現するMessagesレコードのID(とN-gramなので単語の出現位置)を保持します。

subjectカラムとbodyカラムでそれぞれに対して索引を作成しています。こうすることにより、「どこかに○○が含まれているメールを検索」といった検索だけではなく、「Subjectに○○が含まれているメールを検索」、「本文に○○が含まれているメールを検索」というような細かい検索ができるようになります。細かい検索が必要ない場合はMessagesテーブルに検索対象をすべて入れたカラムを1つ作り、そのカラムに対して索引を作成してもよいでしょう。

Groonga::Schema.define do |schema|
  schema.create_table("Messages") do |table|
    ...
    table.text("text")
  end

  schema.create_table("Terms",
                      :type => :patricia_trie,
                      :default_tokenizer => "TokenBigram",
                      :key_normalize => true) do |table|
    table.index("text")
  end
end

messages = Groonga["Messages"]
from = "kou@clear-code.com"
to = "info@clear-code.com"
body = "Hello Ruby and milter!"
text = "#{from} #{to} #{body}" # <- textに検索対象をまとめる
messages.add(:from => from
             :to => to,
             :body => body,
             :text => text)

query = "Ruby"
messages.select do |record|
  record["text"].match(query) # <- textカラムで全文検索
end

Rubyでmilterを作る

Rubyでmilter

データの保存・検索の仕組みはできたので、あとは、groongaのデータベースにメールを登録するだけです。

milter managerのRubyバインディングのAPIでは、ユーザがmilterプロトコルのコマンドに対応するメソッドを定義し、ライブラリ側がそのメソッドを呼び出します。今回必要な情報はヘッダーと本文にあります。そのため、今回のmilterは以下のようになります*2

class ArchiveMilter < Milter::ClientSession
  def initialize
    @messages = Groonga["Messages"]
    @values = {}
    @encoding = nil
    @body = ""
  end

  def header(context, name, value)
    case name
    when /\A(Subject|From|To)\z/i
      key = $1.to_s.downcase
      utf8_value = NKF.nkf("-w", value)
      @values[key] = utf8_value
    when /\AContent-Transfer-Encoding\z/i
      @encoding = value
    end
  end

  def body(context, chunk)
    @body << chunk
  end

  def end_of_message(context)
    nkf_option = "-w"
    nkf_option << " -MB" if @encoding == "base64"
    @values["body"] = NKF.nkf(nkf_option, @body)
    @messages.add(@values)
  end
end

このように、Rubyでmilterを作るときは必要な処理の部分だけを記述するだけですみます。つまり、やりたいことを実現するためにどういうデータが必要で、どのタイミングでそのデータを手に入れられるかがわかれば、Rubyでmilterを作ることは簡単だということです。

登録したメールは以下のように検索・表示することができます。

query = "Ruby" # <- 検索キーワード
messages = Groonga["Messages"]
result = messages.select do |record|
  record["subject"].match(query) |
    record["body"].match(query)
end
result.sort([["_score", :desc]]).each do |message|
  puts "-" * 78
  puts "score: #{message.score}"
  puts "Subject: #{message.subject}"
  puts "From: #{message.from}"
  puts "To: #{message.to}"
  puts
  puts message.body
  puts "-" * 78
end

まとめ

Rubyでmilterを作る方法について説明しました。そのために必要な技術として、milterプロトコルの具体的な動作も説明しました。ここで説明されている内容を理解していれば、より詳細なmilter関連情報も理解しやすくなるでしょう。英語ですが、milterに関する情報はmilter.orgにまとまっています。より詳しい情報を知りたい場合はチェックするとよいでしょう。

札幌はやはりやさいい雰囲気に包まれていました。札幌Ruby会議02とは少し違う雰囲気でしたが、似ているとは感じました。

一度、札幌の人たちに会いに行ってみてはいかがでしょうか。

あわせて読みたい

  • 2010-02-14 - iakioの日記 - postgresqlグループ

    「C言語でPostgreSQLを拡張する」というタイトルで石田さんが淡々とライブコーディングされていました。会場とやりとりをしながらコーディングする様子を見ていると、札幌っぽい雰囲気を感じることができるでしょう。

*1  「data」コマンドはmilterプロトコルのバージョン4から追加されたコマンドなのでそれより古い2などを使っている場合は利用できません

*2  milter managerでは「eom」というような省略した名前を「end_of_message」という省略しない自己記述的な名前になっているので注意してください。

2010-02-15

Ruby/groonga 0.9.0, 0.9.1: 高速で使いやすい検索エンジンライブラリへ向けて

今年も肉の日がきましたね。

Ruby/groonga0.9.0がリリースされました。リリース後すぐにgroongaの新しいバージョン0.1.6がリリースされたため、0.1.6に対応した0.9.1がすぐにリリースされました。gemで自動インストールされるgroongaのバージョンが0.1.6になっているだけで、Ruby/groongaの機能は変わっていません。

いつも通り、以下のコマンドでインストールできます。システムにgroongaがインストールされていない場合は自動でダウンロードしてインストールします。

% sudo gem install groonga

全文検索エンジンgroongaの特徴はgroongaのドキュメントを参照してください。

0.9.x

前のリリースでは0.0.7だったバージョン番号が一気に0.9.1まであがっています。バージョン番号から想像できる通り、初のメジャーリリース1.0.0を意識しはじめたということです。

0.9.x系列では1.0.0に向けて以下の2点を重点的に開発していきます。

  1. より使いやすいAPIの提供
  2. 高速化

APIを改良するため、以前のバージョンとは互換性が壊れることがありますが、今後より便利にRuby/groongaを使うために、今のうちに積極的に改良していく予定です。使ってみて、「ここがこうなっていたらもっと使いやすい」、「こういうAPIがあると便利」などという意見があったらぜひお知らせください。

Groonga::Context.default#[]のショートカットとしてGroonga.[]を導入するなど、もうすでに便利なAPIの追加は始まっています。

現在のRuby/groongaにはまだ高速化の余地があります。あまり意識せずにスクリプトを書いてもなるべく高速に動作するようにライブラリ側でできることはなるべくライブラリ側で頑張る方向で開発していく予定です。ユーザが使いやすくなるように処理系が頑張るというのはRuby本体と同じ方向です。

まとめ

今年も年に一度の肉の日がきました。

Ruby/groonga 0.9.1がリリースされています。Rubyで全文検索システムを構築したい場合はRuby/groongaも検討してみてはいかがでしょうか。今のうちに改善案をだしておけばメジャーリリース時にはそれが反映されてより便利に高速な全文検索システムを構築できるようになるかもしれません。

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2010-02-09

告知: LOCAL DEVELOPER DAY '10/Winter: メールフィルタの作り方 - Rubyで作るmilter

来月2/13(土)に札幌で開催されるLOCAL DEVELOPER DAY '10/WinterでRubyでメールフィルターを作る方法について話します。

日時
2010/2/13(土) 12:45〜18:35
場所
札幌市産業振興センター セミナールームA
参加費用
無料
参加登録
必要無し(懇親会は登録が必要)

内容

Rubyでメールフィルターを開発する方法について話します。以下、背景などをまじえてもう少し詳しく説明します。

SendmailやPostfixといったよく使われているメールサーバにはmilterというメールフィルターを追加する仕組みが実装されています。milterを使うことにより、メールサーバに迷惑メール対策機能やウィルスチェック機能、メールアーカイブ機能、添付ファイル自動暗号化機能などを追加することができます。つまり、メールサーバ本体を変更せずに組織のポリシーに合わせたメールシステムを構築することができるということです。

milterという仕組みを使ったメールフィルター*1はすでにたくさん開発されているので、既存のものを組み合わせてメールシステムを構築できることも多いです。しかし、組織特有の事情などがある場合は既存のメールフィルターでは対応できないこともあるでしょう。そういった場合、新しくメールフィルターを開発したり既存のメールフィルターを改造して対応できます。

通常、メールフィルターはC言語で開発する必要がありますが、milter maangerが提供する機能を利用することによってRubyを使って素早くメールフィルターを開発することができます。

今回は、milter managerの機能を使ってRubyでメールフィルターを開発する方法やデバッグの方法などを紹介します。milterという仕組みを知らない方でもわかるように、milterという仕組みから順を追って説明します。ただし、Rubyについて詳しく説明しないので、Rubyがまったくわからない方には少し厳しいかもしれません。札幌でRubyについて詳しくなりたい方はRuby札幌に参加することをオススメします。

まとめ

来月開催されるLOCAL DEVELOPER DAY '10/Winterで、Rubyを用いてメールフィルターを作る方法について話すので、それを告知しました。

JavaScript(Ext JS)やWebアプリケーションのテスト(Selenium)、ドキュメント指向データベース(MongoDB)、リレーショナルデータベース(PostgreSQL)の話などもあるようです。参加登録も必要ないので、興味のある方はお気軽に参加してみてはいかがでしょうか。

*1  混乱するかもしれませんが、「milterという仕組みを使ったメールフィルター」もmilterと呼びます。milterといった場合は仕組みよりメールフィルターのことを指すことが多いです。

2010-01-25

Ruby on Rails Technical Night: Railsで作るActive Directoryと連携した社内システム

先日開催された〜Ruby on Rails Technical Night〜 Ruby on RailsセミナーでActive Directoryと連携したRailsアプリケーションの作り方について話しました。

Railsで作るActive Directoryと連携した社内システム

概要

ActiveLdapという社内システムをRailsを使って実現するときに便利なライブラリをデモを交えながら紹介しました。

社内向けのシステムをWebアプリケーションとして実現することは驚くことではなくなりました。Webアプリケーションなので、もちろんRailsを使っても実現することができます。

そのときに避けて通れないのが既存の社内情報との連携です。社内向けのシステムなので、社内情報と密接に連携し、より便利に使えるものであるべきです。多くの組織では社内情報をActive Directoryを用いて一元管理しています。

Railsアプリケーションとして社内システムを実現する場合も、必要に応じてActive Directory内にある情報を利用します。それを助けてくれるライブラリがActiveLdapです。

内容

Active Directoryをあまり知らない方も参加されるかたでもついてこられるように*1、前半でActive DirectoryとLDAPの基本的なところを説明しました。今回は「図での説明 + まとめ」というように、感覚的にわかってもらった後(なんとなくわかってもらった後)に要点を確認するという流れにしました。参加された方に感想を聞けなかったのですが、いかがだったでしょうか。

その後、実際にデモを行いながら、ActiveLdapがActiveRecordと同じようにActive Directory上の情報を操作できることを説明しました。ActiveRecordと同じように操作できるということは、いつもと同じようにコントローラ部分を書けるということです。つまり、今までのRailsアプリケーション開発の知識を活かしながらActive Directoryを操作するRailsアプリケーションを開発することができるということです。

今回は「コードがバンバン出るような内容を」ということで声をかけてもらったので、ここがメインの内容になっています。残念ながら公開されている資料ではデモを表現することができないので、デモで実行したコマンドなどを載せています。

最後に、実際にアプリケーションを開発するときにぶつかることが多い問題点についてふれました。Active Directoryとの接続の仕方、テストの仕方などです。

まとめ

先日開催されたRailsセミナーでの内容を紹介しました。

ActiveLdapに興味をもたれた方はるびまのActiveLdap を使ってみよう(前編)ActiveLdapのチュートリアルも読んでみてください。次号のるびまでは後編が公開される予定です。そちらも楽しみですね。

今回の内容はActive Directoryに特化した内容もありますが、OpenLDAPなどLDAPサーバ一般に通用する内容も多いです。LDAPサーバと連携するRailsアプリケーションを開発している方も参考にしてみてください。

Active Directory関連のことだけではなくて、コードを書くことについても伝えたかったのですが、欲張りすぎました。話の中ではうまく伝えられませんでしたが、プログラマーの方であれば、グラデーションで繋がる世界: 札幌Ruby会議02に行ってみて初心に帰ったもぜひ読んでみてください。

*1  多くの方はご存知のようでしたが。。。

つづき: 2009-12-21
Tags: Ruby | このエントリの del.icio.us history 5 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-12-15

札幌Ruby会議02: レシピに書かれていないこと

先週末開催された札幌Ruby会議02でライブコーディングしてきました。本州枠の1つで話す機会を作ってくれた実行委員長のしまださん、ありがとうございます。

レシピに書かれていないこと

内容

今回の発表はスライドだけでは伝わらないはずです。いつも、話すことすべてをスライドに盛りこんでいませんが、今回は特にスライドに盛りこまれていないことが多いです*1

仙台では考え方について話しましたが、札幌では技術的なことを話すつもりでした。札幌で何かを伝えることができるのなら、それは技術的なことであって欲しいし、Ruby 逆引きレシピをリリースしているRuby札幌がいる札幌でならそれができるはず、というのが理由です。

そのために、今回はライブコーディングをすることにしました。

自分ができる、一番、技術的なことを伝える方法はプログラムを書くことです。プログラミングに限ったことかもしれませんが、何かを伝えるためには、結果だけではなく、過程も一緒に伝えた方が、より伝わります。ペアプログラミングが成果をあげているという声を聞いたこともあるのではないでしょうか。

ライブコーディングは1対1ではなく、1対nの形になるので、それで本当に伝えることができるのかは不安でしたが、札幌でなら大丈夫だろうということで決行しました。

札幌Ruby会議02の模様はニコニコ動画にアップロードされています。

レシピに書かれていないこと - 須藤 功平

2009-12-22
再生: 76
コメント: 2
マイリスト: 1

レシピに書かれていないこと - 須藤 功平 (20:41)
札幌Ruby会議02 (2009/12/05)mylist: mylist/16614060time table: http://regional.rubykaigi.org/sapporo02

札幌でたいやきってな w

tDiaryの話をした柴田さんも録画してくれました。ありがとうございます。こちらはYouTubeにあります。

FAQ

Q: 練習したのですか?

A: 2, 3回練習しました。

あまり練習すると本番で間違わなくなるのであまり練習しないようにしました。実際、ライブコーディング中にいくつか間違いました。間違ったときにどうやって直していくかという過程も見てほしかったのでよかったです。

実際の開発はデバッグの連続です。間違わずに5分で完成させる動画通りにはいかないものです。

Q: 少し速いですね。

A: 使い慣れたエディタを使ったからです。

自分ではあまり意識して使っていませんが、意識してみると、以下の機能を使っているようです。以下の機能を使うことにより通常より速くコードを書くことができるかもしれません。

  • 動的略語展開: 入力途中の単語を補完します。補完候補は現在開いているすべてのバッファ内にある単語すべてです。デフォルトではM-/にバインドされていますが、補完といえばタブなのでC-c C-iにバインドしています。

    (define-key global-map "\C-c\C-i" 'dabbrev-expand)
  • "end"の挿入: ruby-modeではC-c C-eで"end"を挿入できます。
  • M-f/M-bでの移動: C-f/C-bより大きい単位で移動します。
  • C-hとC-dを同時に使う: C-hはバックスペースでC-dはdeleteですが、行内の箇所を削除する時はC-hとC-dを同時に使って削除しているらしいです。指摘されるまで自分でも気づいていませんでした。
Q: ソースコードは公開されていますか?

A: ラングバプロジェクトのSubversionリポジトリでAGPL v3+で公開しています。

リポジトリのURL
http://groonga.rubyforge.org/svn/examples/message-archiver/

当日利用したリビジョンはr874なので、以下のコマンドで同じソースコードを取得できます。

% svn co -r874 http://groonga.rubyforge.org/svn/examples/message-archiver/

札幌Ruby会議02全体の内容

仙台ともとちぎとも違う雰囲気でした。とてもとても居心地がよかったのが忘れられません。Rubyを使って気分よくプログラムを書いているときと少し似ている気がします。

最初から最後まで最前列で話を聞いていました。文脈は少し違うのですが、ちょうど興味のある話題や、前から考えていたことに関連することが多く、参考にできることがたくさんありました。

田中さんの、ユーザが余計なことを考えなくても簡単に使えるようにしたい、という話、sumimさんや谷口さんの、興味を持って学習するためにはという話、和田さんの、増加するテストに立ち向かう方法の話、高橋さんの、やめる勇気の話、角谷さんの、選んだらそれでハッピーではない、上を見てもキリがないし下は見てもしょうがないんだから自分ができることをやっていく、という話。文脈が違うので自分の中で変換しながら自分に合わせて聞いていたので、発表された方が一番言いたかったこととずれているかもしれません。

ツールでは、しだらさんの紹介していたJekyllも使ってみたくなりました。とちぎではそうでもなかったのですが、今回は使ってみたくなりました。とちぎでは駆け足だったからかもしれません。

Reject Talks

技術的なことは本編とは別のReject Talksが楽しかったです。Reject Talksでは、単に発表者が話すのではなく、観客が随時コメントをしていくという形式に(結果的に)なっていました。その中で、このコードがこう悪い、こうした方がよい、という話をできたことがとても楽しかったです。

話す側と聞く側ではなく、両者が話しあっていることがとても印象的でした。

まとめ

札幌Ruby会議02での発表内容について補足しました。また、1参加者から見た札幌Ruby会議02も簡単にレポートしました。

札幌Ruby会議02がすばらしかったのはスタッフのみなさん、発表者のみなさん、参加者のみなさんなど関係者のみなさんのおかげでしょう。でも、もう少し考えてみると、実行委員長のしまださんがいることが一番大きいのではないかと感じています。みなさんは、司会をしていたしまださんが想いがこもった発表者紹介をしていたのに気づいていましたか。想いのこもった感想を述べていたことに気づいていましたか。しまださんの発表枠はありませんでしたが、しまださんの想いを感じることができたすばらしい札幌Ruby会議02でした。

そんな札幌Ruby会議02に発表者・スポンサーとして参加できてとても光栄です。

今回はActiveLdapには触れられませんでしたが、12/14のRailsセミナーではActiveLdapに触れるので、興味のある方は参加してみてください。

あわせて読みたい

*1  公開用に多少加筆してあります。

Tags: Ruby | このエントリの del.icio.us history 8 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-12-07

告知: 2009/12/14のRailsセミナーはRailsとActive Directoryについて

来月中旬の12/14(月)に〜Ruby on Rails Technical Night〜Ruby on RailsセミナーでActive Directoryと連携したRailsアプリケーションの開発方法について話します。セミナーの概要は以下の通りです。都合があう方はぜひお越しください。

タイトル
Railsで作るActive Directoryと連携した社内システム
日時
2009年12月14日(月)19時30分〜21時
場所
株式会社オプト会議室
参加方法
atndで申し込み

さも1人で話すように書いていますが、そんなことはなく、株式会社ローハイド.の吉見さんと株式会社万葉の河野さんの3人で、3部構成です。運用のお話や開発時のお話をされるようなので、そちらに興味のある方も参加してみてください。

内容

内容はタイトルの通りで、RailsアプリケーションからActive Directoryの情報を利用する方法、注意しなければいけない点などについて話します。社内システムなど、すでにActive Directoryを導入している環境で動作するRailsアプリケーションを開発する場合、独自にアカウントを管理するのではなく、Active Directory上のアカウント情報を利用する方が、利用者の使い勝手もよくなりますし、運用者の負担も減ります。そういった場合にどのようにActive Directory上のアカウント情報を利用するのがよいか、ということをコード例も示しながら説明します。

Active Directoryとの接続には、ActiveLdap(参考: Rubyist Magazine - ActiveLdap を使ってみよう(前編))を使います。つまり、Active DirectoryをLDAPサーバとして使った場合のRailsアプリケーションの作り方とも言えます。よって、この話の内容はOpenLDAPなどActive Directory以外のLDAPサーバにも応用できます。LDAPサーバと連携したRailsアプリケーションに興味がある方にも楽しんでもらえるのではないでしょうか。

まとめ

来月のRailsセミナーでRailsとActive Directoryについて話すことになったので、それの告知をしました。参加費は無料なので興味のある方はお気軽に参加してみてください。現時点で定員の半分ほど埋まっているようなので、気になる方はお早めにどうぞ。

タイトル
Railsで作るActive Directoryと連携した社内システム
日時
2009年12月14日(月)19時30分〜21時
場所
株式会社オプト会議室
参加方法
atndで申し込み
つづき: 2009-12-07
Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-11-30

とちぎRuby会議02の資料公開

先日開催されたとちぎRuby会議02で社長の一人として話しました。声をかけてくれたtoRubyのみなさん、ありがとうございます。

儲かるRuby - 支えるRuby

システム構成

Debian GNU/Linux sidが動いているMacBookを持っていきました。事前の接続テストでうまくプロジェクターに出力できなかったので、ワイクル株式会社kdmsnrさんのMacBook経由で出力しました。突然のお願いにもかかわらず快く貸してくれました。ありがとうございます。

システム構成

Rabbit本体はDebian上で動いていて、携帯電話からのリモート操作もDebian上のRabbitに対して行っています。しかし、画面表示は別マシンのMac OS X上のX11で行っています。プロジェクターへの出力も別マシンのMac OS Xが行っています。

dRubyなど咳プロダクツを用いたプレゼン環境の1つのパターンのデモとして、上記のような構成を用いました。かっこいいですね。

内容

8割ほどRubyととちぎをまじえた発表者紹介・会社紹介をし、最後にかるく現在の仕事の内容、これから向かおうとしている方向を紹介しました。他の方たちのように具体的な方法は提示していません。私たちも続けられるしくみを模索しているのが現状です。

雰囲気がよい

とちぎRuby会議02はとても楽しく居心地のよい雰囲気に満ちていました。どうしてなのかはわかりません。まだ体験していない人はぜひ一度体験してみることをおすすめします。toRubyに参加すると体験できるでしょう。

まとめ

地域Ruby会議2ndシーズン最初のとちぎRuby会議02に参加しました。

お昼ご飯をゆっくり食べていたおかげで開始時刻が遅れてしまい、すみませんでした。あのよい雰囲気の中でtoRuby勉強会を味わう時間が減ってしまったことが残念です*1

とちぎRuby会議にはRuby札幌の方も参加していましたが、12月には札幌Ruby会議02があります。こちらにも発表者として参加する予定です。とちぎRuby会議02のようにとてもすばらしい地域Ruby会議になりそうな予感がします。都合のあう方は参加してみてはいかがでしょうか。

札幌Ruby会議02までには関西Ruby会議02TokyuRuby会議01もあります。こちらの内容もとてもおもしろそうですね。

*1  お昼ご飯タイムもとてもよい雰囲気でした。

Tags: Ruby | このエントリの del.icio.us history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-10-25

Ruby/groonga 0.0.7

Ruby/groongaとActiveGroongaの新しいバージョンがリリースされました。

いつも通り、以下のコマンドでインストールできます。

% sudo gem install groonga

0.0.7は最新のgroonga0.1.4に対応しています。

groongaが正式リリース前なので、まだRuby/groongaユーザもあまり多くはありませんが、徐々に使われはじめています。例えば、えにしテックスープカレー好きdaraさんが作ったbuzztterでRuby/groongaが使われています。

daraさんからはRuby/groongaに対するパッチをいくつかもらったりもしたので、Ruby/groongaのコミッタになってもらいました。APIの相談にものってくれる頼もしいCTOです。

今回のリリースでも便利な機能が入っているので、いくつか紹介します。

キーワードリンク

groongaを使ってはてなのようなキーワードリンクをRubyで付与することができます。

グニャラくんのところではSennaを使っていますが、同様の機能をRuby/groongaにも取り込みました。

Ruby/groongaを使うと以下のように書けます。

# -*- coding: utf-8 -*-
require 'groonga'

Groonga::Context.default_options = {:encoding => "utf-8"}
Groonga::Database.create
words = Groonga::PatriciaTrie.create(:key_type => "ShortText",
                                     :key_normalize => true)
words.add('リンク')
words.add('リンクの冒険')
words.add('冒険')
words.add('㍊')
words.add('ガッ')
words.add('MUTEKI')
text = 'muTEkiなリンクの冒険はミリバールでガッ'
tagged_text = words.tag_keys(text) do |record, word|
  "[#{word}(#{record.key})]"
end
puts tagged_text
  # => [muTEki(muteki)]な[リンクの冒険(リンクの冒険)]は
  #    [ミリバール(ミリバール)]で[ガッ(ガッ)]

クエリからスニペット生成

groongaでは独自の構文のクエリから検索条件を指定することができます。buzztterで検索条件を指定するところでも使われています。例えば、以下のような構文があります。

  • 「単語1 単語2」: 単語1と単語2の両方にマッチする条件
  • 「単語1 OR 単語2」: 単語1または単語2にマッチする条件
  • 「単語1 - 単語2」: 単語1にマッチするが単語2にマッチしない条件

もう少し詳しい説明はgroongaのチュートリアルに載っています。

ここまでは前のリリースでもできたところです。今回のリリースからはさらにスニペット(検索語周辺のテキスト)も簡単に生成できるようになりました。

# "description"カラムに「ruby」または「groonga」が入っているレコードを検索
query = "ruby OR groonga"
records = table.select do |record|
  record["description"].match(query)
end

# 「ruby」または「groonga」が含まれる周辺のテキストを表示
tags = [["<", ">"]]
records.each do |record|
  puts record["name"]
  snippet = records.expression.snippet(tags, :normalize => true)
  snippet.execute(record["description"]).each do |text|
    puts "==="
    puts record["description"] # => Rubyでgroonga使って全文検索
    puts "---"
    puts text                  # => <Ruby>で<groonga>使って全文検索
  end
end

どのようになるかはRuby/groongaのサンプルアプリケーションで試してみてください。ここの検索ボックスもクエリ文字列に対応しているので、「OR」や「-」を使ったクエリを使うことができます。

まとめ

Ruby/groonga 0.0.7の新機能を紹介しました。

groongaの機能を手軽に使えるRuby/groongaを試してみてはいかがでしょうか。

つづき: 2010-02-09
Tags: Ruby | このエントリの del.icio.us history 3 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-10-02

インストールするだけでActiveScaffoldのメニューを日本語化

以前、ActiveScaffoldの地域化の中で、ActiveScaffoldLocalizeを使ってActiveScaffoldのメニューを日本語化する方法を紹介しました。

時は流れて、ActiveScaffoldLocalizeから日本語メニューのリソースが削除(!)されたり、ActiveScaffold本体に各言語のリソースが含まれるようになったりしました。しかし、本体には日本語リソースが含まれていなかったため、Web上には、いまだにActiveScaffold Japanese L10Nを使う、自分で日本語リソースを作成して使う、というような情報がでていました。

本家にフィードバックして取り込んでもらえれば、多くの人がより手軽に日本語化されたActiveScaffoldを使えるだろうに、ということで、先日、本家に日本語リソースを取り込んでもらいました。これからは、ActiveScaffoldLocalizeなどを使わずにActiveScaffold本体のみで日本語メニューを使うことができます。

手順

簡単に日本語メニューのActiveScaffoldを使う手順を説明します。

まず、ActiveScaffoldに依存しない、一般的なI18nまわりを整備します。

% rails shelf
% cd shelf
% script/generate resource book title:string
% rake db:migrate
% gem install amatsuda-i18n_generators -s http://gems.github.com/
% script/generate i18n ja

続いて、ActiveScaffoldまわりを整備します。

% script/plugin install git://github.com/activescaffold/active_scaffold.git

config/routes.rb:

-  map.resources :books
+  map.resources :books, :active_scaffold => true

app/controllers/books_controller.rb:

class BooksController < ApplicationController
  active_scaffold :book
end

app/views/layouts/application.html.erb:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
    <title>ActiveScaffold l10n</title>
    <%= javascript_include_tag(:defaults) %>
    <%= active_scaffold_includes %>
  </head>

  <body>
    <h1>ActiveScaffold l10n</h1>
    <%= yield %>
  </body>
</html>

これでメニューが日本語化されます。

日本語メニューのActiveScaffold

日本語関連の手順は最初のI18nまわりのところだけです。ActiveScaffold関連の手順にはまったく日本語関連の手順はありません。通常のインストール手順で日本語メニューが表示されるのは、ActiveScaffold本体に日本語リソースが含まれるようになったおかげです。

データを入れるとこのようになります。

日本語メニューのActiveScaffold(データ入り)

まとめ

ActiveScaffold本体に日本語リソースが含まれて、より簡単に日本語メニューのActiveScaffoldを使えるようになったことを紹介しました。

フリーソフトウェアを改良してよりよくした場合は、手元での変更やブログに書くにとどめずに、本家にフィードバックしてみてはいかがでしょうか。より多くの人が便利に使えるようになるだけではなく、多くの場合、自分のメンテナンスコストも下がります。

Tags: Ruby | このエントリの del.icio.us history 3 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-09-28

るびま0027号

週末にるびま0027号がリリースされました。5周年だそうです。おめでとうございます。

ささださんのコメント

n周年のときに毎年ささださんがコメントを書いていますが、今年のささださんのコメントが明るめになっているのが印象的です。「脱ささだ体制」が進んでいる影響でしょうか。

URLを集めていて気づいたのですが、4周年のときだけ「ko1-comment」ではなく、「4th-ko1」なんですね。

おすすめ記事

今回のるびまにはActiveLdapの日本語チュートリアルなどで活躍されている高瀬さんActiveLdap を使ってみよう(前編)という記事があります。

前編ということだけあり、基本的な部分から丁寧に解説されています。なかでも、前半のLDAPの説明部分はLDAPを知らない方にもわかりやすくまとまっています。ActiveLdapの入門だけではなく、LDAPの入門としても参照しやすい記事です。

ActiveLdapについても便利に使える感じが伝わる記事で、後編が楽しみです。

RubyでLDAPを操作したい場合はぜひ高瀬さんの記事を参考にしてください。

まとめ

るびま0027号の興味深い記事を2つ紹介しました。

自分でも何かできることはないか、と思っている方は、ただのRubyistであるかずひこさんのRubyist にできることがおすすめです。あるいは、記事の提供や編集でるびまに参加するというのもいかがでしょうか。

まだRubyNewsを読んだことがない方は一度読んでみてはいかがでしょうか。日本Ruby会議2009の間にたくさんのフリーソフトウェアがリリースされたことがわかります。

今回もここでは紹介しきれないくらい盛りだくさんのるびま0027号をぜひ読んでみて下さい。

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-09-14

とちぎRuby会議02参加登録受付中

10月24日に開催されるとちぎRuby会議02の参加登録が行われています。懇親会参加の状況から推測するとまだ登録できそうです。

今回は、超優良企業の1つとして声をかけてもらい、出場者として参加できることになりました。ワイクル角さんヴァインカーブやまだあきらさん、タワーズ・クエストの和田さんも出場者として参加されます。とても楽しみですね。

咳さんdRuby本をテキストとしたtoRuby勉強会も開催されるそうです。こちらもとても楽しみですね。

都合があう方は参加してみてはいかがでしょうか。

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-09-11

もうすぐRetrospectiva 2.0がリリース

Rubyで実装されたプロジェクト管理システムであるRetrospectivaのバージョン2.0がまもなくリリースされます。cozmixng.orgで最新バージョンが運用されているので、それを触ってみることで最新の機能を確認することができます。見てもらえばわかる通り、日本語表示にも対応しています。

Retrospectivaは一時期開発が停滞していて、その間にRedmineの方が普及しました。しかし、その後、再び開発が活発になり、現在は2.0 RC1がリリースされています。1.xから2.0では多くの改良が行われています。そのいくつかを紹介します。

簡単インストール

Single Step Installerが用意されていて、コマンド一発でインストールできるようになっています。以前より導入の敷居が下がっています。

アジャイル開発支援

AgilePMというアジャイル開発を支援するプラグインが公開され、プロジェクト管理機能がさらに充実しています。tDiaryプロジェクト用のAgilePMがあるので、そこで触ってみることができます。ただし、現時点ではまだ利用されていないのであまり雰囲気がわからないかもしれません。これからのtDiaryプロジェクトの利用に期待しましょう。

git対応

Subversionだけではなく、gitにも対応しました。また、Retrospectiva自体のバージョン管理システムもSubversionからgitに移行しています。

最近はgitを採用するプロジェクトも増えているため、これは嬉しい機能ではないでしょうか。

まとめ

まもなくリリースされるRetrospectiva 2.0を簡単に紹介しました。以前は「ブログがついたTrac」みたいな書かれ方をされていたRetrospectivaですが、実際に使ってみるとその表現が間違っていたことに気付いた人も多かったのではないでしょうか。以前からコミットログで連携する機能などがあり、使っていた人は「便利なプロジェクト管理ツール」という方がしっくりくることに気付いていたはずです。2.0ではより便利で有用な機能がスマートなインターフェイスで追加されています。2.0の紹介のために「ブログがついた〜」と書かれることは減ることでしょう。

Redmineもよいですが、プロジェクト管理ツールとしてRetrospectivaも検討してみてはいかがでしょうか。

もし、使用してみてRetrospectivaの開発に参加したくなった場合はRetrospectivaを使って開発に参加するとよいでしょう。まずは、未翻訳メッセージの翻訳から参加するのが敷居が低いでしょう。kou@clear-code.comまで連絡してもらえれば相談にのります。

Tags: Ruby | このエントリの del.icio.us history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-08-07

groongaをRackに載せて全文検索

Ruby/groongaのサンプルアプリケーションのデモを用意しました。

RailsなどのWebアプリケーションフレームワークを使うほどのものではないので、ActiveGroongaは使わずに、Ruby/groongaとRackの組み合わせになっています。Rackについてはyharaさんの5分でわかるRackなどを読んでみてください。

デモはPassengerで動かしています。PassengerにRackを設置したことがある人なら10分もかからずにサンプルを動かせるのではないかと思います。

機能

デモを見てもらえばわかる通り、小さなサンプルですが以下のように一通りの機能は備えています。

  • 複数キーワードによる絞り込み
  • スコア順による並べ替え
  • 検索キーワードの正規化(「Ruby」でも「ruby」でも検索可能)
  • キーワード周辺の文章の表示

それぞれ、もう少し詳しく見ていきましょう。

複数キーワードによる絞り込み

通常の検索サイトでは空白で複数のキーワードを区切ることによって検索結果を絞り込むことができます。例えば、「Ruby クリアコード」で検索すると、「Ruby」と「クリアコード」両方にマッチするページがヒットします。いわゆるAND検索です。

まず、1つのキーワードだけを扱う場合のコードを示して、次に複数のキーワードを扱うコードを示します。

1つのキーワードだけを扱う場合はとても単純です。3行です。

# 文書が格納されたテーブルを取得
documents = Groonga::Context.default["documents"]
# 文書テーブルから指定されたキーワードにマッチするレコードを検索
records = documents.select do |record|
  # HTTPの"query"パラメータで指定された単語が
  # "content"カラムにマッチするかチェック
  record["content"] =~ request["query"]
end

全文検索を指示するために「=~」演算子を使うなんてとてもRubyらしい書き方ですね。

複数のキーワードで絞り込みを行う場合はrecord["content"] =~ "keyword"という条件をANDでつなげていきます。イメージは以下の通りです。

records = documents.select do |record|
  (record["content"] =~ keyword1) &
    (record["content"] =~ keyword2) &
    ...
end

サンプルではこのようなコードになっています。

words = request["query"].split
records = documents.select do |record|
  expression = nil
  words.each do |word|
    sub_expression = record["content"] =~ word
    if expression.nil?
      expression = sub_expression
    else
      expression &= sub_expression
    end
  end
  expression
end

ちなみに、injectを使うとこうなります。

records = documents.select do |record|
  words.inject(nil) do |expression, word|
    sub_expression = record["content"] =~ word
    if expression.nil?
      sub_expression
    else
      expression & sub_expression
    end
  end
end

お好みでどうぞ。

スコア順による並び替え

このサンプルでは、「同じ文書中に何回キーワードが出現するか」をスコアとして扱っています。スコアは検索結果のレコードが持っているので、それを使って並び替えます。1行です。

# スコアの大きい順に並び替えて、上位20件だけ使う。
records = records.sort([[".:score", "descending"]], :limit => 20)

groongaでは「:」から始まる特別なアクセス用の名前があります。「:score」もその1つでスコアの値にアクセスするために使います。「:score」の他にはレコードのキーにアクセスする「:key」などがあります。

検索キーワードの正規化

groongaは、全文検索用の索引を作るときにキーワードを正規化することができます。これにより大文字小文字を区別せず「Ruby」でも「ruby」でも同じように検索することができます。

正規化するためにしなければいけないことは、索引用テーブルを作成する時に:key_normalize => trueオプションを指定するだけです。

Ruby/groongaではテーブルやカラムを定義するためのDSLを用意しています。サンプルのためのテーブル・カラム定義は以下のようになっています。少しActiveRecord風です。

# スキーマ定義開始
Groonga::Schema.define do |schema|
  # 文書格納用テーブル作成
  schema.create_table("documents") do |table|
    table.string("title") # 文書のタイトル
    table.text("content") # 文書の内容
    table.string("path")  # 文書の置き場所
    table.time("last-modified") # 文書の最終更新時刻
  end

  # 索引用テーブル作成
  schema.create_table("terms",
                      :type => :patricia_trie,
                      :key_normalize = true, # キーワードを正規化
                      :default_tokenizer => "TokenBigram") do |table|
    table.index("documents.title")   # 文書のタイトルの索引を作成
    table.index("documents.content") # 文書の内容の索引を作成
  end
end

一応コメントを入れましたが、コメントがなくても何をしているのかがわかったのではないでしょうか。

:key_normalize => trueを指定しておくと、あとはgroongaがうまいことやってくれるので、検索時には特に何もする必要はありません。

キーワード周辺の文章の表示

キーワード周辺の文章を表示することにより、その文書が探している文書かどうかを判断しやすくなります。

たとえば、「Ruby」で検索するとRuby-GNOME2 0.18.0リリース*1がヒットしますが、その場合は「...されました。 Ruby-GNOME2はGTK+を含むGNOME関連ライブラリのRubyバインディング...」という文章も一緒に表示されます。これがあれば、文書を全部読まなくてもおおよその内容を想像しやすくなります。

この機能はKWICやスニペットなどと呼ばれていて、groongaではスニペットと呼んでいます。

スニペットの生成は以下のようになります。

# キーワードを囲むタグ
open_tag = "<span class=\"keyword\">"
close_tag = "</span>"
# スニペットオブジェクトの作成
snippet = Groonga::Snippet.new(:width => 100,
                               :default_open_tag => open_tag,
                               :default_close_tag => close_tag,
                               :html_escape => true,
                               :normalize => true) # キーワードを正規化
# 検索キーワードを登録
request["query"].split.each do |word|
  snippet.add_keyword(word)
end

# 本文からスニペットを生成
segments = snippet.execute(record[".content"])
# 整形
separator = "\n<span class='separator'>...</span>\n"
snippet_text = segments.join(separator)
response.write("<p class=\"snippet\">#{snippet_text}</p>")

整形用のタグを入れるコードも一緒になっているので多少長くなっていますが、スニペット作成のための処理は以下の3ステップだということがわかります。

  1. Groonga::Snippet.new
  2. snippet.add_keyword
  3. snippet.execute

簡単ですね。

まとめ

サンプルアプリケーションを例にして、Ruby/groongaを使うと実用的な機能が揃った検索ページを簡単に作成できることを紹介しました。

サンプルアプリケーションはリリースされたばかりのRuby/groonga 0.0.6の中に入っています。

サンプルアプリケーションを参考にしながらgroongaを使った全文検索ページを作ってみてはいかがでしょうか。

最後に、コマンド列でサンプルアプリケーションのセットアップの方法を示します。

% sudo gem install groonga
% cp -r `gem environment gemdir`/gems/groonga-0.0.6/example/ ./
% cd example/search
% ../index-html.rb data/database ~/public_html/ # 最後の引数はHTMLのあるディレクトリ
% rackup config.ru
% firefox http://localhost:9292/

*1  そういえば、先日、Ruby-GNOME2 0.19.1がリリースされました。

つづき: 2009-12-21
Tags: Ruby | このエントリの del.icio.us history 8 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-07-31

日本Ruby会議2009の資料公開

日本Ruby会議2009で発表した資料を公開しました。

発表を聞いてくれたみなさん、ブースにきてくれたみなさん、スタッフのみなさん、ありがとうございました。

つづき: 2009-12-21
Tags: Ruby | このエントリの del.icio.us history 5 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-07-19

日本Ruby会議2009でのリリース予定

せっかくの機会なので、日本Ruby会議2009期間中にフリーソフトウェアをいくつかリリースする予定です。

リリース準備ができているフリーソフトウェア

以下のフリーソフトウェアはリリース準備ができているので、会場のネットワーク環境をうまく使えれば期間中にリリースします。

午前11時頃か午後3時頃にスポンサーブース内でリリース作業をする予定です。 日本Ruby会議2009は3日間あるので、リリース作業はそれぞれの日に分散させる予定です。

リリースできる可能性があるフリーソフトウェア

もしかしたら、以下のフリーソフトウェアもリリースできるかもしれません。

1つ実装したい機能が残っているので、それが実装できればリリースします。が、時間的に厳しそうです。

リリースしたかったフリーソフトウェア

以下のフリーソフトウェアもリリースしたかったのですが、準備が十分ではないので、おそらくリリースできないと思います。残念です。

Ruby/Libglade2にGC関連のSEGVバグがあり、それを修正する時間がとれなかったので、日本Ruby会議2009のタイミングではリリースできそうにありません。

また、ソフトウェアではありませんが、GUI関連でRuby GUI調査2008のレポートを日本語に翻訳したものもリリースしたかった*1のですが、間に合いませんでした。Alexさんからは編集可能な形で原文をもらっているので、もし、協力してくれる方がいればkou@clear-code.comまでご連絡ください。

まとめ

日本Ruby会議2009期間中にリリースを予定しているフリーソフトウェアを紹介しました。

RSS Parserrcairoなど、ここに挙げたフリーソフトウェア以外でも構いませんので、なにかコメントなどがあればスポンサーブースで声をかけてください。

*1  永井さんがLightning TalksでRuby/Tkは本当にダメな子なのか?というお話をするようですし。

2009-07-15

日本Ruby会議2009の発表セッション

日本Ruby会議2009のセッション詳細が公開されました。

日本Ruby会議2009は3トラックで3日間の開催のため、たくさんのセッションがありますが、このうち、2セッションで発表します。

ActiveLdap - 2009年07月18日土曜日 14:30-15:30 Lightning Talks

1つ目は2回目のライトニングトークの一番最後です。

ライトニングトークではActiveLdapチュートリアル)について話します。ActiveLdapの開発に関わるようになってから約3年経ちますが、ついに発表する機会に恵まれました。

クリアコードは今月から第四期に入っていますが、ActiveLdapはクリアコード設立初期にLDAPを利用する機会があったことがきっかけで開発に関わるようになったフリーソフトウェアです。当時は多くの問題を抱えていたActiveLdapですが、そのときからコツコツ開発を続けていたため現在では当時よりもかなり便利なライブラリとなりました。その成果を日本Ruby会議で発表できることはとても感慨深いものです。

CとRubyとその間 - 2009年07月18日土曜日 16:00 - 18:30

2つ目は同じ日の同じ会場の次の枠で、CとRubyとその間です。

クリアコードではActiveLdap以外にもたくさんのフリーソフトウェアの開発に関わっていますが、その中でもCとRubyそれぞれのよいところを活かしたフリーソフトウェアについて話します。この発表ではそのようなフリーソフトウェアの例としてmilter managerActiveGroongaを紹介しながら、CとRubyを活かすことのメリットについて話します。

Rubyから使える高速なkey-valueストアとしてはTokyo CabinetLocalmemcacheが有名です。

ActiveGroonga(とその下の層のRuby/groongaは)これらと同様に高速なkey-valueストア機能も備えるgroongaをよりRubyらしく使いやすいAPIで提供します。

milter managerについては最近いろいろなところで話しましたが、ActiveGroongaについて話すことは今回が初めてです。

まとめ

日本Ruby会議2009で発表するセッションを紹介しました。他にも興味深いセッションがたくさんあるので迷うと思いますが、興味があれば上記のセッションにも参加してみてください。

クリアコードはスポンサーとなっているため、スポンサーブースを出します。セッションには参加できなかった方も、ぜひ、足を運んでください。

つづき: 2009-07-19
Tags: Ruby | このエントリの del.icio.us history 1 user | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-07-06

るびま0026号

先日、るびま0026号がリリースされました。

今回はRegionalRubyKaigi特集のようで、5Kaigiのレポートが載っています。5Kaigiのうち、仙台Ruby会議01に参加しましたが、そのレポートもあります。

仙台Ruby会議01

仙台Ruby会議01のレポートid:monyakataさんが丁寧にすっきりとまとめてくれています。とても読みやすいので参加できなかった人は読んでみてはいかがでしょうか。

レポートによるとまず好きなこと、そしてそれを続けることのセッションが一番参加者が多かったそうです。参加してくれた皆さん、関係者の皆さん、ありがとうございました。

レポート中にある通り、仙台Ruby会議01のサイトには東北・仙台情報が豊富にあります。もし、レポートを読んで仙台を感じたくなったときはサイトにある情報が役に立つことでしょう。お菓子やたいやき情報もあるので、甘いものが好きな人はぜひ参考にしてください。

RubyNews

あまり話題にのぼらない感がある常設コーナーのRubyNewsですが、毎回充実しています。読むと、Ruby界隈では思ったよりいろんなことがあったなぁと感じることができます。今回のRubyNewsはいつも以上に充実しています。読んでみると知らないことも多いかもしれません。

最近は助田さんがruby-talkでリリースアナウンスのあったソフトウェアを紹介していて、海外のRuby関連ソフトウェアに関する情報源としてとても貴重です。実は、RubyNewsにはruby-listでリリースアナウンスのあったソフトウェアが載っているので、日本でのRuby関連ソフトウェアのとても貴重な情報源になっています。

もちろん、肉リリースされたRabbitの情報も載っていますし、RSS Parserのリリースも載っています。ソフトウェアをリリースしたときはruby-listでもリリースアナウンスをしてみてはいかがでしょうか。きっとRubyNewsに載せてもらえます。

まとめ

るびま0026号がリリースされました。るびまは今回のRegionalRubyKaigiレポートのように毎号変わるコーナーにも有用な記事が多くありますが、実は常設コーナーの中にも有用な情報があります。常設コーナーを読み飛ばしている方はもったいないですよ。

高橋編集長の巻頭言はよく読まれていると思いますが、編集後記もわりとおもしろいですよ。

仙台Ruby会議01レポートだけではなく、zundaさん編集のRubyNewsも紹介してみました。

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-07-02

オープンソースカンファレンス2009 Hokkaido 北海道情報セキュリティ勉強会枠での資料公開

まっちゃだいふくさんに声をかけてもらったことがきっかけで、オープンソースカンファレンス2009 Hokkaidoのせきゅぽろ枠でmilter managerの話をしてきました。声をかけてくれたまっちゃだいふくさん、参加してくれたみなさん、ありがとうございました。

資料: milter manager

また、Ruby札幌がUstreamで配信してくれたので、動画もあります。

動画: OSC 2009 Hokkaido milter manager

内容

今回はmilterとmilter managerの話をする前に、迷惑メール対策の現状と有効な対策方法についても話しました。これは、第2回静岡ITPro勉強会での佐藤さんの公演内容を参考資料として利用しています。利用を快諾してくれた佐藤さんありがとうございます。札幌のみなさんにも迷惑メール対策の現状と有効な対策方法を伝えられたのではないかと思います。

一般的な迷惑メール対策の話の後にmilterとmilter managerの話をしました。第2回静岡ITPro勉強会の時よりも時間が少ないということもあり、今回はあまり突っ込んだ話をせずに、雰囲気が伝わる程度に抑えました。

話の後、司会をしてくれたまっちゃだいふくさんが今回省略したあたりをフォローしてくれました。ありがとうございます。

まっちゃだいふくさんは勉強会の時間外に、いろいろアドバイスをしてくれます。そのため、発表者として参加したこちらもとても勉強になっています。

そして、まっちゃだいふくさんがもってきてくれたお菓子はとてもおいしかったです。

Ruby札幌

せきゅぽろ枠の時間以外はRuby札幌にお世話になりました。

Ruby札幌とせきゅぷろの枠はセミナーに参加したのですが、それ以外の時間はRuby札幌のブースにおじゃまさせてもらいました。daraさんからbuzztterのバックエンドをgroongaにしたいということを聞いたので、Ruby/groongaで実現するために少し相談しました。Ruby/groonga 0.0.3はbuzztterのバックエンドとして使える機能を提供することになるでしょう。

今回、ActiveLdapを使っている島田さん以外のRuby札幌の人たちとも話すことができたのはよかったです。ActiveLdapやRSS Makerあたりのレビューにも参加することができました。

今回、Ruby札幌の人たちはとても人柄がよいことがわかりました。とてもすばらしいです。また、Ruby札幌がRabbitを応援していることもすばらしいです。

まとめ

オープンソースカンファレンス2009 Hokkaidoのせきゅぷろ枠でmilter managerの話をしてきました。この話は、まっちゃだいふくさんのおかげで実現しました。話の後のフォローなどいろいろありがとうございます。

Ruby札幌はすばらしいです。Ruby会議2009ではニュースがあるようですし、その後には札幌Ruby会議02もあるようです。Ruby札幌から目が離せません。

つづき: 2009-12-21
2009-06-21

ActiveLdap 1.0.9リリース

LDAPのエントリをRubyオブジェクトとして操作するためのライブラリActiveLdapの1.0.9がリリースされました。([ANN] ActiveLdap 1.0.9

ActiveLdapを知らない人はチュートリアルを読んで雰囲気を感じてください。

1.0.9

1.0.9の目玉はRuby 1.9.1とRails 2.3.2の対応です。特に、Rails 2.3.2の対応は待ち望まれていた機能です。trunkではずいぶん前から対応していたにも関わらずリリースされていなかったため、ユーザの方には不便な思いをさせてしまっていました。

1.0.9はそのバージョン番号からもわかるかもしれませんが、近いうちに1.1.0がリリースされるという意味が含まれています。逆に言うと、1.1.0に必要な機能を追加するまでRails 2.3.2に対応したリリースがない状況を解決するために、1.1.0の一部機能が未実装のリリースが1.0.9になります。

とはいえ、1.0.9から1.1.0の間にAPI非互換がでるというわけではないので1.0.9とRails 2.3.2の組み合わせは安心して使えます。1.1.0では1.0.9よりも(非互換なしで)パワーアップするということです。

まとめ

Ruby 1.9.1とRails 2.3.2に対応したActiveLdap 1.0.9がリリースされました。Ruby 1.9.1やRails 2.3.2と一緒にActiveLdap 1.0.9を使ってください。

近いうちに1.1.0もリリースされる予定なので、そちらも楽しみにしていてください。

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-06-08

Ruby/groonga 0.0.2リリース

最新のgroongaに対応したRuby/groonga 0.0.2がリリースされました。

Ruby/groonga 0.0.2ではよりAPIが使いやすくなっています。

メソッドの個別化

groongaはgrn_objで抽象化されていて、ハッシュテーブルでも転置インデックスカラムでもgrn_obj_search()で検索できます。Ruby/groongaでもそれを踏襲してGroonga::Object#searchだけを定義して使いまわしていました。しかし、0.0.2ではGroonga::Hash#serachやGroonga::IndexColumn#searchなど、それぞれのオブジェクト毎に定義するようにしました。

こうすることにより以下のような挙動になるため、使いやすいAPIになりました。

  • grn_obj_search()に対応していないオブジェクト(例えばGroonga::Array)に対して#searchしようとするとNoMethodErrorと適切に問題を報告する。
  • 省略可能なオプション引数をより正確に検証して適切なエラーを報告する。
  • それぞれの#search毎にドキュメントを用意することができるので、適切な内容のドキュメントがかかれたAPIになる。

利用できないのであれば、メソッドが定義されていない方がよいAPIだと思います。無駄なものがない方が適切なAPIに誘導しやすくなります。

無駄なものはない方がよいということは、メソッドだけではなく、省略可能なオプション引数にも言えます。1つのメソッドで何でもやろうとすると余計なオプションまで受け付ける必要があります。あるいは、余計なオプションを排除するためのコードが増えてしまいます。こうならないために、適切な粒度で別々のメソッドを定義することが有効です。

例えば、オプション名を検証するコードは以下のように書けます。(エラーメッセージに入力値と問題となった値を両方含めていることにも注意してください。問題を解決するための重要な情報です。)

def search(options={})
  valid_keys = [:name, :path]
  invalid_keys = options.keys - valid_keys
  unless invalid_keys.empty?
    message = "invalid option name(s): #{invalid_keys.inspect}: #{options.inspect}"
    raise ArgumentError, message
  end
end

もし、1つのメソッドでたくさんの状況を考慮しなければいけないとこのようになります。

def search(type, options={})
  case type
  when :fast
    valid_keys = [...]
  when :remote
    valid_keys = [...]
  else
     raise ArgumentError, "invalid type: ..."
  end

  invalid_keys = options.keys - valid_keys
  unless invalid_keys.empty?
    message = "invalid option name(s): #{invalid_keys.inspect}: #{options.inspect}"
    raise ArgumentError, message
  end

  case type
  when :fast
    query = options[:query]
    ...
  when :remote
    remote = DRbObject.new("druby://#{options[:host]}:2929")
    remote.search(options[:query])
  ...
  end
end

これよりは、メソッドを分けた方がすっきりします。

def fast_search(options={})
  valid_keys = [...]
  ...
end

def remote_search(options={})
  valid_keys = [...]
  ...
end

...

あとは、総称的なメソッドを1つ用意すればメソッド分割前と同じように使えます。

def search(type, options={})
  case type
  when :fast
    fast_search(options)
  when :remote
    remote_search(options)
  ...
  end   
end

ここまできたらもう一歩です。オブジェクト指向プログラミングでcase whenやswitch caseで分岐している時はオブジェクトが足りない匂いを感じとってください。このような場合はそれぞれの条件毎にオブジェクトを作り、それぞれのオブジェクトで同じ名前のメソッドを定義します。

class FastSearcher
  def search(options={})
    ...
  end
end

class RemoteSearcher
  def search(options={})
    ...
  end
end

これで、条件分岐がなくなり、総称的なメソッドも定義しなくてもよくなります。それらは言語がやってくれるからです。

searcher = FastSearcher.new
searcher.search(:query => ...)

と、だいぶ遠回りをしましたが、Ruby/groonga 0.0.2では以上のようなAPI設計ポリシーに従って、オブジェクト毎にメソッドを実装するようになりました。これにより使いやすさが向上しています。

また、メソッドが分割されることにより、ドキュメントを書きやすくなります。読む側も読みやすくなります。

まとめ

Ruby/groonga 0.0.2はAPIの使いやすさが向上しています。これは、適切な粒度に実装を分割したからです。使いやすいAPIを検討しているのであれば、実装の粒度を細かくすることを検討してみてください。無駄がなくすっきりして使いやすいAPIになるかもしれません。

Tags: Ruby | このエントリの del.icio.us history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-06-05

Ruby 1.9.1とREXMLとXML宣言のエンコーディング

Ruby 1.9.1付属のREXMLではXML宣言のエンコーディングの扱いに問題があるためvalidなXMLでもパースできない場合があるという話です。

問題

Ruby 1.9では文字列や正規表現がエンコーディング情報を持つため、REXMLのように正規表現ベースでXMLをパースしている場合は、エンコーディングを適切に設定しないとパースに失敗することがあります。

例えば、tDiaryのseach-yahoo.rbプラグインがこの問題に遭遇しています。

原因

REXMLは内部でUTF-8を用いています。そのため、パース対象のXMLのエンコーディングをUTF-8に変換しながらパースします。この処理はREXML::SourceまたはREXML::IOSourceで行われます。

しかし、REXML::IOSourceに問題があり、UTF-8に変換しないままパースしてしまう場合があります。これは、入力XMLのエンコーディングがUTF-8に設定されていない、かつ、XML宣言のエンコーディングがUTF-8になっている場合です。ちなみに、REXML::Sourceではこの問題は起きません。

tDiaryのsearch-yahoo.rbでは入力XMLのエンコーディングがASCII-8BITでXML宣言のエンコーディングがUTF-8になっていたため問題に遭遇しました。

search-yahoo.rbではopen-uriを使って入力XMLをHTTP経由で取得しています。open-uriはContent-Typeを見て適切なエンコーディングを設定してくれますが、今回はcharsetが指定されていなかったとのことです。このため、open-uriで取得した入力XMLがASCII-8BITになっていました。

xml = open("http://.../xxx.xml") {|f| f.read}
xml.encoding # => ASCII-8BIT
document = REXML::Document.new(xml) # => パースエラー

解決法

この問題に遭遇してしまった場合は、以下のような解決法があります。

  • 入力XMLのエンコーディングをUTF-8に設定する。
  • REXML::IOSourceの代わりにREXML::Sourceを使う。
  • パッチ付きでバグ報告済みなので修正されるのを待つ。

入力XMLのエンコーディングをUTF-8に設定する場合は以下のようになります。

xml = open("http://.../xxx.xml") {|f| f.read}
xml.force_encoding("utf-8")
document = REXML::Document.new(xml)

REXML::Sourceを使う場合は以下のようになります。

xml = open("http://.../xxx.xml") {|f| f.read}
document = REXML::Document.new(REXML::Source.new(xml))

修正されるのを待つ場合は、修正されるまで待ってください。

まとめ

Ruby 1.9で正規表現ベースのコードがうまく動かない場合はマッチ対象の文字列のエンコーディングを確認しましょう。

ちなみに、REXML::IOSource#matchではエンコーディング関係のエラーを握りつぶしているため、実際に発生するREXML::ParseExceptionだけ見てもエンコーディングミスマッチがどこで起こっているかはわかりません。問題が発生したときは問題解決につながるエラーメッセージを提供したいものですね。

Tags: Ruby | このエントリの del.icio.us history 3 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-05-11

Ruby/groonga 0.0.1リリース

データベース機能も備える全文検索エンジンgroongaをRubyから利用するための拡張ライブラリRuby/groongaがリリースされました。

Ruby/groongaはRubyGemsに対応しているので、以下のようにコマンド一発でインストールできます。(事前にmakeやgccやRubyのヘッダファイルなど拡張ライブラリのビルドに必要なソフトウェアを揃えておいてください。)

% sudo gem install groonga

Ruby/groongaを利用するためには最新のgroonga 0.0.4が必要ですが、もし、システムにインストールされていない場合は自動的にダウンロードし、groongaのRubyGemsディレクトリの中にインストールします。この場合、最適化オプション(gccの-O2オプション)付きでビルドされますが、最適化オプション付きでgroongaをビルドすると、とても時間がかかります。(30分とか。)慌てずにのんびり待ってください。*1

サンプル

Ruby/groongaでは、よりRubyらしい読み書きしやすいAPIでgroongaを利用できるようにすることを目的としています。例えば、groongaのテーブルはRubyのHashのように扱うことができます。また、テーブルやカラムの型に応じてRubyとgroonga上のデータを適切に変換することにより、特別なことを意識せずにgroongaのデータベース機能を使うことができます。

Cで書かれたインデックスを自動更新するプログラムをRubyで書くとこうなります。

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

require 'rubygems'
require 'groonga'

# 初期化
Groonga::Context.default_options = {:encoding => :utf8}
Groonga::Database.create

# テーブル定義
## <bookmarks>テーブルとそのカラムたちを定義
bookmarks = Groonga::Array.create(:name => "<bookmarks>")
bookmarks.define_column("uri", "<shorttext>")
bookmarks.define_column("comment", "<shorttext>")

## <lexicon>テーブルとそのカラムたちを定義
lexicon = Groonga::Hash.create(:name => "<lexicon>",
                               :key_type => "<shorttext>")
## MeCabで検索用単語を切り出す
lexicon.default_tokenizer = "<token:mecab>"
comment_index_column = lexicon.define_column("comment-index", bookmarks,
                                             :type => "index")

# ポイント: インデックス自動更新の設定
comment_index_column.source = "<bookmarks>.comment"

# ブックマークの登録: 3件
def add_bookmark(bookmarks, uri, comment)
  bookmark = bookmarks.add
  bookmark["uri"] = uri
  bookmark["comment"] = comment
end

add_bookmark(bookmarks,
             "http://groonga.org/",
             "an open-source fulltext search engine and column store")
add_bookmark(bookmarks,
             "http://qwik.jp/senna/",
             "an embeddable fulltext search engine")
add_bookmark(bookmarks,
             "http://cutter.sourceforge.net/",
             "a unit testing framework for C")


# 検索: 2回
def search(comment_index_column, word)
  result = comment_index_column.search(word)
  puts("search result: <#{word}>: #{result.size}")
  puts("uri\t\t\t | comment")
  result.each do |record|
    bookmark = record.key
    puts("#{bookmark['uri']}\t | #{bookmark['comment']}")
  end
  puts
end

search(comment_index_column, "search")
# 結果: <search>で検索したら2件ヒット
# search result: <search>: 2
# uri			 | comment
# http://groonga.org/	 | an open-source fulltext search engine and column store
# http://qwik.jp/senna/	 | an embeddable fulltext search engine

search(comment_index_column, "testing")
# 結果: <testing>で検索したら1件ヒット
# search result: <testing>: 1
# uri			 | comment
# http://cutter.sourceforge.net/	 | a unit testing framework for C

注意事項

Ruby/groongaには以下のような既知の問題があります。

  • RubyのGCでgroongaのオブジェクトを開放しすぎてしまう
  • それほど高速化のためのチューニングをしていない
  • リファレンスマニュアル・チュートリアルが半分位しか用意されていない
  • APIがまだ流動的

GCの問題は、groongaのgrn_ctxというメモリ管理機能を提供するオブジェクトとRubyがオブジェクトをどの順番でGCするかはわからないという動作のために起きています。これはgroonga本体とも協調しながら解決する予定です。ちなみに、通常のアプリケーションでこの問題が発生する可能性があるのはプロセスの終了時だけです。そのため、この問題のためにデータが壊れてしまうということはないと考えられます。

今回のリリースではよりRubyらしいAPIの提供の優先度を高くしたため、高速化のためのチューニングはそれほど行われていません。いくつかチューニング案があるので、それらを適用することにより、Rubyの読み書きしやすいAPIを利用しながらより高速な全文検索機能とデータベース機能を利用できるようになるでしょう。

ドキュメントが完備されていないのは、APIがまだ流動的なことやgroongaのすべての機能を網羅していないこととも関係があります。groonga本体もまだAPIが改良され続けています。それに追従したり、より使いやすいAPIを目指してRuby/groongaのAPIはこれから変更されるでしょう。その過程でドキュメントも充実していく予定です。

次のステップ

ラングバプロジェクトではRuby/groongaの開発に参加してくれる人を募集しています。興味のある方は開発者向け情報をご覧ください。

クリアコードではRubyの拡張機能を書けるプログラミングが好きな開発者を募集しています。興味のある方は採用情報をご覧ください。

*1  -O0オプション(非最適化オプション)をつけるとすぐにビルドできます。

つづき: 2009-12-21
Tags: Ruby | このエントリの del.icio.us history 12 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-04-30

ActiveLdap: ldap_mapping

これまで、ActiveLdapにはまとまった日本語の情報がありませんでしたが、id:tashenさんがActiveLdapのチュートリアルを翻訳してくれています。原文に最新の状況に追従していない部分があるため、いくつか古い情報もあるのですが、現在、最新の状況に追従するように作業が進んでいます。(最初の方はわりと最新の状況に追従しています。)

せっかくなので、ldap_mappingのあたりを簡単に説明します。できるなら、この内容を本家のドキュメントにうまくマージしたいと思っています。

ldap_mapping

ActiveRecordでは何もしなくてもカラムへアクセスすることができるのですが、ActiveLdapではldap_mappingでLDAPとRubyのオブジェクトを対応させる必要があります。ActiveRecordでは1つのテーブルが1つのクラスに対応し、各レコードがインスタンスに対応しますが、LDAPではそのような対応関係を自動的に判断することが難しいためです。どのようなエントリの集合を1つのクラスに対応させるかはアプリケーション毎に異なります。そのため、ActiveLdapでは明示的にユーザに指定してもらう方法をとっています。適切なデフォルト値がない場合はデフォルト値を提供しない方が混乱しないと思います。

ldap_mappingでは以下の3つの情報を使ってクラスに対応するエントリの集合を決めます。

  • objectClass
  • 検索対象のツリー
  • 検索範囲

objectClass

オブジェクトクラスposixGroupに属しているエントリを対象とする場合は以下のようになります。

class Group < ActiveLdap::Base
  ldap_mapping :classes => ["posixGroup"], ...
end

これで、posixGroupに属するLDAPエントリそれぞれがGroupクラスのインスタンスに対応することになります。

もし、すべてのLDAPエントリを扱いたい場合はオブジェクトクラスtopを指定します。

class Entry < ActiveLdap::Base
  ldap_maaping :classes => ["top"], ...
end

topはすべてのエントリが属しているオブジェクトクラスなので、ldap_mappingにオブジェクトクラスtopを指定することで、すべてのエントリの集合とEntryクラスを対応させることができます。これは、LDAPツリーを表示するアプリケーションを作るときに便利です。

余談ですが、ActiveLdapにはActiveRecordのacts_as_tree相当の機能が標準で組み込まれています。LDAPはツリー構造なので標準で組み込まれていることは自然ですね。Entryクラスのインスタンスもchildrenやparentなどのメソッドが使えるため、簡単にツリー状のビューを作成することもできます。

検索対象のツリー

LDAPのエントリの集合は検索対象のツリーでも絞り込むことができます。以下のように、それぞれのツリーで扱いが異なる場合に役立ちます。

class Group < ActiveLdap::Base
  ldap_mapping :classes => ["posixGroup"],
               :prefix => "ou=Groups", ...
end

class AdministratorGroup < ActiveLdap::Base
  ldap_mapping :classes => ["posixGroup"],
               :prefix => "ou=AdministratorGroups", ...
end

Groupは通常ユーザ用のグループで、AdministratorGroupは管理者ユーザ用のグループです。多くの場合、役割毎にLDAPツリーをわけて管理していると思うので、それをRubyの世界でも利用するということです。

このようにクラスをわけることによって、メソッド内で管理者グループかどうかで処理を振り分けなくてもよくなります。

class Group < ActiveLdap::Base
  def users
    # 一般ユーザの配列を返す
  end
end

class AdministratorGroup < ActiveLdap::Base
  def users
    # 管理者ユーザの配列を返す
  end
end

例としてusersメソッドを出しましたが、実は、ActiveLdapにはActiveRecordのhas_manyのように、LDAPエントリ間の関連をRubyで簡単に扱えるようにする機能があり、usersのようなメソッドは自分で定義する必要はありません。別の機会にでも紹介したいと思います。

検索範囲

検索対象のツリーのうち、どのツリーを検索範囲にするのかを指定できます。

class Group < ActiveLdap::Base
  ldap_mapping :classes => ["posixGroup"],
               :prefix => "ou=Groups",
               :scope => :sub, ...
end

この例では、検索ツリー以下にあるサブツリー全体からエントリを検索します。もし、サブツリーの直下にだけ必要なエントリがある場合は:oneを指定することにより、よけいな検索を避けることができます。アプリケーションにあわせて適切な検索範囲を指定して下さい。

まとめ

ActiveLdapでのLDAPのエントリとRubyのオブジェクトを関連付けるためのメソッドldap_mappingについて簡単に紹介しました。ここでは以下の3つについてだけ触れましたが、他にdn_attributeという重要なオプションがあります。

  • objectClass
  • 検索対象のツリー: LDAPでのbase
  • 検索範囲: LDAPでのscope

より詳しくはActiveLdapの日本語チュートリアルを見てください。

Tags: Ruby | このエントリの del.icio.us history 3 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-04-14

問題解決につながるエラーメッセージ

プログラムを書いていると問題に遭遇します。問題に遭遇したときはエラーメッセージが問題解決の重要な情報になります。しかし、エラーメッセージがあるだけでは問題解決にはつながりません。問題解決に役立つエラーメッセージとそうでもないエラーメッセージがあります。

ここでは、Rubyでの例をまじえながら問題解決に有用なエラーメッセージを紹介します。ライブラリなど多くの人が使うようなプログラムを作成する場合は参考になるかもしれません。

問題解決への道

問題に遭遇してから問題を解決するまでには以下の順で作業をする必要があります。

  1. 問題の把握
  2. 問題の原因の調査
  3. 原因の解決方法の検討
  4. 解決方法の実装

役立つエラーメッセージがあると「1. 問題の把握」、「2. 問題の原因の調査」、「3. 原因の解決方法の検討」がはかどります。

問題の値を示す

エラーが発生すれば問題が起こっている事実は把握できます。次にすることは、どのような問題が起こっているかを調査することです。

String#gsubにはいくつかの使い方がありますが、その1つは以下のように正規表現と文字列を引数にする使い方です。

>> "abcde".gsub(/c/, "C")
=> "abCde"

もちろん、違うオブジェクトを渡すとエラーが発生します。

>> "abcde".gsub([:first], [:second])
TypeError: can't convert Array into String
        from (irb):2:in `gsub'
        from (irb):2

配列を文字列に変換できなかったといっています。しかし、ここでは引数に配列を2つ指定しています。このエラーメッセージでは「配列を文字列に変換できなかった」ことはわかりますが、「どの配列を文字列に変換できなかった」かはわかりません。

正規表現のリテラルでも、正規表現の構文が間違っている場合はエラーが発生します。

>> Regexp.new("(")
RegexpError: premature end of regular expression: /(/
        from (irb):3:in `initialize'
        from (irb):3:in `new'
        from (irb):3

この場合は「正規表現に問題がある」というだけではなく、「どの正規表現に問題がある」かも示しています。

このように、問題を起こしたオブジェクトの情報も示すことで「問題を把握」しやすくなります。エラーメッセージには、問題を起こしたオブジェクトの情報も含めるようにしましょう。

どう悪いのかを示す

問題が把握できたら、どうしてその問題が発生したのか、原因を調べます。多くの場合、エラーメッセージに問題の原因は書かれています。しかし、そうではない場合もあります。できるだけ、エラーメッセージには問題の原因も含めるようにしましょう。

Time.iso8601はISO 8601で定められた文字列のフォーマットをパースし、Timeオブジェクトにします。

>> require 'time'
=> true
>> Time.iso8601("2009-04-10T12:02:54+09:00")
=> Fri Apr 10 03:02:54 UTC 2009

不正なフォーマットの場合はエラーが発生します。

>> Time.iso8601("2009-04-10I12:02:54+09:00")
ArgumentError: invalid date: "2009-04-10I12:02:54+09:00"
        from /usr/lib/ruby/1.8/time.rb:376:in `iso8601'
        from (irb):6

この例では真ん中あたりの「T」が「I」になっているためフォーマットに適合していません。

もし、「『I』という不正な文字があります」というようなメッセージが入っていると、問題の原因を簡単に把握できるようになります。

エラーメッセージには大雑把な原因だけではなく、できるだけ詳しく原因を書くようにしましょう。

期待を示す

問題の原因がわかったら、その問題を解決する方法を検討します。期待している値がわかると、解決する方法を検討しやすくなります。

String#deleteは1つ以上の引数をとります。1つも引数を与えない場合はエラーが発生します。

>> "abcde".delete("a")
=> "bcde"
>> "abcde".delete
ArgumentError: wrong number of arguments
        from (irb):2:in `delete'
        from (irb):2

エラーメッセージを見ると「引数の数が違う」ということがわかります。これで「問題の原因」を把握することができます。

しかし、「問題の原因」はわかってもどうすればその問題を解決できるかはわかりません。引数の数を変えればよいということはわかりますが、いくつにすればよいかがわからないのです。

期待している値を示すと、問題を解決しやすくなります。

>> "abcde".gsub
ArgumentError: wrong number of arguments (0 for 2)
        from (irb):3:in `gsub'
        from (irb):3

このエラーメッセージからはString#gsubが2つの引数を期待していることがわかるので、解決案として「引数を2つ渡す」というアイディアが浮かびます。次にすることは「引数に何を2つ渡すか」を考えることです。

エラーメッセージに「期待していること」を含めると、解決案が浮かびやすくなります。できるだけ、期待していることも含めるようにしましょう。

まとめ

Rubyを例にして問題解決に役立つエラーメッセージについて紹介しました。

問題解決に役立つエラーメッセージの特長は、テストの実行結果にもあてはまります。クリアコードが開発に関わっているテスティングフレームワークではテストの実行結果にこだわっています。

  • Cutter: C言語用テスティングフレームワーク
  • UxU: Firefoxアドオン開発用テスティングフレームワーク

あなたが使っているテスティングフレームワークは問題解決に役立つような情報を提供していますか?

2009-04-10

クリアコードの公開リポジトリ

すでにお気づきの方もいるかもしれませんが、先日から、クリアコードで開発したプログラムが入ったSubversionリポジトリリポジトリの更新状況のRSS)の公開を始めました。

クリアコードでは既存のフリーソフトウェアの開発に参加したり、新しくmilter managerなどのフリーソフトウェアを開発したりしていますが、それらの開発成果の公開場所はケースバイケースとなっています。

  • 既存のフリーソフトウェアの開発に参加する場合は、基本的に、開発成果はアップストリームに還元しています。
  • 新しくフリーソフトウェアを開発する場合は、基本的には関連コミュニティで標準的なホスティングサイトを利用しています。例えば、Ruby関連のソフトウェアであればRubyForge、GNOME関連のソフトウェアであればgnome.orgといった具合です。
  • 関連するソフトウェアがGitHubGoogle Codeを利用している場合は、それらのサイトを利用することもあります。
  • 特に標準的なホスティングサイトが無い場合はSourceForgeを利用しています。

このように、クリアコードの開発成果のソースコードは様々なホスティングサイトのリポジトリにて管理、および公開されています。

まもなくクリアコードは設立から3年が経とうとしていますが、その間、プロジェクトを作るまでもないような小規模なソースコードがいくつかたまってきました。この度、そのようなソースコードをSubversionリポジトリで公開することにしました。

このリポジトリには現在、ページの一部を折りたたむfolding.jsや、このククログを生成するためのtDiary関連のスクリプト(日記のデータをSubversionで管理するIOバックエンド日記を静的なHTMLに変換するスクリプトなどの記事で述べた物)、Thunderbird用の各種アドオンのソースコードが入っています。誰でも自由にチェックアウトできますので、注意事項をご了承の上でどうぞご利用ください。

以下、現在入っているプログラムを簡単に紹介します。

注意事項

  • これらのプログラムはすべて無保証です。
  • プログラムは予告なく追加・削除・変更されることがあります。

JavaScript関連

tDiary関連

ククログだけではなく、milter managerのブログでも使っています。使い方はmilter managerのtdiary.confが参考になると思います。

  • /tdiary/subversionio.rb: tDiaryのSubversionバックエンド
  • /tdiary/gitio.rb: ↑のSubverionバックエンドのgitバージョンです。
  • /tdiary/html-archiver.rb: tDiaryのデータをHTML化するで紹介した物ですが、その後、カテゴリに対応するなどいくつかの点で改良されています。
  • /tdiary/patches/customizable-style-path.rb: スタイルファイルのパスをカスタマイズできるようにします。RDスタイルなど、標準では有効になっていないスタイルを使うときに便利です。本家に提案したのですが、rejectされたのでここに入っています。
  • /tdiary/plugin/classed-category-list.rb: class付きでカテゴリリストを生成します。ククログのトップに並んでいる「タグ: ...」の部分です。
  • /tdiary/plugin/date-to-tag.rb: 日付を本文の下に生成します。今思えば名前が悪いですね。後で変えるかもしれません。
  • /tdiary/plugin/link-subtitle.rb: サブタイトルをその記事のリンクにします。通常は日付がリンクになるのですが、このククログでは日付は本文の下に置いてあるので、代わりにサブタイトルをリンクにしています。
  • /tdiary/plugin/multi-icon.rb: ページアイコンとして、favicon.ico(ICO形式の画像)とfavicon.png(PNG形式の画像)を両方指定できるようにします。
  • /tdiary/plugin/title-navi-label.rb: 「前の日記」「次の日記」リンクのラベルをリンク先の日記のタイトルにします。
  • /tdiary/plugin/zz-permalink-without-section-id.rb: section_footerプラグインが生成するpermalinkから「#pXX」を削除します。ククログでは、1記事を1セクションにして同じ日には複数の記事を書かないという方針で運営しており、セクションIDが必要ないため、このプラグインを作成しました。ファイル名の「zz-」は、このプラグインの読み込み順序を最後の方にさせるためのものです。

Thunderbird関連

Thunderbird Add-ons - クリアコードで公開している、Thunderbirdのバグを回避するパッチや挙動の変更を行う拡張機能です。公開ページにも書いてありますが、これらの拡張機能は無保証です。業務上の必要性からの導入をお考えの場合は、Mozilla Firefox & Mozilla Thunderbird保守・サポートサービスのご利用もご検討ください。

おまけ

リポジトリの更新状況を配信しているRSSは、Subversionに標準添付のcommit-email.rbで生成しています。今回、RSSのタイトルや説明を日本語にしたかったので、Subversionのtrunkに--rss-titleと--rss-descriptionオプションを追加しました。また、--repository-uriで指定されたリポジトリのURIをコミットメールのX-SVN-Repositoryヘッダに設定するようにもしています。

Subversionリポジトリの整形表示にはRepos Styleを利用しています。mod_dav_svnにはSVNIndexXSLTというオプションがあって、それを利用しています。

つづき: 2009-12-21
2009-04-06

2009年3月の肉の会

クリアコードでは毎月29日頃に肉の会と呼ばれる社内食事会を行っています。 肉の会というだけあって肉を食べることが多いのですが、今回は肉ではなく、社内でたこやきを食べまくりました。

今回はゲストとしてITproの高橋さんに参加してもらえました。(忙しい中、ありがとうございます!)

高橋さんがおみやげとして発売したばかりの「Ruby技術者認定試験 公式ガイド」を持ってきてくれました。「クリアコードさんへ」とサインももらいました。ありがとうございます!

高橋さんのサイン

この公式ガイドには模擬試験が2回分ついています。Rubyには自信があったのでやってみたところ、90%くらいしか正解できませんでした。普段使わないメソッドや引数が問題になっていると間違えてしまいます。

ひっかけっぽい問題も何題かあり、ひっかけようとしているなぁとニヤニヤしながら解いていたのですが、何題か間違えてしまいました。油断してはいけません。

この模擬試験をやってみて、Hash#invertをはじめて知りました。もうすでにかなりRubyを知っている人も、Ruby技術者認定試験の問題を解いてみると知らないことが見つかるかもしれません。問題を見て、ひっかけっぽいな、とニヤニヤしながら解くのも楽しいと思います。

模擬試験の前にはコンパクトにまとめられたRubyの解説がついているので、まだRubyに詳しくない人のとっかかりにもよさそうな気がします。mapとcollectをきちんと対等に扱っているので、変に偏らなそうなのもよいと思います。*1 Rubyで開発されたデスクトップ・アプリケーションとしてRabbitが挙がっていることも、とてもよいと思います。

Ruby技術者認定試験 公式ガイド (ITpro BOOKs) Ruby技術者認定試験 公式ガイド (ITpro BOOKs)
伊藤忠テクノソリューションズ/ITpro/Rubyアソシエーション
日経BP社
¥ 2,100

*1  私はcollectに偏っています。

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-04-02

Rails 2.3.2でActiveLdapを使う

2009/3/24時点でのActiveLdapの最新リリースは1.0.2ですが、ActiveLdap 1.0.2はRails 2.3.2には対応していません。これは、ActiveLdap 1.0.2の方がRails 2.3.2より早くリリースされたからです。基本的に、ActiveLdapはリリース時点での最新のRailsに対応していますが、未来のRailsには対応できていません。

Rails 2.3.2とRuby 1.9.1とActiveLdap

さて、そんなActiveLdapですが、trunkではRails 2.3.2に対応しています。(Issue 18 - ruby-activeldap - [Rails 2.3 Support] :: Running WEBrick Hangs - Google Code

また、Rails 2.3.2だけではなく、Ruby 1.9.1にも対応しています。(Issue 20 - [Ruby 1.9 Support] :: Running Tests - Google Code) 対応の過程でAlexey.Chebotarさんが、Ruby/LDAPをRuby 1.9.1に対応しました。

Ruby 1.9.1対応Ruby/LDAP

Ruby/LDAPはOpenLDAPのRubyバインディングで最終リリースが2006/8/9です。Alexeyさん以外にもRuby 1.9.1対応した成果をフィードバックしている人がいるのですが、まだ対応してもらえていないようです。

Alexeyさんもメンテナの方に連絡をとってみたということですが、まだ対応してもらえていないようです。パッチの形で転がっているよりも、パッチが取り込まれた形の方が利用しやすいということと、AlexeyさんがRuby/LDAPをメンテナンスする意志があるということだったので、ActiveLdapのリポジトリにRuby 1.9.1対応版のRuby/LDAPが入っています。(リポジトリ)Ruby/LDAPをRuby 1.9.1で使いたいという方はこれを使ってみるとよいと思います。

Rails 2.3.2とActiveLdapのtrunk

少し脱線しましたが、ActiveLdapのtrunkをRailsアプリケーションで使う方法がActiveLdapプロジェクトのWikiにあります。(UsingTrunkWithRailsJa) Rails 2.3.2でActiveLdapを使いたい場合はこの方法を参考にしてください。

当初は次のリリースは1.1.0と考えていたのですが、1.1.0の前に、Rails 2.3.2に対応している現時点のものを1.0.3をリリースすることも考えています。いくつか予定されていた項目がまだ実現されていないので1.1.0を出すことはできないのですが、その前にRails 2.3.2対応版を出すことには価値があるかもしれないと思うようになってきました。*1

1.1.0に予定されていた項目の1つにドキュメントの整備というものがあります。この項目もまだ実現できていない項目の1つなのですが、現在、id:tashenさんがチュートリアルの日本語訳を行ってくれています。(ありがとうございます!)

ActiveLdapは活発に開発が行われているライブラリなので、RubyでLDAPを便利に操作したい場合はActiveLdapを使うことを検討してみてください。そして、より使いやすく有用なライブラリにするために、バグ修正やドキュメント作成などでプロジェクトに協力してくれる人を募集しています。お待ちしています。

*1  Rails 2.3.2に対応していないというIssueが2つ登録されたりしたので。

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-03-24

Railsで画像アップロード

Railsで画像をアップロードするときはどうやっているんでしょうか。

Fleximageというプラグインがあります。よいAPIだと思うのですが、あまり使っている人がいないようなので紹介します。

ここでは1からサンプルRailsアプリケーションを作成しながらFleximageの使い方を紹介します。順番にコマンドを実行・コードを変更していくと動くように書いてあります。

下準備

まず、sampleというRailsアプリケーションを作成します。

% cd /tmp
% rails sample
% cd sample

次に、Fleximageをインストールし、画像用のテーブルを作成します。

% script/plugin install git://github.com/Squeegy/fleximage.git
% script/generate scaffold photo title:string image_filename:string image_width:integer image_height:integer
% rake db:migrate

以下のカラムは特別なカラムです。Fleximageは以下のカラムが定義されていると、そこに値を設定してくれます。

image_filename
アップロードした画像のファイル名
image_width
画像の幅
image_height
画像の高さ

これらはすべて省略可能です。今回は、せっかくなので、すべて定義しました。

Fleximageを使うため、モデルを以下のように変更します。

app/models/photo.rb:

class Photo < ActiveRecord::Base
  acts_as_fleximage :image_directory => 'public/images/uploaded'
end

これで、アップロードされた画像は"#{RAILS_ROOT}/public/images/uploaded"以下に保存されます。

ここからは、コードを変更し、アプリケーションを動かし、動作を確認しながら進めていきます。そのため、ここでサーバを起動しておきます。

% script/server

アップロード

http://localhost:3000/photos/にアクセスすると以下のような見慣れたscaffoldの画面になるので、「New Photo」リンクから画像アップロードフォームへ進みます。

indexページ

scaffoldのフォームでは画像をアップロードできないので、app/views/photos/new.htmlを以下のように変更します。

app/views/photos/new.html:

<h1>New photo</h1>

<% form_for(@photo, :html => {:multipart => true}) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :title %><br />
    <%= f.text_field :title %>
  </p>
  <p>
    <%= f.label :image_file %><br />
    <%= f.file_field :image_file %>
  </p>
  <p>
    <%= f.submit 'Create' %>
  </p>
<% end %>

<%= link_to 'Back', photos_path %>

これで、フォームは以下のようになります。

画像アップロードフォーム

タイトルとアップロードする画像のパスを指定して「Create」ボタンを押します。

画像がアップロードされ、ファイル名やサイズが設定されていることが確認できます。

アップロード後

せっかく画像をアップロードしたので、アップロードした画像も表示するようにします。app/views/photos/show.html.erbに以下を追加します。ここでは、タイトルの下に追加しました。

app/views/photos/show.html.erb:

<p>
  <%= image_tag(photo_path(@photo, :format => "png")) %>
</p>

PNGフォーマットを指定しているので、コントローラ側でPNGフォーマットを受けつけるようにします。

app/controllers/photos_controller.rb:

def show
  @photo = Photo.find(params[:id])

  respond_to do |format|
    format.html # show.html.erb
    format.xml  { render :xml => @photo }
    format.png  # <- 追加
  end
end

PNGフォーマットのビューを作ります。ここが、FleximageのAPIのよいところです。

Fleximageを使ったビューは「#{アクション名}.#{フォーマット}.flexi」というファイル名になります。今回の場合だと、「show.png.flexi」になり、以下のような内容になります。

app/views/photos/show.png.flexi:

# -*- ruby -*-
@photo.operate do |image|
end

これで、http://localhost:3000/photos/1.pngで画像を表示することができるようになりました。画像表示ページは以下のようになります。

画像付き表示ページ

サムネイル

画像表示ページではアップロードされた画像をそのまま表示しました。画像一覧ページでは画像のサムネイルを表示することにします。

サムネイル画像はhttp://localhost:3000/photos/1/thumbnail.gifで表示するようにします。そのため、まず、config/routes.rbを変更します。

config/routes.rb:

map.resources :photos, :member => {:thumbnail => :get}

これで、thumbnail_photo_pathが使えるようになるので、index.html.erbを書き換えます。

app/views/photos/index.html.erb:

<h1>Listing photos</h1>

<table>
  <tr>
    <th>Title</th>
    <th>Thumbnail</th>
    <th colspan="3">Action</th>
  </tr>

<% @photos.each do |photo| %>
  <tr>
    <td><%=h photo.title %></td>
    <td><%= image_tag(thumbnail_photo_path(photo, :format => "gif") %></td>
    <td><%= link_to 'Show', photo %></td>
    <td><%= link_to 'Edit', edit_photo_path(photo) %></td>
    <td><%= link_to 'Destroy', photo, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New photo', new_photo_path %>

ここでは意味もなくフォーマットにgifを指定していますが、これはFleximageが画像フォーマットを簡単に変更できることを示すためです。

コントローラにGIFフォーマットに対応したthumbnailアクションを定義します。

app/controllers/photos_controller.rb:

def thumbnail
  @photo = Photo.find(params[:id])

  respond_to do |format|
    format.gif
  end
end

サムネイルのGIF画像用のビューは以下のようになります。とてもすっきり書けています。

app/views/photos/thumbnail.gif.flexi:

# -*- ruby -*-
@photo.operate do |image|
  image.resize('80x60')
end

変更した一覧ページを表示するとこうなります。

サムネイル付き画像一覧ページ

アップロードした画像がサムネイルとして表示されています。

まとめ

すっきりしたAPIのRails用画像アップロードプラグインFleximageを紹介しました。

ここでは触れませんでしたが、Fleximageは機能が豊富でカスタマイズ性にも優れています。例えば、アップロードされた画像を表示するときに影をつけたり、文字を入れたりといろいろ加工することができます。また、ファイルではなく、データベースに画像を保存することもできます。

Fleximageは日本語の情報がほとんどないのが不思議ですね。あまり使われていないのかもしれません。

おまけ

ImageMagickを使って画像に影をつけるシェルスクリプトです。これを使うと、このページのスクリーンショットについているような影をつけることができます。

add-shadow.sh:

#!/bin/sh

if [ $# != 2 ]; then
    echo "Usage: $0 INPUT SHADOWED_OUTPUT"
    exit 1
fi

convert $1  \( +clone -background black -shadow 80x6 \) \
    +swap -background none -layers merge +repage $2

このように使います。

% ./add-shadow.sh image.png shadowed-image.png
つづき: 2009-12-21
Tags: Ruby | このエントリの del.icio.us history 27 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-03-23

仙台Ruby会議01

先日、仙台Ruby会議01で1コマ話してきました。

まず好きなこと、そしてそれを続けること

東北にいたころにお世話になっている人たちも観にきてくれて、たくさんの方に聞いてもらえました。ありがとうございます。

何人か直接感想を伝えてくれた方もいたので、少しでも言いたかったことが伝わったような気がしています。仙台で話せてよかったです。ただ、いくつか言い損ねたこともありました。

使うこと

前半では、作る側である前に使う側という立場であったから、作る側の考え方が養われたということに触れたのですが、後半の「今日からできること」ではそれに触れませんでした。使うことでよりソフトウェアに愛着がもてるかもしれないので、使うということも大事なことだと思います。

今なら、Ruby 1.9.1を使うというのもエキサイティングでよいかと思います。使うことでバグを踏んで、直す機会に巡りあえるかもしれません。

手を動かしている人は私だけではない

自分の例を使って、東北にいてもプログラミングを続けることはできるし、認めてもらえることもあるという話をしました。そして、プログラミングして直すということがどちらにも有効ではないかと続くのですが、そこで、とてもよい実例をあげそこねてしまいました。

ちょうど1つ前に話していた藤岡さんがまさにそのよい例だと思います。藤岡さんはcgi.rbを直すことで認められ、Rubyのコミッタになっています。

当時はまだ東北にいましたが今は東北にいない自分の例だけよりも、今も東北にいて活躍されている藤岡さんに触れなかったことが悔やまれます。より身近にいる藤岡さんのことに結びつけて聞いてもらえていたら、もっと実感して聞いてもらえたかもしれません。

最近は地方でもすごい

都会に行かないと話せる人がいないという少し昔の話をしましたが、今はその状況は大きく変わっていると思います。それは、仙台Ruby会議01に参加した人たちならわかるはずです。前述の藤岡さんもいますし、仙台Ruby会議01を先頭に立って引っ張ってくれた片平さんもいます。

これは、はっきりと言うべきでした。

感謝

今回、仙台Ruby会議01で話すことができたのは、角谷さんと片平さんのおかげでした。仙台Ruby会議01の話者がすべて決まった後なのに、私も入れてくれました。入ってきた私のために、1コマ増やせるように動いてくれ、実際に1コマ増やしてもらえました。本当にありがとうございました。

東北にいるたくさんのすばらしいRubyistを知ることができた仙台Ruby会議01は、とてもすばらしいRuby会議だったと思います。

Ruby会議2009は、私個人だけではなく、スポンサーとしてクリアコードでも参加したいと思います。

Tags: Ruby | このエントリの del.icio.us history 1 user | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2009-01-26

tDiaryのデータをHTML化する

tDiaryをローカルなネットワークに配置して、tDiaryが表示する内容を静的なHTMLとして公開したい場合はよくありますよね。ククログもそんなよくある使い方の1つです。

tDiaryには静的なHTMLを生成するためのsqueezeプラグインがありますが、squeezeプラグインが出力するHTMLは以下の点でCGIで表示される内容と異なります。

  • 各日付のページしか生成しない
    • 最新の日記n件ページや月別ページやカテゴリページは生成しない
  • リンクがCGI用のリンクのままで、次の日記のページに移動するリンクが壊れている
  • テーマファイルや画像はコピーしてくれないので、生成したHTMLの入ったディレクトリ以下だけでは完結しない

ただし、これはsqueezeプラグインが検索エンジンへの入力データとしてのHTML生成を目的としているためです。よくある使い方では、生成されたHTMLはCGIで出力されているように表示できることが目的なので、上記のようなミスマッチが発生します。

そこで、ククログではhtml-archiver.rbという静的なHTMLを生成するスクリプトを使っています。html-archiver.rbは最後の方に載せています。

html-archiver.rbの使い方

html-archiver.rbを使うと、CGIで出力されている内容と同じように表示されるHTMLが生成されます。生成例は今見ているこのページです。

使い方はこうなります。

% ruby html-archiver.rb --tdiary tdiayr.rbのあるディレクトリ --conf tdiary.confのあるディレクトリ 出力先ディレクトリ

例えば、以下のような場合を考えます。

  • tdiary.rbは~tdiary/work/ruby/tdiary/core/にある
  • tdiary.confは~tdiary/public_html/にある
  • HTMLは~tdiary/public_html/html/以下に出力する

この場合はこのようなコマンドになります。

% ruby html-archiver.rb --tdiary ~tdiary/work/ruby/tdiary/core/ --conf ~tdiary/public_html/ ~tdiary/public_html/html/

機能

  • 日付ページの生成:
  • 最新n件ページの生成:
  • 月別ページの生成:
  • RSS 1.0の生成:
  • テーマファイルのコピー
  • 画像のコピー

制限

  • ツッコミが生成されるかどうかは試していない
  • カテゴリ一覧ページがきちんと生成されるかは(最近は)試していない
  • タブインデント(tDiary本体のコーディングスタイルに合わせているため)
  • 思ったほど使う場面が少ないかもしれない(もしかしたら、tDiaryが表示する内容を静的なHTMLとして公開することがそんなにないかもしれない)

ライセンス

GPL3あるいは3以降の新しいバージョンのGPL

html-archiver.rb

#!/usr/bin/env ruby
# -*- coding: utf-8; ruby-indent-level: 3; tab-width: 3; indent-tabs-mode: t -*-

require 'uri'
require 'cgi'
require 'fileutils'
require 'pathname'
require 'optparse'
require 'ostruct'
require 'enumerator'
require 'rss'

options = OpenStruct.new
options.tdiary_path = "./"
options.conf_dir = "./"
opts = OptionParser.new do |opts|
	opts.banner += " OUTPUT_DIR"

	opts.on("-t", "--tdiary=TDIARY_DIRECTORY",
			  "a directory that has tdiary.rb") do |path|
		options.tdiary_path = path
	end

	opts.on("-c", "--conf=TDIARY_CONF", "a path of tdiary.conf") do |conf|
		options.conf_dir = conf
	end
end
opts.parse!

output_dir = ARGV.shift

Dir.chdir(options.conf_dir) do
	$LOAD_PATH.unshift(File.expand_path(options.tdiary_path))
	require "tdiary"
end

module HTMLArchiver
	class CGI < ::CGI
		def referer
			nil
		end

		private
		def env_table
			{"REQUEST_METHOD" => "GET", "QUERY_STRING" => ""}
		end
	end

	module Image
		def init_image_dir
			@image_dest_dir = @dest + "images"
		end
	end

	module Base
		include Image

		def initialize(rhtml, dest, conf)
			@ignore_parser_cache = true

			cgi = CGI.new
			setup_cgi(cgi, conf)
			@dest = dest
			init_image_dir
			super(cgi, rhtml, conf)
		end

		def eval_rhtml(*args)
			link_detect_re = /(<(?:a|link)\b.*?\bhref|<img\b.*?\bsrc)="(.*?)"/
			super.gsub(link_detect_re) do |link_attribute|
				prefix = $1
				link = $2
				uri = URI(link)
				if uri.absolute? or link[0] == ?/
					link_attribute
				else
					%Q[#{prefix}="#{relative_path}#{link}"]
				end
			end
		end

		def save
			return unless can_save?
			filename = output_filename
			if !filename.exist? or filename.mtime != last_modified
				filename.open('w') {|f| f.print(eval_rhtml)}
				filename.utime(last_modified, last_modified)
			end
		end

		protected
		def output_component_name
			dir = @dest + output_component_dir
			name = output_component_base
			FileUtils.mkdir_p(dir.to_s, :mode => 0755)
			filename = dir + "#{name}.html"
			[dir, name, filename]
		end

		def mode
			self.class.to_s.split(/::/).last.downcase
		end

		def cookie_name; ''; end
		def cookie_mail; ''; end

		def load_plugins
			result = super
			@plugin.instance_eval(<<-EOS, __FILE__, __LINE__ + 1)
				def anchor( s )
					case s
					when /\\A(\\d+)#?([pct]\\d*)?\\z/
						day = $1
						anchor = $2
						if /\\A(\\d{4})(\\d{2})(\\d{2})?\\z/ =~ day
							day = [$1, $2, $3].compact
							day = day.collect {|component| component.to_i.to_s}
							day = day.join("/")
						end
						if anchor then
							"\#{day}.html#\#{anchor}"
						else
							"\#{day}.html"
						end
					when /\\A(\\d{8})-\\d+\\z/
						@conf['latest.path'][$1]
					else
						""
					end
				end

				def category_anchor(category)
					href = "category/\#{u category}.html"
					if @category_icon[category] and !@conf.mobile_agent?
						%Q|<a href="\#{href}"><img class="category" src="\#{h @category_icon_url}\#{h @category_icon[category]}" alt="\#{h category}"></a>|
					else
						%Q|[<a href="\#{href}">\#{h category}</a>]|
					end
				end

				def navi_admin
					""
				end

				@image_dir = #{@image_dest_dir.to_s.dump}
				@image_url = "#{@conf.base_url}#{@image_dest_dir.basename}"
			EOS
			result
		end

		private
		def setup_cgi(cgi, conf)
		end
	end

	class Day < TDiary::TDiaryDay
		include Base

		def initialize(diary, dest, conf)
			@target_date = diary.date
			@target_diaries = {@target_date.strftime("%Y%m%d") => diary}
			super("day.rhtml", dest, conf)
		end

		def can_save?
			not @diary.nil?
		end

		def output_filename
			dir, name, filename = output_component_name
			filename
		end

		def [](date)
			@target_diaries[date.strftime("%Y%m%d")] or super
		end

		def relative_path
			"../../"
		end

		private
		def output_component_dir
			Pathname(@target_date.strftime("%Y")) + @target_date.month.to_s
		end

		def output_component_base
			@target_date.day.to_s
		end

		def setup_cgi(cgi, conf)
			super
			cgi.params["date"] = [@target_date.strftime("%Y%m%d")]
		end
	end

	class Month < TDiary::TDiaryMonth
		include Base
		def initialize(date, dest, conf)
			@target_date = date
			super("month.rhtml", dest, conf)
		end

		def can_save?
			not @diary.nil?
		end

		def output_filename
			dir, name, filename = output_component_name
			filename
		end

		def relative_path
			"../"
		end

		private
		def output_component_dir
			@target_date.strftime("%Y")
		end

		def output_component_base
			@target_date.month.to_s
		end

		private
		def setup_cgi(cgi, conf)
			super
			cgi.params["date"] = [@target_date.strftime("%Y%m")]
		end
	end

	class Category < TDiary::TDiaryView
		include Base

		def initialize(category, diaries, dest, conf)
			@category = category
			diaries = diaries.reject {|date, diary| !diary.visible?}
			_, diary = diaries.sort_by {|date, diary| diary.last_modified}.last
			@target_date = diary.date
			super("latest.rhtml", dest, conf)
			@diaries = diaries
			@diary = diary
		end

		def can_save?
			not @diary.nil?
		end

		def output_filename
			category_dir = @dest + "category"
			category_dir.mkpath
			category_dir + "#{@category}.html"
		end

		def relative_path
			"../"
		end

		def latest(limit=5)
			@diaries.keys.sort.reverse_each do |date|
				diary = @diaries[date]
				yield(diary)
			end
		end

		protected
		def setup_cgi(cgi, conf)
			super
			cgi.params["date"] = [@target_date.strftime("%Y%m")]
		end
	end

	class Latest < TDiary::TDiaryLatest
		include Base

		def initialize(date, index, dest, conf)
			@target_date = date
			@index = index
			super("latest.rhtml", dest, conf)
		end

		def relative_path
			if @index.zero?
				""
			else
				"../"
			end
		end

		def can_save?
			true
		end

		def output_filename
			if @index.zero?
				@dest + "index.html"
			else
				latest_dir = @dest + "latest"
				FileUtils.mkdir_p(latest_dir.to_s, :mode => 0755)
				latest_dir + "#{@index}.html"
			end
		end

		protected
		def setup_cgi(cgi, conf)
			super
			return if @index.zero?
			date = @target_date.strftime("%Y%m%d") + "-#{conf.latest_limit}"
			cgi.params["date"] = [date]
		end
	end

	class RSS < TDiary::TDiaryLatest
		include Base

		def initialize(dest, conf)
			super("latest.rhtml", dest, conf)
		end

		def mode
			"latest"
		end

		def relative_path
			""
		end

		def can_save?
			true
		end

		def output_filename
			@dest + output_base_name
		end

		def output_base_name
			"index.rdf"
		end

		def do_eval_rhtml(prefix)
			load_plugins
			make_rss
		end

		private
		def make_rss
			base_uri = @conf['html_archiver.base_url'] || @conf.base_url
			rss_uri = base_uri + output_base_name

			@conf.options['apply_plugin'] = true
			feed = ::RSS::Maker.make("1.0") do |maker|
				setup_channel(maker.channel, rss_uri, base_uri)
				setup_image(maker.image, base_uri)

				@diaries.keys.sort.reverse[0, 15].each do |date|
					diary = @diaries[date]

					maker.items.new_item do |item|
						setup_item(item, diary, base_uri)
					end
				end
			end

			feed.to_s
		end

		def setup_channel(channel, rss_uri, base_uri)
			channel.about = rss_uri
			channel.link = base_uri
			channel.title = @conf.html_title
			channel.description = @conf.description
			channel.dc_creator = @conf.author_name
			channel.dc_rights = @conf.copyright
		end

		def setup_image(image, base_uri)
			return if @conf.banner.nil?
			return if @conf.banner.empty?

			if /^http/ =~ @conf.banner
				rdf_image = @conf.banner
			else
				rdf_image = base_uri + @conf.banner
			end

			maker.image.url = rdf_image
			maker.image.title = @conf.html_title
			maker.link = base_uri
		end

		def setup_item(item, diary, base_uri)
			section = nil
			diary.each_section do |_section|
				section = _section
				break if section
			end
			return if section.nil?

			item.link = base_uri + @plugin.anchor(diary.date.strftime("%Y%m%d"))
			item.dc_date = diary.last_modified
			@plugin.instance_variable_set("@makerss_in_feed", true)
			subtitle = section.subtitle_to_html
			body_enter = @plugin.send(:body_enter_proc, diary.date)
			body = @plugin.send(:apply_plugin, section.body_to_html)
			body_leave = @plugin.send(:body_leave_proc, diary.date)
			@plugin.instance_variable_set("@makerss_in_feed", false)

			subtitle = @plugin.send(:apply_plugin, subtitle, true).strip
			subtitle.sub!(/^(\[([^\]]+)\])+ */, '')
			description = @plugin.send(:remove_tag, body).strip
			subtitle = @conf.shorten(description, 20) if subtitle.empty?
			item.title = subtitle
			item.description = description
			item.content_encoded = body
			item.dc_creator = @conf.author_name
			section.categories.each do |category|
				item.dc_subjects.new_subject do |subject|
					subject.content = category
				end
			end
		end
	end

	class Main < TDiary::TDiaryBase
		include Image

		def initialize(cgi, dest, conf, src=nil)
			super(cgi, nil, conf)
			calendar
			@dest = dest
			@src = src || './'
			init_image_dir
		end

		def run
			@date = Time.now
			load_plugins
			copy_images

			all_days = archive_days
			archive_categories
			archive_latest(all_days)

 			make_rss
			copy_theme
		end

		private
		def copy_images
			image_src_dir = @plugin.instance_variable_get("@image_dir")
			image_src_dir = Pathname(image_src_dir)
			unless image_src_dir.absolute?
				image_src_dir = Pathname(@src) + image_src_dir
			end
			@image_dest_dir.rmtree if @image_dest_dir.exist?
			if image_src_dir.exist?
				FileUtils.cp_r(image_src_dir.to_s, @image_dest_dir.to_s)
			end
		end

		def archive_days
			all_days = []
			@years.keys.sort.each do |year|
				@years[year].sort.each do |month|
					month_time = Time.local(year.to_i, month.to_i)
					month = Month.new(month_time, @dest, conf)
 					month.save
					month.send(:each_day) do |diary|
						all_days << diary.date
 						Day.new(diary, @dest, conf).save
					end
				end
			end
			all_days
		end

		def archive_categories
			cache = @plugin.instance_variable_get("@category_cache")
			cache.categorize([], @years).each do |category, diaries|
				categorized_diaries = {}
				diaries.keys.each do |date|
					date_time = Time.local(*date.scan(/^(\d{4})(\d\d)(\d\d)$/)[0])
					@io.transaction(date_time) do |diaries|
						categorized_diaries[date] = diaries[date]
						DIRTY_NONE
					end
				end
 				Category.new(category, categorized_diaries, @dest, conf).save
			end
		end

		def archive_latest(all_days)
			conf["latest.path"] = {}

			latest_days = []
			all_days.reverse.each_slice(conf.latest_limit) do |days|
				latest_days << days
			end

			latest_days.each_with_index do |days, i|
				date = days.first.strftime("%Y%m%d")
				if i.zero?
					latest_path = "./"
				else
					latest_path = "latest/#{i}.html"
				end
				conf["latest.path"][date] = latest_path
			end
			latest_days.each_with_index do |days, i|
				latest = Latest.new(days.first, i, @dest, conf)
				latest.save
				conf["ndays.prev"] = nil
				conf["ndays.next"] = nil
			end
		end

		def make_rss
			RSS.new(@dest, conf).save
		end

		def copy_theme
			theme_dir = @dest + "theme"
			theme_dir.rmtree if theme_dir.exist?
			theme_dir.mkpath
			tdiary_theme_dir = Pathname(File.join(TDiary::PATH, "theme"))
			FileUtils.cp((tdiary_theme_dir + "base.css").to_s, theme_dir.to_s)
			if @conf.theme
				FileUtils.cp_r((tdiary_theme_dir + @conf.theme).to_s,
									(theme_dir + @conf.theme).to_s)
			end
		end
	end
end

cgi = HTMLArchiver::CGI.new
conf = TDiary::Config.new(cgi)
conf.show_comment = true
conf.hide_comment_form = true
def conf.bot?; false; end
output_dir ||= Pathname(conf.data_path) + "cache" + "html"
output_dir = Pathname(output_dir).expand_path
output_dir.mkpath
HTMLArchiver::Main.new(cgi, output_dir, conf, options.conf_dir).run
つづき: 2009-04-06
Tags: Ruby | このエントリの del.icio.us history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2008-12-05

tDiaryのSubversionバックエンド

みなさんはtDiaryの日記データをどのようにバックアップしているのでしょうか。cronでアーカイブしていたり、dbi_ioでデータベースに保存して、データベースの内容をアーカイブしていたりしているのでしょうか。

開発者の人なら日記のデータもソースコードと同じようにバックアップしたいですよね。つまり、バージョン管理をして、レポジトリの内容をアーカイブするバックアップです。そんな人はこのようなSubversionIOはいかがでしょうか。保存する毎にSubversionリポジトリに日記データをコミットします。

require 'tdiary/defaultio'

module TDiary
	class SubversionIO < DefaultIO
		def transaction( date, &block )
			dirty = TDiaryBase::DIRTY_NONE
			result = super( date ) do |diaries|
				dirty = block.call( diaries )
				diaries = diaries.reject {|_, diary| /\A\s*\Z/ =~ diary.to_src}
				dirty
			end
			unless (dirty & TDiaryBase::DIRTY_DIARY).zero?
				run( "svn", "add", File.dirname( @dfile ) )
				run( "svn", "add", @dfile )
				Dir.chdir( @data_path ) do
					run( "svn", "ci", "-m", "update #{date.strftime('%Y-%m-%d')}" )
				end
			end
			result
		end

		private
		def run( *command )
			command = command.collect {|arg| escape_arg( arg )}.join(' ')
			result = `#{command} 2>&1`
			unless $?.success?
				raise "Failed to run #{command}: #{result}"
			end
			result
		end

		def escape_arg( arg )
			"'#{arg.gsub( /'/, '\\\'' )}'"
		end
	end
end

# Local Variables:
# ruby-indent-level: 3
# tab-width: 3
# indent-tabs-mode: t
# End:

使い方は以下の2ステップです。

  1. tdiary.confで@io_classに指定する
  2. 日記のデータディレクトリをSubversionのワーキングコピーにする

tdiary.confの設定

まず、上記のソースコードをsubversionio.rbとしてどこかに保存してください。ここでは/home/tdiary/lib/以下に保存したとします。

次にtdiary.confに以下の内容を追記します。

subversion_io_dir = "/home/tdiary/lib" # <- 保存した場所にあわせて変更
require "#{subversion_io_dir}/subversionio"
@io_class = TDiary::SubversionIO

データディレクトリの設定

データディレクトリ(tdiary.conf内の@data_pathで指定したディレクトリ)は/home/tdiary/data/として進めます。また、作業しているユーザはtDiaryのCGIを動かすユーザとします。(ここではtdiaryユーザ)

まず、レポジトリに日記データ用のパスを作ります。既存のSubversionリポジトリを利用する場合は、例えばこのようになります。

[tdiary]% svn mkdir -m 'create tDiary data path' https://.../repos/tdiary-data

ローカルに新しくSubversionのリポジトリを作成して、そこに日記データをコミットするようにする場合はこのようになります。新しく作成するリポジトリは/home/tdiary/repos/に作ることにします。

[tdiary]% svnadmin create /home/tdiary/repos

Subversionリポジトリから日記データ保存用のパスを、tDiaryのデータディレクトリにチェックアウトします。今すでにあるtDiaryのデータディレクトリはどこかによけておきます。

[tdiary]% mv /home/tdiary/data /home/tdiary/data.bak
[tdiary]% svn co file:///home/tdiary/repos /home/tdiary/data

既存のデータをワーキングコピーに移動し、日記データだけをレポジトリにコミットします。

[tdiary]% cd /home/tdiary/data
[tdiary]% cp -rp ../data.bak/* ./
[tdiary]% svn add 200*
[tdiary]% svn add 199* # <- もし2000年より前のデータがあるなら
[tdiary]% svn ci -m 'import'

完了

以上で設定は完了です。日記を保存するとリポジトリにコミットされます。

制限

日記本文しか対応していません。ツッコミや画像には対応していません。

svnコマンドをインストールしている必要があります。

tDiary本体のコーディングスタイルに合わせているためタブインデントになっています。

ライセンス

AGPL3あるいは3以降の新しいバージョンのAGPL

つづき: 2009-04-06
Tags: Ruby | このエントリの del.icio.us history 4 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2008-11-13

Test::Unit 2.0.1リリース

Test::Unit 2.0.1が RubyForge上でリリースされました。RubyGemsも提供されているの で以下のようにインストールできます。

% sudo gem install test-unit

経緯

Test::UnitはRuby 1.8.xに標準添付されている単体テストフレーム ワークです。しかし、Ruby 1.9.1からは miniunitが標準 添付され、Test::UnitはRubyForgeで開発が継続されることになりま した。これからもTest::Unitを使うときはRubyGemsでインストール することになるでしょう。

Ruby 1.8.xに標準添付されているTest::Unitは互換性のために、 Test::Unit 1.2.3としてリリースされています。Ruby 1.9.1でも Ruby 1.8.xに標準添付されているTest::Unitと同じTest::Unitを使 用したい場合は以下のようにします。

% sudo gem install test-unit --version '= 1.2.3'

テストファイル内(変更前):

require 'test/unit'
...

テストファイル内(変更後):

require 'rubygems'
gem 'test-unit', '= 1.2.3'
require 'test/unit'
...

余談ですが、Ruby 1.9.1でTest::Unitが標準添付から外れ、 miniunitが標準添付になったのはTest::Unitのソースがメンテナン スしづらくなっていたのが主な理由です。

Test::Unit 2.x

Ruby 1.8.xに標準添付されているTest::Unitは長い間メンテナンス はされていましたが、特に機能拡張などは行われていませんでした。 しかし、その間にもテスト環境を便利にするライブラリが公開され てきました。例えば、RSpecのような BDD用のフレームワークや、 expectations のような軽量の単体テストフレームワーク、 Shoulda/ test/spec/ Mochaのように Test::Unitを拡張するライブラリなどです。Test::Unitは少し時代 遅れになってしまったのです。

最近のテスト用のフレームワークは DSL化の方向に向かっているようにも見えます。 これはRSpecの影響が大きいのでしょう。expectationsやShouldaもテスト用の DSLを提供します。

しかし、Test::UnitはDSLを提供しません。テストを「英語らしく」 ではなく「Rubyプログラムらしく」書きます。好みにもよりますが、 これはTest::Unitのメリットの1つと言えます。

Test::Unitに他のフレームワークやライブラリの機能を、 Test::Unitの「Rubyプログラムらしく」テストを書ける特性を活か したまま追加すれば、Test::Unitはもっと便利で使いやすいテスト フレームワークになるでしょう。Test::Unit 2.x系列はRuby 1.8.x に標準添付されていた頃とは違い、そのような方針の元で活発に開 発されていく系列になります。

例えば、以下のような機能が他のフレームワークやライブラリから 移植されています。

  • 差分表示
  • ネストしたテスト定義
  • 色付け
  • C-cでテスト中断時にもテスト結果を表示
  • 複数のsetup/teardown
  • ...

ここでは「差分表示」と「ネストしたテスト定義」だけ紹介します。 *1

差分表示

RSpecでは比較結果が異なった場合に差分を表示して違いをわかり やすく表示してくれます。

diff_spec.rb:

require 'rubygems'
require 'spec'

describe String do
  it do
    ["I", "am", "a", "boy"].join("\n").should == ["I", "was", "a", "boy"].join("\n")
  end
end

実行結果(差分表示部分のみ):

% ruby diff_spec.rb -D
...
Diff:
@@ -1,5 +1,5 @@
 I
-was
+am
 a
 boy
...

同様の機能がTest::Unit 2.0.1にもあります。

test_diff.rb:

require 'rubygems'
gem 'test-unit'
require 'test/unit'

class TestDiff < Test::Unit::TestCase
  def test_diff
    assert_equal(["I", "am", "a", "boy"].join("\n"),
                 ["I", "was", "a", "boy"].join("\n"))
  end
end

実行結果(差分表示部分のみ):

% ruby test_diff.rb
...
diff:
  I
- am
+ was
  a
  boy
...

この例では、ほとんど同じ差分表示ですが、Test::Unit 2.0.1の 差分表示がRSpecの差分表示よりも便利なこともあります。

今度は"\n"ではなく" "でjoinして1行の文字列として比較します。

test_diff.rb:

require 'rubygems'
gem 'test-unit'
require 'test/unit'

class TestDiff < Test::Unit::TestCase
  def test_diff
    assert_equal(["I", "am", "a", "boy"].join(" "),
                 ["I", "was", "a", "boy"].join(" "))
  end
end

実行結果(差分表示部分のみ):

% ruby test_diff.rb
...
diff:
- I am a boy
?    ^
+ I was a boy
?   + ^
...

Test::Unit 2.0.1では必要なら同じ行のうち、どの列が異なってい るのかも表示します。RSpecでは行単位の差分までで列単位までの 差分は表示しません。

余談ですが、この差分表示形式はPythondifflib ライブラリで使われている形式です。

ネストしたテスト定義

Shouldaではcontextをネストさせることにより、便利にテストを書 くことができます。以下はShouldaのページからの引用です。 *2

class UserTest < Test::Unit::TestCase
  context "A User instance" do
    setup do
      @user = User.find(:first)
    end

    should "return its full name" do
      assert_equal 'John Doe', @user.full_name
    end

    context "with a profile" do
      setup do
        @user.profile = Profile.find(:first)
      end

      should "return true when sent #has_profile?" do
        assert @user.has_profile?
      end
    end
  end
end

ネストされた"with a profile"のcontext内では上位の"A User instance"のcontext内のsetupが実行された後に実行されます。つ まり、以下のような実行順序になります。

  • setup: "A User instance" context
    • should: "return its full name"
  • setup: "A User instance" context
    • setup: "with a profile" context
      • should: "return true when sent #has_profile?"

実行されるフィクスチャ(setup)がネストで自然に表現されていま す。

Test::Unit 2.0.1では以下のように書きます。

class UserTest < Test::Unit::TestCase
  def setup
    @user = User.find(:first)
  end

  def test_full_name
    assert_equal('John Doe', @user.full_name)
  end

  class ProfileTest < UserTest
    def setup
      super
      @user.profile = Profile.find(:first)
    end

    def test_profile
      assert_true(@user.has_profile?)
    end
  end
end

これは以下のように実行されます。

  • UserTest#setup
    • UserTest#test_full_name
  • UserTest#setup
    • UserTest::ProfileTest#setup
      • UserTest::ProfileTest#test_profile

実行されるフィクスチャ(setup)がネストとクラス階層で自然に表現 されています。

まとめ

Test::UnitはRuby 1.9.1からは標準添付ではなくなりましたが、 Test::Unit 2.xとして活発に開発が続けられています。既存の他の フレームワークやライブラリのよいところは積極的に導入している ため、Ruby 1.8.xに標準添付されているTest::Unitよりもはるかに 使いやすくなっています。

今回は紹介しませんでしたが、他のフレームワークやライブラリに はないTest::Unit 2.x独自の便利な機能もあります。Test::Unitの 「Rubyプログラムらしい」テストの書き方が好きな場合はこれから もTest::Unitを使ってみてはいかがでしょうか。

*1  「C-cでテスト中断時にもテスト結果を表示」は開発が進んでい き、たくさんのテストがある場合には地味ですがとても便利な機能 なのです。RSpecにも実装されています。

*2  ただし、Test::UnitはTest::Unit::TestCaseに修正してある。

2008-11-10

Ruby-GetText-Packageとrake gems:installの共存

Ruby-GetText-Packageだけというわけではないですが、app/controllers/application.rbで何かを行う*1gemを使っているとrake gems:installで足りないgemをインストールできません。経験したことがあるけど別に手動でインストールすればいいやということで、おそらく、わりとうやむやにされていることが多い問題ではないでしょうか。

例えば、Ruby-GetText-Packageだと以下のようにapp/controllers/application.rbを変更する必要があります。

class ApplicationController < ActionController::Base
  init_gettext "blog"
end

Ruby-GetText-Packageのgemがない場合は「init_gettext」が定義されていないため「NameError」が発生します。そのため、rake gems:installをしようとすると以下のように失敗してしまいます。

% rake gems:install
(in /tmp/blog)
rake aborted!
undefined method `init_gettext' for ApplicationController:Class

(See full trace by running task with --trace)

これを回避するために「足りないgemがあるときはinit_gettextを使わない」という方法があります。 あまりきれいな方法ではありませんが、紹介します。

class ApplicationController < ActionController::Base
  if Rails.configuration.gems.reject {|gem| gem.loaded?}.empty?
    init_gettext "blog"
  end
end

もし、config/environment.rbでconfig.active_record.observersを指定しているなどして、app/model/以下も読み込まれるのであれば、ダミーのN_を定義しておくとよいでしょう。

class ApplicationController < ActionController::Base
  if Rails.configuration.gems.reject {|gem| gem.loaded?}.empty?
    init_gettext "blog"
  else
    class ActiveRecord::Base
      def self.N_(*args); end
    end
  end
end

これでRuby-GetText-Packageを使っているときでもrake gems:installが動くようになります。

% rake gems:install
(in /tmp/blog)
gem install gettext
Bulk updating Gem source index for: http://gems.rubyforge.org/
Successfully installed gettext-1.93.0
1 gem installed
Installing ri documentation for gettext-1.93.0...
Installing RDoc documentation for gettext-1.93.0...

以下、「足りないgemがあるとき」の判断方法について少し書いてみます。

足りないgemがあるとき

Railsでは「足りないgemがあるかどうか」を示すAPIを提供しているのはRails::Initializerです。ただ、残念ながらconfig/environment.rbの中で作ったRails::Initializerはどこにも保存されていないので、無理やり引っ張り出す必要があります。具体的には以下のようなコードになります。

initializer = nil
ObjectSpace.each_object(Rails::Initializer) do |object|
  initializer = object
  break
end
initializer.gems_dependencies_loaded

しかし、この方法ではRails::InitializerがGCされてしまっていると動きません*2

また、ObjectSpaceはできれば使いたくないものです。そのため、もう少し安全で、何をしているのかがまだわかりそうな方法の方がよさそうです。そのための「足りないgemがあるかどうか」を判断する方法が以下のようになるというわけです。

Rails.configuration.gems.reject {|gem| gem.loaded?}.empty?

ただ、この方法はRails 2.1.2(や2008-10-30でのmaster)では動きますが、もし、Rails内部の「足りないgemがあるかどうか」を判断する方法が変わった場合は動かなくなる可能性もあります。 「あまりきれいな方法ではありませんが」と書いたのはこのためです。

まとめ

Ruby-GetText-Packageを使っている場合でもrake gem:installを利用する方法を紹介しました。

同じような問題は他のライブラリでも起こりうると思うので、そのような場合も同じように問題を回避できると思います。

*1  もう少しいうと、読み込まれた時に実行される場所(例えばクラス定義の中)で何かを行う場合。メソッド定義の中などその場では実行されないものは関係ない。

*2  多くの場合はそんな状況にはならないでしょう

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2008-10-30

Rabbit 0.5.8リリース

Ruby-GNOME2を使って実装されているプレゼンテーションツールRabbit 0.5.8がリリースされました。

0.5.8では部分的にClutterをサポートしています。

Clutter

Clutterは高速で、視覚的にリッチで、アニメーションするGUIを作成するためのライブラリです。Clutterのこれらの特徴はOpenGLをバックエンドに使う事で実現されています。 ClutterはLinux/Mac OS X/Windowsなどマルチプラットフォームで動作します。さらに、組み込み環境でも動作し(OpenGL ESを利用)、デモ動画も公開されています。

ライブラリを使用する視点で見ると、GStreamer/cairo/Pango/GTK+などGNOME関連のライブラリと親和性が高いこともあり、便利で使いやすいAPIになっています。

ClutterにはRuby/Python/Perl/Valaなど各種言語用のバインディングがあります。リッチなインターフェイスを作成したい場合にClutterを利用してみてはどうでしょうか。

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2008-10-20

Ruby-GNOME2 0.18.0リリース

0.17.0のリリースから1ヶ月も経っていませんが、Ruby-GNOME2バージョン0.18.0がリリースされました。

Ruby-GNOME2はGTK+を含むGNOME関連ライブラリのRubyバインディング集です。

目玉

このリリースの目玉はメモリリークの修正と、新規バインディングの追加です。

メモリリークはRuby/GLibの中にもあり、Ruby-GNOME2関連ライブラリ全体で影響を受ける可能性が高いものでした。0.17.0を利用している場合は0.18.0に更新することをおすすめします。

新規バインディングとして以下の2つが追加されました。ただし、まだどちらも「実験的」マークがついていて、今後APIが変更される可能性があります。

  • Ruby/GtkSourceView2
  • Ruby/GooCanvas
Ruby/GtkSourceView2

Ruby/GtkSourceView2はソースコードハイライトウィジェットであるGtkSourceView 2.x系列をサポートします。以前のリリースにも含まれているRuby/GtkSourceViewはGtkSourceView 1.x系列をサポートしていて、2.x系列はサポートしていませんでした。

今回、別ライブラリになっているのはAPIに非互換性が発生したためです。それぞれのライブラリはrequireが異なります。Ruby/GtkSourceViewからRuby/GtkSourceView2へ移行する場合は以下のように変更する必要があります。

変更前:

require 'gtksourceview'

変更後:

require 'gtksourceview2'

GtkSourceView 1.x系列は開発が終了していて、現在は2.x系列が開発されています。今後のことを考えるとRuby/GtkSourceViewからRuby/GtkSourceView2へ移行を検討した方がよいのではないかと思います。

Ruby/GooCanvas

Ruby/GooCanvasは描画にcairoを用いるキャンバスウィジェットであるGooCanvasのバインディングです。

キャンバスウィジェットとは図形や他のウィジェットなどを自由に配置できるウィジェットです。Inkscapeなどのようなグラフィックツールを思い浮かべるとイメージしやすいかもしれません。Inkscapeでは丸や四角などの図形を好きな場所に配置することができます。キャンバスウィジェットを用いることで、そのような機能を持つアプリケーションを簡単に開発することができます。

現在、GTK+にはキャンバスウィジェットが含まれていませんが、将来のGTK+ではGooCanvasがGTK+のキャンバスウィジェットとして取り込まれるのではないかと予想しています。GTK+ではProjectRidley/CanvasOverview - GNOME Live!で検討しているようです。GooCanvasを含むいくつかのキャンバスウィジェットを比較しています。

ちなみに、Ruby-GNOME2にはRuby/GnomeCanvas2というGnomeCanvasのバインディングがあります。ただし、GnomeCanvasは非推奨ライブラリになっています。そのため、Ruby/GnomeCanvas2も将来的にRuby-GNOME2から削除される可能性があります。

これから新しくキャンバスウィジェットを用いたアプリケーションを開発する場合はRuby/GooCanvasも候補のひとつに入れた方がよいかもしれません。ただし、まだ「実験的」な段階なのでAPIが変更される可能性があることに注意する必要があります。

協力のお願い

アナウンスメールにもありますが、Ruby-GNOME2プロジェクトでは協力してくれる方を募集しています。例えば、バインディングを開発してくれる方、ドキュメントを書いてくれる方、英語のドキュメントを日本語化してくれる方、リリース作業をしてくれる方などを募集しています。

興味のある方はruby-gnome2-devel-ja MLまでお願いします。

0.17.0がリリースされてから何人かの方がドキュメント関連作業で協力してくれています。ありがとうございます!

つづき: 2009-07-31
Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2008-10-01

rcairo 1.8.0リリース

2008/09/26にマルチプラットフォームで動作するベクトルベースの グラフィックライブラリであるcairo 1.8.0がリリースされました。また、同日のうちにcairoを Rubyから利用するためのライブラリ rcairo 1.8.0が リリースされました。([ruby-list45520] [ANN] rcairo 1.8.0 )rcairo 1.8.0はcairo 1.8.0に対応しています。rcairoの基本的 な使い方はるびまの記事「cairo: 2 次元画像描画ライブラリ」 にまとまっ ています。

ちなみに、誰も気づいていないかもしれませんが、cairoの最新API への対応は各種言語バインディングの中ではrcairoが最速です。

cairo 1.8.0

cairo 1.8.0ではテキストの扱いが改良されています。例えば、テキ ストを検索、選択、コピペできるようなPDFを出力するようになって います。また、「ユーザフォント」という機能が導入されています。

「ユーザフォント」はその名の通り、ユーザ(cairoを使う開発者) 独自のフォントを定義・利用できる機能です。利用例として、SVGフォ ントやFlashフォントなど標準化されていないフォーマットのフォン トの実装があげられています。通常は利用することはないと思いま すが、cairoがより広く利用される機会を増やす機能になるかもし れません。

もちろん、rcairoでは「ユーザフォント」もサポートしています。

rcairoの今後

rcairoには、cairoのバインディングだけではなく、rcairo独自の cairoをもっと便利に使うための機能が追加されています。例えば、 Cairo::Color もそのひとつです。

rcairoをもっと便利に使えるようにするためには、レンダリングエ ンジンのような機能が必要だと考えています。例えば、以下のよう にすれば表を描画できるというような機能を考えています。

table = Cairo::Table.new(:width => 400)
table << Cairo::Table::Header.new("column1", "column2", "column3")
table << Cairo::Table::Row.new("1x1 value", "1x2 value", "1x3 value")
table << Cairo::Table::Row.new("2x1 value", "2x2 value", "2x3 value")
context.show_table(table)

現在考えているサポートしたい描画対象は以下の通りです。もし興 味があったらぜひ手伝ってください。

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2008-09-27

ActiveScaffoldの地域化

現在、Railsに対応した国際化の仕組みがいくつかあります。しかし、それぞれが 独自の方法で実現しているため、それらを組み合わせて使うと混沌 とした状態に陥ることも少なくありません。

ここでは、モデルから動的にきれいな画面とコントローラ部分を生 成するActiveScaffoldを用 いた場合の国際化(i18n)と地域化(l10n)の実現方法のひとつを 紹介します。この方法では、 ActiveScaffoldLocalizeRuby-GetText-Package を組み合わせます。混沌とする部分はそれなりになじませます。

国際化の仕組み

Railsで使用できる国際化の仕組みの比較はRails Wiki (英語)が詳しいです。

Ruby-GetText-Package には、以下のような地域化対象のメンテナン スのことを考慮した機能があるので、地域化対象メッセージが増加 したり更新される場合には有力な候補になるでしょう。

  • 地域化対象のメッセージを抽出する機能
    • テーブルにカラムを追加した場合、画面に表示するメッセージを追加・更新した場合などに利用
  • 抽出したメッセージを既存の翻訳済みメッセージにマージする機能
    • 翻訳者が新しい地域化対象のメッセージを翻訳する場合に利用
  • wikipedia:gettext用の翻訳支援ツールを利用可能(.po のフォーマットがGNU gettextと互換性があるため)
    • Emacs用のpo-modeや.po専用のエディタ

Railsやプラグインなどが提供しているメッセージだけを地域化した いなど、地域対象メッセージが変化しない場合はその他の仕組みも 有力な候補になるでしょう。例えば、ActiveScaffold用の ActiveScaffoldLocalizeがその場合です。

ActiveScaffoldLocalize

ActiveScaffoldは、国際化の仕組みとしてObject#as_を提供してい ます。その仕組みを利用して国際化・地域化を実現しているのが ActiveScaffoldLocalizeです。

ActiveScaffoldLocalizeには日本語用のメッセージも含まれている ので、以下のようにすればActiveScaffoldのメッセージを日本語に することができます。

% rails shelf
% cd shelf
% script/generate resource book title:string
% rake db:migrate
% script/plugin install git://github.com/activescaffold/active_scaffold.git
% script/plugin install git://github.com/edwinmoss/active_scaffold_localize.git

config/routes.rb:

-  map.resources :books
+  map.resources :books, :active_scaffold => true

app/controllers/books_controller.rb:

class BooksController < ApplicationController
  active_scaffold :book
end

app/views/layouts/application.html.erb:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
    <title>ActiveScaffold l10n</title>
    <%= javascript_include_tag(:defaults) %>
    <%= active_scaffold_includes %>
  </head>

  <body>
    <h1>ActiveScaffold l10n</h1>
    <%= yield %>
  </body>
</html>

app/controllers/applications.rb:

class ApplicationController < ActionController::Base
  # ...
  private
  before_filter :localize_active_scaffold
  def localize_active_scaffold
    ActiveScaffold::Localization.lang = "ja-jp"
    true
  end
end

サーバを起動してhttp://localhost:3000/books/にアクセスします。

% script/server
% firefox http://localhost:3000/books/

ActiveScaffold + ActiveScaffoldLocalize

見ての通り、「検索」などのメニューは日本語になりますが、テー ブル名からきている「Books」やカラム名の「Title」などは日本語 になりません。

ActiveScaffoldLocalizeの方針では、これらを日本語にするために以下のような内容の config/initializers/lang/ja-jp.rb*1を作成します。

config/initializers/lang/ja-jp.rb:

# -*- coding: utf-8 -*-

ActiveScaffold::Localization.define('ja-jp') do |lang|
  lang["Books"] = "本一覧"
  lang["Book"] = "本"
  lang["Title"] = "タイトル"
end

config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。

ActiveScaffold + ActiveScaffoldLocalize + モデルの地域化

(「本を作成」ではなく「本一覧を作成」になっているのはこ のパッチ で直ります。)

ActiveScaffoldLocalizeのこのやり方は手軽ですが、地域化対象の メッセージが変更になった場合(例: 「Title」から「Name」に変更) や、地域化対象のメッセージをtypoした場合(例: 「Title」ではな く「title」としていた)に気づきにくいという問題があります。 このような問題に対してはRuby-GetText-Packageが有効です。

ということで、ActiveScaffoldのメッセージは ActiveScaffoldLocalizeで地域化し、それ以外は Ruby-GetText-Packageで地域化するようにします。

Ruby-GetText-Package

ActiveScaffoldLocalizeとRuby-GetText-Packageのすみわけは上述 の通りですが、エラーメッセージの地域化はRuby-GetText-Package ではなく、ActiveScaffldLocalizeに任せます。これは、 ActiveScaffoldがエラーメッセージ部分を上書きしているため、 Ruby-GetText-Packageが提供するエラーメッセージ国際化処理とな じまないためです。

また、Ruby-GetText-Packageが取得したロケール情報を使って ActiveScaffoldLocalizeのlangを設定していることもコツのひとつ です。

config/environment.rb:

# ...
Rails::Initializer.run do |config|
  # ...
  config.gem "gettext", :lib => "gettext/rails"
  # ...
end

lib/active_scaffold_gettext.rb:

module ActiveScaffoldGetText
  include GetText::Rails

  bindtextdomain(GETTEXT_DOMAIN)
end

class Object
  def as__with_gettext(message, *args)
    return nil if message.nil?
    localized_message = ActiveScaffoldGetText.send(:sgettext, message)
    if localized_message == message
      as__without_gettext(message, *args)
    else
      localized_message % args
    end
  end
  alias_method_chain :as_, :gettext
end

module ActiveScaffold::DataStructures
  class Column
    def initialize_with_gettext(name, active_record_class)
      initialize_without_gettext(name, active_record_class)
      self.label = "#{active_record_class.name.demodulize}|#{@label.humanize}"
    end
    alias_method_chain :initialize, :gettext
  end
end

config/initializers/gettext.rb:

GETTEXT_DOMAIN = "your-rails-application"
require 'active_scaffold_gettext'

class ActiveRecord::Errors
  # restore default error messages overridden by Ruby-GetText-Package.
  @@default_error_messages = {
    :inclusion => "is not included in the list",
    :exclusion => "is reserved",
    :invalid => "is invalid",
    :confirmation => "doesn't match confirmation",
    :accepted  => "must be accepted",
    :empty => "can't be empty",
    :blank => "can't be blank",
    :too_long => "is too long (maximum is %d characters)",
    :too_short => "is too short (minimum is %d characters)",
    :wrong_length => "is the wrong length (should be %d characters)",
    :taken => "has already been taken",
    :not_a_number => "is not a number",
    :greater_than => "must be greater than %d",
    :greater_than_or_equal_to => "must be greater than or equal to %d",
    :equal_to => "must be equal to %d",
    :less_than => "must be less than %d",
    :less_than_or_equal_to => "must be less than or equal to %d",
    :odd => "must be odd",
    :even => "must be even"
  }

  alias_method :on, :on_without_gettext
  alias_method :[], :on
end

lib/tasks/gettext.rb:

namespace :gettext do
  namespace :po do
    desc "Update pot/po files."
    task :update => :environment do
      require 'gettext/utils'

      module GetText::ActiveRecordParser
        class << self
          alias_method :add_target_original, :add_target
          def add_target(targets, file, msgid)
            if /\|/ !~ msgid
              add_target_original(targets, file, msgid.classify)
              add_target_original(targets, file, msgid.classify.pluralize)
            end
            add_target_original(targets, file, msgid)
          end
        end
      end

      targets = Dir.glob("{app,config,components,lib}/**/*.{rb,erb,rjs}")
      GetText.update_pofiles(GETTEXT_DOMAIN, targets, "#{GETTEXT_DOMAIN} 0.0.1")
    end
  end

  namespace :mo do
    desc "Create mo-files"
    task :create do
      require 'gettext/utils'
      GetText.create_mofiles(true, "po", "locale")
    end
  end
end

app/controllers/application.rb:

class ApplicationController < ActionController::Base
  init_gettext GETTEXT_DOMAIN
  # ...
  private
  before_filter :localize_active_scaffold
  def localize_active_scaffold
    posix_locale = GetText.locale.to_posix
    posix_locale = "#{posix_locale}-#{posix_locale}" if /_/ !~ posix_locale
    lang = posix_locale.gsub(/_/, '-').downcase
    ActiveScaffold::Localization.lang = lang
    true
  end
end

翻訳メッセージのファイルpoを作って翻訳します。

% rake gettext:po:update
% mkdir po/ja
% msginit -i po/your-rails-application.pot -o po/ja/your-rails-application.po -l ja_JP
# 途中でメールアドレスを聞かれるので入力する

po/ja/your-rails-application.po:

# ...
#: app/models/book.rb:-
msgid "Book"
msgstr "本"

#: app/models/book.rb:-
msgid "Books"
msgstr "本一覧"
# ...
#: app/models/book.rb:-
msgid "Book|Title"
msgstr "タイトル"
# ...

翻訳メッセージをmoにコンパイルしてアクセスするとテーブル名や カラム名などが日本語になります。

% rake gettext:mo:create

config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。

ActiveScaffold + Ruby-GetText-Package

まとめ

ActiveScaffoldLocalizeとRuby-GetText-Packageを使って、 ActiveScaffoldを用いたアプリケーションの国際化・地域化を実現する方法 のひとつを紹介しました。

基本的に複数の国際化のしくみを同時に使うと問題が起きますが、 今回は以下のようにそれぞれの長所を活かすようにすみわけて、問 題を回避しています。

  • ActiveScaffoldが利用する固定のメッセージは ActiveScaffoldLocalizeで地域化
  • モデル関連や追加・更新が行われるメッセージについては Ruby-GetText-Packageで地域化

*1  config/initializers/lang/以下にファイルを作るというのはActiveScaffoldLocalizeの方針ではありません。ファイルの場所は特に方針はないようです。

つづき: 2009-09-28
Tags: Ruby | このエントリの del.icio.us history 5 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2008-08-12

Rabbit 0.5.7リリース

昨日、 RDるびまの記事 )でスラ イドが書けるプレゼンテーションツール Rabbit がリリースされました。

サンプル

Rabbitではスライドを画像 HTML(+画像) PDF一覧表示 )などで出力することができます。

発表した後に資料を公開する場合や配布資料を作成する場合に利用 するとよいでしょう。

対象ユーザ

RDで書けることがウリというくらいなので、テキストエディタでス ライドを作成したい人が対象になります。おそらく、そのような人 はプログラマであることが多いと思うので、Rabbitはプログラマ向 けのプレゼンテーションツールといえるかもしれません。

スライドをテキストで作成すると以下のような利点があります。

  • バージョンコントロールシステムとの親和性が高い (diffの表示など)
  • 使い慣れたテキストエディタで編集できるため、編集作業の効 率がよい
  • 単なるテキストなので、専用のスライド表示ソフトウェアを用 いなくても内容を確認できる

一方、GUIの編集インターフェイスを備えたプレゼンテーションツー ル(wikipedia:PowerPointwikipedia:Keynoteなど)と比較すると、以下のような欠 点があります。

  • 見た目を微調整しずらい
  • 簡単な図を挿入することが面倒
    • 画像作成ソフトを起動して図を作成し、スライドに挿入

RabbitはRDで書かれたテキストだけではなく、PDFを入力としても 受け付けます。つまり、PDFビューアにもなります。

そこで、上記のような編集時の欠点を解決するために、別途PDF出 力ができるソフトウェアでスライドを作成し、Rabbitで表示すると いうことができます。RabbitをPDFビューアとして使うことにより、 Rabbitのユニークで実用的なユーザインターフェイスを使うことが できます。Rabbitの使い勝手に興味がある場合はこの方法を試して みるとよいかもしれません。

Rabbitのユーザインターフェイスに関してはまた別の機会にしてお きます。

まとめ

Rabbitの外面だけを紹介しました。難易度が高いと言われているイ ンストール方法や特徴的なユーザインターフェイスなどについては 触れませんでした。

Tags: Ruby | このエントリの del.icio.us history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2008-08-01

ActiveLdap 1.0.1リリース

LDAPのエントリを ActiveRecord風のAPIでア クセスするためのライブラリ、 ActiveLdap 1.0.1がリリースされました。

ActiveLdapとは

ActiveRecord風のAPIとは1エントリを1オブジェクトとして扱える ということです。例えば、ユーザの説明を変更する場合は以下のよ うになります。

alice = User.find("alice")
alice.description = "New user"
alice.save!

ActiveRecordと同じように、各クラス間の関係を設定して便利にア クセスすることもできます。

class User < ActiveLdap::Base
  belongs_to :groups, :many => "memberUid"
end

class Group < ActiveLdap::Base
  has_many :users, :wrap => "memberUid"
end

alice = User.find("alice")
alice.groups # => [Group("friend"), Group("office"), ...]
alice.groups << Group.find("home")
alice.groups # => [Group("friend"), Group("office"), Group("home"), ...]

friend = Group.find("friend")
friend.users # => [User("alice"), User("bob"), ...]

ActiveRecordと同じように、Ruby on Railsと使用することもでき ます。

% script/plugin install http://ruby-activeldap.googlecode.com/svn/tags/r1.0.1/rails/plugin/active_ldap
% script/generate scaffold_active_ldap
% vim config/ldap.yml

ActiveLdapは以下のライブラリをバックエンドとして利用できます。

  • Ruby/LDAP
    • 拡張ライブラリ(速い、インストールが大変かもしれない)
  • Net::LDAP
    • Rubyのみで実装(遅い、インストールは簡単)
    • 2008/06/17時点の最新版0.0.4では動かない。 Subversion 版を利用する必要がある。
  • JNDIのLDAPサービスプロバイダ (実験的)
    • JRubyでのみ利用可能。

ベンチマーク

以下はActiveLdapに付属するベンチマークの結果です。ベンチマー クでは100エントリを検索しています。「Rehearsal(リハーサル)」 を行って、それぞれ2回ずつ実行しているのは、以前はキャッシュ などで2回目以降の結果がよくなることなどがあったためです。現 在はあまり意味がありませんが、歴史的に残っています。

% ruby benchmark/bench-al.rb --config benchmark/config.yaml
Populating...
Rehearsal -------------------------------------------------------
  1x: AL              0.080000   0.010000   0.090000 (  0.098738)
  1x: AL(No Obj)      0.010000   0.000000   0.010000 (  0.016623)
  1x: LDAP            0.000000   0.000000   0.000000 (  0.008674)
  1x: Net::LDAP       0.030000   0.000000   0.030000 (  0.045199)
---------------------------------------------- total: 0.130000sec

                          user     system      total        real
  1x: AL              0.080000   0.020000   0.100000 (  0.100959)
  1x: AL(No Obj)      0.010000   0.010000   0.020000 (  0.020697)
  1x: LDAP            0.000000   0.000000   0.000000 (  0.010129)
  1x: Net::LDAP       0.030000   0.000000   0.030000 (  0.042075)
Entries processed by Ruby/ActiveLdap: 100
Entries processed by Ruby/ActiveLdap (without object creation): 100
Entries processed by Ruby/LDAP: 100
Entries processed by Net::LDAP: 100
Cleaning...

各項目はそれぞれ以下の通りです。

  • AL: Ruby/LDAPバックエンドのActiveLdapで検索を行い、各エ ントリをオブジェクト化する(ActiveRecord風のAPIを利用す る場合)
  • AL(No Obj): Ruby/LDAPバックエンドのActiveLdapで検索を行 い、各エントリの結果をオブジェクト化しない(エントリを配 列やハッシュなどを使って表現)
  • LDAP: Ruby/LDAPで検索を行う
  • Net::LDAP: Net::LDAPで検索を行う

上記の結果からは以下のことが言えます。

  • 本当に速度が重要な場合にはRuby/LDAPを直接利用する方がよ い。
  • 利用できるならば、Net::LDAPよりもRuby/LDAPバックエンドを 利用した方がよい。
  • Net::LDAPを直接利用するよりも、オブジェクト化しない ActiveLdap + Ruby/LDAPバックエンドの方が速い。

多くの場合、1度に100エントリを処理することは少ないでしょう。 そのため、通常はActiveLdapで各エントリをオブジェクト化しても 問題は少ないといえます。

もし、1度に多くのエントリを扱う場合で、読み込み専用ならば、 オブジェクト化しない方法で利用することでパフォーマンスを改善 することができます。

まとめ

ActiveLdapを利用することでLDAPのエントリをオブジェクト指向的 なAPIで自然に処理することができます。

ActiveLdapは複数のLDAPバックエンドに対応しており、Rubyがイン ストールされている環境さえあれば動かすこともできます。 (Net::LDAPバックエンド使用時。ただしそんなに速くない)また、 JRubyでもほとんどの機能が動きます。

もし、Ruby/LDAPを利用できる環境であれば、Net::LDAPを直接利用 するよりも、ActiveLdap + Ruby/LDAPバックエンドを利用した方が よりオブジェクト指向らしいAPIでLDAPのエントリを操作できます。 また、速度が要求される場合であれば、オブジェクト化を行わない (オブジェクト指向らしいAPIを利用しない)ことにより、より高 速にLDAPのエントリを読み込むことができます。

つづき: 2009-02-25
Tags: Ruby | このエントリの del.icio.us history 3 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2008-06-15

コミットログでRetrospectivaと連携

Rubyで実装されたSubversion*1リポジトリブラウザ(兼ITS(Issue Tracking System)/BTS(Bug Tracking System))としてRetrospectivaがあります。

Retrospectivaにはコミットログを解析してより便利にRetrospectivaを使うための機能がいくつかあります。Retrospectivaを採用しているプロジェクトであれば、コミットログの書き方をRetrospectivaの解釈できる書き方にすることにより、さらにRetrospectivaを便利に使うことができます。

チケットへのリンク

RetrospectivaはチケットベースのITS機能を備えています。コミットログには「XXX番のチケットの問題を修正した」というようなログを書くことも多いでしょう。その時、以下のフォーマットでチケットの番号を書くことにより、そのコミットログをRetrospectiva上で見ると該当するチケットにリンクが張られます。(

[#XXX]

この機能はブラウザ上から変更履歴を見ているときにとても便利です。

チケットの更新

RetrospectivaにはExtensionという拡張機能の仕組みがあります。この仕組みを利用したSCM Ticket Updateという拡張機能を導入することにより、コミットログでチケットの状態を更新することができます。

SCM Ticket Updateの導入方法は以下の通りです。(現時点(2008-05-23)のtrunkを利用している場合)

% RAILS_ENV=production ruby script/rxm checkout http://retrospectiva.googlecode.com/svn/extensions/1-1/scm_ticket_update
% RAILS_ENV=production ruby script/rxm install scm_ticket_update
% # Retrospectivaを再起動

拡張機能をインストールした場合はRetrospectivaを再起動することを忘れないでください。

この拡張機能を入れた後は以下のような書式でコミットログを書くことにより、コミットと一緒にチケットも更新することができます。

チケット#123の状態を修正済みに変更:

クラッシュバグを修正 [#123] (status:fixed)

チケット#29の割り当てユーザをaliceに変更:

[#29] テストを追加 (assigned:alice)

他にも以下のような書式が使えます。

[#N] (NAME1:VALUE1 NAME:VALUE2 ...) ログ

また、もし変更後の値に空白が入っている場合は「"..."」とダブルクォートで囲みます。

[#2929] (status:fixed milestone:"2.9 (バラ)") fix a trivial bug.

この機能を使うと、コミットした後にブラウザからチケットを変更する作業がなくなるのでチケットのクローズし忘れも減るかもしれません。また、コミットメールで(コミットログから)チケットがクローズされたことが分かるのも便利な点です。

まとめ

このようにコミットログの書き方を少しRetrospectivaよりにするだけでもっと便利にRetrospectivaが使えるようになります。

Retrospectivaが要求している書き方を使ってもコミットログが見づらくなるわけではないので、少し意識して使ってみてはいかがでしょうか。

*1  Gitにも微妙に対応している

つづき: 2009-08-07
Tags: Ruby | このエントリの del.icio.us history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | Permalink
2008-05-23

最新
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|