正規表現

正規表現とは

正規表現とは、「文字列をパターンとして一般化したもの」です。
次は、正規表現の一例です(※ここでは、Rubyの記法に従って正規表現を/{regexp}/のように/(スラッシュ)で囲った形で記述します。)

1
/foo/

これは、「"foo"を含む文字列」を一般化したものです。
これでは少しイメージしづらいので、もう少し複雑な例をみてみましょう。

1
/(foo|bar)baz/

これは、"foobaz"または"barbaz"のどちらかを含む文字列を一般化したものです。

このように、正規表現を使うと文字列を一つのパターンとして記述することができます。
正規表現はプログラミング言語でサポートされている場合が多く、駆使すれば文字列を処理するための非常に強力な手段となりえますので、使えるようになって損することはないでしょう。
ここでは、Rubyを例に、正規表現の基本的な使い方を説明します。

基礎的な用語

  • パターン: 正規表現で記述された、文字列を一般化したもの
  • マッチ: ある文字列が特定のパターンに合致すること
  • キャプチャ: あるパターンにマッチした文字列を取り出すこと

正規表現の基本三演算

正規表現の誕生時から存在するもっとも基本的な演算が、以下の三種類の演算です。

連接

先ほどの例の/foo/という正規表現は、「/f//o//o/という三つの正規表現を連結したもの」と見ることができます。
複数の正規表現を連結すると、前のパターンの直後に連結した次のパターンがくるものにマッチします。
このように、正規表現を複数連結させて新たな正規表現をつくる操作を連接といいます。

選択

正規表現で|(パイプ)を使うと、|で区切られたパターンのうちのいずれかにマッチします。

例えば、/foo|bar/とすると、"foo"または"bar"のいずれかの文字列にマッチします。
このような演算を、選択といいます。

繰り返し

*(スター)を用いると、パターンの任意回数の繰り返しを表現することができます。
例えば、/a*/というパターンは以下の文字列にマッチします。

1
2
3
4
5
6
7
"" # 空文字列
"a"
"aa"
"aaa"
:
"aaaaaa..."
:

この*を、量指定子といいます。

演算子の優先順位

数式の+,-,*,/のような演算子に計算順序があるように、正規表現にも演算の順序があります。
上記の三演算の順位は以下のようになっています。

繰り返し > 連接 > 選択

例えば、/ab*|cd/というパターンはどのような文字列にマッチするでしょうか?(\A\zはそれぞれ\A\zを表します。)

  1. 繰り返しが最優先される
    /a{0回以上のb}|cd/

  2. 連接が次に優先される
    /a{0回以上のb}|cd/

  3. 選択が適用される
    /a{0回以上のb}/ または /cd/

結果、以下のようになります。

1
2
3
4
5
"a"   => /ab*/ ("b"が0回)にマッチする
"ab"  => /ab*/ ("b"が1回)にマッチする
"abb" => /ab*/ ("b"が2回)にマッチする
"ac"  => "a"が /ab*/ ("b"が0回)に部分的にマッチする
"cd"  => /cd/ にマッチする

数式で()をつけると優先順位が変わるように、正規表現でも()により演算の順番を制御することができます。
例えば上の例で、/a(b*|c)d/とすると、/a{0回以上のb}d/ または /acd/と解釈されます。
このように、()により正規表現をくくることを、グループ化といいます。

Step Up! : Rubyの正規表現を簡単にチェック

「Rubyで正規表現の挙動をお手軽に確かめたい!」といった場合に便利な、RubularというWebサービスがあります。
このページを使うと、パターンと文字列を簡単に試すことができてオススメです。

より高度な正規表現の構文

上記の演算だけでは複雑になってしまうようなパターンを簡単に表現するために、シンタックスシュガーと呼ばれる便利な構文があります。

量指定子

繰り返しの項目で、「0回以上の繰り返し」を表す*という量指定子を紹介しました。
この他にも、以下のような量指定子が使えます。

プラス演算

+は、「1回以上の繰り返し」を表します。
例えば、/ba+r/とすると、以下のようにマッチします。

