やってみる

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

「正規表現」を読む3

 難しすぎる。死ぬ。

成果物

情報源

前回まで

アンカー

アンカーは幅0の文字列にマッチするメタ文字列です。幅0とは文字と文字の間、文字列の先頭、文字列の末尾、などを意味します。ある特定の条件を満たす「位置」にマッチします。

  • ^ 行頭にマッチします。行頭とは、文字列の先頭もしくは改行の次を 意味します。
  • $ 行末にマッチします。 行末とは文字列の末尾もしくは改行の手前を意味します。
  • \A 文字列の先頭にマッチします。
  • \Z 文字列の末尾にマッチします。 ただし文字列の最後の文字が改行ならばそれの手前にマッチします。
  • \z 文字列の末尾にマッチします。
  • \b 単語境界にマッチします。 単語を成す文字と単語を成さない文字の間にマッチします。 文字列の先頭の文字が単語成す文字であれば、文字列の先頭 の位置にマッチします。
  • \B 非単語境界にマッチします。 \bでマッチしない位置にマッチします。

 ^\Aは同じってことでOK? $\Zも同じだよね? \zも同じにみえるんだけど何が違うんだ? 複数行モードのときに違いが出るのかな?

# 文字列中の real にマッチする
/real/.match("surrealist") # => #<MatchData "real">
# 先頭に real とないとマッチしない
/\Areal/.match("surrealist") # => nil
# 単語境界がrealの前にないのでマッチしない
/\breal/.match("surrealist") # => nil

単語を成す文字、成さない文字の定義はエンコードによって異なります。以下の例で「全角」括弧は EUC-JP では単語を成す文字と見なされますが、UTF-8 では見なされません。その結果、以下のような挙動をします。

# -*- coding:utf-8 -*-
# デフォルトは UTF-8
/foo\b/.match("あいうfoo%") # => #<MatchData "foo">
/\bfoo\b/.match("あいうfoo%") # => nil
/\bfoo\b/e.match("(foo)".encode("EUC-JP")) # => nil
/\bfoo\b/.match("(foo)") # => #<MatchData "foo">

Unicode の規格では、単語を成す文字を Word というプロパティで定義しています。

 大抵はUnicodeを使うだろうし、それで有効な単語境界が日本語文字にないなら使わないだろう。さらば\b

先読み、後読み(lookahead, lookbehind)

ある位置から続く文字列がある部分式にマッチするならばその位置にマッチするという正規表現を書くことができます。

 なにいってんの?

「ある位置から続く文字列(先読み、lookahead)/ある位置の手前までの文字列(後読み、lookbehind)」と「マッチする(肯定、positive)/マッチしない(否定、negative)」の組み合わせで4つのパターンがあります。

  • (?=pat) 肯定先読み(positive lookahead)
  • (?!pat) 否定先読み(negative lookahead)
  • (?<=pat) 肯定後読み(positive lookbehind)
  • (?<!pat) 否定後読み(negative lookbehind)

\K 後読みの別表記、このメタ文字列の手前までを後読みします。 つまり /pat1\Kpat2/ は /(?<=pat1)pat2/ と同様の意味となります。

# 以下の例では、後読みと先読みを使って <b> と
# </b> に挟まれているという条件を正規表現中に記述しつつ
# <b> </b> 自体にはマッチさせていない。
/(?<=<b>)\w+(?=<\/b>)/.match("Fortune favours the <b>bold</b>")
# => #<MatchData "bold">
# 以下は上の正規表現と同じものを表す
/<b>\K\w+(?=<\/b>)/.match("Fortune favours the <b>bold</b>")
# => #<MatchData "bold">

 「後読み」というのは結果に含まれないパターンのこと。で合ってる? ぜんぜんわからん。難しすぎる。頭おかしくなりそう。

条件分岐

