2011年8月7日日曜日

Ruby で簡単なテキスト処理 - ファイル処理と正規表現

1. 目的

テキストファイル内の、特定の文字列を変換し、新たなファイルに保存したい。

 

2. 問題

例えば、あるディレクトリ中の HTML ファイルを対象にして、img 要素の属性を変更したい。ただし、元のファイルを直接変更せず、拡張子を .php に変更したファイルを新たに作成する。

img 要素の属性を変更する仕様は、

<img class=”XXXXX” src=”YYYYY” alt=”ZZZZZ” />

と記述があったら、

<img clas=”XXXXX” src=”<?php echo PATH . "YYYYY"; ?>" alt="ZZZZZ" />

とする。

 

3. 方法

ファイルの操作。

  • Dir.glob : ディレクトリの中のファイル名を取得する。
  • File
    • open : ファイルを開く。
    • basename : 拡張子を除く、ファイル名を取得する。

エンコーディング。

特殊な記法。

 

4. ソースコード

# -*- coding: utf-8 -*-

Encoding.default_external = 'UTF-8'

Dir.glob("*.html") do |f|
  File.open(f, "r") do |rf|
    File.open(File.basename(f, ".*") + ".php", "w") do |wf|
      rf.each do |line|
        if %r|(.*<img.+src=")(.+?)"| =~ line
          wf.puts %Q|#$1<?php echo PATH . "#$2"; ?>"#$'|
        else
          wf.puts line
        end
      end
    end
  end
end

上記は、UTF-8 で記述。

 

5. 変更したい点

上記は、以下の点が固定されている。

  • 対象とするファイル
  • ファイル名の変換の方法
  • 特定の文字列の変換の仕方

これらの値と処理を変更できるようにしたい。

 

手続きオブジェクトの利用

処理を変更可能にする場合、関数の引数に、関数を渡せるように変更すれば良い。シンプルな処理なので、ストラテジーパターンを適用するほどでもない。 Ruby は関数を直接渡せないので、この場合、手続きオブジェクト を代わりに与える。

手続きオブジェクトとはブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクトしたものです。Proc クラスのインスタンスとして実現されています。

関数の引数に、関数を渡せる言語では、素直に与えられた関数を呼び出すだけ良い。これに対して、Ruby で手続きオブジェクトを呼び出すには、Proc#call メソッドを使う。例えば、f を手続きオブジェクトだとすると、

f.call(引数)

と書いて呼び出す。

上記 f に対応した、無名関数に相当する手続きオブジェクト作るには、

lambda { |引数| 式 }

と記述。lambda の代わりに proc と書くことができるが、lambda の方がシンプルのようだ。

( cf. Ruby のブロックと Proc )

 

正規表現にマッチした変数を遅延評価

パターンマッチに成功した値を格納する $1 のような特殊な変数の値は、パターンマッチが行われたタイミングに依存する。そのため、$1 を含む正規表現リテラルを交換できるように処理から抽出する場合、変数の評価が、パターンマッチ後に行われるようにしなければならない。

評価を遅延させるためには、上記の手続きオブジェクトを利用する。

( cf. メタプログラミングRuby, p123 )

 

オープンクラス

拡張子を除いたファイル名を取得するメソッド名が一見したところわかりづらいので、既存の File クラスにメソッドを追加する。

 

変更したソースコード
# -*- coding: utf-8 -*-

Encoding.default_external = 'UTF-8'

class File
  def self.without_ext(filename)
    self.basename(filename, ".*")
  end
end

def convert(targets, cnv_filename, f)
  Dir.glob(targets) do |filename|
    File.open(filename, "r") do |read_file|
      File.open(cnv_filename.call(filename), "w") do |write_file|
        read_file.each do |line|
          write_file.puts(f.call(line))
        end
      end
    end
  end
end

re = %r|(.*<img.+src=")(.+?)"|
rplc = lambda{ %Q|#$1<?php echo PATH . "#$2"; ?>"#$'| }

convert("*.html", 
        lambda { |filename| File.without_ext(filename) + ".php" }, 
        lambda { |line| if re =~ line then rplc.call else line end })

 

矢印ラムダ

Ruby 1.9 より、lambda の代わりに矢印ラムダを使える。

使い方は、Arrow Lambdas, a Ruby 1.9 Vignette | Rails Fire によると、

-> { } # the basic form
->(x) { x * 2 } # with an argument
-> x { x * 2} # (without parenthesis, too)
->(x, y) { x * y } # multiple arguments
->(x, y = 2) { x * y } # default arguments
->(x, y, &z) { x * y * z.call } # take a block

cf. The Ruby programming language - Google ブックス

こちらの書き方のほうが、引数が、関数の引数らしく見えるかも。

 

関連記事