1
2
3
br   =>マッチしない
bar  => /ba+r/ ("a"が1回)にマッチ
baar => /ba+r/ ("a"が2回)にマッチ

疑問符演算

?は、「0回または1回の繰り返し」、すなわち1回以下の繰り返し(=あるかないか)を表します。
例えば、/apples?/"apple"と"apples" のいずれかにマッチします。

範囲量指定子

{n,m}(n,mは0以上の整数、n≦m)は、最小n回〜最多m回の繰り返しを表します。
例えば、x{1,5}は、"x", "xx", …, "xxxxx"の5つのパターンにマッチします。
n=mの場合は、ちょうどn回の繰り返しを表し、{n,n}={n}と書くことができます。
また、{n,}のように最大回数を省略するとn回以上{,n}のように最小回数を省略するとn回以下のパターンにマッチさせることができます。

任意の1文字

.(ドット)を使うと、任意の一文字を表現できます。
ただし、.はa~zのようなアルファベットだけではなく、数字や_などの記号、スペースにもマッチするので、使用する場合は意図しない文字列がマッチしないように注意が必要です。

文字クラス

「AからZまでのアルファベット」や「0から9までの数字」といった、「一連の文字群」を表現する方法として、[](ブラケット)を利用する方法があります。
これを使うと、以下のような表現をすることができるようになります。

  • /[abcde]/: /a|b|c|d|e/と等価
  • /[13579]/: /1|3|5|7|9/と等価

範囲指定

上記のように文字を列挙していくと、種類が多くなったときに記述が大変です。
その場合は、[]内で-(ハイフン)を使うことで範囲を指定できます。

  • /[0-9]/: 0から9の数字
  • /[a-z]/: aからz(小文字)までのアルファベット
  • /[A-Z]/: AからZ(大文字)までのアルファベット
  • /[a-zA-Z]/: 大文字・小文字を含むアルファベット

これにより、例えば

  • /[0-9]{8}/: 8桁の数字
  • /[a-zA-Z]{1,4}/: 1文字以上4文字以下のアルファベット

のように、より簡単に正規表現を扱えます。

Note : 文字クラスにおける範囲

文字クラスにおいて、-を用いて指定される範囲は、ASCII文字コードの順序に準拠しています。
したがって、/[A-z]/という指定の場合、アルファベットだけでなく[,\,],…のような記号が含まれることに注意してください。

否定

[]の先頭に^(ハット)を記述すると、否定の意味になります。
すなわち、[]内で指定された文字にマッチしなくなります
例えば、 /[^abc]/とすれば、"a","b","c"という文字にマッチさせないようにできます。

エスケープシーケンス

正規表現中で/(スラッシュ).(ドット)のような制御文字(メタ文字)を通常の文字として使いたい場合、\(バックスラッシュ)を前に置きます。
例えば、"a*b"という文字列にマッチさせたい場合を考えます。
このとき、単純に/a*b/としてしまうと*は0回以上の繰り返しとみなされてしまい、意図した挙動が得られません。
これを、/a\*b/というように記述すると、ただしくマッチさせることができるようになります。
他にも / => \/, \ => \\ などと記述します。
このように、特殊な文字を通常の文字として扱うための操作をエスケープといいます。

\は、特殊文字のエスケープの他に、文字に特別な意味を与える場合にも使われます。
以下に代表的なエスケープシーケンスを示します。

エスケープシーケンス 意味 等価な表現
\t タブ(水平タブ) -
\n 改行 -
\f 改ページ -
\r 復帰 -
\d 数字 [0-9]
\D 数字以外 [^0-9]
\w 文字列記号 [A-Za-z0-9]
\W 文字列記号以外 [^A-Za-z0-9]
\s 空白文字 [\t\n\f\r]
\S 空白文字以外 [^\t\n\f\r]

アンカー

正規表現には、アンカーと呼ばれる「位置にマッチするメタ文字」があります。
以下に、いくつかのアンカーの例を示します。

アンカー 位置
^ 行頭
$ 行末
\A 文字列の先頭
\z 文字列の末尾
\b 文字列の間

^(ハット)$(ダラー)を組み合わせることで、正規表現で部分・完全一致を表現することができるようになります。

