Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Ruby正規表現で条件分岐を使う方法 ― Onigmoエンジンの条件式を徹底解説

2013年にリリースされたRuby 2.0の数多くの新機能の中で、私が最も注目していなかったのは、新しい正規表現エンジン「Onigmo」でした。正規表現はどの言語でも同じもの――Rubyがどう実装しているかを気にする必要はあるのでしょうか?

ところが実際には、Onigmoには便利な仕掛けがいくつも隠されており、そのひとつが正規表現の中で条件分岐を使えるという機能です。

この記事では、正規表現の条件式について詳しく掘り下げながら、Ruby実装ならではのクラスや、その制限を回避するためのテクニックまで解説していきます。それでは始めましょう!

グループとキャプチャの基本

正規表現の条件式を理解するには、まず「グループ化」と「キャプチャ」の仕組みをおさえる必要があります。

たとえば、次のようなアメリカの都市リストがあるとしましょう。

Fayetteville, AR
Seattle, WA

ここから都市名と州の略称を分けたい場合、ひとつの方法は複数回に分けてマッチを行うことです。

PLACE = "Fayetteville, AR"

# 都市名: カンマ以外の任意の文字にマッチ
PLACE.match(/[^]+/) 
# => #<MatchData "Fayetteville">

# 区切り文字: カンマと任意のスペースにマッチ
PLACE.match(/, */)
# => #<MatchData ", ">

# 州名: 文字列末尾の2文字の大文字コードにマッチ
PLACE.match(/[A-Z]{2}$/) 
# => #<MatchData "AR">

これは動作しますが、いかにも冗長です。グループを使えば、たった1つの正規表現で都市名と州名の両方をキャプチャできます。

先ほどの正規表現をつなぎ合わせ、各部分を丸括弧 () で囲んでみましょう。丸括弧こそが、正規表現におけるグループ化の記法です。

PLACE = "Fayetteville, AR"
m = PLACE.match(/([^]+)(, *)([A-Z]{2})/) 
# => #<MatchData "Fayetteville, AR" 1:"Fayetteville" 2:", " 3:"AR">

ご覧のとおり、この正規表現は都市名と州名の両方をキャプチャします。キャプチャした値には、MatchData を配列のように扱うことでアクセスできます。

m[1]
# => "Fayetteville"
m[3]
# => "AR"

しかし、このような番号によるグループ化には落とし穴があります。キャプチャしたデータは配列の順序に依存するため、配列内の位置が変わればコードを修正しなければならず、放置すればバグの温床になるのです。

たとえば、「, 」という区切り文字をわざわざキャプチャするのは無駄だと判断して、その部分の括弧を外してみましょう。

m = PLACE.match(/([^]+), *([A-Z]{2})/) 
# => #<MatchData "Fayetteville, AR" 1:"Fayetteville" 2:"AR">

m[3]
# => nil

すると m[3] にはもう州名が入っていません。バグの完成です。

名前付きグループ

グループに名前を付けると、正規表現のコードはぐっと読みやすく、意味の明確なものになります。構文は先ほどとよく似ていて、丸括弧で囲んだうえで次のように名前を指定します。

/(?<groupname>regex)/

これを都市/州の正規表現に適用すると、次のようになります。

m = PLACE.match(/(?<city>[^]+), *(?<state>[A-Z]{2})/)
# => #<MatchData "Fayetteville, AR" city:"Fayetteville" state:"AR">

キャプチャしたデータには、MatchData をハッシュのように扱ってアクセスできます。

m[:city] 
# => "Fayetteville"

条件式(コンディショナル)

正規表現における条件式は、/(?(A)X|Y)/ という形式で書きます。主な使い方は以下のとおりです。

# Aが真ならXを評価し、偽ならYを評価する
/(?(A)X|Y)/

# Aが真ならX
/(?(A)X)/

# Aが偽ならY
/(?(A)|Y)/

条件部分 A によく使われるのは、次の2パターンです。

  • 名前付きグループ、または番号付きグループがキャプチャ済みかどうか
  • 先読み・後読み(look-around)が真と評価されるかどうか

それぞれの使い方を見ていきましょう。

グループがキャプチャされているか判定する

グループの有無をチェックするには ?(n) という構文を使います。ここで n は整数、あるいは <> または '' で囲んだグループ名です。

# 番号1のグループがキャプチャされているか?
/(?(1)foo|bar)/

# "mygroup" という名前のグループがキャプチャされているか?
/(?(<mygroup>)foo|bar)/

実例:電話番号の解析

アメリカの電話番号を解析する場面を想像してみてください。これらの番号には3桁の市外局番が含まれますが、番号が「1」で始まる場合にのみ市外局番が必須になります。

1-800-555-1212 # 有効
800-555-1212 # 有効
555-1212 # 有効

1-555-1212 # 無効!!

条件式を使えば、「番号が1で始まるときだけ市外局番を必須にする」というロジックを実現できます。

# 複雑に見えますが、シンプルなパーツの組み合わせです
# `^(1-)?` 文字列が "1-" で始まるか? 始まるならグループ1としてキャプチャ
# `(?(1)` グループ1に何かキャプチャされたか?
# `\d{3}-` キャプチャされていれば、3桁の数字とハイフン(市外局番)を必須でマッチ
# `|(\d{3}-)?` キャプチャされていなければ、3桁の数字とハイフンを任意でマッチ
# `\d{3}-\d{4}` 残りの電話番号部分。こちらは常に必須。

re = /^(1-)?(?(1)\d{3}-|(\d{3}-)?)\d{3}-\d{4}/

"1-800-555-1212".match(re)
#=> #<MatchData "1-800-555-1212" 1:"1-" 2:nil>

"800-555-1212".match(re)
#=> #<MatchData "800-555-1212" 1:nil 2:"800-">

"555-1212".match(re)
#=> #<MatchData "555-1212" 1:nil 2:nil>

"1-555-1212".match(re)
=> nil

グループベースの条件式の限界

グループベースの条件式には、ひとつ重要な問題があります。グループがマッチすると、その文字列部分は「消費」されてしまうのです。つまり、消費された文字は条件式側では使えなくなります。

たとえば、次のコードは「USD」という文字列が存在する場合に「100」へマッチさせようとしていますが、失敗します。

"100USD".match(/(USD)(?(1)\d+)/) # nil

Perlなど一部の言語では、条件式の中に先読み(look-ahead)を組み込めるため、文字列の任意の場所にあるテキストを条件のトリガーにできます。しかしRubyにはこの機能がないため、少し工夫が必要になります。

先読み・後読み(look-around)の活用

幸い、先読み・後読みの表現をうまく利用すれば、Rubyの正規表現条件式の制限を回避できます。

先読み・後読みとは何か?

通常、正規表現エンジンは文字列を先頭から末尾へと順番に走査しながらマッチを探します。ワープロでカーソルを左から右へ移動させるイメージです。

一方、先読み(look-ahead)と後読み(look-behind)は少し特殊な動作をします。これらは文字を一切消費せずに文字列を調べることができ、評価が終わってもカーソルは開始時の位置にとどまったままです。

先読み・後読みについて学ぶなら、Rexeggの「Lookahead and Lookbehind Tutorial」が非常におすすめです。

構文は以下のとおりです。

種類構文
先読み(肯定)(?=query)\d+(?= dollars) は "100 dollars" の中の 100 にマッチ
先読み(否定)(?!query)\d+(?! dollars) は直後に "dollars" が続かない場合に 100 にマッチ
後読み(肯定)(?<=query)(?<=lucky )\d は "lucky 7" の中の 7 にマッチ
後読み(否定)(?<!query)(?<!furious )\d は直前が "furious " でない場合に 7 にマッチ

先読みを駆使して条件式を強化する

条件式で参照できるのは、「すでに設定済みのグループ」だけです。通常、これはグループの内容がすでに消費されており、条件式からは参照できないことを意味します。

しかし!先読みを使えば、文字を一切消費せずにグループを設定できるのです。ちょっと驚きではないでしょうか?

先ほど動かなかったこのコードを覚えていますか?

"100USD".match(/(USD)(?(1)\d+)/) # nil

グループを先読みの中でキャプチャするように書き換えると、あら不思議、ちゃんと動くようになります。

"100USD".match(/(?=.*(USD))(?(1)\d+)/)
=> #<MatchData "100" 1:"USD">

この正規表現を分解して、何が起きているのか見てみましょう。

  • (?=.*(USD)) ―― 先読みで文字列全体を走査し、「USD」を見つけてグループ1にキャプチャする(文字は消費しない)
  • (?(1) ―― グループ1が存在するか?
  • \d+ ―― 存在するなら、1個以上の数字にマッチする

なかなか巧妙だと思いませんか?Rubyの正規表現条件式と先読みを組み合わせれば、標準機能だけでは実現できない柔軟なパターンマッチングが可能になります。ぜひ自分のプロジェクトでも試してみてください。


  1. Rubyのラムダ徹底解説――基本から実践的な応用テクニックまで

    Rubyにおいてブロックは非常に重要な存在であり、ブロックなしにこの言語を語ることはできません。では、ラムダはどうでしょうか?ラムダを愛している人はいるのでしょうか?実際のところ、何年もラムダを使わずに過ごすことだって可能かもしれません。まるで昔の時代の遺物のように見えることもあるでしょう。 しかし、それは完全な真実ではありません。少し掘り下げてみると、ラムダには興味深い技が数多く隠されているのです。 この記事では、まずラムダの基本的な使い方から始め、その後、より高度で面白い応用へと進んでいきます。すでに毎日ラムダを使いこなしている方は、ぜひ後半の応用編までスクロールしてください。 ラムダにつ

  2. Rubyの正規表現(Regex)を徹底解説!基礎から実践テクニックまで

    Rubyの正規表現(ruby regex)は、文字列の中から特定のパターンを見つけ出し、データを抽出してさらに処理を行うための強力なツールです。 正規表現の代表的なユースケースとしては、大きく分けて「バリデーション(検証)」と「パース(解析)」の2つがあります。 たとえば、メールアドレスを考えてみましょう。Rubyの正規表現を使えば、「有効なメールアドレスとはどのような形式か」を定義できます。つまり、プログラムが有効なメールアドレスと無効なものを自動的に判別できるようになるのです。 Rubyでは、正規表現を2つのスラッシュ(/)で囲んで定義します。これにより、他の構文と区別されます。最もシン