Excelマクロの操作 ← : ブロックを使う : → Seleniumの操作

Ruby: ブロックを使う

Rubyのブロックで一番使われているのは eachメソッド系だろうと思いますが以下のような使われ方をします。 (以下のコードはruby 2.0 で確認しています。)

(1..10).each{|x| p x}

このコードは (1..10)で1から10までの要素をもつ配列を、 each でその要素を1個づつブロックに渡して処理(画面に出力)をします。

{ } でかこまれだ部分がブロックです。|x| で受け取る引数名を指定しブロックの中で使います。 引数の数はメソッドがどう定義されているかに依存します。 ここでのeachメソッドはひとつだけ値を渡しますので引数をひとつだけ受け取ります。 例えば配列のeachは以下と同等に定義されています。

def each()
  i = 0
  while i < self.size
    yield self[i]
    i += 1
  end
end

先頭から順に配列要素を取出してひとつづつ yield に渡しています。このyieldの部分に ブロックのコードが入る感じになります。先の例だとこんな感じです。

def each()
  i = 0
  while i < self.size
    yield self[i]       # → {|x| p x}  xにself[i]を受け取る
                        # → p self[i]
    i += 1
  end
end

このようにブロックを使うと共通に使える一連の前処理や後処理を隠してしまって 異なる部分だけに注目したブロック部分だけ書けば良いようにすることが出来ます。

言い換えると、同じパターンのコードを毎回書かなくても済みます。

引数の個数はyield で渡される引数の分だけ、ブロックの先頭で|p1,p2,..|と定義して受け取れば良いわけです。 yieldの場合、数が違ってもエラーと成りませんので注意が必要です。

Ruby: ブロックの使用例1, 文字列の処理

ブロックを使う処理を書いてみましょう。ここではカンマで区切られた文字列の要素を取出して 処理する例題をあげてみます。

#! ruby -EUTF-8
# -*- mode:ruby; coding:utf-8 -*-

def split_and_count(str)
  dt = str.split(/,/).inject(Hash.new(0)){|hash, a| hash[a.strip] += 1; hash}
  if block_given?
    yield dt
  end
  dt
end

if $0 == __FILE__

  split_and_count("abc,1,2,3,2,3,3,xyz,xyz"){|dt| 
    dt.sort.each{|k,v| print "#{'%-5s'%k}:#{'%4d'%v} 回\n".encode("CP932")}
  }
  split_and_count("abc,1,2,3,2,3,3,xyz,xyz,なんとか,かんとか,なんとか"){|dt| 
    print "2回以上の文字列 = ".encode("CP932")
    print (dt.sort.map{|x,v| x if v >= 2}.compact.join(',')+"\n").encode("CP932")
  }
end

まず、ruby 2.0のデフォルト文字コードはUTF-8に成りました。 特に指定しなければUTF-8と成りますのでWindowsで利用する場合には表示等で SHIFT-JIS(CP932)でないと文字化けする場合がありますので注意して下さい。 文字コードを意識するためにも先頭でコードの指定をするようにした方が良いでしょう。

def split_and_count(str) では引数としてstrを取るとだけ指定していますが この様に定義したメソッドにもブロックを渡すことが出来ます。

dt = str.split(/,/).inject(Hash.new(0)){|hash, a| hash[a.strip] += 1; hash}
では渡された文字列をカンマで分解した文字列の配列を作り、
各要素の前後の空白を無くした文字列(a.strip)をキーとするハッシュを作り、 個数をカウントアップし、結果のハッシュを dt に入れています。

Hash.new(0)と初期値を0と明示的に指定することで、 新規のキーの時にnilへの加算となることを防いでいます。

if block_given?
はブロックが与えられたかを検証するコードです。ブロックが与えられた時だけ
yield dt を呼び出します。

最後のdt行でこのメソッドの戻り値として作ったハッシュを返しています。

if $0 == __FILE__
の行は、このプログラムファイルが直接実行されたかの判定です。
直接実行されたときだけtrueとなり以下の行を実行します。 require 等で組み込まれたときには以下の行を実行しません。 ファイル内に組み込みのテストコードを書く時の常套手段です。

