やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

「正規表現」を読む2

 難しすぎる。吐きそう。

成果物

情報源

前回まで

特別な文字列に対するマッチ

文字列の中には、CR LF のように、複数の文字一続きで1つの意味を表すようなものが存在します。そのような文字列にマッチするようなメタ文字列として以下が存在します。

\R 改行 (?>\x0D\x0A|[\x0A-\x0D\u{85}\u{2028}\u{2029}]) (Unicode 以外では (?>\x0D\x0A|[\x0A-\x0D]) になります)
\X Unicode 結合文字シーケンス (eXtended grapheme cluster) (?>\P{M}\p{M}*)
# \u{0308} はウムラウト
/\X/.match("e\u{0308}") # => #<MatchData "ë">
$&.codepoints # => [101, 776]
/\w/.match("e\u{0308}") # => #<MatchData "e">
$&.codepoints # => [101]
記号 意味
\R 改行コード
\X Unicode結合文字シーケンス

 ぐぐってみた。

ドイツ語などで、母音の音色がそれに後続する音節の影響で変化すること。 母音変異。 また、変化したその母音。 変母音。

 使わないな。\Rだけ覚えておけばよさそう。

繰り返し

以下のメタ文字列は繰り返しを表現します。直前の部分式を何回繰り返すかを指定します。このような繰り返しを表すメタ文字列を量指定子(quantifier)と呼びます。

* 0回以上
+ 1回以上
? 0回もしくは1回
{n} ちょうどn回(nは数字)
{n,} n回以上(nは数字)
{,m} m回以下(mは数字)
{n,m} n回以上m回以下(n,mは数字)

 これ大事。

以下の例で、量指定子の基本的な使いかたを示しています。

# 以下の正規表現は 最初に大文字が1文字以上(H)で、小文字が1文字以上(l)、
# lが2文字(ll)の後ろにoが続く文字列にマッチします。
"Hello".match(/[[:upper:]]+[[:lower:]]+l{2}o/) # => #<MatchData "Hello">

これらは「欲張り(greedy)」にマッチします。マッチが成功する、最長の文字列にマッチしようとします。そのため、これらの量指定子は特に最大量指定子(greedy quantifier) と呼ばれます。

 そうなんだ。「1文字以上」という条件のうち最長の文字列にマッチしようとする。なので最大量指定子と呼ばれる。

最小量指定子(reluctant quantifier)

一方、以下のメタ文字列(普通の繰り返しメタ文字列に ? を付加したもの) はマッチが成功する、最短の文字列にマッチします。そのため、これらの量指定子は特に最小量指定子(reluctant quantifier)と呼ばれます。

*? 0回以上
+? 1回以上
?? 0回もしくは1回
{n,}? n回以上(nは数字)
{,m}? m回以下(mは数字)
{n,m}? n回以上m回以下(n,mは数字)

以下の例では、最小量指定子を使うことで、(\d+)がマッチする場所を変えています。

/^.*(\d+)\./.match("Copyright 2013.")  # => #<MatchData "Copyright 2013." 1:"3">
/^.*?(\d+)\./.match("Copyright 2013.")  # => #<MatchData "Copyright 2013." 1:"2013">

また、ネストしていない括弧の対応を取るためにも使えます。

# ここでは <b> と </b> の対応を取る
%r{<b>.*</b>}.match("<b>x</b>y<b>z</b>") # => #<MatchData "<b>x</b>y<b>z</b>">
%r{<b>.*?</b>}.match("<b>x</b>y<b>z</b>") # => #<MatchData "<b>x</b>">

 これは使えそう。というか理解していないと思い通りにできなさそう。この説明はありがたい。

絶対最大量指定子(possessive quantifier)

以下のメタ文字列は、最大量指定子のように最長のマッチをしますが、一度マッチすると、その後マッチに失敗してもバックトラックしません。つまりマッチ済みの文字列を手放さずにマッチに失敗します。これらの量指定子は絶対最大量指定子と呼ばれます。

*+ 0回以上
++ 1回以上
?+ 0回もしくは1

