excelmap.rb

Excel ファイルをテキストファイルの内容で上書きするスクリプトを作った。関係ないセルはもちろんクリアしない。きっとどこかに存在して被ってるシリーズ(きっとというか被ってる人を知ってる)。
ソースはこの記事の末尾にあります。TODO もあるんで、知ってれば教えてくださると助かります。るびまで添削してもらおうかなぁ。
optparse って便利なんだけどしっくりこないなぁ……。


オプションはこんな感じ:

$ ruby excelmap.rb --help
Usage: excelmap [options]
    -h, --help                       print this message
    -s SHEET_NO                      Sheet number
    -p Y,X                           starting Point
    -r ROW_SKIP_SIZE                 Row skip size
    -c COL_SKIP_SIZE                 Column skip size
    -t TERMINATOR                    split by Terminator
    -i, --ignore-empty-data          Ignore empty data

ターミネイタじゃなくてセパレータの方がいいな……でも s が被りまくりだからいっか。


使い方:


タブ区切りとかカンマ区切りとかのテキストファイルを用意する。

0_0	0_1	0_2	0_3	0_4
1_0		1_2		1_4
2_0	2_1	2_2	2_3	2_4
3_0		3_2		3_4
4_0	4_1	4_2	4_3	4_4


Excel ファイルを用意する(別に空じゃなくても良い)。


実行。

$ ruby excelmap.rb -p3,2 -s2 -r3 -c1 test.txt test.xls

意味は、開始セルは3行・2列目、シートは2番目、3行飛び、1列飛びで test.txt の内容を test.xls にマップする。だ。


結果:


テキストファイルの空文字列部分に当たるセルに入力しておく。


先ほどと同じオプションで実行。

$ ruby excelmap.rb -p3,2 -s2 -r3 -c1 test.txt test.xls


結果:

空白文字で上書きされちゃってる。通常動作はただのマッピング。有無を言わさずマッピング。


空白文字で上書きしたくない時は -i, --ignore オプションを用いる。またファイルを前の状態にもどして、

$ ruby excelmap.rb -p3,2 -s2 -r3 -c1 -i test.txt test.xls


結果:

入力した文字が残ってる。


これで年5回の単調で間違いやすい from テキスト to Excel 転記作業から開放される。今回は汎用的なので他の作業にも使えるな。何で今まで作らなかったか過去の自分を小一時間問い詰めたい。


TODO:

  • ヘルプ表示の Usage 行に必須引数 "textfile excelfile" 表示を付け足す方法を見つける。
  • Excel ファイルが前もって作成されてないと動かない。新規作成できるようにする。
  • Application にならうなら Book クラスも作らないといけないんだけど、まぁいいや。
  • 座標計算がバカですねぇ
  • Delegate も豪快ですねぇ
  • オプション処理なげぇ


一応ライセンス付けといた。Ruby's なので煮るなり焼くなり撒くなり。
ソース:

#!/usr/bin/env ruby
require 'win32ole'
require 'optparse'

# License: Ruby's

class WIN32OLE_EXT
   def WIN32OLE_EXT.get_absolute_path filename
      fso = WIN32OLE.new('Scripting.FileSystemObject')
      return fso.GetAbsolutePathName(filename)
   end
end


module Excel
   require 'delegate'
   class Application < SimpleDelegator
      def initialize visible
         @excel = (WIN32OLE.new('Excel.Application'))
         @excel.visible = visible
         super(@excel)
         begin
            yield self
         ensure
            @excel.Workbooks.Close
            @excel.Quit
         end
      end
      #module CONST; end
      #WIN32OLE.new.const_load(@excel, CONST)
      ##puts CONST.constants.each{|e| puts e + " = " + eval("CONST::#{e}").to_s}

      def open file
         path = WIN32OLE_EXT.get_absolute_path file
         @book = @excel.Workbooks.Open(path)
         begin
            yield @book
         ensure
            @book.Close(false)
         end
      end
   end
end


def parse_option argv
   options = {
      :col_skip_size => 0,
      :row_skip_size => 0,
      :sheet_no => 1,
      :py => 0,
      :px => 0,
      :term => "\t",
      :ignore => false,
   }

   opt = OptionParser.new
   opt.on('-h', '--help', 'print this message') {
      puts opt
      exit 0
   }
   opt.on('-s SHEET_NO', 'Sheet number') { |v|
      options[:sheet_no] = v.to_i
      raise "sheet number should be over 1." unless v.to_i >= 1
   }
   opt.on('-p Y,X', 'starting Point') { |v|
      if v =~ /(\d+),(\d+)/
         options[:py] = $1.to_i
         options[:px] = $2.to_i
      else
         raise "#{v} can't point excel's cell."
      end
   }
   opt.on('-r ROW_SKIP_SIZE', 'Row skip size') { |v|
      options[:row_skip_size] = v.to_i
   }
   opt.on('-c COL_SKIP_SIZE', 'Column skip size') { |v|
      options[:col_skip_size] = v.to_i
   }
   opt.on('-t TERMINATOR', 'split by Terminator') { |v|
      options[:term] = v
   }
   opt.on('-i', '--ignore-empty-data', 'Ignore empty data') {
      options[:ignore] = true
   }
   opt.parse!(argv)

   unless argv.size == 2
      puts opt
      exit 1
   end
   options[:textfile] = argv.shift
   options[:excelfile] = argv.shift

   return options
end


if __FILE__ == $0
   # option
   OPT = parse_option(ARGV)

   # read
   table = []
   File.foreach(OPT[:textfile]) do |l|
      row = l.chomp.split(OPT[:term], -1)
      table << row
   end

   # write
   Excel::Application.new(false) { |xl|
      excelpath = WIN32OLE_EXT.get_absolute_path(OPT[:excelfile])
      xl.open(excelpath) { |book|
         sheet = book.worksheets.item(OPT[:sheet_no])

         table.each_with_index do |row, ri|
            y = OPT[:py] + (ri * (OPT[:row_skip_size] + 1))
            row.each_with_index do |field, ci|
               x = OPT[:px] + (ci * (OPT[:col_skip_size] + 1))
               next if field.nil?
               next if (field.empty? && OPT[:ignore])
               sheet.Cells.Item(y, x).Value = field
            end
         end
         book.Save
      }
   }
end