split_and_count("abc,1,2,3,2,3,3,xyz,xyz"){|dt|
は文字列を渡して、ブロックにはカンマで分解した文字列の個数のハッシュを dt として受け取っています。

dt.sort.each{|k,v| print "#{'%-5s'%k}:#{'%4d'%v} 回\n".encode("CP932")}
はブロックの中のコードです。 dt の各要素を出力しています。

encode("CP932")で文字コードをWindows用に変更しています。 プログラムをCMD.exeのコンソールで実行する場合には UTF-8で出力しても自動的に変換されて正常に表示されますが、 RDE等で見ると文字化けします。

split_and_count("abc,1,2,3,2,3,3,xyz,xyz,なんとか,かんとか,なんとか"){|dt|
print "2回以上の文字列 = ".encode("CP932")

print (dt.sort.map{|x,v| x if v >= 2}.compact.join(',')+"\n").encode("CP932")

の部分は、同じsplit_and_countを使って出現頻度が2回以上の文字列を出力します。

この様にsplit_and_countで分解した文字列のハッシュに対して、異なる処理を簡単に 実行させることが出来ます。

Ruby: 関数オブジェクトの利用

ブロックとして渡せるのはひとつだけですが、 ブロックを関数オブジェクトとして受け渡すと、 複数の処理パートを受け渡すことが出来ます。

下記に指定されたディレクトリ以下のサブディレクトリを全部辿って、 ディレクトリ毎の処理(dir_proc)、ファイル毎の処理(file_proc)、サブディレクトリ単位での処理(end_proc)を 実行する例(visit_subdir)をあげます。

下記の呼び出し例は指定されたディレクトリ以下のファイルのサイズの合計を計算します。

  #! ruby -EUTF-8
  # -*- mode:ruby; coding:utf-8 -*-
  
  class VisitAllDir
    attr_accessor :max_level,:deep_dirs,:local_stack,:values,:level_max
    attr_accessor :show_error
    
    def initialize dir
      @max_level = 10       ## ネストが深い時、深さ制限する値
      @local_stack = []     ## サブディレクトリ単位の処理で使用する作業用変数のスタック
      @values = nil         ## サブディレクトリ単位の処理で使用する作業用変数の配列又はnil
      @too_deep  = []       ## 深さ制限を越えたディレクトリパスの配列
      @level_max = 0        ## 最も深い階層数
      @show_error = true    ## エラーが発生した時に表示するかしないか
    end
    
    def visit_subdir(dir, level, dir_proc, file_proc, end_proc)
    
      @level_max = level if level > @level_max
      
      if level >= @max_level
        @too_deep << dir
      else
        begin
          Dir.foreach(dir) do |f|
            full_path = ''
            if (f != '.' and f != '..')
              if dir == File::Separator
                full_path = dir +  f
              else
                full_path = dir + File::Separator + f
              end
              
              if File.directory?( full_path)
                @local_stack.push(@values)
                @values = nil
                dir_proc.call(full_path) if dir_proc 
                visit_subdir(full_path, level+1, dir_proc, file_proc, end_proc)
                @values = @local_stack.pop
              else
                file_proc.call(full_path) if file_proc 
              end
            end
          end
        rescue =>err
          print "#{err}\n" if @show_error
        end
        end_proc.call(dir) if end_proc 
      end
    end
    
    def visit_all(dir, level, dir_proc, file_proc, end_proc)
      @local_stack = []
      @values = nil
      @too_deep  = []
      
      @values = [0,0]
      dir_proc.call(dir) if dir_proc
      
      visit_subdir(dir, level, dir_proc, file_proc, end_proc)
    end
    
    def dump_too_deep
      print "\n\n----- Too deep subdirs, nest over #{@max_level} level -------\n"
      n = 1
      @too_deep.each do |dir|
        printf "%10d :%s\n",n,dir
        n += 1
      end
    end
    
  end
  
  if $0 == __FILE__
    
    STDOUT.sync=true
    
    open('work.txt','w') do |wf|
      
      dir = 'C:\Documents and Settings\All Users' # 適当なディレクトリ名に変更してください。
      v = VisitAllDir.new(dir)
      
      dir_proc = proc do |dir|
        v.values = [0,0] if v.values == nil
        wf.print "-- #{dir}\n"
      end
      
      file_proc = proc do |file|
        v.values = [0,0] if v.values == nil
        v.values[0] += File.size(file)
      end
      
      end_proc = proc do |dir|
        if v.values
          printf "%10d,%10d : %s\n",v.values[0],v.values[1],dir
          v.local_stack.last[1] += (v.values[0] + v.values[1]) if v.local_stack.last
        end
      end
      
      
      print "ThisFolder,SubFolders : Path\n"
      v.visit_all(dir, 0,dir_proc,file_proc,end_proc)
      print "Total = #{v.values[0] + v.values[1]}\n"
      #  v.dump_too_deep
      print "\n\n level max = #{v.level_max}\n"
    end
  end

今回は機能をクラスとして作っています。

def visit_subdir(dir, level, dir_proc, file_proc, end_proc)
が注目しているメソッドです。引数として明示的にdir_proc,file_proc,end_procと
言う名前で関数オブジェクトを受け取ります。 (注:引数に型の指定はされていません)

begin
  Dir.foreach(dir) do |f|

beginでエラーをハンドリングできるようにして、Dir.foreach でサブディレクトリや ファイルを順に取出して処理します。

if File.directory?( full_path)
  @local_stack.push(@values)
  @values = nil
  dir_proc.call(full_path) if dir_proc 
  visit_subdir(full_path, level+1, dir_proc, file_proc, end_proc)
  @values = @local_stack.pop
else
  file_proc.call(full_path) if file_proc 
end

ディレクトリなら現在使用している作業用変数をスタックに積み、 nilにすることでサブディレクトリ単位の作業用変数を管理します。

dir_proc.call(full_path) if dir_proc
でdir_procが渡されていればディレクトリ名を渡して呼び出します。

サブディレクトリの処理から戻ってきたら作業用変数をスタックから戻します。

file_proc.call(full_path) if file_proc
ファイルだったら file_procを呼び出します。

begin
  Dir.foreach(dir) do |f|
  :
  end
rescue =>err
  print "#{err}\n" if @show_error
end
end_proc.call(dir) if end_proc

rescueでエラーが起こったときに必要ならエラーを表示させて、処理を中止させます。

Dir.foreachが終わったらend_procを呼び出しています。

STDOUT.sync=true

これは出力をバッファリングせずに直ぐに出力させるためのおまじないです。

open('work.txt','w') do |wf|
  :
  dir_proc = proc do |dir|
    v.values = [0,0] if v.values == nil
    wf.print "-- #{dir}\n"
  end

dir_proc = proc do |dir| で関数オブジェクトを作成していますが 関数オブジェクトは作成された時に利用可能な変数等の内容を利用可能です。 ここではopenで書込み用に作成した wf をブロックの中で出力用に使用しています。 実際に関数オブジェクトが呼び出される時に、このwfを使って出力されます。

file_proc = proc do |file|
  v.values = [0,0] if v.values == nil
  v.values[0] += File.size(file)
end

end_proc = proc do |dir|
  if v.values
    printf "%10d,%10d : %s\n",v.values[0],v.values[1],dir
    v.local_stack.last[1] += (v.values[0] + v.values[1]) if v.local_stack.last
  end
end

file_procではファイルのサイズをvalues[0]に足しこみ、 end_procでスタック上のひとつ上のディレクトリ用のvalues[1]の所に足し込んでいます。

values[ ]の意味は実現したい機能に応じて自由に意味付けすることが可能です。

呼び出し部分を下記の様にするとディレクトリの深さが分ります。

if $0 == __FILE__
  v = VisitAllDir.new(dir)
  v.max_level = 1000
  v.show_error = false
  v.visit_all('c:', 0,nil,nil,nil)      ## 適当なディレクトリ名に変更してください。
  print "level max = #{v.level_max}\n"
end



Excelマクロの操作 ← : ブロックを使う : → Seleniumの操作


お勧めのRuby開発環境

Trail4You 仮想マシンバザール : Ruby統合開発環境

仮想マシン上にruby統合開発環境をインストールしてあります。 rvm, git もインストール済みで各種rubyを切替ながら試せます。