正規表現


制作会社で働く人向け「はじめての正規表現」(http://d.hatena.ne.jp/jdg/20080921/1222002244)という面白い記事があって、その中で「見積もり」とか「見積」を統一的に「見積り」に正規表現で変換しようとする話がありました。
こういうのはなかなかむずかしくて、まずサンプルテキストを作ります。

りんごの見積り
みかんの見積もり
見積はいくらですか?
見積もった結果を教えて下さい
見積った結果を教えてちょうだい
りんごの見積もみかんの見積も見積すぎです
りんごの見積りもみかんの見積りも見積りすぎかな
見積もろうとか見積もらないとか見積もれないとか言うな
見積ろうとか見積らないとか見積れないとか言え

これを、下のように変換したい。

りんごの見積り
 => りんごの見積り
みかんの見積もり
 => みかんの見積り
見積はいくらですか?
 => 見積りはいくらですか?
見積もった結果を教えて下さい
 => 見積った結果を教えて下さい
見積った結果を教えてちょうだい
 => 見積った結果を教えてちょうだい
りんごの見積もみかんの見積も見積すぎです
 => りんごの見積りもみかんの見積りも見積りすぎです
りんごの見積りもみかんの見積りも見積りすぎかな
 => りんごの見積りもみかんの見積りも見積りすぎかな
見積もろうとか見積もらないとか見積もれないとか言うな
 => 見積ろうとか見積らないとか見積れないとか言うな
見積ろうとか見積らないとか見積れないとか言え
 => 見積ろうとか見積らないとか見積れないとか言え

作ったRubyプログラムがつぎ。

while line = gets
  puts line
  print (" => ")
  puts line.gsub(/見積/, "見積もり").gsub(/見積もり(?=[っらるれろ])/, "見積も").gsub(/見積もりも(?=[っらるれろ])/,"見積も").gsub(/見積もり(もり|り)/, "見積もり").gsub(/見積も/, "見積")
end

一発で行かなくて、一度"見積もり"という表記に変えてから、順次解析して、"見積り"に落しています。走らせるときは漢字コードを必ず指定してください。

ruby -Ks seiki.rb < seiki.txt

というようなかんじ。これはShift-JISの場合。UTF-8ならu、EUCならeですね。

?=  先読み(lookahead)。パターンによる位置指定(幅を持たない)
?!  否定先読み(negative lookahead)。パターンの否定による位置指定(幅を持たない)

とか知らなかったので、大変勉強になりました。

追記:「見積り」「見積」を「見積もり」にする正規表現は上記から最後のgsub(/見積も/, "見積")をとったものになります。

while line = gets
  puts line
  print (" => ")
  puts line.gsub(/見積/, "見積もり").gsub(/見積もり(?=[っらるれろ])/, "見積も").gsub(/見積もりも(?=[っらるれろ])/,"見積も").gsub(/見積もり(もり|り)/, "見積もり")
end

感想:
1.できると思っていなかったのでとても満足。
2.よく、正規表現のテキストに「ぜんぶ正規表現でやろうとするな。手の方が速くて確実な場合もあるし、正規表現でやろうとして泥沼にはまる。」と書いてあって、「そんなものかな? とくに表記の揺れの統一はむずかしいよね。」くらいに考えていたけど、こんなものこそ目と手でやると大変。
3.サンプル大切。というかテスト大切。順々に考えたつもりでも、いじくっている内に最初の方からおかしくなっていく。テストの網羅性も大切。
4.頭のいいやりかたを目指すのではなく、遠回りでも確実な方法を考える。今回の場合、「見積もり」と「見積」を「見積り」にするために、まずすべて「見積もり」にしてから考えたのがうまくいった。
5.gsubをつなげて行くのはrubyでなくても思いつく方法ではあるが、rubyは考えやすい。幅を持たない先読み検索は正規表現の話で勉強になった。rubyでは使えないかもしれないと思いつつ試したら使えた。漢字コードの指定は大切。

以上