アトミックグループを用いることで同じことができます。

 なにいってるのかわからん。コード例をくれ。

 自分で試すか。

# ここでは <b> と </b> の対応を取る
%r{<b>.*</b>}.match("<b>x</b>y<b>z</b>") # => #<MatchData "<b>x</b>y<b>z</b>">
%r{<b>.*?</b>}.match("<b>x</b>y<b>z</b>") # => #<MatchData "<b>x</b>">
p %r{<b>(.*)+</b>}.match("<b>x</b>y<b>z</b>") # => #<MatchData "<b>x</b>y<b>z</b>" 1:"">

 これでいいのか?

キャプチャ

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

 後方参照ってやつね。稀によく使うわ。

正規表現内では \1, \2, ... という記法で後方参照できます。また、\k<1>, \k<2>, ... や \k'1', \k'2', ... という記法を使うこともできます(10を越える数字を渡すことができます)。また、Regexp#match で得られた MatchData からは MatchData#[]で取り出せます。

 そいつは助かる。

また、$1, $2, ... という特殊変数によって n 番目の括弧にマッチした部分文字列を参照できます。これらの特殊変数はマッチ処理が終わったあとでしか使えないことに注意してください。

# (..) に at がマッチしたのを \1 で参照し、マッチが成功している。
m = /[csh](..) [csh]\1 in/.match("The cat sat in the hat")
# => #<MatchData "cat sat in" 1:"at">
# Regexp#match でマッチしたテキストは MatchData#[] で参照できる
m[1] # => "at"

 ..というのは何らかの2文字を表しているのかな?

1,2,... ではなく、名前を付けることができます。 (?pat)もしくは(?'name'pat)と記述します。キャプチャした文字列は MatchData#[] に Symbol を渡すことで参照できます。これは名前付きキャプチャと呼ばれます。

m = /\$(?<dollars>\d+)\.(?<cents>\d+)/.match("$3.67")
# => #<MatchData "$3.67" dollars:"3" cents:"67">
m[:dollars] # => "3"
m[:cents] # => "67"

 名前があったほうがわかりやすい。でも正規表現は長くなった。

名前付きキャプチャは正規表現内で \k、\k'name' という記法で参照できます。

/(?<vowel>[aeiou]).\k<vowel>.\k<vowel>/.match('ototomy')
# => #<MatchData "ototo" vowel:"o">

 長いしわかりにくいから?<>のほうがいい。

注: 名前付きキャプチャと数字によるキャプチャは併用できません。

 残念。

リテラル正規表現内に名前付きキャプチャがあり、 =~ の左辺で用いた場合には、その名前のローカル変数にキャプチャした文字列を代入します。

/\$(?<dollars>\d+)\.(?<cents>\d+)/ =~ "$3.67" # => 0
dollars # => "3"
cents # => "67"

これは便利! なんて気が効くんだRuby

注: ローカル変数への代入が行われるのは、左辺の正規表現リテラルが#{}による式展開を含んでいない場合に限られます。

 えー。残念。微妙だな。

数字による後方参照では、負の数による、 \k<-1>, \k<-2>, ... や \k'-1', \k'-2', ... という記法での相対的な指定が可能です。-1 は後方参照が書かれた位置の1つ手前の位置にあるキャプチャを表し、-2, -3, で2つ手前、3つ手前を表します。これは非常に多くのキャプチャを持つような正規表現を記述するためや、正規表現に別の正規表現を式展開で埋め込む場合などに便利です。

/(.)(.)\k<-2>\k<-1>/.match("xyzyz") # => #<MatchData "yzyz" 1:"y" 2:"z">

 \kはいらんな。

グループ

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

# The pattern below matches a vowel followed by 2 word characters:
# 'aen'
/[aeiou]\w{2}/.match("Caenorhabditis elegans") #=> #<MatchData "aen">
# Whereas the following pattern matches a vowel followed by a word
# character, twice, i.e. <tt>[aeiou]\w[aeiou]\w</tt>: 'enor'.
/([aeiou]\w){2}/.match("Caenorhabditis elegans")
    #=> #<MatchData "enor" 1:"or">

