2007/12/31

JRubyと日本語 解析(2)

とりあえず、JRubyではUTF8でソース書いて使うのが良いみたい。正規表現は/ほげ/uのようにuオプションを使えばヒットする。ここ参照

JRubyの内部はRubyStringというクラスで文字列を持っているっぽい。これはByte列で文字列をもっている。Kernel#openとかでファイルを開いたときは、Shift_JIS(Windows-31J)のファイルならばShift_JIS(Windows-31J)のバイト列で保持するし、InputStreamReaderでエンコードを指定して読み込んだとかJavaのAPI経由ならUTF-8で持っているみたい?

SJISを読んでEUCを出力するときは、Openの場合はSJISから

open(ARGV[0]).each do |line| puts NKF.nkf('-eS', line) end

InputStreamReaderの場合はUTF-8から

def open_with_encoding(path, encode, &block) reader = nil begin reader = java.io.BufferedReader.new( java.io.InputStreamReader.new( java.io.FileInputStream.new(ARGV[0]), encode)) line = nil while (line = reader.readLine()) != nil block.call(line) end ensure reader.close unless reader == nil end end open_with_encoding(ARGV[0], 'Windows-31J') do |line| puts NKF.nkf('-eW', line) end

JRubyのNKFはEUCを指定したときマルイチとか丸付き文字が入っていると、例外を吐いて落ちるみたい。これは内部で使用しているCharsetEncoderでWindows-31Jの丸付き数字をEUCにマッピングできないため、CharacterCodingExceptionをスローしてしまい、invalid encodingと怒られる。丸付き数字をEUCに変換は今のところあきらめるしかないようだ。

そもそもEUC-JPには丸付き文字(NEC拡張)が存在せず、Windowsで扱えるのは(Shift_JISに対するWindows-31Jみたいな)NEC拡張を含むEUC-JPを使っているからだそうで、Javaでは1.5からEUC_JP_Solarisというのを指定すれば使えるようだ。(JRubyのNKFは普通のEUC-JPを指定しているのでこれを試しに書き換えてみた。)ただしこれを使うと「~」がうまく扱えなくなった。参考

JRubyと日本語 解析(1)

JRubyの日本語処理についてソースを見てみる。対象はJRuby1.0.3

org.jruby.Mainから始まり、コマンドライン解析を行うorg.jruby.util.CommandLineParserでは-Kオプションが存在する。(Usageには存在しないオプションもいくつか存在するようだ。)

次にorg.jruby.Mainのインタープリター実行処理であるrunInterpreterメソッドでCommandlineParser#getScriptSourceでソースファイル(.rb)を読み出すReaderを設定する。このメソッドで、コマンドラインオプションで設定したorg.jruby.util.KCodeを使わずに、KCode.NONEというKCodeオブジェクト強制的に使用している。

そこに以下のようなコメントがある。

// KCode.NONE is used because KCODE does not affect parse in Ruby 1.8

Ruby1.8ではStringの内部として文字列をバイト列で保持するため、問題ないように思われるが、このKCode.NONEを使用した場合、ソースをiso-8859-1で解析するため、ShiftJISのソースを読ませたときに「十」のような2バイト目が\になるような文字が含まれていると正しくソースを解析できずにエラーになる。

ということで、CommandlineParser#getScriptSource()のハードコード部分を、設定されているKCodeオブジェクトを使用するように変更。また、java.util.KCodeのdecoderとencoderメソッドでUTF-8とそれ以外しか判定がない部分にSJISとEUCの処理を追加する。

とりあえずこれで、JRubyをビルドして以下のスクリプトを実行してみた。

puts "あ" puts "十"

jruby -Ks sjis.rb B ^

エラーにはならなくなったが文字が壊れた。RubyStringにするときにおかしいのか、Yaccの処理が2バイト文字を扱えないのか。とりあえずだめだということがわかった。

2007/12/21

Rubyと日本語1.9

