pure ruby の各種フォーマット対応Spreadsheetパーサ(xlsx対応!)2009/08/12

roo - rubyforge プロジェクトページ

OOo, Google Document, Excel(2007の新フォーマット対応)と、幅広いフォーマットへの対応を謳う、スプレッドシートのパーサ

ドキュメントの更新が追い付いていないようですが、最新版は1.3.9

  • ファイルの新規作成には未対応
  • ファイルの更新は、Google Spreadsheetのみ対応

(セルに値を設定するメソッドまでは全てのフォーマットに対して実装されているようですが)

xlsxなファイルを扱う必要があって、自前でなんとでもなりそうだったけど、既存のライブラリを見付けたのでメモ。

ちなみにxlsも対応していることになっていますが、Spreadsheetに丸投げなので、 xlsだけの対応で良い場合は、Spreadsheetを直接利用するべきだと思います。

(Spreadsheet方が高機能で、実装の筋も良い)

また、rooではGoogle Spreadsheet以外ではファイルの更新に対応していませんが、Spreadsheet.open(filename) とか無造作にやっているので、 Spreadsheetのデフォルト値'rb+'が採用されてファイルにロックがかかります。

--- /lib/ruby/gems/1.8/gems/roo-1.3.9/lib/roo/excel.rb.old
+++ /lib/ruby/gems/1.8/gems/roo-1.3.9/lib/roo/excel.rb
@@ -126,7 +126,7 @@
       unless File.file?(@filename)
         raise IOError, "file #{@filename} does not exist"
       end
-      @workbook = Spreadsheet.open(filename)
+      @workbook = Spreadsheet.open(filename, 'rb')
       @default_sheet = self.sheets.first
     ensure
       #if ENV["roo_local"] != "thomas-p"

ぐらいしておくと良いかも。

インストール

rubyforgeにgemがあります。

$ sudo gem install roo

ファイルを開く

require 'rubygems'
require 'roo'

spreadsheet = Openoffice.new("myspreadsheet.ods")      # Openoffice Spreadsheet
spreadsheet = Excel.new("myspreadsheet.xls")           # Excel(xls)
spreadsheet = Google.new("myspreadsheetkey_at_google") # Google Spreadsheet
spreadsheet = Excelx.new("myspreadsheet.xlsx")         # Excel(xlsx)

の個別のクラスを直接利用する他

spreadsheet = Roo::Spreadsheet.open("myspreadsheet.xlsx")

でファイルの拡張子から前述のクラスを自動で判別してもらうことができます。

値の参照

Spreadsheetライブラリとは異なり、Worksheet(やrow, cell)を抽象化したクラスはありません。 GenericSpreadsheetクラスを継承した、Openoffice, Google, Excel, Excelxといったクラスに #cell(row, col, sheet=nil)のアクセサがあります。

spreadsheet.cell(2, 1)
spreadsheet.cell(2, 'A')
spreadsheet.cell(2, 'a')
spreadsheet.cell('A', 2)

これらは同じセルの値を参照します。 数値は1 orignです。(表計算ソフトの画面との整合性を優先したのでしょうが、ちょっと好きになれない)

ワークシートの選択は3つめの引数に、シートの名称を与えます。

spreadsheet.cell('A', 2, 'Sheet 1')

省略した場合は #default_sheet に設定されたシート名が使用されます。 シート名の一覧は #sheets で得られます。

spreadsheet.sheets #=> ["Sheet 1", "Sheet 2", "Sheet 3"]
spreadsheet.default_sheet = spreadsheet.sheets.first #=> "Sheet 1"

問題点

いくつか問題点が存在すると思っています。

名前空間の汚染

Openoffice, Google, Excel, Excelxといったクラス名が、グローバルに定義されています。 module Roo 配下になっているのは、前述のRoo::Spreadsheet.openメソッドと Roo::VERSION 定数だけです。

そもそも、OOoのSpreadsheetアプリのデータファイルのパーサ に Openofficeのクラス名を与えるセンスは許せない気がします。

アクセサがださい

ワークブック(ワークシート)に対するアクセサには、セル値を参照する #cell, セルの値の型を参照する #celltype, セルの内容が式である場合にその式を取り出す #formula の他、限定的なサポートのようですが、書式を参照する #font などもありますが、そのいずれも Enumerableではありません。

Enumerableなメソッドが一つもありません

lib/roo/roo_rails_helper.rb に、spreadsheet(これもグローバルに定義された関数形式のメソッドだな……)という、 ワークシートの内容をtableタグでhtml出力してくれるメソッドが定義されていますが、こいつは

@rspreadsheet.first_row.upto(@rspreadsheet.last_row) do |y|
  .. do something ..
  @rspreadsheet.first_column(sheet).upto(@rspreadsheet.last_column(sheet)) do |x|
    .. do something ..
  end
end

と、ワークシートの使用範囲の下限.upto(上限)でループを回しているので、超格好悪いです。

なにより面倒臭いので GenericSpreadsheet にメソッドを追加します。

class GenericSpreadsheet
  def to_a(sheet = nil)
    sheet ||= @default_sheet
    raise RangeError unless sheets.include?(sheet)
    
    [*header_line.succ..last_row(sheet)].map |row|
      [*1..last_column(sheet)].map do |col|
        @cell[sheet][ [row, col] ]
      end
    end
  end
end

本当は GenericSpreadsheet#each にしたかったのですが、@cellの構成が変態的で結局Arrayを生成するしかなさそうだったので諦めました。

HashのKeyがArrayって危険な気がするのですが……。

また、@cell[sheet][ [row, col] ] の内部表現は各パーサに任されているようなので、値の取得にはpublicメソッドの cell(row, col, sheet) を利用するべきかも知れません。 とりあえず、Excelx(xlsx)では、日付/日時のパース(@cellの中身はStringだけど、#cellの戻り値はDateないしDateTime)ぐらいしか 処理がされていない、にも関わらず判定コストが高そうなメソッドになっているので、上記のようにしています。

コメント

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※なお、送られたコメントはブログの管理者が確認するまで公開されません。

名前:
メールアドレス:
URL:
コメント:

トラックバック

このエントリのトラックバックURL: http://dragonstar.asablo.jp/blog/2009/08/12/4506735/tb

※なお、送られたトラックバックはブログの管理者が確認するまで公開されません。