(?:pat) という記法を使うとキャプチャせずにグループ化することができます。性能が多少改善する場合がありますが、多少見にくくなります。

# 最初のキャプチャは n で二番目のキャプチャが ti であり、
# \2 で二番目のキャプチャを後方参照しています
/I(n)ves(ti)ga\2ons/.match("Investigations")
    # => #<MatchData "Investigations" 1:"n" 2:"ti">
# 最初のグループは (?: ) を使っているのでキャプチャが作られず、
# 1番目は ti がキャプチャされます。
# そして ti を \1 で参照しています。
/I(?:n)ves(ti)ga\1ons/.match("Investigations")
    # => #<MatchData "Investigations" 1:"ti">

 (?:pat)は使わなくていいかな。

アトミックグループ(atomic grouping)

(?>pat) という記法で「アトミック」なグループを作れます。

 出たな。

通常の正規表現では、ある部分式がマッチに成功した後、続く式がマッチに失敗した場合、バックトラックによって成功した部分の一部を手放してマッチにリトライします。しかし、アトミックなグループがマッチした後、後続の式がマッチに失敗した場合、一部だけをバックトラックで巻き戻すのではなく、このグループのマッチ全体を巻き戻します。つまり、正規表現のマッチのバックトラックを抑制します。

 バックトラックってなに?

典型的にアトミックグループはバックトラックの回数を減らし正規表現を高速化するために用います。

# 以下のマッチはまず .* が Quote" にマッチした後、
# 正規表現末尾の " のマッチに失敗します。その後
# 一文字だけバックトラックして、" のマッチに成功します。
/".*"/.match('"Quote"') # => #<MatchData "\"Quote\"">
# 一方、以下のマッチはまず .* が Quote" 全体にマッチした後、
# 正規表現末尾の " のマッチに失敗します。その後
# バックトラックで"がマッチした状態まで戻り、
# (?>.*)以外の選択子がないのでマッチ全体が失敗します。
/"(?>.*)"/.match('"Quote"') # => nil
# 一方、以下のマッチはまず .* が Quote" 全体にマッチした後、
# 正規表現末尾の " のマッチに失敗します。その後
# バックトラックで"がマッチした状態まで戻り、
# 次の可能性(つまり | の右側)のマッチを試します。
# 結果としてマッチが成功します。
/"(?:(?>.*)|(.*))"/.match('"Quote"') # => #<MatchData "\"Quote\"" 1:"Quote">

 はあ。(?>pat)がアトミックグループとかいうやつだと。そこまでしかわからんかった。とにかく性能アップのために使うものらしい。じゃあ使わなくっていいや。

部分式呼び出し(subexpression call)

\g もしくは \g'name' という記法で、nameと名付けられた部分正規表現にマッチしようとします。この記法は、部分式呼び出しと呼ばれます。 name には名前、数字、のいずれを用いることもできます。これは後方参照とは異なります。後方参照は前でマッチした文字列と同じ文字列にマッチしようとしますが、部分式呼び出しは nameと名付けられた「部分正規表現」にマッチしようとします。

 なにいってるかわかんね。

部分式呼び出しの記法は正規表現内に再帰的記述を可能とし、通常の正規表現では記述不可能な処理(例えば対応した括弧の検出) を可能とします。以下の例では実際に何回かっこがネストしていてもマッチに成功します。ただし、再帰的な正規表現を書く場合は、無限ループに陥る可能性があるため、停止条件に注意してこの機能を使ってください。

