daily gimite RSSフィード

2009/09/26

[] HpricotからNokogiriに移行するときの罠(特にXML名前空間20:30 はてなブックマーク -  HpricotからNokogiriに移行するときの罠(特にXML名前空間) - daily gimite

HpricotからNokogiriに移行しようとしていくつか罠にはまったのでメモしておきます。

基本的には

  • require "hpricot" → require "nokogiri"
  • Hpricot(html) → Nokogiri::HTML(html)
  • Hpricot::XML(xml) → Nokogiri::XML(xml)

と書き換えるだけで、運が良ければそのまま動くと思います。

Nokogiri(text)というのもあるのですが、これはXMLHTMLかを自動判定するらしく、失敗することもあるのでお勧めしません。

NokogiriはHpricotと違ってXML名前空間をきちんと解釈するので、XML名前空間を使ったXMLを解析する場合には注意が必要です。XML名前空間を使ったXMLというのは、以下のようにxmlnsなんとかというのが入っているやつです。

<feed xmlns="http://www.w3.org/2005/Atom"
    xmlns:gs="http://schemas.google.com/spreadsheets/2006">
  <entry>
    <gs:cell>hoge</gs:cell>
  </entry>
</feed>

この例だとdoc.search("entry")とかdoc.search("gs:cell")では何も引っかかりません。それぞれ以下のように書く必要があります。

namespaces = {
  "atom" => "http://www.w3.org/2005/Atom",
  "gs" => "http://schemas.google.com/spreadsheets/2006",
}
doc.xpath(".//atom:entry", namespaces)
doc.xpath(".//gs:cell", namespaces)

いくつか注意点。

要するに面倒くさいです。こっちの方がXML的に正しい実装ではあるんでしょうけど…。XML名前空間って、やりたい事はわかるし、便利なときは便利なんでしょうけど(XHTMLの中に直接SVGを書くとか)、大抵のときは話を面倒くさくしているだけのような…。

という点が主な原因で、実はgoogle-spreadsheet-rubyはとりあえずNokogiriへの移行を見送りました。

  • NokogiriはRuby 1.9String#encodingをちゃんと設定してくれる(といっても入力文字列のencodingを使ってくれるわけではなくて、Nokogiri::XML(xml, nil, "UTF-8")とか書かないと駄目ですが)
  • Nokogiriの方が速いらしい
  • MechanizeもNokogiriに移行したらしい
  • Hpricotは作者(_why氏)が行方不明?

というあたりから、Nokogiriに移行した方がいいような気はするのですが。

2008/12/24

[][] Google SpreadsheetのRubyライブラリ 16:55 はてなブックマーク -  Google SpreadsheetのRubyライブラリ - daily gimite

を作ったので公開しました。

Google Spreadsheet Ruby

インストール方法:

  $ gem sources -a http://gems.github.com
  $ sudo gem install gimite-google-spreadsheet-ruby

使用例:

  require "rubygems"
  require "google_spreadsheet"

  # ログイン。
  session = GoogleSpreadsheet.login("username@gmail.com", "mypassword")

  # http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=ja
  # の最初のワークシート。
  ws = session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg").worksheets[0]

  # A2セルの内容を取得。
  p ws[2, 1] #==> "hoge"

  # セルの内容を変更。ws.save()を呼ぶまで変更はサーバに反映されないので注意。
  ws[2, 1] = "foo"
  ws[2, 2] = "bar"
  ws.save()

Google SpreadsheetのRubyライブラリはすでにいくつかあったのですが(gdata-rubyroo)、

  • インタフェースがいまいち
  • 複数のセルへの変更をいっぺんにサーバに送る機能がない
  • 以前、ちょっと必要になっていい加減に作ったやつがあった

などの理由で、前に作ったやつを整理・拡張して公開しました。

今回初めてgithubgemパッケージ化機能を試してみました。svn レポジトリを github に移行させてみるが参考になりました。

2008/08/02

[][][] HotRubyがC Rubyより速い本当の理由は? 12:26 はてなブックマーク -  HotRubyがC Rubyより速い本当の理由は? - daily gimite

JavaScriptが速くて、Rubyが遅い理由というエントリがあったのですが、コメントやトラックバック、追記などを読むと

  • 実用上HotRubyがC Rubyより速いというわけではまったくない
  • プリミティブ型の有無が原因という話はどうやら間違い

のようです。

とはいえ「↓のベンチマークでHotRubyがC Rubyより速い」というのは事実です。これがなぜなのかちょっと気になったので考えてみました。

startTime = Time.new.to_f
  
sum = ""
50000.times{|e| sum += e.to_s}
  
endTime = Time.new.to_f
puts (endTime - startTime).to_s + ' sec'

これについてはRubyが遅いのは本当にBoxingのせいか?が参考になります。まず、C Rubyではベンチマークの += を << に書き換えるとめっちゃ(手元の1.9では約84倍)速くなります。これはなぜかというと、

  • += では毎回新しいStringオブジェクトを作って文字列全体をコピーする。ので、文字列長が長くなるほど時間がかかり、全体の計算量はO(N^2)になる。
  • << では既存のStringオブジェクトの後ろに文字列をくっつけるだけなので、全体のコピーは起こらない。たまにバッファが足りなくなったときは全体のコピーが必要だが、バッファを倍々で伸ばせば全体ではO(N)になる。

インスタンスの生成も減りますが、それよりO(N^2)がO(N)になったのが大きいです。上の記事の+=版でだんだん時間がかかってるのはこのせいですね。

そうなると上の記事の最後の記述、「HotRuby版では+=でも時間が一定になった」というのが気になります。JavaScriptでは+=を使っても上のコードはO(N)になるということです。これが上のベンチマークでHotRubyがC Rubyより速くなった理由のようです。しかし、どうやってるのでしょう…?

まず「JavaScriptの+=はRubyの<<相当の実装になってるのではないか」と思ったのですが、HotRuby上で a += b を実行すると、JavaScript上では単なる+=ではなく、以下のようなコードが実行されます*1

function stringPlus(a, b) {
  var c = a.__native + b.__native;
  return {__native: c};
}

a = stringPlus(a, b);

これを破壊的な連結(Rubyでいう<<)で実装するのはかなり難しそうです。 d = stringPlus(a, b); だったら破壊的に連結してはまずいですからね。

次に考えたのが、JavaScriptの文字列の連結はこんな感じになってるのではないか、というものです。

つまり、a.__nativeのバッファに余裕があったら、そこに詰めてしまう。ただし、個々の変数は文字列の始点と終点を保持してるので、これによってa.__nativeの内容が変わることはない、というもの。この実装ならRubyの<<版と同じで、上のベンチマークはO(N)になります。

これは単なる想像で、実際こうなってるのかは調べていません(知ってる人かFirefoxなどのソースを見た人がいたら教えてください…)が、JavaScriptではRubyString#<<相当がなく、+=を多用しがちなことを考えると、この手の工夫にはそれなりの効果があるのかもしれません。

2008/8/2追記: shinhさんがFirefoxの(だと思う)ソースから該当箇所を見つけてくれました。どうも上の想像は当たっていたみたいです。

2008/8/4追記: id:bosconoさんによるとWebKitも似たようなことをやってるようです。

*1:これでもかなり端折ってますが。

shiroshiro 2008/08/02 17:30 「JavaScriptの」というのはちょっと変な言い方で、言語仕様が文字列のアクセスや変更に対して定量的な指定をしていなければ(例えば「文字列のランダムアクセスはO(1)でなければならない」とか) 処理系は好きなように実装できるわけで、例えばropeを使えば文字列連結はO(1)で済みます。JavaScriptの用途を考えれば十分にあり得る戦略でしょうね。

GimiteGimite 2008/08/02 19:17 それは確かにそうですね。ここで「JavaScriptの」と書いたのは「FirefoxやOperaのJavaScriptの実装」という意図でした…。

ゴンスケゴンスケ 2008/08/05 20:35 全然関係ないんですが,会社の共有パソコンにGLAUNCHがはいってました (笑

2008/07/21

[][] HotRuby (Ruby VM on JavaScript) をいろいろいじってRPCとか実装してみた 19:51 はてなブックマーク -  HotRuby (Ruby VM on JavaScript) をいろいろいじってRPCとか実装してみた - daily gimite

HotRubyというJavaScript上で実装されたRuby VMを発見して、

  • これを使えばWebアプリのサーバ側もクライアント側もRubyで書く、とかできるのか
  • つまりGoogle Web ToolkitのRuby
  • そのためにはまずRPCかな

ということでDRb風のRPCを実装してみました。その過程でHotRubyに足りない機能を追加したりバグを直したり、かなりごちゃごちゃいじりました(後述)。

ソースはgithubからどうぞ。

注: 実験レベルのものです。現状実用にはなりません。

デモとしてシンプルなチャットを作りました: HotRuby+RPC Chat (Firefox 3以外では動かないかも)

クライアント側のソースはこんな感じです。

@log_div = $native.document.getElementById("log")
@author_field = $native.document.getElementById("author")
@message_field = $native.document.getElementById("message")
@form = $native.document.getElementById("form")
@server = RPCClient.new("rpc/chat_server").root

def update_log()
  # RPC呼び出し
  messages = @server.recent_messages(10)
  @log_div.innerHTML = messages.
    map(){ |m| CGI.escapeHTML(m["author"] + ": " + m["body"]) }.
    join("<br>")
end

on_submit = proc() do |e|
  $native.Event.stop(e)
  # RPC呼び出し
  @server.post(@author_field.value, @message_field.value)
  update_log()
  @message_field.value = ""
  @message_field.focus()
end
$native.Event.observe(@form, "submit", on_submit, true)

# 10秒ごとにログを更新
while true
  update_log()
  sleep(10)
end

メッセージの取得(recent_messages)と投稿(post)をRPCでやっています。内部ではXMLHTTPRequestが呼ばれています。RPC以外はかなりJavaScriptくさいRubyコードになっています。Ruby friendlyなUIライブラリがほしいところですね。

対応するサーバ側はこんな感じです。

class ChatServer < RPCServer
  
  extend(Publishable)
    
    def initialize(*args)
      super
      @messages = []
      post("System", "Chat server has started.")
    end
    
  published
    
    def recent_messages(n)
      n = [n, @messages.size].min
      return @messages[-n..-1]
    end
    
    def post(author, body)
      @messages.push({"author" => author, "body" => body})
    end
    
end

独自拡張のpublishedというアクセス権限を持ったメソッドだけが、クライアントから呼び出せるようにしました。Web上に公開されるので、DRbみたいになんでも呼べてしまうのは危ないですからね。

これを作る過程でHotRubyに加えた変更は、こんな感じです(抜けがあるかも)。

  • (JS側の)非同期関数を(Ruby側では)同期的に呼び出せるように。
    • RPCとかsleepとかを同期関数のように呼べているのはこのおかげ。
    • これのせいでJavaScript側のコードが全体的に複雑に…。
  • スレッドもどきの実装。
    • 上記非同期関数が呼ばれるところでしかコンテキストスイッチしません。
  • 例外の実装。(てきとー)
  • VMオブジェクトをシングルトンに。
    • Multi VMができなくなってしまいましたが…。この方が楽だったもんで。
  • 組み込みクラスの足りないメソッドを一部実装。
  • instructionのバグ修正。(topn, setn, expandarray)
  • 未実装だったinstructionを一部実装。(invokeblock)
  • method_missingを実装。
  • メソッド引数の*args, &blockに対応。
  • JavaScript上でRubyのクラス/メソッドを定義するときのインタフェースの変更。
  • 特異クラスの導入。
  • その他リファクタリング(のつもり)。

色々どうでもいいところまでいじりすぎたので、本家へのパッチという形にするのが難しくなってしまいました。とりあえず変更後のHotRuby.jsはこちら

変更後のHotRubyを試すためのコンソール(diy.htmlベース)を用意しました。

いまだに未実装なもの:

  • モジュール。→実装されました。
  • ブロック中のbreak/return。→実装されました。
  • 標準クラスとそのメソッドはまだまだ未実装。→主要クラスはそれなりに実装されました。
  • eval。無理。
  • require。
  • 他にもある気がする。

あとFlash対応のコードは放置しているので、たぶん動かなくなっていると思います。

2008/05/04

[][] Railsで undefined method `<=' for :db:Symbol (NoMethodError) 21:22 はてなブックマーク -  Railsで undefined method `<=' for :db:Symbol (NoMethodError) - daily gimite

手元のアプリのRailsのバージョンを1系から2.0.2にあげたら

undefined method `<=' for :db:Symbol (NoMethodError)
        from /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/connection_adapters/abstract/quoting.rb:61:in `quoted_date'
        from /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/connection_adapters/abstract/quoting.rb:29:in `quote'

という謎のエラーがおきたので調べていたんですが、

entry= MyEntry.new()
rss= RSS::Parser.parse("...", false)
entry.latest_fetched= rss.items[0].date
entry.save()

というようにRSSのアイテムのdateをActiveRecordに突っ込んでいたのが原因でした。どうもRSSのdateは勝手に特異メソッドでto_sを置き換えてるらしく、それがActiveSupport(こちらも勝手にTime#to_sを拡張している)と干渉しているようです。

entry.latest_fetched= rss.items[0].date.dup()

とすると回避できました*1

*1:dupでは特異メソッドは継承されないから。