1
2
3
4
/regexp/: 部分一致
/^regexp/: 前方一致
/regexp$/: 後方一致
/^regexp$/: 完全一致

正規表現のオプション

Rubyの正規表現では、//{option}というように、正規表現の直後に特定のアルファベットを記述することで、正規表現の挙動を変化させることができます。
指定可能なオプションは以下の通りです。

オプション 動作
/.../i アルファベットの大文字小文字を無視する
/.../m メタ文字としての.(ドット)が、改行にマッチする(複数行に渡る文字列パターンマッチが可能になる)
/.../x 空白を無視する(正規表現内で空白や改行によるインデントなどが可能になる)
/.../o パターン内の#{}の展開を一度だけしか行わない(ループの度に毎回評価されるようなケースを回避できる)

Rubyで正規表現を扱う

それでは、実際にRubyで正規表現を使ってみましょう。

正規表現によるマッチング

Rubyで正規表現による文字列のマッチングを行う方法には、大きく分けて次の二通りがあります。

  1. =~演算子を使う
    =~演算子は、文字列に対して正規表現とのパターンマッチを行うメソッドです。
    戻り値は、マッチした場合にはマッチした位置(インデックス)、マッチしなかった場合はnilとなります。

  2. matchメソッドを使う
    matchメソッドはStringクラスに定義されるメソッドで、string.match(regexp)の形で引数の正規表現とのパターンマッチを行います。
    戻り値は、マッチした場合にはMatchDataオブジェクト、マッチしなかった場合はnilとなります。

regexp.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
regexp = /bcd/
str1 = 'abcdef'
str2 = 'fedcba'

# 方法1: =~演算子
p str1 =~ regexp
#=> 1
p str2 =~ regexp
#=> nil

# 方法2: matchメソッド
p str1.match(regexp)
#=> #<MatchData "bcd">
p str2.match(regexp)
#=> nil

=~演算子やmatchメソッドによるパターンマッチでは、文字列中で最初にマッチした部分のみが返されます。

キャプチャ

()で括ってグループ化したパターンに対してマッチングを行うと、マッチした文字列を抜き出して後で使うことができるようになります。

=~演算子によるパターンマッチの場合

=~演算子によるパターンマッチの場合、キャプチャされた値はRegexpクラスのlast_matchメソッドで取得可能です。

capture.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
str = 'http://example.com?key1=value1&key2=value2'
regexp =  /(http|https):\/\/.+(\.com|\.co\.jp)\?(.+)/

str =~ regexp

p Regexp.last_match[0] # マッチした文字列全体
#=> "http://example.com?key1=value1&key2=value2"

p Regexp.last_match[1] # 一つ目のキャプチャ("http"または"https")
#=> "http"

p Regexp.last_match[2] # 二つ目のキャプチャ(".com"または".co.jp")
#=> ".com"

p Regexp.last_match[3] # 三つ目のキャプチャ("?"以降の文字列)
#=> "key1=value1&key2=value2"

p Regexp.last_match[4] # 三つ目のキャプチャ(ないのでnil)
#=> nil

Note : $を用いたキャプチャした文字列の取得

キャプチャした値は、Regexp.last_matchメソッド以外にも、$を使って取得することができます。

capture_dollar.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
str = 'http://example.com?key1=value1&key2=value2'
regexp = /(http|https):\/\/.+(\.com|\.co\.jp)\?(.+)/

str =~ regexp

p $1 # 一つ目のキャプチャ("http"または"https")
#=> "http"

p $2 # 二つ目のキャプチャ(".com"または".co.jp")
#=> ".com"

p $3 # 三つ目のキャプチャ("?"以降の文字列)
#=> "key1=value1&key2=value2"

p $4 # 四つ目のキャプチャ(ないのでnil)
#=> nil

ただし、二つの記法が混じると可読性が落ちますので、あらかじめコーディング規約としてどちらかに統一しておくことが望ましいです。

matchメソッドによるパターンマッチの場合

matchメソッドによるパターンマッチの場合、キャプチャした値は、matchメソッドの戻り値であるMatchDataオブジェクトから取得できます。