/\A(?<paren>\(\g<paren>*\))*\z/ =~ '(())' # => 0
# ^1
#      ^2
#           ^3
#                 ^4
#      ^5
#           ^6
#                      ^7
#                       ^8
#                       ^9
#                           ^10
# 1. \A が文字列の先頭にマッチする
# 2. 名前付きキャプチャの内側に入る
# 3. 名前付きキャプチャ内の最初の括弧 \( に文字列の最初の ( マッチする
# 4. \g<paren> で名前付きキャプチャ <paren> にマッチしようとする
# 5. 上の処理の結果(?<paren>pat)に再びマッチしようとする
# 6. \( に前から二番目の ( にマッチする
# 7. \g<paren> でさらに (?<paren>pat) にマッチしようとするが、
#     開き括弧 ( がもう文字列内にないので失敗する。
#    しかし、その後ろに量指定子*があるので、0回マッチとして成功する
# 8. 1つ目閉じ括弧 ) がマッチし、\g<paren>による再帰呼び出し全体が
#    成功する
# 9. 2つ目の閉じ括弧 ) が \) にマッチし、(?<paren>pat)というマッチに成功する
# 10. 文字列の末尾へのマッチに成功する

 ようするに\g<name>は同じパターンが何度も続くことを示せるわけか。

 戻り値が0なのがわからんけど。なんの値なんだ?

また、パターン全体は特別に \g<0> もしくは \g'0' という記法で再帰呼び出しできます。番号指定による呼出も可能で、\g もしくは \g'n' という記法が使えます。相対番号で部分式を指定することもでき、これは \g<-n> \g'-n' (前側での相対位置) や \g<+n> \g'+n' (後側での相対位置) という記法を用います。

 名前を数字にしてもいいということね。

以下の記法を用いて、部分式呼び出しでの上のレベル、下のレベルでのマッチにアクセスすることができます。levelで呼出しのネストレベルを、nで位置を指定できます。

  • \k<n+level> (n >= 1)
  • \k<n-level> (n >= 1)
  • \k'n+level' (n >= 1)
  • \k'n-level' (n >= 1)
  • \k<-n+level> (n >= 1)
  • \k<-n-level> (n >= 1)
  • \k'-n+level' (n >= 1)
  • \k'-n-level' (n >= 1)

 レベルというののはネスト階層数のこと。つまり同じパターンを繰り返す数のことか。名前とループ数の区切り文字は-+どちらでもよいと。

また、以下の記法で名前付きキャプチャも同様のことができます。

  • \k<name+level>
  • \k<name-level>
  • \k'name+level'
  • \k'name-level'

以下の例は回文にマッチする正規表現です。

/\A(?<a>|.|(?:(?<b>.)\g<a>\k<b+0>))\z/.match("rekxker")
# => #<MatchData "rekxker" a:"rekxker" b:"k">

 むずかしすぎて全然わからん。回文とは逆からみても同じ文字列になるものを指すが、そのチェックをしているのが上記の正規表現ということらしい。でも、どうしてそうなるのかさっぱりわからない。

以下の例では、開始タグと終了タグを対応付ける正規表現です。

r = Regexp.compile(<<'__REGEXP__'.strip, Regexp::EXTENDED)
(?<element> \g<stag> \g<content>* \g<etag> ){0}
(?<stag> < \g<name> \s* > ){0}
(?<name> [a-zA-Z_:]+ ){0}
(?<content> [^<&]+ (\g<element> | [^<&]+)* ){0}
(?<etag> </ \k<name+1> >){0}
\g<element>
__REGEXP__
r.match('<foo>f<bar>bbb</bar>f</foo>').captures
# => ["<foo>f<bar>bbb</bar>f</foo>", "<bar>", "bar", "f<bar>bbb</bar>f", "</foo>"]

 吐き気をもよおす。こんなコード読みたくない。まったくわからん。

最左位置での再帰呼び出しは禁止されています。

(?<name>a|\g<name>b)   => error
(?<name>a|b\g<name>c)  => OK

 わからん。

選択

縦棒 | によって2つの部分正規表現のどちらか一方にマッチすれば良い部分正規表現を記述できます。

/\w(and|or)\w/.match("Feliformia") # => #<MatchData "form" 1:"or">
/\w(and|or)\w/.match("furandi")    # => #<MatchData "randi" 1:"and">
/\w(and|or)\w/.match("dissemblance") # => nil

このメタ文字は選択子(alternation operator)と呼ばれます。

 ふーん。誤字じゃなかったんだ。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux