give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

正規表現で楽々コード置換

会社で紹介した正規表現の入門的なテクニックをこっちにも載せておきます。

まずは例題から

ちょっと訳あって、これまで型付けDataTableを使って書いていたロジックを、型無しのプレーンなDataTableに書き換える必要が出てきました。
イメージ的にはこんな感じです(もちろん説明のためにかなり簡略化しています)。

// 変更前
BookShopDataSet.BookTable table = FindBooks();
BookShopDataSet.BookTableRow row = table[0];

Assert.AreEqual("詳説 正規表現", row.Title);
Assert.AreEqual("ジェフリ− E.F.フリ−ドル", row.Author);
Assert.AreEqual("オライリージャパン", row.Publisher);
// 他にもたくさんのカラムを検証

// 変更後
DataTable table = FindBooks();
DataRow row = table.Rows[0];

Assert.AreEqual("詳説 正規表現", row["Title"]);
Assert.AreEqual("ジェフリ− E.F.フリ−ドル", row["Author"]);
Assert.AreEqual("オライリージャパン", row["Publisher"]);
// 他にもたくさんのカラムを検証

面倒なのはAssertの部分でカラムの数だけ同じような変換を繰り返す必要がある、という点です。
変換のルールは「row.Title」だったら、「row["Title"]」に変換していく、という感じです。


さて、みなさんだったらどうやって変換していきますか?
特にいい方法も思いつかないし、プチプチと手作業で変換していく、という人は意外と多いのではないでしょうか?


こういうときは正規表現を使うと簡単に置換できます。


では実際にやってみましょう。
エディタの置換機能を使って、以下のような条件を入力して実行します。


検索文字列: row\.(\w+)
置換文字列: row["$1"]


するとあら不思議!一発で変換が完了します。

よくわかる(?)解説

正規表現は知らない人にとっては意味不明な呪文です。
しかし実際には呪文ではなく、それぞれにちゃんと意味があります。


まず検索文字列の「row\.(\w+)」は以下のように分解できます。


row
\.
( )
\w
+


次にそれぞれの意味を説明してみます。


row => "row"という文字列
\. => ドット
( ) => 括弧に囲まれた部分を$1に格納する
\w => 任意の英数字(a〜z、A〜Z、0〜9、_ )
+ => 直前の文字が一回以上連続する


つまり、「row\.(\w+)」は


"row"ではじまり、その次にドットがきて、任意の英数字が一回以上連続する文字列を検索せよ。さらに英数字が連続する部分は$1に格納せよ。


という意味になります。


次に置換文字列の「row["$1"]」の解説ですが、こちらは簡単です。
$1は検索時に格納した文字列を表しています。それ以外は普通の文字列です。


つまり、置換前が「row.Title」であれば「$1 = Title」となり、「row["$1"]」は「row["Title"]」となるわけです。


どうでしょう?呪文の意味が少しはつかめてきたでしょうか?

プログラム中で正規表現を活用する

上で紹介したのはエディタを使った置換テクニックですが、正規表現をサポートしている言語ならプログラム中でも正規表現を活用できます。


たとえば、プログラム中で何らかの文字列処理を実装する機会は結構多いと思います。
正規表現を知らない人が先ほどと同じ置換処理をプログラミングすると、大体こんな感じになると思います。*1

var text = '';
text += 'Assert.AreEqual("詳説 正規表現", row.Title);\n';
text += 'Assert.AreEqual("ジェフリ− E.F.フリ−ドル", row.Author);\n';
// ちょっとバリエーションを持たせてみる
text += 'Assert.AreEqual("オライリージャパン", row.Publisher, "株式会社は省略");';

var lines = text.split('\n');
for (var i in lines) {
    var line = lines[i];
    var rowStart = line.indexOf('row.');
    var columnStart = rowStart + 4;
    var columnToLineEnd = line.substring(columnStart);
    var blacketEnd = columnToLineEnd.indexOf(');');
    var column = columnToLineEnd.substring(0, blacketEnd);
    var replaced = line.substring(0, rowStart) + 'row["' + column + '"]);';  
    document.write(replaced + '<br />');
}

// 実行結果 => あれ?3行目がバグった!
// Assert.AreEqual("詳説 正規表現", row["Title"]);
// Assert.AreEqual("ジェフリ− E.F.フリ−ドル", row["Author"]);
// Assert.AreEqual("オライリージャパン", row["Publisher, "株式会社は省略""]);


正規表現を使わない場合の典型的な症状をリストアップしてみましょう。

  • 見つかった文字列の開始位置と終了位置を変数に格納する
  • substring関数で文字列を切り出す
  • 文字列を一文字ずつ切り出してループ処理する
  • etc...

でもこれだと実装するのに時間もかかるし、ちょっとパターンの異なる文字列を渡されると、すぐにバグが出たりします。上の例みたいに。
そして何しろ、コードがものすごく読みにくくなってしまうので全く良いことナシです。


一方、正規表現を使えばほとんどの文字列処理はかなりシンプルかつ安全に実装できます。
たとえば上のロジックはこのように書き換えられます。

var text = '';
text += 'Assert.AreEqual("詳説 正規表現", row.Title);\n';
text += 'Assert.AreEqual("ジェフリ− E.F.フリ−ドル", row.Author);\n';
// ちょっとバリエーションを持たせてみる
text += 'Assert.AreEqual("オライリージャパン", row.Publisher, "株式会社は省略");';

var lines = text.split('\n');
for (var i in lines) {
  var line = lines[i];
  var replaced = line.replace(/row\.(\w+)/, 'row["$1"]');
  document.write(replaced + '<br />');
}

// 実行結果 => 成功!
// Assert.AreEqual("詳説 正規表現", row["Title"]);
// Assert.AreEqual("ジェフリ− E.F.フリ−ドル", row["Author"]);
// Assert.AreEqual("オライリージャパン", row["Publisher"], "株式会社は省略");


最初のロジックと比較すると比べ物にならないぐらいシンプルになっているのが分かると思います。
さらに、パターンが異なる文字列が渡されても、正規表現を使った場合は問題なく処理できています。
正規表現の意味さえ分かれば、あとはこっちのモンです。

正規表現を覚えて「できるプログラマ」になろう!

おいらが新人だった頃、先輩から「正規表現は必ず習得しろ。絶対に役立つから。」と言われたのが正規表現を勉強したきっかけでした。
もちろん、おいらも最初は「正規表現 = 意味不明な呪文」でしたが、ちゃんと理解できると色々な場面で役立つ強力なツールだということが理解できました。


まだ正規表現をマスターしていないというプログラマは、ぜひ今から勉強を始めてみてください。
色々な場面で生産性がグンと向上するはずです。


ちなみにおいらはこの本を読んで勉強しました↓

詳説 正規表現 第3版

詳説 正規表現 第3版

  • 作者: Jeffrey E.F. Friedl,株式会社ロングテール,長尾高弘
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2008/04/26
  • メディア: 大型本
  • 購入: 24人 クリック: 754回
  • この商品を含むブログ (84件) を見る

正規表現について全く何も知らない初心者が読んでも理解できるよう、手取り足取り教えてくれます。
後半になるにつれて内容が高度になってきますが、全部理解できなくても最初の2〜3章が理解できれば、そこそこ使えるようになるはずです。
さあ、がんばって「できるプログラマ」にステップアップしましょう!

(2014.06.18追記) Rubyバージョンを作ってみました

最近はRubyで仕事をしているので、この問題のRubyバージョンを作ってみました。

TEXT = <<-EOS
Assert.AreEqual("詳説 正規表現", row.Title);
Assert.AreEqual("ジェフリ− E.F.フリ−ドル", row.Author);
Assert.AreEqual("オライリージャパン", row.Publisher, "株式会社は省略");
EOS

# 正規表現を使わない場合
def replace_text(text)
  text.each_line.map{|line|
    row_start = line.index('row.')
    column_start = row_start + 4
    column_to_line_end = line[column_start..-1]
    bracket_end = column_to_line_end.index(');')
    column = column_to_line_end[0, bracket_end]
    line[0, row_start] + 'row["' + column + '"]);'
  }.join("\n")
end
puts replace_text(TEXT)

# 実行結果 => あれ?3行目がバグった!
# Assert.AreEqual("詳説 正規表現", row["Title"]);
# Assert.AreEqual("ジェフリ− E.F.フリ−ドル", row["Author"]);
# Assert.AreEqual("オライリージャパン", row["Publisher"]);
# Assert.AreEqual("オライリージャパン", row["Publisher, "株式会社は省略""]);

# 正規表現を使う場合
puts TEXT.gsub(/row\.(\w+)/, 'row["\1"]')

# 実行結果 => 成功!
# Assert.AreEqual("詳説 正規表現", row["Title"]);
# Assert.AreEqual("ジェフリ− E.F.フリ−ドル", row["Author"]);
# Assert.AreEqual("オライリージャパン", row["Publisher"]);
# Assert.AreEqual("オライリージャパン", row["Publisher"], "株式会社は省略");


うむ、正規表現とgsubメソッドを使うと一撃ですね。。。

*1:実務で見かけるコードはたいてい、もっと悲惨だと思いますが。。。