(?(cond)pat) もしくは (?(cond)truepat|falsepat) という記法で条件分岐を記述できます。

 Rubyでいうif修飾子や、C言語でいう三項演算子みたいなものかな?

(?(cond)pat) は cond が真の場合は部分式 pat が使われます。 (?(cond)truepat|falsepat) は cond が真の場合は部分式 truepat が使われ、偽の場合には falsepat が使われます。

 やっぱそれっぽい。

条件可能な条件として以下があります。

  • (n) (nは整数)指定した番号の後方参照に何かがマッチしていれば真
  • (<name>), ('name') 名前指定の後方参照が何かにマッチしていれば真

以下の例は

set var=val
print var

という2つの命令を持つコマンドにマッチするような正規表現です。

re = /\A(?:(set)|(print))\s+(\w+)(?(1)=(\d+))\z/
re.match("set x=32") # => #<MatchData "set x=32" 1:"set" 2:nil 3:"x" 4:"32">
re.match("print x") # => #<MatchData "print x" 1:nil 2:"print" 3:"x" 4:nil>
re.match("set y") # => nil

 頭痛い。

オプション

/pat/という正規表現の直後に以下のアルファベットを置くことで、正規表現にオプションを指定することができます。

  • /pat/i 大文字小文字を無視する
  • /pat/m メタ文字「.」が改行にマッチするようになる
  • /pat/x フリーフォーマットモードになり、空白を無視する。コメントの仕様が変化する
  • /pat/o パターン内の #{} の展開を1回限りしかしない。

 iはIgnoreCaseの略と思われる。よく使いそう。

i, m, x のオプションは (?on:pat) もしくは (?on-off:pat) という記法で部分正規表現にのみ適用することができます。on、offにはi,m,xのいずれかを置きます。onにはpatの中でのみ局所的に有効にしたいオプションを、offには局所的に無効化したいオプションを指定します。

/a(?i:b)c/.match("aBc") # => #<MatchData "aBc">
/a(?i:b)c/.match("abc") # => #<MatchData "abc">

(?on) もしくは (?on-off) という形式を使うと、そこから後の部分正規表現のみオプションを有効化することができます。

/a(?i)bc/.match("aBc") # => #<MatchData "aBc">
/a(?i)bc/.match("aBC") # => #<MatchData "aBC">
# かっこの中で(?i)を指定すると、そのかっこの終わりまで有効
/a(?:(?i)bc)d/.match("aBCd") # => #<MatchData "aBCd">
/a(?:(?i)bc)d/.match("aBCD") # => nil

オプションは Regexp.new の引数に指定することもできます。

Regexp.new("abc", Regexp::IGNORECASE)                     # => /abc/i
Regexp.new("abc", Regexp::MULTILINE)                      # => /abc/m
Regexp.new("abc # Comment", Regexp::EXTENDED)             # => /abc # Comment/x
Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE) # => /abc/mi

2.0.0以降では、文字クラスの挙動を変更するための d,a,u というオプションを (?on:pat)もしくは(?on)の形で指定することができます。(?on-off:pat), (?on-off) という形式を用いる場合はonの部分にのみ用いることができます(offはできません)。

  • d デフォルト(1.9.3互換) (\w, \d, \s は、非ASCII文字にマッチせず、 \b, \B, POSIXブラケットは、各エンコーディングのルールに従う)
  • a ASCII (\w, \d, \s, POSIXブラケットは、非ASCII文字に マッチしない)
  • u Unicode (\w (\W), \d (\D), \s (\S), \b (\B), POSIXブラケット は、各エンコーディングのルールに従う)

d,a,u のオプションは正規表現直後に置く方式では指定ができません。

 複雑すぎて頭もげる。全部理解して使いこなせる人いるの?

 ある一定以上までは正規表現を使えると嬉しい。でもある一定以上より難しくなったらもう正規表現を使いたくないレベルで難読化されてしまう。かといってほかに方法も知らない。どうしたもんか。

エンコーディング

