Ruby の正規表現を備忘録としてまとめてみた
Ruby の正規表現は多彩かつ強力で, Ruby のプログラミングで欠かせないものです. ただ使い方が多岐にわたるので, 細かい使い方を忘れてしまいがちな自分への備忘録という意味も込めて, まとめてみました.
この記事のサンプルコードは, 次の環境での動作を確認しております:
- Ruby
2.5.1
Regexp
の作り方と使い方
正規表現パターンである Regexp
のインスタンスオブジェクトの作り方と使い方です.
正規表現パターンの作り方
//
, %r{}
, Regexp::new
, Regexp::compile
で, Regexp
オブジェクトを作れます:
/\w+/ # => /\w+/
%r{\w+} # => /\w+/
Regexp.new('\w+') # => /\w+/
Regexp.compile('\w+') # => /\w+/
2 つ目の %r{}
は, /
をマッチさせる時, エスケープする必要がないので便利です:
%r{/\d+/\d+/\d+/}.match('/2018/08/30/') # => #<MatchData "/2018/08/30/">
4 つ目の Regexp::compile
は Regexp::new
のエイリアスとなっており, Ruby らしいですね.
正規表現パターンの使い方
Regexp
オブジェクトを正規表現パターンとして, 文字列に対しての使い方です.
Regexp#match
で MatchData
を取得する
Regexp#match
もしくは String#match
で, パターンにマッチした場合, MatchData
オブジェクトが返り, マッチしなかった場合, nil
が返ります:
/\w+/.match('hello') # => #<MatchData "hello">
/\W+/.match('hello') # => nil
'hello'.match(/\w+/) # => #<MatchData "hello">
'hello'.match(/\W+/) # => nil
match
メソッドの第二引数に整数を渡すと, 検索する開始位置を指定できます:
/\w+/.match('hello', 1) # => #<MatchData "ello">
'hello'.match(/\w+/, 1) # => #<MatchData "ello">
ブロックを渡すと, パターンが見つかった場合, 第一ブロックパラメータに MatchData
オブジェクトが渡されます:
/\w+/.match('hello') do |m|
m # => #<MatchData "hello">
end
'hello'.match(/\w+/) do |m|
m # => #<MatchData "hello">
end
MatchData#[0]
で, パターンにマッチした文字列を取得できます:
m = /\w+/.match('hello') # => #<MatchData "hello">
m[0] # => "hello"
MatchData
はいろいろなインスタンスメソッドがあります:
MatchData.instance_methods - Object.instance_methods
# => [:post_match, :string, :values_at, :to_a, :[], :named_captures, :length, :size, :names, :regexp, :offset, :begin, :end, :captures, :pre_match]
Regexp#=~
でインデックスを取得する
Regexp#=~
や String#=~
を使うと, パターンにマッチした位置を取得します:
/\w+/ =~ 'hello' # => 0
/ / =~ 'hello world' # => 5
'hello' =~ /\w+/ # => 0
'hello world' =~ / / # => 5
名前付きキャプチャで, ローカル変数を割り当てる
Regxp#=~
で, (?<name>)
を使って文字列をキャプチャした場合, name
がローカル変数に割り当てられます:
/(?<words>\w+)/ =~ 'hello'
words # => "hello"
String#=~
で, (?<name>)
を使っても name
はローカル変数に割り当てられません:
'hello' =~ /(?<words>\w+)/
words
# => #<NameError: undefined local variable or method `words' for main:Object>
#{...}
で式展開をパターンで行なった場合も同様です:
/(?<words>#{ '\w' + '+' })/ =~ 'hello'
words
# => #<NameError: undefined local variable or method `words' for main:Object>
Regexp
リテラルが, =~
の左辺にない場合も同様です:
r = /(?<name>\w+)/
r =~ 'hello'
words
# => #<NameError: undefined local variable or method `words' for main:Object>
Regexp#!~
でマッチの成否を確認する
Regexp#!~
や String#!~
で, パターンにマッチしない場合 true
, マッチする場合 false
を返します:
/\W+/ !~ 'hello' # => true
/\w+/ !~ 'hello' # => false
'hello' !~ /\W+/ # => true
'hello' !~ /\w+/ # => false
ただ純粋にパターンのマッチの成否を確認する場合は, Regexp#match?
や String#match?
の方が適しているのかもしれません:
/\w+/.match?('hello') # => true
/\W+/.match?('hello') # => false
'hello'.match?(/\w+/) # => true
'hello'.match?(/\W+/) # => false
ri
コマンドを使ってみる
Regexp#match
, Regexp#=~
, Regexp#!~
, Regexp#match?
, MatchData#post_match
などのインスタンスメソッドに関するより詳しい使い方を知りたいという時, ri
コマンドが使える場合, ri
コマンドを使ってそれらのドキュメントを見てみるといいかもしれません.
実際に ri Regexp#match
とターミナルで入力した時に表示される内容です:
= Regexp#match
(from ruby core)
------------------------------------------------------------------------
rxp.match(str) -> matchdata or nil
rxp.match(str,pos) -> matchdata or nil
------------------------------------------------------------------------
Returns a MatchData object describing the match, or nil if there was no
match. This is equivalent to retrieving the value of the special
variable $~ following a normal match. If the second parameter is
present, it specifies the position in the string to begin the search.
/(.)(.)(.)/.match("abc")[2] #=> "b"
/(.)(.)/.match("abc", 1)[2] #=> "c"
If a block is given, invoke the block with MatchData if match succeed,
so that you can write
/M(.*)/.match("Matz") do |m|
puts m[0]
puts m[1]
end
instead of
if m = /M(.*)/.match("Matz")
puts m[0]
puts m[1]
end
The return value is a value from block execution in this case.
このように実例付きの詳細な説明が表示されるので, 僕はとても重宝させてもらっています.
パターンの書き方
今度は Regexp
オブジェクトを作る時のパターンの書き方です.
基本的なメタ文字
基本的なメタ文字 \
, |
, ()
, []
に関してです.
\
の場合
バックスラッシュ \
を使うと, 続く一文字と合わせて, メタ文字として認識されます:
/\w/.match('a')
もしくは, そのままではメタ文字として認識される文字をそのままの文字としてエスケープできます:
/\/\d+\/\d+\/\d+\//.match('/2018/08/30/') # => #<MatchData "/2018/08/30/">
そのままの \
にマッチさせる場合, \\
となります:
/\\/.match('\\') # => #<MatchData "\\">
|
の場合
縦棒 |
を使うと, パターンのマッチが失敗した場合, もう一方のパターンを試みることができます:
r = /Alfa|Bravo/
r.match('Alfa') # => #<MatchData "Alfa">
r.match('Bravo') # => #<MatchData "Bravo">
|
で区切れるのは 2 つまでと決まっているわけではありません:
r = /Alfa|Bravo|Charlie/
r.match('Alfa') # => #<MatchData "Alfa">
r.match('Bravo') # => #<MatchData "Bravo">
r.match('Charlie') # => #<MatchData "Charlie">
()
の中で |
を使うと, ()
の中での選択になります:
r = /I have (a pen|\d+ pens)\./
r.match('I have a pen.') # => #<MatchData "I have a pen." 1:"a pen">
r.match('I have 2 pens.') # => #<MatchData "I have 2 pens." 1:"2 pens">
()
の場合
丸括弧 ()
を使うと, 囲まれたパターンをひとかたまりとすることができます:
/(\w\w)+/.match('even') # => #<MatchData "even" 1:"en">
また \n
などで後方参照できます:
/(\w+)-\1/.match("so-so") # => #<MatchData "so-so" 1:"so">
[]
の場合
角括弧 []
を使うと, 文字クラスのパターンを作れます.
[ab]
とすると a
もしくは b
のどちらかにマッチします:
r = /[ab]/
r.match('a') # => #<MatchData "a">
r.match('b') # => #<MatchData "b">
[a-z]
とすると, a
から z
までの 26 文字のどれか 1 文字にマッチします:
r = /[a-z]/
('a'..'z').all? { |x| r.match(x) } # => true
('A'..'Z').none? { |x| r.match(x) } # => true
[^a-z]
とすると, a
から z
までの 26 文字以外の 1 文字にマッチします:
r = /[^a-z]/
('A'..'Z').all? { |x| r.match(x) } # => true
('a'..'z').none? { |x| r.match(x) } # => true
[a-z&&[^mn]]
とすると, a
から z
までの文字クラスから m
と n
が除かれた文字クラスになります:
r = /[a-z&&[^mn]]/
('a'..'z')
.all? { |x| !('m'..'n').include?(x) ? r.match(x) : !r.match(x) } # => true
('A'..'Z').none? { |x| r.match(x) } # => true
バックスラッシュ記法
ダブルクオートで囲まれた String リテラルのように, 次のバックスラッシュ記法を使えます:
\t
– 水平タブ\v
– 垂直タブ\n
– 改行\r
– 復帰\b
– バックスペース\f
– 改ページ\a
– ベル\e
– エスケープ文字\nnn
– 8 進数表現\xHH
– 16 進数表現\uHHHH
– ユニコード文字\C-x
– 制御文字\M-x
– メタ\M-\C-x
– メタ制御文字
使用例です:
/\t\v\n\r[\b]\f\a\e/.match("\t\v\n\r\b\f\a\e")
# => #<MatchData "\t\v\n\r\b\f\a\e">
/\134/.match('\\') # => #<MatchData "\\">
/\x5c/.match('\\') # => #<MatchData "\\">
/\u005C/.match('\\') # => <MatchData "\\">
\b
はそのまま使うと, 単語の境界にマッチするアンカとして認識されるので, バックスペースの \b
としてマッチさせる場合, 文字クラス [\b]
として使う必要があります.
また String リテラルの "\s"
は空白 (U+0020
) ですが, 正規表現のパターン /\s/
の場合, 空白文字にマッチするメタ文字となります.
文字クラスの略記法
文字クラスの略記法として, 次のメタ文字があります:
.
–\n
を除いた全ての文字/./m
– 全ての文字 (マルチラインモードのみ)/\w/
– 単語構成文字 ([a-zA-Z0-9]
)/\W/
– 非単語構成文字 ([^a-zA-Z0-9]
)/\d/
– 10 進数字 ([0-9]
)/\D/
– 非 10 進数字 ([^0-9]
)/\h/
– 16 進数字 ([0-9a-fA-F]
)/\H/
– 非 16 進数字 ([^0-9a-fA-F]
)/\s/
– 空白文字 ([ \t\r\n\f\v]
)/\S/
– 非空白文字 ([^ \t\r\n\f\v]
)
使用例です:
/.ello/.match('hello') # => #<MatchData "hello">
/\w+./m.match("hello\n") # => #<MatchData "hello\n">
/\w+ \w+/.match('hello world') # => #<MatchData "hello world">
/\w+\W\w+/.match('hello world') # => #<MatchData "hello world">
/\d \+ \d = \d/.match('1 + 1 = 2') # => #<MatchData "1 + 1 = 2">
/\d\D+\d\D+\d/.match('1 + 1 = 2') # => #<MatchData "1 + 1 = 2">
/\h+ \h+ \h+/.match('65e5 672c 8a9e') # => #<MatchData "65e5 672c 8a9e">
/\h+\H\h+\H\h+/.match("65e5 672c 8a9e") # => #<MatchData "65e5 672c 8a9e">
/\w+\s\w+/.match("hello\nworld") # => #<MatchData "hello\nworld">
/\S+\s\S+/.match("Hello\nWorld") # => #<MatchData "Hello\nWorld">
特殊なメタ文字
\R
と \X
という特殊なメタ文字もあります:
/\R/
– 改行文字 ((?>\x0D\x0A|[\x0A-\x0D])
など)/\X/
– Unicode の結合文字のシーケンス ((?>\x0D\x0A|(?m:.))
など)
使用例です:
/\R+/.match("\r\n\n\r") # => #<MatchData "\r\n\n\r">
/\X+/.match("\r\nabcABC123") # => #<MatchData "\r\nabcABC123">
アンカ
アンカという文字幅が 0 で, 文字と文字の間にマッチするものがあります:
^
– 行頭にマッチします.$
– 行末にマッチします.\A
– 文字列の先頭にマッチします.\Z
– 文字列の末尾にマッチし, もし末尾が改行の場合, その手前でマッチします.\z
– 文字列の末尾にマッチします.\G
– 現在のマッチが試みられる先頭にマッチします.\b
– 単語の境界の場合, マッチします.\B
– 単語の境界でない場合, マッチします.(?=pat)
– 続く文字列がpat
にマッチすることを保証します. (肯定先読み)(?!pat)
– 続く文字列がpat
にマッチしないことを保証します. (否定先読み)(?<=pat)
– 手前の文字列がpat
にマッチすることを保証します. (肯定後読み)(?<!pat)
– 手前の文字列がpat
にマッチしないことを保証します. (否定後読み)\K
– 手前のパターンがマッチすることを保証します.
アンカはあくまでアンカなので, 先読み, 後読みの (?=pat)
, (?!pat)
, (?<=pat)
, (?<!pat)
の pat
や \K
の手前のパターンはマッチ済みの文字列に含まれません.
文字そのものにはマッチしない特殊なメタ文字ですが, とても便利です:
/^\w+\n^\w+/.match("hello\nworld") # => #<MatchData "hello\nworld">
/\w+$\n\w+$/.match("hello\nworld") # => #<MatchData "hello\nworld">
/\A\w+/.match('hello') # => #<MatchData "hello">
/\w+\Z/.match("hello\n") # => #<MatchData "hello">
/\w+\z/.match('hello') # => #<MatchData "hello">
/\G\w+/.match('hello') # => #<MatchData "hello">
/\b\w+\b \b\w+\b/.match('hello world') # => #<MatchData "hello world">
/\w\B\w\B\w\B\w\B\w/.match('hello') # => #<MatchData "hello">
/\w+(?= \w+)/.match('hello world') # => #<MatchData "hello">
/\w+(?! \W+)/.match('hello world') # => #<MatchData "hello">
/(?<=\w{5} )\w+/.match('hello world') # => #<MatchData "world">
/(?<!\W{5} )\w+/.match('hello world') # => #<MatchData "hello">
/\w+ \K\w+/.match('hello world') # => #<MatchData "world">
\G
はパターンが繰り返しマッチする String#gsub
や String#scan
の最初のマッチの先頭と続くマッチの前のマッチの終了位置にもマッチします:
s = "a\nb\nc"
r = /\G(\w)\n/
s.gsub(r, '\1-') # => "a-b-c"
s.scan(r) # => [["a"], ["b"]]
また Regexp#match
や String#match
のオプショナルな第二引数に指定される検索開始位置にもマッチします:
r = /\G\d/
s = 'yu8mada'
pos = 2
r.match(s) # => nil
r.match(s, pos) # => #<MatchData "8">
s.match(r) # => nil
s.match(r, pos) # => #<MatchData "8">
繰り返し
量指定子と呼ばれるものを使うと, 文字の出現回数を指定することができます.
最大量指定子
最大量指定子は次のものがあります:
*
– 0 回以上+
– 1 回以上?
– 0 回もしくは 1 回{n}
– n 回{n,}
– n 回以上{,n}
– n 回以下{n,m}
– n 回以上, m 回以下
最大量指定子は最大限のマッチを試みます:
/\w*/.match('hello') # => #<MatchData "hello">
/\w+/.match('hello') # => #<MatchData "hello">
/\w?/.match('hello') # => #<MatchData "h">
/\w{2}/.match('hello') # => #<MatchData "he">
/\w{2,}/.match('hello') # => #<MatchData "hello">
/\w{,2}/.match('hello') # => #<MatchData "he">
/\w{1,2}/.match('hello') # => #<MatchData "he">
最小量指定子
?
を最大量指定子に付け加えると, 最小限のマッチとなり, 最小量指定子となります:
/\w*?/.match('hello') # => #<MatchData "">
/\w+?/.match('hello') # => #<MatchData "h">
/\w??/.match('hello') # => #<MatchData "">
/\w{2}?/.match('hello') # => #<MatchData "he">
/\w{2,}?/.match('hello') # => #<MatchData "he">
/\w{,2}?/.match('hello') # => #<MatchData "">
/\w{1,2}?/.match('hello') # => #<MatchData "h">
絶対最大量指定子
+
を最大量指定子に付け加えると, 後のマッチが失敗したとしても, マッチ済みの文字列をバックトラックして手放す事をしない, 絶対最大量指定子となります:
/\w*+/.match('hello') # => #<MatchData "hello">
/\w++/.match('hello') # => #<MatchData "hello">
/\w?+/.match('hello') # => #<MatchData "h">
グループ
グループに関する記法です.
非キャプチャ
(?:)
で囲まれたパターンにマッチする文字列はキャプチャされません:
/(G)(N)(U)'s \2ot \3nix/.match("GNU's Not Unix")
# => #<MatchData "GNU's Not Unix" 1:"G" 2:"N" 3:"U">
/(?:G)(N)(U)'s \1ot \2nix/.match("GNU's Not Unix")
# => #<MatchData "GNU's Not Unix" 1:"N" 2:"U">
\1
などで後方参照する必要のないグループを作る時に便利です.
アトミックグループ
(?>)
で囲まれたパターンにマッチ済みの文字列は, 後のマッチが失敗したとしても, バックトラックして手放されません:
/(?>\w+)o/.match('hello') # => nil
(?>)
は一般的に正規表現パターンを最適化する目的で使われます:
s = 'a' * 31 + ',' + 'a' # => "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,a"
Benchmark.bm(17) do |x|
x.report('w/o atomic group') do
/(a+)*$/.match(s) # => #<MatchData "a" 1:"a">
end
x.report('with atomic group') do
/(?>a+)*$/.match(s) # => #<MatchData "a">
end
end
ベンチマークの結果です:
user system total real
w/o atomic group 111.696963 0.057142 111.754105 (111.804440)
with atomic group 0.000011 0.000001 0.000012 ( 0.000010)
メタ文字の +
と *
を合わせて使ってしまうと, これほどまで処理時間に差が出るのですね.
コメント
(?#)
という記法を使うと, パターンの中でコメントを書くことができます:
/\w+(?#hello)/.match('hello') # => #<MatchData "hello">
/\w+(?#hello) \w+(?#world)/.match('hello world') # => #<MatchData "hello world">
ただ (?#)
は //x
によるフリースペースモードでは使うことができません.
非包含演算子 (実験的)
(?~)
という記法を使うと, パターンにマッチする文字列を含んでいない文字列にマッチします.
(?:(?!).)*
という記法と似ていますが, その記法よりも簡単に書くことができます.
C スタイルのコメント /* comment */
にマッチさせるような場合に使われます:
r = /\A\/\*(?~\*\/)\*\/\z/
r.match('/**/') # => #<MatchData "/**/">
r.match('/* Good comment */') # => #<MatchData "/* Good comment */">
r.match('/**/ */') # => nil
r.match('/* Bad comment */ */') # => nil
キャプチャ
()
や (?<name>)
を使った文字列のキャプチャに関するものです.
普通のキャプチャ
()
でパターンを囲むと, そのパターンにマッチする文字列がキャプチャされ, \n
で後方参照できます:
/he(\w)\1(\w) w\2r\1d/.match('hello world')
# => #<MatchData "hello world" 1:"l" 2:"o">
Regexp#match
で返る MatchData
から MatchData#[n]
でキャプチャした文字列を取得できます:
m = /(\w+) (\w+)/.match('hello world')
# = > #<MatchData "hello world" 1:"hello" 2:"world">
m[1] # => "hello"
m[2] # => "world"
後方参照する時, \k<-n>
や \k'-n'
で後方参照した位置から左側のグループを左にかけて相対的に指定できます:
/(\w)(\w)\k<-1>\k<-2> (\w)(\w)\k<-1>\k<-2>/.match('abba cddc')
# => #<MatchData "abba cddc" 1:"a" 2:"b" 3:"c" 4:"d">
名前付きのキャプチャ
(?<name>)
や (?'name')
でパターンを囲むと, そのパターンにマッチする文字列を名前付きでキャプチャでき, \k<name>
や \k'name'
で後方参照できます:
/(?<so>\w+)-\k<so>/.match('so-so') # => #<MatchData "so-so" so:"so">
Regexp#match
で返る MatchData
から MatchData#[:name]
で, 名前付きでキャプチャした文字列を取得できます:
m = /(?<hello>\w+)/.match('hello') # => #<MatchData "hello" hello:"hello">
m[:hello] # => "hello"
/(?<name>pat)/#=~
とすると, pat
にマッチした文字列がローカル変数 name
に割り当てられます:
/(?<hello>\w+)/ =~ 'hello' # => 0
hello # => "hello"
部分呼び出し
()
でパターンを囲み, \g<n>
や \g'n'
を使うと, その囲んだパターンの部分呼び出しを行います:
/(\w+) \g<1>/.match('hello world') # => #<MatchData "hello world" 1:"world">
名前付きのグループ (?<name>)
や (?'name')
でも, \g<name>
や \g'name'
によって同じように部分呼び出しを行います:
/(?<words>\w+) \g<words>/
.match('hello world') # => #<MatchData "hello world" words:"world">
相対的指定
\g<-n>
や \g<+n>
もしくは \g'-n'
や \g'+n'
という記法で, 部分呼び出しするグループを相対的に指定することができます.
\g<-n>
や \g'-n'
の場合, その位置から左側のグループを左にかけて指定していきます:
/(\w+) (\d+) \g<-1> \g<-2>/.match('abc 123 456 def')
# => #<MatchData "abc 123 456 def" 1:"def" 2:"456">
\g<+n>
や \g'+n'
の場合, その位置から右側のグループを右にかけて指定していきます:
/\g<+1> \g<+2> (\w+) (\d+)/.match('abc 123 def 456')
# => #<MatchData "abc 123 def 456" 1:"def" 2:"456">
\g<-n>
と \g<+n>
を同時に使うこともできます:
/\g<+1> (\w+) \g<-1>/.match('Alfa Bravo Charlie')
# => #<MatchData "Alfa Bravo Charlie" 1:"Charlie">
再帰的記述
\g<n>
や \g<name>
などの部分呼び出しを使うと, 再帰的記述が可能になります.
空の HTML タグに再帰的にマッチする例です:
r = /(?<tag><.+?>\g<tag>*<\/.+?>)*/
m = r.match(<<HTML.gsub(/[[:space:]]/, ''))
<div>
<div></div>
<div></div>
</div>
<div>
<div></div>
<div></div>
</div>
HTML
# => #<MatchData "<div><div></div><div></div></div><div><div></div><div></div></div>" tag:"<div><div></div><div></div></div><div><div></div><div></div></div>">
m[0]
# => "<div><div></div><div></div></div><div><div></div><div></div></div>"
再帰的記述をする時の注意点として, 次のように \g<tag>
の再帰的な部分呼び出しが永久に終わらないようにしてしまうと:
r = /(?<tag><\w+>\g<tag><\/\w+>)*/
次のようなエラーが表示されます:
never ending recursion: /(?<tag><.+?>\g<tag><\/.+?>)*/
ネストレベル付きの後方参照
少しだけ複雑な後方参照の仕方です.
再帰的記述の中で, \k<n+level>
や \k<n-level>
などを使うと, ネストレベルを指定してグループの後方参照を行えます.
\k<n+0>
というのは, その後方参照と同じネストレベルの n
番目のグループを後方参照するという意味になります.
また \k<n+1>
の場合, その後方参照よりネストレベルが 1 つ深いグループを後方参照することになり, \k<n-1>
の場合, ネストレベルが 1 つ浅いグループを後方参照することになります.
なお, n >= 1
という条件になります.
後方参照する時のネストレベルを 0
として, 回文にマッチする例です:
r = /\A(|.|(?:(.)\g<1>\k<2+0>))\z/
r.match('akasaka') # => #<MatchData "akasaka" 1:"akasaka" 2:"a">
ネストレベル +1
の後方参照を使った, 簡易な HTML タグにマッチする例です:
r = /\A
(?<element> \g<stag> \g<content>* \g<etag> ){0}
(?<stag> < \g<name> > ){0}
(?<name> [a-zA-Z]+ ){0}
(?<content> \w+ | \g<element> ){0}
(?<etag> <\/ \k<name+1> > ){0}
\g<element>*
\z/x
html = <<HTML.gsub(/[[:space:]]/, '')
<ul>
<li>item1</li>
<li>item2</li>
</ul>
<ul>
<li>item3</li>
<li>item4</li>
</ul>
HTML
r.match(html) do |m|
m[0]
end
# => "<ul><li>item1</li><li>item2</li></ul><ul><li>item3</li><li>item4</li></ul>"
(?<name>)
は \g<stag>
から \g<name>
と 2 つネストしているので, (?<etag>)
からそれを後方参照する場合, (?<etag>)
は \g<etag>
から 1 つネストしているので, 2 - 1 = 1
で \k<name+1>
とする必要があります.
次のような方法でもネストレベルを指定して後方参照することができます:
\k'n+level'
\k'n-level'
\k<-n+level>
\k<-n-level>
\k'-n+level'
\k'-n-level'
\k<name+level>
\k<name-level>
\k'name+level'
\k'name-level'
n
は n >= 1
という条件です.
条件分岐
(?(cond)pat)
もしくは (?(cond)truepat|falsepat)
という形で, 条件 cond
が真ならば pat
もしくは truepat
が使われ, 偽ならば, 何も使われないか falsepat
が使われます.
cond
は後方参照するように \n
の n
もしくは, \k<name>
の <name>
を指定します.
cond
に指定した n
や <name>
などのグループのパターンがマッチしていれば, 真となり, マッチしていなければ, 偽となります.
Yes
で始まると , I do.
と続き, No
で始まると , I don't.
と続く, 正しい相槌のみにマッチする例です:
r = /(?:Yes|(No)), I do(?(1)n't)\./
r.match("Yes, I do.") # => #<MatchData "Yes, I do." 1:nil>
r.match("No, I don't.") # => #<MatchData "No, I don't." 1:"No">
r.match("Yes, I don't.") # => nil
r.match("No, I do.") # => nil
一人称, 二人称の場合は動詞が have
となり, 三人称の場合は has
となる正しい文法のみにマッチする例です:
r = /(?:(I|We|You|They)|He|She|It) ha(?(1)ve|s) a pen\./
r.match('I have a pen.') # => #<MatchData "I have a pen." 1:"I">
r.match('We have a pen.') # => #<MatchData "We have a pen." 1:"We">
r.match('You have a pen.') # => #<MatchData "You have a pen." 1:"You">
r.match('They have a pen.') # => #<MatchData "They have a pen." 1:"They">
r.match('He has a pen.') # => #<MatchData "He has a pen." 1:nil>
r.match('She has a pen.') # => #<MatchData "She has a pen." 1:nil>
r.match('It has a pen.') # => #<MatchData "It has a pen." 1:nil>
r.match('I has a pen.') # => nil
r.match('We has a pen.') # => nil
r.match('You has a pen.') # => nil
r.match('They has a pen.') # => nil
r.match('He have a pen.') # => nil
r.match('She have a pen.') # => nil
r.match('It have a pen.') # => nil
POSIX 文字クラス
文字クラスと同じようなものとして, POSIX 文字クラスというものがあります:
[[:alnum:]]
– 英数字[[:alpha:]]
– 英字[[:blank]]
– スペースとタブ[[cntrl]]
– 制御文字[[:digit:]]
– 数字[[:graph:]]
– 非空白文字 (空白文字と制御文字を含まない文字)[[:lower:]]
– 小文字英字[[:print:]]
– スペースを含んだ[:graph:]
[[:punct:]]
– 句読点[[:space:]]
– 空白文字[[:upper:]]
– 大文字英字[[:xdigit:]]
– 16 進数字
POSIX 文字クラスの利点は, 非 ASCII 文字も包含している点にあります:
/\w+/.match('āīūēō') # => nil
/[[:alnum:]]+/.match('āīūēō') # => #<MatchData "āīūēō">
ヘボン式の長音記号として使われるマクロン ¯
が付いた母音も [[:alnum:]]
などでマッチします.
マッチする文字クラスを POSIX 文字クラスレベルで反転させる場合, [[:^name:]]
とします:
/[[:^alnum:]]+/.match("\s\t\v\n\r\b\f\a\e")
# => #<MatchData " \t\v\n\r\b\f\a\e">
文字クラスの中で &&
を使うと, 積集合となります:
/[[:alnum:]&&[:^upper:]]+/.match("abc123")
# => #<MatchData "abc123">
/[[:alnum:]&&[:^lower:]]+/.match("ABC123")
# => #<MatchData "ABC123">
Ruby は次の非 POSIX 文字クラスもサポートしています.
[[:word:]]
– 次の Unicode 一般カテゴリ: Letter, Mark, Number, Connector_Punctuation[[:ascii:]]
– ASCII 文字
Unicode プロパティ
POSIX 文字クラスのようなものとして, Unicode プロパティというものがあります:
\p{Alnum}
– 英数字\p{Alpha}
– 英字\p{Blank}
– スペースとタブ\p{Cntrl}
– 制御文字\p{Digit}
– 10 進数字\p{Graph}
– 非空白文字 (空白文字と制御文字を含まない文字)\p{Lower}
– 小文字英字\p{Print}
– スペースを含んだ\p{Graph}
\p{Punct}
– 句読点\p{Space}
– 空白文字\p{Upper}
– 大文字英字\p{XDigit}
– 16 進数字\p{Word}
– 次の Unicode 一般カテゴリ: Letter, Mark, Number, Connector_Punctuation\p{ASCII}
– ASCII 文字\p{Any}
– 全ての Unicode 文字\p{Assigned}
– 割り当てられた文字
全ての Unicode プロパティは次の URL から参照できます:
https://github.com/k-takata/Onigmo/blob/master/doc/UnicodeProps.txt
また指定したプロパティがどのような Unicode 文字を表すのかについては, Unicode の仕様を参照していただければと思います.
Unicode プロパティは POSIX 文字クラスと違い, []
の外でも使用できます:
/\p{Alnum}+/.match('āēūēō') # => #<MatchData "āēūēō">
指定の Unicode プロパティを反転させる場合, \p{^name}
もしくは \P{name}
で可能です:
/\p{^Alnum}+/.match("\s\t\v\n\r\b\f\a\e") # => #<MatchData " \t\v\n\r\b\f\a\e">
/\P{Alnum}+/.match("\s\t\v\n\r\b\f\a\e") # => #<MatchData " \t\v\n\r\b\f\a\e">
Unicode プロパティは POSIX 文字クラスに比べて種類が豊富なので, より特定の文字範囲にマッチさせられます:
/\p{Currency_Symbol}+/.match("\u{24 A2 A3 A4 A5}") # => #<MatchData "$¢£¤¥">
文字クラスの中で, 文字クラスの略記法, POSIX 文字クラス, Unicode プロパティを組み合わせることもできます:
r = /[\w&&[:^lower:]&&\p{^Upper}]+/
r.match('1234567890')
#<MatchData "1234567890">
r.match('abcdefghijklmnopqrstuvwxyz') # => nil
r.match('ABCDEFGHIJKLMNOPQRSTUVWXYZ') # => nil
グローバル変数
正規表現関連の便利なグローバル変数があります:
$~
–Regexp.last_match
と同じです.$&
–$~[0]
と同じです.$`
–$~.pre_match
と同じです.$'
–$~.post_match
と同じです.$1
–$~[1]
と同じです.$2
–$~[2]
と同じです.$3
–$~[3]
と同じです.$+
–$~[-1]
と同じです.
使用例です:
/(.)m(.)/.match('yu8mada') # => #<MatchData "8ma" 1:"8" 2:"a">
Regexp.last_match # => #<MatchData "8ma" 1:"8" 2:"a">
$~ # => #<MatchData "8ma" 1:"8" 2:"a">
$& # => "8ma"
$` # => "yu"
$' # => "da"
$1 # => "8"
$2 # => "a"
$3 # => nil
$+ # => "a"
これらのグローバル変数は, スレッドローカル, メソッドローカルです:
def foo
/./.match('a')
$~
end
foo # => #<MatchData "a">
$~ # => nil
また Regexp#match
メソッドに限らず, String#match
, Regexp#=~
, String#=~
, Regexp#!~
, String#!~
でも, 同じようにこれらのグローバル変数が割り当てられます.
オプション
正規表現パターンを作るときに指定できるオプションについてです.
Regexp
リテラルの場合
//mix
や //o
と Regexp
リテラルの後に付け加えることができる 4 つのオプションがあります:
i
– 正規表現パターンが大文字小文字を区別しなくなります:/[a-z]+/i.match('HELLO') # => #<MatchData "HELLO">
m
– マルチラインモードとなり, メタ文字/./
に改行文字"\n"
がマッチするようになります:/hello.world/m.match("hello\nworld") # => #<MatchData "hello\nworld">
x
– フリースペースモードとなり, 空白が無視され,# comment
によるコメントを付けられるようになります:/\A # This is from 'URI::MailTo::EMAIL_REGEXP' [a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+ # Local part @ # At sign [a-zA-Z0-9] # Maybe the first letter of SLD (?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? # Maybe the remaining part of SLD (?: # Non-capturing \. # Period [a-zA-Z0-9] # Maybe the first letter of TLD (?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? # Maybe the remaining part of TLD )* # This group is optional \z/x.match('example@example.com') # => #<MatchData "example@example.com">
o
– 同じパターンの式展開を, 一度だけするようになります:3.times do /#{ puts 'executed'; '[a-z]+' }/.match('hello') end # executed # => #<MatchData "hello"> # executed # => #<MatchData "hello"> # executed # => #<MatchData "hello"> 3.times do p /#{ puts 'executed'; '[a-z]+' }/o.match('hello') end # executed # => #<MatchData "hello"> # => #<MatchData "hello"> # => #<MatchData "hello">
オプションの i
が Regexp::IGNORECASE
の I
で, m
が Regexp::MULTILINE
の M
で, x
が Regexp::EXTENDED
の X
で, o
はおそらく “once” (一回) の “o” なのでしょうか.
部分正規表現のオプション
(?on-off:pat)
という記法を使うと, 部分的に i
, m
x
オプションの有効無効を指定できます:
/(?i:[a-z])[a-z]+/.match('Hello') # => #<MatchData "Hello">
/(?m:\w+.)\w+/.match("hello\nworld") # => #<MatchData "hello\nworld">
/(?x:
\w+ # This matches 'hello'
) \w+/.match('hello world') # => #<MatchData "hello world">
/(?-i:[a-z]+)/i.match('Hello') # => #<MatchData "ello">
/(?-m:\w+.)/m.match("hello\n") # => #<MatchData "hello">
/(?-x:\w+ )\w+/x.match('hello world') # => #<MatchData "hello world">
Regexp.new
の場合
Regexp.new
で Regexp
のパターンを作る場合, Regexp::IGNORECASE
, Regexp::MULTILINE
, Regexp::EXTENDED
を第二引数に渡しても同じことができます:
Regexp.new('', Regexp::IGNORECASE) # => //i
Regexp.new('', Regexp::MULTILINE) # => //m
Regexp.new('', Regexp::EXTENDED) # => //x
//mix
のように複数のオプションを渡す場合は, |
演算子を使うと可能です:
Regexp.new('', Regexp::IGNORECASE | Regexp::MULTILINE | Regexp::EXTENDED)
# => //mix
もしくは Regexp::IGNORECASE
を 1
, Regexp::EXTENDED
を 2
, Regexp::MULTILINE
を 4
に置き換えて指定することもできます:
Regexp.new('', 1) # => //i
Regexp.new('', 2) # => //x
Regexp.new('', 4) # => //m
Regexp.new('', 3) # => //ix
Regexp.new('', 5) # => //mi
Regexp.new('', 6) # => //mx
Regexp.new('', 7) # => //mix
パターンに設定されているオプションに対応する整数を確認する場合, Regexp#options
で確認できます:
Regexp.new('', 1).options # => 1
Regexp.new('', 2).options # => 2
Regexp.new('', 4).options # => 4
Regexp.new('', 3).options # => 3
Regexp.new('', 5).options # => 5
Regexp.new('', 6).options # => 6
Regexp.new('', 7).options # => 7
また true
を渡すと, //i
となります:
Regexp.new('', true) # => //i
正確には false
と nil
以外だと, そのようになります:
Regexp.new('', 'hello') # => //i
正規表現を使えるメソッド
正規表現パターンを引数として渡すことができるメソッドの使用例です.
String#sub
第一引数にパターンを指定して, 第二引数に文字列を指定すると, 一回だけそのパターンにマッチする文字列を, 指定した文字列で置き換えます:
'hello world'.sub(/\w+/, 'word') # => "word world"
第二引数に指定する文字列では \n
や \k<name>
でキャプチャした文字列を埋め込めます:
'hello world'.sub(/(\w+)/, '\1!') # => "hello! world"
'hello world'.sub(/(?<words>\w+)/, '\k<words>!') # => "hello! world"
String#gsub
第一引数にパターンを指定して, 第二引数に文字列を指定すると, そのパターンにマッチする全ての文字列を, 指定した文字列で置き換えます:
'hello world'.gsub(/\w+/, 'word') # => "word word"
String#sub
と同じように, \n
や \k<name>
を第二引数の文字列で使うことができます:
'hello world'.gsub(/(\w+)/, '\1!') # => "hello! world!"
'hello world'.gsub(/(?<words>\w+)/, '\k<words>!') # => "hello! world!"
String#index
第一引数にパターンを指定すると, そのパターンにマッチする文字列の最初のインデックスを取得します:
'abcdef 123456'.index(/\d+/) # => 7
String#scan
第一引数にパターンを指定すると, そのパターンにマッチする文字列を一要素として, 配列を作ります:
'hello world'.scan(/\w+/) # => ["hello", "world"]
String#split
第一引数にパターンを指定すると, そのパターンにマッチする文字列で区切り, 配列を作ります:
"hello world\ncruel world".split(/\s/)
# => ["hello", "world", "cruel", "world"]
パターンでグループを作ってキャプチャすると, キャプチャされた文字列も配列の要素として加わるので, 加えない場合 (?:)
を使います:
'1 + 1 = 2'.split(/ (\+|=) /) # => ["1", "+", "1", "=", "2"]
'1 + 1 = 2'.split(/ (?:\+|=) /) # => ["1", "1", "2"]
まとめ
Ruby の正規表現の使い方を, 備忘録という意味も込めてまとめてみました.
Ruby の正規表現をお手軽に試してみたいという方は Rubular がおすすめです.
軽い気持ちでまとめてみようと思ったら, ちゃんと知らないところが次から次へと出てきて, 全然 Ruby の正規表現知らないじゃんと気づかされて, また記法がたくさんあるので, 全然サクッとまとめることができませんでした.
また Ruby の正規表現は Onigmo という正規表現ライブラリが使われています.
なので Ruby の正規表現に関する根本的な情報を得ようと思ったら, Onigmo のレポジトリを拝見するというのはいいのではないかと思います.
Ruby の正規表現, これから新しい機能, 記法が追加されたりする可能性があると思いますが, 現時点の Ruby (2.5.1
) での正規表現の使い方をざざっとまとめさせていただきました.
参考資料
- https://ruby-doc.org/core-2.5.1/Regexp.html
- https://docs.ruby-lang.org/ja/2.5.0/doc/spec=2fregexp.html
- https://github.com/k-takata/Onigmo/blob/master/doc/RE
- https://github.com/k-takata/Onigmo/blob/master/doc/RE.ja
- https://www.regular-expressions.info/posixbrackets.html
- https://www.regular-expressions.info/recursebackref.html
- https://qiita.com/k-takata/items/4e45121081c83d3d5bfd
関連記事
Ruby のグローバル変数をそれぞれ調べてみた2018.06.12
Git のコミット履歴を大胆に書き換えるなら git rebase -i がオススメ2018.08.23
Git で変更されたファイルを部分的にステージする方法2018.11.01
Ruby の StandardError でどういう間違いをすると, どの例外が発生するのかのメモ2018.07.13
Ruby の関数プログラミングでオイラー積を計算してみた2018.07.02