woshidan's blog

あいとゆうきとITと、とっておきの話。

今日からはじめよう、正規表現 in Ruby (()を使おう(キャプチャ・グループ化)編)

正規表現の中の()

()はキャプチャとグループ化に使用することができます。

 

先にキャプチャについてやりましょう。

 

キャプチャ

さて、キャプチャについてなんですが……

 

丸括弧 ( ) によってキャプチャをすることができます。 括弧に囲まれた部分正規表現にマッチした 前からn番目の開き括弧によって囲まれた部分式にマッチした 文字列を後で参照することができます。

(http://docs.ruby-lang.org/ja/1.9.3/doc/spec=2fregexp.htmlより引用)

 

初心者:……あの、部分正規表現とか、開き括弧とか、読んでてとってもゾワゾワします。

 

正直な感想ありがとうございました。

実際使ってみると、もう少し鳥肌がおさまるんじゃないでしょうか。たとえば、先日の

 

/(go){3}/.match("gogogo") #=> #<MatchData "gogogo" 1:"go">

 

の1:ってなっているところとかが、関係している部分なのですが。

 

正規表現でマッチさせた後の返り値

初心者:じゃあ、キャプチャってその1:のとこなんですね。

    でも、それだと1:ってところに保存していても使えなくないのではないですか?

    一行書いて、標準出力に返り値表示されて終わってません?

 

要するに

後から参照できる形でインデックスをつけて文字列がマッチの結果として保存されても、マッチの結果(返り値)を受け取ってないので後の行で参照ができないということですね。

……返り値を受け取ればいいじゃないですか。

というか、これを始める前は普通にphpで 

 
$pattern = '/bcd/';
$subject = 'abcdef';
$is_matched = preg_match($pattern, $subject, $matched_pattern);
var_dump($is_matched);
var_dump($matched_pattern);
//int(1)
//array(1) {
//  [0]=>
//  string(3) "bcd"
//}

 

とか書いてたじゃないですか、君。

 

初心者:あれ、そうだ。じゃあ、なんでいままで受け取ってなかったんですか?

 

いや、今までの目的は正規表現を確かめるだけで、

その後、マッチした文字列に関して処理をする必要がなかったから、

わざわざ行数増やす事ないかなって。

 

単体でさえも長いのに5分割になってますからね、この記事。

 

それに、返り値の形式や受け取り方は言語や関数によって仕様が

いままでの部分より大きく異なるから、説明が面倒くさいかなって。

 

初心者:なるほど。とりあえず、じゃあ、今日はrubyで。

 

とりあえずって、いやいやphpJavaについては、自分で必要になったときに調べてください。

用語や大体何してるかが分かれば調べられますから。

 

改めて、キャプチャしてみる

じゃあ、例を書いてみましょう。

idという項目のデータが入った行があるから、

そこから後の処理で使いやすいよう項目名と番号番号の前半、後半を別々にとる正規表現

キャプチャを使って書いた例です。

 

match_data = /(\w+):(\d+)-(\d+)/.match("id:1234-5678")
=> #<MatchData "id:1234-5678" 1:"id" 2:"1234" 3:"5678">
match_data[1] #=> "id"
match_data[2] #=> "1234"
match_data[3] #=> "5678"

 

初心者:おお! おお! すごい!

    これですよ! 私のやりたかったことは! やったぁ!

 

そんなに喜んでもらえるとは思いませんでしたが、よかったね。

 

じゃあ、説明しますよ。

()を正規表現の中で使うと、その中に書いた正規表現にマッチした部分に、

Rubyのmatchメソッドの場合、前から順に1,2,3(このへんは言語や関数によって違う)と番号がつきます。

で、その返り値には、マッチした文字列が配列のように入っているので、対応する番号をインデックスに入れてやると

マッチした文字列を後から欲しい部分だけ取り出す事が出来ます。

まあ、上の例を見たほうが分かりやすいですかね。

 

あ、あと0にはパターン全体(/(\w+):(\d+)-(\d+)/)にマッチした文字列("id:1234-5678")が入っています

 

(文字クラス)と()(キャプチャ、グループ化)

そして、もう1つ。

 

()の中にそのまま文字を書いていくと、それを普通のリテラルの形として扱います。

どういうことかというと、ときどき文字クラス()と混同してしまって、

()の中に文字を並べていったら、()にあたる部分で、

その中のどれかとマッチする、みたいに誤解する事があるからです。

 

つまり、こういうことですが。

 

/(cat)/.match("c") #=> nil
// ???ってなっちゃう人は多分下の文字クラスと混同している
/[cat]/.match("c") #=> #<MatchData "c">

 

この原因はたぶん、()の中に文字クラスを放り込んでもいいから起こるんでしょうね。

 

/([cat])/.match("c") #=> #<MatchData "c" 1:"c">

 

初心者:こんなにごついコードがあったら覚えていませんか?

 

そうですね。なんで気づかないのかというと、文字クラスの略記法は[]をつけなくていいからですね。

 

/(\w)/.match("c") #=> #<MatchData "c" 1:"c">

 

気づかず使えちゃうんですよね。いいことでもあるんですけど。

引っかかったら、少しこのことを思い出してくださいね。

 

あと、同じ原因だと思うのですが、逆に()の部分で文字列を引っ掛けられない、

みたいな感覚を持ってしまうこともあるみたいです。

 

初心者:どういうことですか?

 

上で言っている通りですよ。()を選択(この中のどれか一つを選んだらいい)のための記号、みたいに考えてしまうんですね。

 

繰り返しになっちゃうから、ここではそういう人もいます、って言って終わりにしたいんですけど、

とりあえず、

  • (cat)( ( ):キャプチャ、グループ化 ) 3文字の文字列catがあったらマッチする。マッチする部分は3文字。()内に文字を書くほど、その部分でマッチする文字列の長さが大きい
  • [cat]( [ ]:文字クラス ) 1文字の文字c,a,tのいずれかにマッチする。マッチする部分は1文字。内に文字を書くほどその部分でマッチする文字の種類が多い

こういうことですね。

()の中にを入れる事も、[]の中に()もできるけど、特に後者の場合はややこしくなるから、自分で考えてみてくださいね。 

名前付きキャプチャ

初心者:そういえば、名前つきキャプチャって見たんですけど!

 

これを使うくらいだったら、

たぶん、返り値を受け取る変数に適切な命名をしたほうが読みやすい気がするんだけど、一応説明しますね。

 

以下のように書いて、さっきのマッチした文字列が入っている配列の番号を文字列に変えて、ハッシュのようにする事が出来ます。

 

match_data = /(?<category>\w+): (?<first_name>\w+) (?<last_name>\w+)/.match("name: Preg Exp")
# => #<MatchData "name: Preg Exp" category:"name" first_name:"Preg" last_name:"Exp">
irb(main):120:0> match_data[:category] #=> "name"
irb(main):121:0> match_data[:first_name] #=> "Preg"
irb(main):122:0> match_data[:last_name] #=> "Exp"

 

初心者:え、使いやすそうではないですか?

 

あなたは最近ずっと正規表現を使って仕事をしているから忘れているかもしれませんが、

 

普通の人は()の中に?とか<とかが並んでいたら、引きます。

読む気失せます。

 

いったんおうちに帰って冷静になってください。悪い事言わないから、たぶん、こっちのほうがいいですよ……。

 

match_data = /(\w+): (\w+) (\w+)/.match("name: Preg Exp")
#=> #<MatchData "name: Preg Exp" 1:"name" 2:"Preg" 3:"Exp">
category = match_data[1] #=> "name"
first_name = match_data[2] #=> "Preg"
last_name = match_data[3] #=> "Exp"

  

グループ化

 

よく分かってない人:で、ようやくグループ化ですね。

 

うん、150行か……。

 

よく分かってない人:何の話ですか?

 

あ、いや、こっちの話です。 グループ化というと、

 

丸括弧は部分式をグループ化するためにも使えます。( ) で囲まれた部分式は一つのものとして取り扱われ、量指定子などを続けて書くことができます。

(http://docs.ruby-lang.org/ja/1.9.3/doc/spec=2fregexp.htmlより引用)

 

って言われてもやっぱり初見の説明だと把握しにくいでしょうが、

いままで、一文字の文字の繰り返しについては量指定子を使って扱って来ましたね。

 

初心者:これですね!

 

/a+/.match("aaaaa") #=> #<MatchData "aaaaa">

 

それでグループ化っていうのはその繰り返しの範囲を()でくくった範囲にのばしましょうって話です。つまり、こういうこと。

 

/(abc)+/.match("abcabcabc") #=> #<MatchData "abcabcabc" 1:"abc">

 

初心者:なるほどー。

    +で繰り返しマッチされているのがa一文字じゃなくてabcの3文字の文字列にーって、

    あれ、キャプチャもされてますね。

 

うん、キャプチャもされますよ。一番最初に言った通り、

()という記号には「キャプチャ」と「グループ化」の二つの機能があります。

それは、()を使った時点で同時に機能するんだけど、違う意味の機能です

 

初心者:最初っからキャプチャもされてグループ化もされるって覚えてたらだめなんですか?

 

それをして、きちんといま自分が何をしたくて()を書いているか分かるならいいんだけど。

(君はそれができてなかったから今勉強してるんですけど)

はじめてのことを覚えるときは一つ一つ覚えた方が混乱が少ないから回り道に見えるけど、分けて説明しました。

 

実際に()を使うときは、どちらかの意味だけを意識して使うことのほうが、多いと思いますよ。

結果的に両方使ってもね。

 

アトミックグループ 

初心者:アトミックグループってなんですか? ほらほらこれこれ、これです。

 

どんぐり拾ってきたみたいなノリで説明を求めないでください。

 

というか、それまで使いますか……?

 

まあ、いいでしょう。

量指定子編に出て来たバックトラックって覚えてますか?

 

初心者:量指定子がついている左の方のパターンが、右のほうの正規表現の都合を見て、自分の取り分を減らすんですよね。

 

ええと、何ですか、なんとなく恨みがましい表現は。

正規表現に自分にマッチする文字列が多少短くなったからっていって悲しい、とか悔しいって気持ちはないというか、

バックトラックが起きたときに君がそう思ってるみたいな言い方なんだけど、

機械に感情はありません。

 

初心者:コードには思いを込めるものです!

 

うざい!

phprubyとかもうちょい高級よりの言語ならまだし、

正規表現に思いを込められても、読みにくいだけだと思うよ。

というか、あんまり凝りだすと一週間したら読めなくなりますよ!

 

さて、アトミックグループですけど、

()の中の正規表現の頭に?>とつけることで作れます。

 

アトミックグループと普通のグループの違いが出るのは、バックトラックのときです。

ちょっと、簡単な例が思いつかなかったから、今回は具体例はごめんなさい。

 

バックトラックのとき、普通のグループの場合、バックトラックは文字単位で行うんだけど、

アトミックグループの場合、バックトラックのときは、そのグループに一回以上マッチしている部分があったらそれを、最後の一回分から順に、それぞれ一回分ずつ手放します。

 

初心者:はあ、言っている意味がよく分からないですけど、

    バックトラックのときマッチしなくなるのも

    グループでくくっている単位ごとになるってことですね。

    で、なんでアトミックなんですか?

 

えっ、そこ?

……データベースに関連する言葉でACIDって知ってますか?

 

初心者:RASISじゃなくて?

 

RASISはもっと一般的なコンピュータシステムに対する評価指標かな。

この辺(http://e-words.jp/w/RASIS.html)読んで復習しておいてください。

 

ACIDのAが原子性(Atomicity)なんだけど、この原子性と同じ意味で使ってます。

要するに処理に必要な単位を一つにまとめて原子みたいに分割できないようにして扱う、みたいな。

 

()でくくったグループの一単位をバックトラックのときも分割しないように、

一単位ごと巻き戻す様子がアトミックというんですね。

 

で、一文字巻き戻すたびに後ろの部分がマッチが出来るかどうか調べるよりも、

グループの単位で巻き戻して調べる方が、調べ直す回数が減るから、高速化に役に立つ、と言われています。

私は実際に使った事ないから、分かりませんけど。

 

バックトラックが起こりそうな正規表現で動かしているとき、

実際に処理がすごく重かったら使ったらいいのかな、って感じでしょう。

 

今日はここまで。