エクセルファイルにアクセス

仕事でExcelファイルを使うのだが、いつも集計作業が手作業になりがち。
複雑な処理をしたいけど、マクロをいまから覚えたくもない。
Rubyでなら Enumerableのメソッドをチェーンして書きまくれるのに…といつも思って、
コピペしてテキストファイルにして split してから作業してました。

耐えられなくなって調べてみたら、Rubyにはwin32oleというライブラリがあり、
これを使うとExcelファイル等にRubyからアクセスできるようになる。
CygwinRubyを使えばそのまま使える。
http://jp.rubyist.net/magazine/?0004-Win32OLE

でもこれを使うとなんだかメソッドが一般的すぎて使いにくい。
せっかく覚えても、そんなに必要になる機会が多いわけでもないので、すぐに忘れる。
使いたいときに忘れのでまた「るびま」を読みなおすということに。
3回くらい「るびま」を読んだのでこれはいかんと思って、
よく使う機能をまとめたモジュールを作った。

require 'win32ole'

require 'enumerator'
require 'time'
require 'forwardable'

module Excel
  def self.open(filename)
    excel = WIN32OLE.new('Excel.Application')
    books = excel.Workbooks.Open(filename)

    begin
      yield(Books.new(books))
    ensure
      books.Close
      excel.Quit
    end
  end

  class Books
    include Enumerable
  
    def initialize(books)
      @books = books
    end
  
    def [](sheet)
      Sheet.new(@books.Worksheets(sheet))
    end
  
    def each
      @books.Worksheets.each do |e|
        yield(Sheet.new(e))
      end
    end
  end

  class Sheet
    include Enumerable
    extend  Forwardable

    def initialize(sheet)
      @sheet = sheet
    end

    def [](x, y)
      Value.new(@sheet.Cells.Item(x, y))
    end

    def each
      @sheet.UsedRange.Rows.each do |row|
        yield(row.Columns.enum_for(:each).collect {|c| Value.new(c)})
      end
    end

    def_delegator :@sheet, :name
  end

  class Value
    def initialize(obj)
      @obj = obj
      @value = obj.Value
      if @value.is_a?(String) && @value =~ %r(\d{4}/\d\d/\d\d \d\d:\d\d:\d\d)
        @value = Time.parse(@value)
      end
    end

    attr_reader :value

    def comment
      comment = @obj.Comment
      comment && comment.Text
    end
  end
end

以下は添付画像のようなExcelシートを操作するを例。

Book操作

「シート1」にアクセス

シート番号とシート名でのアクセスが可能

 Excel.open("test.xls") do |books|
   books[1]
 end
 => #<Excel::Sheet:0x4986428 @sheet=#<WIN32OLE:0x4986440>>

 Excel.open("test.xls") do |books|
   books["シート1"]
 end
 => #<Excel::Sheet:0x497fa38 @sheet=#<WIN32OLE:0x497fa50>>
「シート」にマッチするシートの配列を取得

Enumerableをincludeしているので後述するSheetクラスを操作するコードを記述できる。

 Excel.open("test.xls") do |books|
   books.select {|e| e.name =~ /1/}
 end
 => [#<Excel::Sheet:0x489a220 @sheet=#<WIN32OLE:0x489a268>>] # シート1

シート操作

「シート1」のB3取得
  Excel.open("test.xls") do |books|
    books["シート1"][2, 3]
  end
  => #<Excel::Value:0x489a208 @obj=#<WIN32OLE:0x489a220>, @value="\212J\216n\216\236\215\217">
行ごとにイテレート
  Excel.open("test.xls") do |books|
    books["シート1"].each do |_, name, start, elapse|
      puts "#{name.value} #{start.value} #{elapse.value}"
    end
  end

テスト名 開始時刻 経過時間 (秒)
test_foo Wed Oct 17 00:11:00 GMT+9:00 2007 60.0
test_bar Wed Oct 17 00:13:10 GMT+9:00 2007 400.0
test_baz Wed Oct 17 00:21:20 GMT+9:00 2007 120.0

セルに付随するコメントを取得

  Excel.open("test.xls") do |books|
    puts books["シート1"][3, 4].comment
  end

  "再計測が必要かも"