プログラミング/ruby/正規表現 の履歴(No.6)
更新近年導入された新しい機能を中心に、いろいろ試した結果をメモします†
最近の正規表現 (PCRE) はいろいろ拡張されていて、以前では考えられなかったほど高機能になっています ・・・
ということを最近になって知ったので、近年(?)導入された新しい機能を中心に、いろいろ試した結果をメモしました。
http://docs.ruby-lang.org/ja/1.9.3/doc/spec=2fregexp.html
ここを見ながら、ruby 1.9.3 および ruby 2.1.5 で検証しました。
この2つのバージョン間で、結果に差異は無かったようです???
と Qiita で書いた ところ、
@riocampos さん:
正規表現エンジンは、Ruby 1.9系だと鬼車、2.0系以降だと鬼雲が使われています。
違いとして顕著なのは、 Unicode 文字プロパティに関しての部分だと思いますので、気になることは滅多に無いと思います。
との情報をいただきました。
リンク先のライブラリの説明を読むと、Ruby のマニュアルよりも詳細な仕様を参照することができます。
このあたりの拡張仕様は、呼び出し元の言語やバージョンによっても実装レベルや細かい仕様が異なるため、以下はあくまで上記バージョンの Ruby の場合であって、他の言語 (C#, Java Script, php, ...) では似たようなことができることも、できないこともある、という方向でご理解ください。
/ /x による正規表現の分かち書き†
複雑な正規表現は非常に読みづらくなりがちですが、 /some_expression/x のように末尾に x を付けることで、
- 内部の空白、改行文字を無視する
- # から始まるコメントを無視する
ようになり、複数行にわたってコメントを付けながら正規表現を書けます。
例えば、
p '[a]' =~ /
\[ # 開き括弧
. # 任意の1文字
\] # 閉じ括弧
/x # x が付いてる!
# => 0 # マッチした位置を返す (失敗なら nil)
# ruby では !!0 == true であることに注意しましょう
は、
p '[a]' =~ /\[.\]/
# => 0
と同じ意味になります。
文字クラスの中の # や空白はエスケープの必要がない†
上記3つのルールの他にもいくつか知っておくべきルールがあります。
- 正規表現中に # や空白を含めるには \ でエスケープすればよい
- ただし、[ ] で囲まれた文字クラスの中の # や空白はエスケープの必要がない
- コメント中に / があれば正規表現の終了を意味するため、\ でエスケープしなければならない
p ' #'.match(/[ #]*/x) # この 空白 や # は通常の文字扱い
# => #<MatchData " #">
p ' #'.match(/ #/x) # こちらの 空白 や # はコメント扱い
# => #<MatchData "">
p ' #'.match(//) # これと同じことになる
# => #<MatchData "">
p / # \/ # コメント中でも \/ のようにエスケープが必要
/x
# => / # \/
# /x
2. について Perl では [ ] の中の空白やコメントも除去されるそうです。
一方、Python では Ruby と同様だそうです。
https://github.com/k-takata/Onigmo/issues/43
名前付きグループ (?<name> )†
正規表現をグループ化したものに名前を付けておいて、後で参照できます。
p "aabaac" =~
/
(?<cap1>..) # 'aa' にマッチして、cap1 という名でキャプチャする
. # 'b' にマッチ
\k<cap1> # cap1 という名でキャプチャした 'aa' にマッチ
/x
# => 0
p Regexp.last_match.names
# => ["cap1"]
p Regexp.last_match['cap1']
# => "aa"
p Regexp.last_match[:cap1] # Symbol でも取り出せます
# => "aa"
p Regexp.last_match.length
# => 2
p Regexp.last_match[0]
# => "aabaa"
p Regexp.last_match[1] # 数字でも取り出せます
# => "aa"
p $0 # $0 は実行中のプログラムの名前
# => "irb"
p $& # $& がマッチした文字列全体
# => "aabaa"
p $1
# => "aa"
MatchData へのインデックスは String でも Symbol でも良い†
上記の通り
名前付きグループによるキャプチャを通常通り数値で参照することも可能†
上記の通り
名前無しグループはキャプチャしなくなる†
名前付きグループを使うと、名前無しグループはまったくキャプチャされなくなります。
p "abcd" =~ /(?<a>.)(.)(?<b>.)(.)/
# => 0
p Regexp.last_match
# => #<MatchData "ab" a:"a" b:"c">
p Regexp.last_match.length
# => 3
p Regexp.last_match[1]
# => "a"
p Regexp.last_match[2]
# => "c"
p "abcdc" =~ /(?<a>.)(.)(?<b>.)(.)\1/
# SyntaxError: (24: numbered backref/call is not allowed. (use name): /(?<a>.)(.)(?<b>.)(.)\k<-1>/
# from /usr/bin/2:in `<main>'
p "abcdc" =~ /(?<a>.)(.)(?<b>.)(.)\k<-1>/
# SyntaxError: (25: numbered backref/call is not allowed. (use name): /(?<a>.)(.)(?<b>.)(.)\k<-1>/
# from /usr/bin/2:in `<main>'
正規表現中では名前付きグループを数値で後方参照できない†
名前付きグループを使うと正規表現中では \1 や \k<-1>, \g<1> などが SyntaxError を投げるようになります。
マニュアルの記述はここです。
https://github.com/k-takata/Onigmo/blob/master/doc/RE.ja#L313
以下でも確認します。
p "aba" =~
/
(?<cap1>.) # 'a' にマッチ
. # 'b' にマッチ
\1 # 1番目の括弧でキャプチャした 'a' にマッチ? → できない!
/x
# SyntaxError: (5: numbered backref/call is not allowed. (use name): /
# (?<cap1>..) # 'aa' にマッチ
# . # 'b' にマッチ
# \1 # 'aa' にマッチ???
# /x
# from /usr/bin/2:in `<main>'
p "aabaac" =~
/
(..) # 'aa' にマッチ
. # 'b' にマッチ
\1 # 1番目の括弧でキャプチャした 'aa' にマッチ → 名前が付いていなければOK
/x
# => 0
名前付きキャプチャの例†
p "abcabcabcabc" =~ /(...)\k<-1>*/ # 始めの3文字の繰り返し
# => 0
p '<tag param="value">bra bra bra</tag>' =~
/
< # <
([^>\s]+) # タグ名
[^>]* # パラメータ部分
> # >
[^<]* # innerHTML
<\/\k<-1>> # 閉じタグ
/x
# => 0
同じ名前が複数回現れるときは最後のキャプチャを返す†
p m = "ab".match /(?<a>.)(?<a>.)/
# => #<MatchData "ab" a:"a" a:"b">
p m.names
# => ["a"]
p m[:a]
# => "b"
最後のキャプチャの値が得られるみたいです?
#<MatchData "ab" a:"a" a:"b"> の中に a:"a" があるのが気になりますが・・・
(追記) 鬼雲の説明書き によると、 同じ名前で複数ある場合、上記のようにキャプチャ値の後方参照はできるものの、 下記にある部分式呼び出し(正規表現自体の後方参照)はできなくなるそうです。
複雑な文字列を要素に分解†
名前付きグループを使うと複雑な文字列を分かりやすく要素に分解できます。
p result = "Osamu Takeuchi <osamu@big.jp>".match /^(?<name>.+)\s<(?<email>[^@]+@[^@]+)>$/
# => #<MatchData "Osamu Takeuchi <osamu@big.jp>" name:"Osamu Takeuchi" email:"osamu@big.jp">
p result[:name]
# => "Osamu Takeuchi"
p result[:email]
# => "osamu@big.jp"
ん〜でも、、、
p "Osamu Takeuchi <osamu@big.jp>" =~ /^(.+)\s<([^@]+@[^@]+)>$/
# => 0
p $1
# => "Osamu Takeuchi"
p $2
# => "osamu@big.jp"
数値の方がシンプルかも???
相対的な後方参照 \k<-n>†
上記の例を、相対位置を使って書けば以下のようになります。
p "aabaac" =~
/
(..) # 'aa' にマッチ
. # 'b' にマッチ
\k<-1> # 直前の括弧でキャプチャした 'aa' にマッチ
/x
# => 0
2つ前の括弧なら \k<-2> とか。
正規表現を結合するときに便利†
相対位置で指定しておくことにより、複数の正規表現を結合した場合にも、グループの出現位置によらず、 後方参照との対応関係が崩れなくなります。
p regexp = /(..).\k<-1>/
# => /(..).\k<-1>/
p /#{regexp}#{regexp}/ # 2つ繋げても意味が変わらない
# => /(?-mix:(..).\k<-1>)(?-mix:(..).\k<-1>)/
MatchData へも負のインデックスを与えられる†
p "Osamu Takeuchi <osamu@big.jp>" =~ /^(.+)\s<([^@]+@[^@]+)>$/
# => 0
p Regexp.last_match[-1] # 最後にマッチしたキャプチャ
# => "osamu@big.jp"
p Regexp.last_match[-2] # その前にマッチしたキャプチャ
# => "Osamu Takeuchi"
正規表現自体を後方参照 \g<-n>†
上で見てきた \数値 や \k<数値>, \k<名前> といった参照は、 「指定されたグループにキャプチャされた文字列」にマッチする正規表現でしたが、
\g<名前>, \g<数値> といった参照は、「指定されたグループの正規表現自体」を参照する正規表現です。
どういうことかというと、こういうことです。
p "ab" =~ /(.)\k<-1>/ # \k<-1> が展開されると /(.)a/ になる
# => nil
p "ab" =~ /(.)\g<-1>/ # \g<-1> が展開されると /(.)./ になる
# => 0
注) 呼び出された式集合のオプション状態が呼出し側のオプション状態と異なっている
とき、呼び出された側のオプション状態が有効である。
https://github.com/k-takata/Onigmo/blob/master/doc/RE.ja#L381
正規表現のサブルーチンのように使うことができます†
p "ba" =~
/
(?<a>a){0} # 正規表現 \g<a> を定義
(?<b>b){0} # 正規表現 \g<b> を定義
\g<b> # \g<b> を呼び出し
\g<a> # \g<a> を呼び出し
/x
# => 0
定義時に {0} を付けておくことで、定義部分がマッチしないようにしています。
同じ表現を複数回含む複雑な正規表現を簡易化して書けると思います。
実際に複数回記述するのに比べて、同じ表現を繰り返し呼ぶ方が 内部的に省メモリ化できるんだと思います。
こちらにも同様の例がありました。
https://github.com/k-takata/Onigmo/blob/master/doc/RE.ja#L347
正規表現が複数回呼ばれたときの内部キャプチャ†
p "ab" =~
/
(?<a>(.))
\g<a> # \g<a> を呼び出し
/x
# => 0
p Regexp.last_match
# => #<MatchData "ab" a:"b">
最後のキャプチャ内容が返されるようです。
正規表現の再帰呼び出し†
\g< > の書式は、自身を含むグループや、自身より後に出てくるグループを参照することもできます。
自身より後を参照する例:
\g<+1> により、グループ番号1の (c) ではなく、直後の ([ab]) を参照しています
p "cbcbaa" =~ /(c)\g<+1>([ab])/
# => 2
特に、自身を含むグループを参照することにより、「正規表現の再帰呼び出し」 が可能になります。
通常の正規表現では、与えられた文字列の中で 「ネスト可能な括弧の対応が取れているかどうか」 をチェックすることはできませんが、\g<> を使った正規表現の再帰呼び出しを行うことで そういった用途でも正規表現だけで事が済みます。
p '((a) (b(c)))'.match(
/
( # グループ番号 (1) = 対応の取れた括弧から括弧まで
\( # 開き括弧
(?:
\g<-1> # グループ (1) を包含する(対応の取れた括弧の対)
|
[^()] # 括弧以外の文字
)*
\) # 閉じ括弧
)
/x
)
# => #<MatchData "((a)((b)))" 1:"((a)((b)))">
ライブラリのバグ?†
上記の例で括弧内の値をキャプチャしようと以下のようにしたところ、
p "(abc)(abc)" =~ /(\(((?:\g<-2>|[^(])*)\))/
# => 0
p $&
# => "(abc)"
p $1
# => "(abc)"
p $2
# ArgumentError: negative string size (or size too big)
というエラーが出てしまいました。
$& や $1 は正しく取れているのを見てもマッチ自体は成功しているのですが、 $2 が正しくキャプチャされず、内部で不正な値になってしまっているようです。
このマッチについては [] 内に ) を含めると正しく動くように見えました。
p "(abc)(abc)" =~ /(\(((?:\g<-2>|[^()])*)\))/
# => 0
p $~
# => #<MatchData "(abc)" 1:"(abc)" 2:"abc">
ところが括弧をネストさせてみると、
p "((abc)(abc))" =~ /(\(((?:\g<-2>|[^()])*)\))/
# => 0
p $~
# => #<MatchData "((abc)(abc))" 1:"((abc)(abc))" 2:"abc)">
となって、$2 == "abc)" は明らかに不正な値です。
現バージョンに関しては、正規表現の再帰呼び出しから内部で キャプチャされた値を取り出すのは難しいのかもしれません。
本件について、正規表現ライブラリ Onigumo の開発サイトで報告しました。
https://github.com/k-takata/Onigmo/issues/48
条件付き正規表現 (?(n) )†
詳しくはこちらなど。
http://www.rexegg.com/regex-disambiguation.html#conditionals
キャプチャ番号 n がマッチしたときに限って評価されるグループを (?(n) ) あるいは 名前を使って (?(<name>) ) の形で記述できます。
p '[abc]'.match(
/
(\[)? # 省略可能な開き括弧 = グループ番号 1
.* # 内容
(?(1)\]) # 開き括弧があったときのみ閉じ括弧が必要
/x
)
# => #<MatchData "[abc]" 1:"[">
p '[abc]'.match(
/
(?<bracket>\[)? # 省略可能な開き括弧 = グループ番号 1
.* # 内容
(?(<bracket>)\]) # 開き括弧があったときのみ閉じ括弧が必要
/x
)
=> #<MatchData "[abc]" bracket:"[">
残念ながら現状では ?(-1) のようには書けません。
https://github.com/k-takata/Onigmo/blob/master/doc/RE.ja#L290
p '[abc]'.match(
/
(\[)? # 省略可能な開き括弧
.* # 内容
(?(-1)\]) # 開き括弧があったときのみ閉じ括弧が必要
/x
)
# SyntaxError: (6: invalid conditional pattern: /
# (\[)? # 省略可能な開き括弧
# .* # 内容
# (?(-1)\]) # 開き括弧があったときのみ閉じ括弧が必要
# /x
# from /usr/bin/1:in `<main>'
?(-1) の形式がダメなため、正規表現を後から連結して使うような用途には使えません。
代替案†
上記の例を相対指定で行うための代替案として以下が考えられます。
p ['[abc]', 'abc'].map { |s|
s.match(
/
\[(.*)\] # 括弧入りの文字列
|
\g<-1> # 括弧なしの文字列
/x
)
}
# => [#<MatchData "[abc]" 1:"abc">, #<MatchData "abc" 1:"abc">]
どちらもキャプチャグループ1で "abc" をキャプチャできているあたり、\g<-1> のありがたみが感じられます。
確認†
p "aaab".match /a*?b/ # 前方には短くならない
# => #<MatchData "aaab">
p "".match /(?:()|()|()|()|())*/ # トライアンドロールバック
# => #<MatchData "" 1:"" 2:nil 3:nil 4:nil 5:nil>
p "".match /(?:()|()|()|()|())*\3/ # トライアンドロールバック
# => #<MatchData "" 1:"" 2:"" 3:"" 4:nil 5:nil>