Ruby1.9での変更点にあった。
Ruby1.9を語る

  • 文字列をバイトベースではなく、文字ベースで扱うようにした
    それぞれの文字はエンコーディングに対応
  • エンコーディングの指定
    openもエンコーディングに対応
    open(path, "r:EUC-JP"){|f| f.read}
    #デフォルトは、Rubyスクリプトのソースコードと同じ
    こうするとEUC-JPの文字列を得る (RubyスクリプトがUTF-8だからといって、それに自動的に変換されたりはしない)

すばらしい!Rubyっていまいち日本語が使いにくいなと思っていたところだったので非常にうれしい。JRubyへの実装はどういう状況なんだろう。楽しみ。

2007/12/19

JRubyと日本語

JRubyでどうも日本語の処理がうまくないなぁということでちょっと調べてみた。

環境はXP Proに以下

C:\test>ruby -v ruby 1.8.6 (2007-03-13 patchlevel 0) [i386-mswin32] C:\test>jruby -v ruby 1.8.5 (2007-12-15 rev 5200) [x86-jruby1.0.3]

(1)スクリプトに日本語を直接書いたとき(Shift_JISで保存)。

puts "こんにちは、JRuby!" puts "①" puts '十' puts "~"

Rubyは-Ksオプションをつければ動くけど、JRubyはエラーになる。

:1: sjis.rb:3: Invalid char `\201' in expression (SyntaxError)

「十」の2バイト目が\と同じになるというむかーし昔のCGIでよくでた文字化けと同じ。 puts '十\'で回避可能。
ちなみにJRubyには-Kオプションがない。

(2)ファイル入出力

こんにちは、JRuby! ① 表 ~

っていう内容のファイルをSJIS、EUC、UTF8で保存して

require 'kconv' require 'nkf' open(ARGV[0]).each do |line| puts line.tosjis puts NKF.nkf('-s', line) end

で読み込んで吐かせるだけ。

rubyはちゃんと動くんだけどJRubyだと。

C:\test>jruby output.rb euc.txt Kconv :こんにちは、JRuby! nkf :こんにちは、JRuby! Kconv :ュ。 nkf :ュ。 Kconv :ノス nkf :ノス Kconv :。チ nkf :。チ

C:\test>jruby output.rb utf8.rb Kconv :puts "縺薙s縺ォ縺。縺ッ縲゛Ruby!" nkf :puts "縺薙s縺ォ縺。縺ッ縲゛Ruby!" C:/tools/dev/jruby-1.0.3/lib/ruby/1.8/kconv.rb:207:in `tosjis': invalid encoding (ArgumentError) from :1 from :1:in `each' from :1

ばけらった。
どうもopen(file)のところでプラットフォームのデフォルトエンコードで読んだ文字列がByte列でもっていないようでNKFとかKConvできてない気がする。