通常、正規表現エンコーディングソースコードエンコーディングと同じであると見なされます。ただし正規表現が ascii 互換の文字しか含まない場合はエンコーディングは US-ASCII になります。

以下のオプションで変更することができます。

正規表現オブジェクトのエンコーディングRegexp#encoding で取得できます。

# -*- coding:utf-8 -*-
/あいう/.encoding # => #<Encoding:UTF-8>
/abc/.encoding # => #<Encoding:US-ASCII>
/abc/u.encoding # => #<Encoding:UTF-8>

正規表現エンコーディングと文字列のエンコーディングが非互換である場合、Encoding::CompatibilityError が発生します。

 面倒臭そう。

エンコーディングについては 多言語化 も参考にしてください。

Regexp#fixed_encoding? で正規表現エンコーディングが「固定」さているかどうかを調べることができます。これが真である場合には文字列とのエンコーディングが一致していないとマッチ時に例外が発生します。これが偽である場合にはASCII互換な文字列であればマッチの判定をさせることができます。Regexp.new に Regexp::FIXEDENCODING を指定することで明示的に指定することが可能です。

# -*- coding:utf-8 -*-
/あいう/.fixed_encoding? # => true
/abc/.fixed_encoding? # => false
/abc/e.fixed_encoding? # => true
/abc/ =~ "あいう" # => nil
/abc/e =~ "あいう"
# ~> -:6:in `<main>': incompatible encoding regexp match (EUC-JP regexp with UTF-8 string) (Encoding::CompatibilityError)

 基本的にすべてUTF-8ってことでいいよね?

コメント

(?#comment here) という記法で正規表現内にコメントを書くことができます。この記法はフリーフォーマットモードでは使えません。かわりに # で行末までがコメントになります。

 絶対に使わない。ただでさえ読みづらい正規表現にこんなの書きたくない。

フリーフォーマットモード

上に説明している x オプションを使うと空白を無視するようになります。これをフリーフォマットモード(free format mode, free spacing modeとも) と呼びます。

 ふーん。

フリーフォーマットモードでは # から行末まではコメント扱いされます。

float_pat = /\A
  \d+ # 整数部
  (\. # 小数点
    \d+ # 小数部
  )?  # 小数点 + 小数部 はなくともよい
\z/x
float_pat.match("3.14") # => #<MatchData "3.14" 1:".14">

 おお、これは複雑な正規表現を書くときにいいかも。

空白を表現したい場合はエスケープをしてください。

/x y/x.match("x y") # => nil
/x\ y/x.match("x y") # => #<MatchData "x y">

\s や \p{Space} のような文字クラスを使うのが良い場合も多いでしょう。

 なるほど。

非包含オペレータ (absence operator) (実験的)

(?~式) という記法で、式にマッチする文字列を含まない任意の文字列にマッチします。

 は?

例えば (?~abc) は "", "ab", "aab", "abb", "ccdd" などにはマッチしますが、 "abc", "aabc", "ccabcdd" などにはマッチしません。

 否定形ってことか。

/\/*(?~*\/)*\// は C スタイルのコメントにマッチします。例えば "//", "/ foo bar /" など。

 読む気なくす。でもこれ、理解していないと表現できないマッチが結構ありそう。

一覧

Rubyで利用可能なメタ文字、メタ文字列の一覧です。

  • .任意の1文字(改行を含まない)

文字クラス

  • [..] いずれかの1文字
  • [^..] どの文字でもない
  • && 共通部分(文字クラス内のみ)
  • x-y xからyまでの文字(文字クラス内のみ)
  • \w 単語構成文字 [a-zA-Z0-9_]
  • \W 非単語構成文字 [^a-zA-Z0-9_]
  • \s 空白文字 [ \t\r\n\f]
  • \S 非空白文字 [^ \t\r\n\f]
  • \d 10進数字 [0-9]
  • \D 非10進数字 [^0-9]
  • \h 16進数字 [0-9a-fA-F]
  • \H 非16進数字 [^0-9a-fA-F]
  • \p{property-name} Unicode プロパティ
  • \p{^property-name} \P{property-name} 否定 Unicode プロパティ
  • [:alnum:] など POSIX文字クラス (文字クラス内のみ)

特別な意味を持つ文字列

  • \R 改行文字 (Linebreak)
  • \X Unicode の結合文字シーケンス

繰り返し

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

キャプチャ & グループ化

  • (pat) 通常のキャプチャ&グループ化
  • \1, \2, ... キャプチャを参照
  • \k<1>, \k<2>, ... キャプチャを参照
  • \k'1', \k'2', ... キャプチャを参照
  • \k<-1>, \k<-2>, ... キャプチャを参照(相対後方参照)
  • \k'-1', \k'-2', ... キャプチャを参照(相対後方参照)
  • (?<name>pat), (?'name'pat) 名前付きキャプチャ
  • \k<name> 名前付きキャプチャを参照
  • \k'name' 名前付きキャプチャを参照
  • (?:pat) キャプチャしないグループ化
  • (?>pat) アトミックグループ化

部分式呼び出し

  • \g<name> 名前指定呼出し
  • \g'name' 名前指定呼出し
  • \g<n> 番号指定呼出し (n >= 1)
  • \g'n' 番号指定呼出し (n >= 1)
  • \g<0> パターン全体の再帰呼び出し
  • \g'0' パターン全体の再帰呼び出し
  • \g<-n> 相対番号指定呼出し (n >= 1)
  • \g'-n' 相対番号指定呼出し (n >= 1)
  • \g<+n> 相対番号指定呼出し (n >= 1)
  • \g'+n' 相対番号指定呼出し (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'-n-level' (n >= 1) ネストレベル付き後方参照
  • \k<name+level> ネストレベル付き後方参照
  • \k<name-level> ネストレベル付き後方参照
  • \k'name+level' ネストレベル付き後方参照
  • \k'name-level' ネストレベル付き後方参照

選択子

  • pat1|pat2 どちらか一方がマッチすれば良い

アンカー

  • ^ 行頭にマッチします。
  • $ 行末にマッチします。
  • \A 文字列の先頭にマッチします。
  • \Z 文字列の末尾にマッチします。ただし文字列の最後の文字が改行ならば それの手前にマッチします。
  • \z 文字列の末尾にマッチします。
  • \b 単語境界にマッチします。
  • \B 非単語境界にマッチします。
  • (?=pat) 肯定先読み
  • (?!pat) 否定先読み
  • (?<=pat) 肯定後読み
  • (?<!pat) 否定後読み
  • \K 左側を肯定後読み

条件

  • (?(cond)pat) cond が成立している場合のみ pat
  • (?(cond)truepat|falsepat) condが成立している場合は truepat 、成立していない 場合は falsepat

オプション

  • (?on:pat) patの間だけ on オプション(i,m,x)を有効にする 2.0.0以降はd,a,uオプションも使える。
  • (?on-off:pat) patの間だけ on オプションを有効にし、offオプションを無効にする

コメント

  • (?#comment here) コメント

非包含オペレータ

  • (?~pat) 非包含オペレータ

特殊変数

パターンマッチしたときに、以下の特殊変数にマッチの情報をセットします。

  • $~ 最後にマッチしたときの情報(MatchData オブジェクト)
  • $& マッチしたテキスト全体
  • `$`` マッチしたテキストの手前の文字列
  • $' マッチしたテキストの後ろの文字列
  • $1, $2, ... キャプチャ文字列
  • $+ 最後(末尾)のキャプチャ文字列

これらの変数はスレッドローカルかつメソッドでローカルな変数です。

参考文献

所感

 正規表現ってこんなにむずかしいのかよ。絶対使いこなせないわ。

対象環境

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