capture_match_data.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
str = 'http://example.com?key1=value1&key2=value2'
regexp =  /(http|https):\/\/.+(\.com|\.co\.jp)\?(.+)/

match_obj = str.match(regexp)

p match_obj[0] # マッチした文字列全体
#=> "http://example.com?key1=value1&key2=value2"

p match_obj[1] # 一つ目のキャプチャ("http"または"https")
#=> "http"

p match_obj[2] # 二つ目のキャプチャ(".com"または".co.jp")
#=> ".com"

p match_obj[3] # 三つ目のキャプチャ("?"以降の文字列)
#=> "key1=value1&key2=value2"

p match_obj[4] # 四つ目のキャプチャ(ないのでnil)
#=> nil

Step Up! : エスケープシーケンスを使わないメタ文字のエスケープ

URLやディレクトリのパスなどでは、”/”というメタ文字とされている文字が頻出します。
このようなメタ文字を普通の文字として扱うためには、``によってエスケープする必要がありました。
しかし、正規表現にメタ文字を利用しない場合は、%r記法というものでエスケープせずに書く方法があります。

1
2
p %r(http://example.com/foo/bar/baz.html)
#=> /http:\/\/example.com\/foo\/bar\/baz.html/

メタ文字が使えなくなってしまうという欠点もありますが、簡単に書ける場合もありますので、覚えておきましょう。

名前付きキャプチャ

キャプチャが増えると、番号による取得ではわかりづらくなってしまうことがあります。
例えば、以下の例を考えてみましょう。

capture_without_name.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
str = 'https://www.val.co.jp/company/outline/index.html'
regexp = /(http|https):\/{2}(([^\.]+)\.([^\/]+))\/(.+\.(([^\.]+)))/

match_obj = str.match(regexp)

puts match_obj[1]
#=> "https"

puts match_obj[2]
#=> "www.val.co.jp"

puts match_obj[3]
#=> "www"

puts match_obj[4]
#=> "val.co.jp"

puts match_obj[5]
#=> "company/outline/index.html"

puts match_obj[6]
#=> "html"

上記のように、キャプチャが入れ子になっていたりすると、それぞれの結果で何が返ってくるかがイメージしづらいです。
Rubyでは、(?\<capture_name\>regexp)というように記述することで、キャプチャに名前を付与することができます。

capture_with_name.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
str = 'https://www.val.co.jp/company/outline/index.html'
regexp = /(?<url>http|https):\/{2}(?<fqdn>(?<host_name>[^\.]+)\.(?<domain_name>[^\/]+))\/(?<path>.+\.(?<ext>([^\.]+)))/

match_obj = str.match(regexp)

puts match_obj[:url]
#=> "https"

puts match_obj[:fqdn]
#=> "www.val.co.jp"

puts match_obj[:host_name]
#=> "www"

puts match_obj[:domain_name]
#=> "val.co.jp"

puts match_obj[:path]
#=> "company/outline/index.html"

puts match_obj[:ext]
#=> "html"

キャプチャしないグループ化

以下の例のように選択などでグループ化するとき、キャプチャする必要がないものも取得されてしまいます。

unnecessary_capture.rb
1
2
3
4
5
6
7
8
9
url = "http://example.com"
regexp = /(http|https):\/\/.+(\.com|\.co\.jp)/

url =~ regexp

p Regexp.last_match[1]
#=> "http"
p Regexp.last_match[2]
#=> ".com" : これが不要の場合

このような例では、(?:regexp)というように記述することで、キャプチャをせずにグループ化することができます。
キャプチャはメモリを必要とする分パフォーマンスの低下にもつながるので、不要なキャプチャはしないようにするのが良いでしょう。

grouping_without_capture.rb
1
2
3
4
5
6
7
8
9
str = "http://example.com"
regexp = /(http|https):\/\/.+(?:\.com|\.co\.jp)/

str =~ regexp

p Regexp.last_match[1]
#=> "http"
p Regexp.last_match[2]
#=> nil : キャプチャされない!

置換

ある文字列を正規表現のルールに基づいて置換させたい場合、Rubyではsubgsubメソッドを利用します。
subメソッドは最初にマッチした文字列、gsubメソッドは文字列内でマッチした全ての部分を指定の文字列に置き換えます。
メソッドの呼び出し方は、(g)sub({正規表現}, {置換する文字列})とする方法や、(g)sub({正規表現}) {|| ...}のようにブロックの戻り値に置き換える方法があります。

例として、次のdivタグのクラスを"foo"から"hoge"に置き換えてみます。

example1.html
1
2
3
<div class="foo">
  <p>hello world</p>
</div>

今回置き換える箇所は一箇所なので、subメソッドを利用して置き換えます。

sub.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
str = File.read("#{__dir__}/example1.html")

regexp = /(class=")\S+(")/
replaced = str.sub(regexp) do
  [
    Regexp.last_match[1],
    'hoge',
    Regexp.last_match[2]
  ].join('')
end

puts "置き換え前:"
puts str
puts "=============\n置き換え後:"
puts replaced

=begin
置き換え前:
<div class="foo">
  <p>hello world</p>
</div>
=============
置き換え後:
<div class="bar">
  <p>hello world</p>
</div>
=end

また、置き換えたい場所が複数ある場合には、gsubメソッドを利用します。
次のファイルについて、すべてのdivタグのクラスを"hoge"に置き換えてみましょう。

example2.html
1
2
3
4
5
6
7
8
9
<div class="foo">
  <p>hello world</p>
</div>
<div class="bar">
  <p>hello ruby</p>
</div>
<div class="baz">
  <p>hello</p>
</div>
gsub.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
str = File.read("#{__dir__}/example2.html")

regexp = /(class=")\S+(")/
replaced = str.gsub(regexp) do
  [
    Regexp.last_match[1],
    'hoge',
    Regexp.last_match[2]
  ].join('')
end

puts "置き換え前:"
puts str
puts "=============\n置き換え後:"
puts replaced

=begin
置き換え前:
<div class="foo">
  <p>hello world</p>
</div>
<div class="bar">
  <p>hello ruby</p>
</div>
<div class="baz">
  <p>hello</p>
</div>
=============
置き換え後:
<div class="hoge">
  <p>hello world</p>
</div>
<div class="bar">
  <p>hello ruby</p>
</div>
<div class="baz">
  <p>hello</p>
</div>

=end

欲張りな量指定子と控えめな量指定子

以下の文から、最初に出てくる<div>~</div>を取り出すことを考えます。

example2.html
1
2
3
4
5
6
7
8
9
<div class="foo">
  <p>hello world</p>
</div>
<div class="bar">
  <p>hello ruby</p>
</div>
<div class="baz">
  <p>hello</p>
</div>

このとき、以下のようなコードだとどうなるでしょうか?

greedy_quantifier.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
str = File.read("#{__dir__}/example2.html")
regexp = /(<div.+>.+<\/div>)/m

puts str.match(regexp)[1]

=begin
<div class="foo">
  <p>hello world</p>
</div>
<div class="bar">
  <p>hello ruby</p>
</div>
<div class="baz">
  <p>hello</p>
</div>
=end

最初に登場する<div>から最後に登場する</div>まですべて取れてしまいましたね…
実は正規表現における*+といった量指定子は、通常できるだけ長い文字にマッチするようになっています。
これを、欲張りな量指定子といいます。
それでは、できるだけ短い文字でマッチさせるようにはどうすればいいでしょうか。
これを実現するのが控えめな量指定子で、*?+?のように欲張りな量指定子の後ろに?をつけることで表現します。

lazy_quantifier.rb
1
2
3
4
5
6
7
8
9
10
str = File.read("#{__dir__}/example2.html")
regexp = /(<div.+?>.+?<\/div>)/m

puts str.match(regexp)[1]

=begin
<div class="foo">
  <p>hello world</p>
</div>
=end

通常のパターンマッチは欲張りである、ということを覚えておきましょう。

参考著書

本資料は、以下を参考に作成されたものです。

新屋良磨、鈴木勇介、高田兼 正規表現技術入門 技術評論社 2015年