(3)正規表現 Rubyだと日本語一文字ずつ扱うのに$KCODE='sjis'とかして "日本語".split(//)みたいなイディオムがあるらしいんだけど JRubyだとうまくいかなかったり、日本語を使った正規表現が マッチしなかったり。

$KCODE='SJIS' require 'nkf' open(ARGV[0]).each do |line| i=0 line.split(//).each do |c| puts "#{i}:#{c}" i += 1 end end

rubyは動くけどJRubyはエラー。これもlineに一行取った時点で文字列が壊れる。

ついでに1行野郎のテスト。

C:\test\kcode>ruby -e "open(ARGV[1]).grep(/#{ARGV[0]}/) {|s| puts s }" あ sjis.txt あいう①~十abc123

C:\test\kcode>jruby -e "open(ARGV[1]).grep(/#{ARGV[0]}/) {|s| puts s }" あ sjis.txt 's' は、内部コマンドまたは外部コマンド、 操作可能なプログラムまたはバッチ ファイルとして認識されていません。

おや?JRubyの起動バッチの仕組みがいけてないっぽいなぁ。

教えて、Rubyの日本語の扱いに詳しい人。

2007/08/31

Firefoxを別プロファイルで起動する。

Firefoxのエクステンションを開発するときにいつも使っている設定ではなく、必要なエクステンションだけの設定で作業したい。 Firefoxではプロファイルという単位で設定を記憶しているので、開発用のプロファイルを作成し開発時はそのプロファイルで作業する。

要はエクステンション開発環境の設定を行う。

開発プロファイルを作成する

OSXの場合、ターミナルで以下のコマンドでFirefoxを起動すると、プロファイルマネージャが起動する。

$HOME/Applications/Firefox.app/Contents/MacOS/firefox-bin -ProfireManager

ここで、新しいプロファイルを作成する。

作成したプロファイルで起動するには「-p プロファイル名」というオプションで起動する。 develという名前で作成したとすると次のようになる。

$HOME/Applications/Firefox.app/Contents/MacOS/firefox-bin -p devel

いちいちコマンドを叩くのが面倒なので.bashrcにエイリアスを作っておくとかシェルを書いておくと楽になる。

設定をする。

さっきのプロファイルで起動したら、開発に便利な設定を行う。アドレスバーにabout:configと入力し、設定画面を開く。で、次の3つの設定変更を行う。

  • javascript.options.showInConsole = true :chromeにエラーがあった場合コンソールにログを出力する
  • nglayout.debug.disable_xul_cache = true:XULのキャッシュを行わないようにする。これによって再起動しないでもWindowとかDialogへの変更が反映されるようになる。ただし、JARではなく、ディレクトリで開発している前提で、オーバーレイを使用する場合はやはり再起動が必要。
  • browser.dom.window.dump.enabled = true:dump()メソッドで、標準コンソールに出力でできるようにする。詳しくはwindow.dumpを参照。また、nsIConsoleServiceも使える。
  • javascript.options.strict = true:Errorコンソールに厳密なJavaScriptの警告を出力する。

あと、開発時はExtension Wizardを使うと、スケルトンコードが簡単に作れる。

2007/07/19

SVK

SVKをいれてみた。

Windows版バイナリをダウンロードしてインストール。2.0.0をいれた。

ここを参考にミラーして、ブランチ作って、コミット、さてPushというところでうんともすんとも言わずにハングアップ。

コミットログに日本語が入っているとだめっぽい。いろいろ調べたら昔のバージョンはいけたらしいが、2.0.0だとだめっぽい。

公式をみたら2.0.1がでていてチェンジログにi18nを直したと書いてあったので、ソースをダウンロード、展開、Perlだから問題ないだろ~と勝手におもいつつWindows版のインストール先に上書きコピー

そしてsvk push //testとすると日本語ログでもすんなり通った。

こんな無理やりな対応でいいんだろうか。少なくとも仕事じゃ使えんなぁ。

早く正式な2.0.1Windowsバイナリが欲しい

Capistrano

Ruby on RailsのデプロイツールであるCapistranoを試すときにはまった。

rake deployを実行すると
“/subversion.rb:87:in “’: No such file or directory - svn log –no-auth-cache ...” というエラーがでてうまくいかない。

どうやらCapistranoがsvnコマンドを使ってデプロイするバージョンを調べるらしいのだが、ここでsvnコマンドを実行するのはリモートじゃなくローカルだった。

だから一生懸命、set :svnでリモートのsvnコマンドのパスを指定しても無駄だったのだorz

クライアント(capコマンドをたたく開発マシン)のWindowsにSubversionをいれて、環境変数PATHにパスが通っていることを確認していったん再起動。その後実行したらめでたく通りました。

Railsの処理フロー


Railsのソースをざっくり読んでみた。やっと仕組みの概要がわかった(気がする)のでまとめてみる。

違ってたら教えてくださいまし。

  1. ブラウザからリクエストがくる。
    例:http://www.example.com/recipe/list
  2. Apacheなどのドキュメントルートが/var/www/rails_app/publicであるとする。
    rails_appはrailsコマンドで生成したアプリケーションディレクトリ。
  3. publicディレクトリに生成される.htaccessでModRewriteによるリクエストURLの書き換えを行う。
    1. DocumentRoot(http://www.example.com)へのアクセスの場合、index.htmlへのアクセスに書き換える。
    2. ファイル部分に「.」が含まれないURL(たとえばhttp://www.example.com/recipe/list)場合、.htmlを付与(/recipe/list.html)する。
    3. 書き換えた結果のURLにマッチするファイルがある場合はそれを返す。(Railsを起動しない)
      これによりページキャッシュとして静的ファイルを出力しておいて高速化とかできる。
    4. 該当ファイルがない場合は、dispatch.cgiを起動する。
  4. dispatch.cgiではconfig/environment.rbやらboot.rbなどを読み込んでRailsに必要なライブラリのロードや、初期化を行う。
    その後railtie/lib/dispatch.rbのDispatcher#dispatchを呼び出す。
  5. リクエストからActiveController::CGIRequestとCGIResponseオブジェクトを生成する。
  6. Routesを呼び出してリクエストからどのコントローラを使用するか決定し、コントローラを生成して返す。
    http://www.example.com/recipe/listならばrecipeコントローラ。
    config/routes.rbでカスタマイズ可能。
  7. ActionController#processを呼び出す。
    processではsessionやparamsなどの初期化を行い、リクエストから該当するアクションを呼び出す。
    recipeコントローラのlistアクション(メソッド)
  8. アクションではモデル(ActiveRecord)の操作をしたり業務ロジックを実行する。
  9. 処理後、ActionViewのrenderを実行する。
    テンプレートを指定しない場合app/view/コントローラ名/アクション名.rhtmlのテンプレートをベースにレンダリングする。(app/view/recipe/list.rhtml)
    ActionControllerに設定したインスタンス変数はテンプレートでも使用できる。
  10. Dispatcherでresponse#out($stdout)でレスポンスを返す。

こんなところですかね?

2007/06/24

Rails Migration

Migrationは物理的なデータベースによって使われるスキーマの変更を 管理するためのものです。新しい機能のために、 データベースにフィールドを追加する必要がある時 他の開発者にどのように変更を伝えるか、 商用サーバにそれをどう適用すればよいかを 伝えるための共通の問題に対するソリューションです。 Migrationを使う事で、変更をクラスとして記述する事ができ、 バージョン管理システムで管理し、他のデータベースに対して 1、2または5つ以上のバージョンを適用することができます。

Migrationの簡単な例は次のようにものです:

class AddSsl < ActiveRecord::Migration
    def self.up
      add_column :accounts, :ssl_enabled, :boolean, :default => 1
    end

    def self.down
      remove_column :accounts, :ssl_enabled
    end
  end

このMigrationはaccountテーブルにbooleanのフラグを追加し、 Migrationを戻すとそのフラグを削除するものです。 これは全てのMigrationが、移行に必要な実装/削除に必要な変更を記述するため、 2つのクラスメソッドupとdownを持つことを示しています。 これらのメソッドは、migrationのメソッドであるadd_columnやremove_columnだけでなく、 変更に必要なデータを生成するための通常のRubyコードを含む事ができます。

データの初期化が必要な場合のもう少し複雑なmigrationの例は次のようになります:

  class AddSystemSettings < ActiveRecord::Migration
    def self.up
      create_table :system_settings do |t|
        t.column :name,     :string
        t.column :label,    :string
        t.column :value,    :text
        t.column :type,     :string
        t.column :position, :integer
      end

      SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
    end

    def self.down
      drop_table :system_settings
    end
  end

このMigrationは最初にsystem_settingテーブルを追加し、 ActiveRecordモデルを利用して最初の行を作成しています。 また、これは完全なテーブルスキーマをブロックコールで作成する、 create_tableのより高度な文法を使用しています。

使用可能な変換

  • create_table(name, options):nameというテーブルを作成し、 ブロックでadd_columnと同じフォーマットで列を作成できるようにします。 上記の例を参照してください。このoptionsハッシュはDEFAULT CHARSET="UTF-8"のような フラグメントをcreate tableの定義に追加するためのものです。
  • drop_table(name):nameテーブルをdropします。
  • rename_table(old_name, new_name):old_nameというテーブルを new_nameにリネームします。
  • add_column(table_name, column_name, type, options): table_nameというテーブルにcolumn_nameという名前の新しい列を追加します。 typeは以下のタイプです:
    :string、:text、:integer、:float、:decimal、:datetime、:timestamp、:time、:date、:binary、 :boolean。
    デフォルト地はoptionsハッシュに{:default => 11}のようにして追加できます。 ほかにも:limitや:null(例えば{ :limit => 50, :null => false})を追加できます。 詳しくはActiveRecord::ConnectionAdapters::TableDefinition#columnを参照してください。
  • rename_column(table_name, column_name, new_column_name): 列をリネームします。タイプと内容は保持します。
  • change_column(table_name, column_name, type, options): add_columnと同じパラメータで、列を別のタイプに変更します。
  • remove_column(table_name, column_name):table_nameという名前の テーブルからcolumn_nameという列を削除します。
  • add_index(table_name, column_names, index_type, index_name): 新しいインデックスを列名または(もし指定されていれば)index_nameをつかって作成する。 index_type(例えばUNIQUE)も指定できる。
  • remove_index(table_name, index_name): index_nameで指定したindexを削除する。

復元不可能な変更

いくつかの変更は戻す事のできない破壊的なものがあります。 そのようなのMigrationはdownメソッドで IrreversibleMigration例外をraiseするべきです。

RailsからMigrationを実行する

RailsはMigrationを作ったり適用するのを助けるいくつかのツールを持っています。

新しいMigrationを作るには、script/generate migration MyNewMigration を使います。ここでMyNewMigrationはこのMigrationの名前です。 このジェネレータはnnn_my_new_migration.rbというファイルをdb/migrate/ディレクトリに 作成します。nnnはmigration番号です。n個のMyNewMigrationのself.upとself.downメソッドを 書き換える事になるでしょう。

現在設定されているデータベースに対してMigrationを実行するには rake migrateを実行します。このコマンドは全ての未実行の migrationを実行してデータベースを更新し、もし存在しなければschema_infoテーブルを 作成します。

データベースを前のmigrationバージョンにロールバックするには rake migrate VERSION=Xを実行します。 Xはダウングレードしたいバージョンの番号です。 もしmigrationがIrreversibleMigration例外をスローした場合 そのステップは手作業で行う必要があります。

データベースサポート

Migrationは現在MySQL, PostgreSQL, SQLite、SQL Server、 Sybase、そしてOracleを サポートしています。

サンプル

全てのmigrationがスキーマを変更するとは限りません。 データを修正するものもあります:

  class RemoveEmptyTags < ActiveRecord::Migration
    def self.up
      Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
    end

    def self.down
      # not much we can do to restore deleted data
      raise IrreversibleMigration
    end
  end

他にはdownではなくupの時に列を削除することもあります:

class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
    def self.up
      remove_column :items, :incomplete_items_count
      remove_column :items, :completed_items_count
    end

    def self.down
      add_column :items, :incomplete_items_count
      add_column :items, :completed_items_count
    end
  end

そして、直接SQLを実行したい場合は

  class MakeJoinUnique < ActiveRecord::Migration
    def self.up
      execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
    end

    def self.down
      execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
    end
  end

テーブルを変えた後にそのモデルを使う場合

しばしばMigrationで列を追加して、その直後にデータを設定したい場合があるでしょう。 その場合、新しい列を追加した後、モデルにその列がある事を保証するために Base#reset_column_informationを呼ぶ必要があります。例えば:

 class AddPeopleSalary < ActiveRecord::Migration
    def self.up
      add_column :people, :salary, :integer
      Person.reset_column_information
      Person.find(:all).each do |p|
        p.update_attribute :salary, SalaryCalculator.compute(p)
      end
    end
  end

詳細の制御

デフォルトでは、Migrationは、なにを行ったかを各ステップにどのくらいかかったかの ベンチマークとともにコンソールに出力することで表します。

ActiveRecord::Migration.verbose = falseとすることでそれらを抑制する事ができます。

また、say_with_timeメソッドを使ってベンチマークに独自のメッセージを 挿入することができます:

  def self.up
    ...
    say_with_time "Updating salaries..." do
      Person.find(:all).each do |p|
        p.update_attribute :salary, SalaryCalculator.compute(p)
      end
    end
    ...
  end

"Updating salaries..."がブロックが終わったときのベンチマークとともに出力されます。

2007/06/19

JRuby On Rails

いよいよJRuby on Rails。 (うちのMacではNetBeansが遅すぎるのでWindowsに乗り換えたorz)

JRubyのインストール

NetBeansにバンドルされているが、最新版を使うためにインストールする。

  1. ここから最新版をダウンロードする。
    2007/6/19時点で1.0が最新
  2. 好きなディレクトリ(インストール先)に展開する
  3. 2のディレクトリを環境変数JRUBY_HOMEを設定する
  4. 環境変数PATHに%JRUBY_HOME%\binを追加する

Railsのインストール

コマンドプロンプトを開いて以下のコマンドを実行する。

jruby -S gem install rails -y

jruby -SでJRubyのbinにあるgemコマンドを実行してくれる。
-yは依存関係もダウンロード
次にJDBCでActibeRecordを使うためのライブラリのインストール

jruby -S gem install ActiveRecord-JDBC -y

NetBeansの設定

NetBeans 6.0 M9ではデフォルトで使用するJRubyがバンドルされている0.9.8なので最新版を使うようにする。
  1. NetBeansを起動する
  2. メニューからTools→Options
  3. Miscellaneous→Ruby Installationを選択
  4. インストールした1.0のパスに変更してOKを押す

Railsプロジェクトの作成

やっとRailsプロジェクトの作成。

  1. ProjectPaneで右クリックしてメニューからNew Projectを選択
  2. CategoliesからRubyを選択
  3. ProjectからRuby on Rails Applicationを選択してNext
  4. ProjectNameは任意でFinish

できたプロジェクトを右クリック→Run ProjectでWEBrickが起動しRailsアプリケーションが動く。

Scaffoldとかやりたいけど、とりあえず今日はここまで。

NetBeans 6.0 M9インストール

どうもNetBeansが最近なかなかイイということで、試しに使ってみようと思う。 なにより6.0からRubyとRuby on Railsサポートされているからだ。

インストールはダウンロードして展開して
netbeans-6.0m9-full-macosx-ppc.commandを実行。
ひたすらデフォルトで進める。ただそれだけ。

Java6が入っていればそちらを認識してくれる。

それにしても重い。Java6が悪さをしているのかと思ったがそうでもないようだ。 むぅ。

OSXでJava6

どうやらまだDeveloper Preview版しかないようだ。 (2006/09/12から更新が止まっている模様) とりあえずインストール方法は以下。 Tiger 10.4.5以降が必要(Intel / PPCどちらでも可)

  1. ADCに入会する。
  2. ADC Member Site→Download→Java
  3. Java SE 6.0 Release 1 DP6(Disk Image)をダウンロード ReleaseNoteは目を通すこと。
  4. ダウンロードしたJavaSE6Release1.pkgを実行する
  5. インストーラに従ってインストールする
Java6をいれてもJava5は残るらしい。 JVMの切り替えかたは以下の通り。
  1. /Application/Utilities/Java/Java SE 6にあるJava Preferences.appを実行する
  2. 基本タブを開く
  3. Use VersionをJava SE 6にする
  4. Java Application Runtime SettingsでJava SE6をドラッグして一番上にする
  5. Saveボタンをおして保存する
これでターミナルから実行するときもダブルクリックの時もJava6が使われる。 PPC版ではJITコンパイラが実装されてないらしい。 微妙だが、まぁ何とかなるだろう。