Chienomi

正規表現マスターの「コツ」

「正規表現がわからない」という人は結構多い。

私としては、正規表現(Perl regexp)の習得は特に苦もなく、長大な正規表現もすんなり書けるようになったのであまり実感はないのだが、どうも正規表現の記述に関しては得手不得手がはっきり出る部分であるようだ。

私が正規表現を教えてきた感覚で言うと、正規表現はむしろプログラミングに関してある程度知識がある人のほうがハマりやすい。 プログラミングが関数型言語を除けば手続き的であるのに対し、正規表現は宣言的であるため、その切り替えができず、概念がごっちゃになる部分が大きいようだ。

正規表現を理解するには、正規表現が宣言的であり、どのような概念の集合なのかということを理解するのが重要だ。 そして、それだけを理解すれば単純な要素の複合であるため、全く難しくはない。

ここではMimir Yokohamaのクラス2授業であるプログラミング系科目で教えている正規表現の内容から、特に肝となる部分のエッセンスを抽出し、紹介したいと思う。 察しのいい、定義によって明らかにされれば論理的導出で理解できる人にとってはこの内容が十分助けになるだろう。

より具体的な説明や、詳細な大切、具体例などが欲しい人は、ぜひMimir Yokohamaを頼って欲しい。

正規表現の要素

基本

正規表現の要素はBREの場合

  • 文字
  • グループ
  • 文字列
  • 位置

が存在する。

EREの場合は「文字列」がない。

Perl RegexやOniguruma/Onigmoの場合、「パターン」が追加される。 まぁ、さらに「条件」や「実行」や「演算」が追加されたりもするのだけど、そこは置いておく。 正規表現わからないと言っている人の話題はそこではないだろうから。

文字

文字は正規表現の基本要素であり、正規表現の考え方としては文字が並ぶことで構文となる、と考えられている。

正規表現は文法の条件の記述であり、文字というのは固定の文字ということではなく、条件に合致する文字を示すものである。

通常の文字、例えばaは、aという文字だけが条件に合致する文字があることを示す。

[abc]a, b, cのいずれかが条件に合致する文字であるということだ。

これは例えば文字クラスやUnicodeスクリプトにおいても、どのような文字が条件に合致するかということを示しているだけであり、文字には変わりない。

グループ

グループは文字条件を1文字以外に拡張するものである。

文字はあくまで文字として定義されたものが1文字に一致するのだが、グループ化されるのは正規表現であるために、 0文字以上の 任意の文字列が条件に当てはまることになる。

量は文字, 文字列, グループ(パターン)の反復を示す。

単純に「〜がn回あって」という話だと考えていい。もちろん、場合によっては「n回またはm回」であったり、「n回以上」であったり、「n回以下」であったり、「n回からm回」であったりする。

文字列

文字列はキャプチャによって得られる既知の文字列である。

例えばBREにおいて

\(ab\)\1

というと、ababということになる。

文字列はパターンでなく、あくまで文字列なので、

\(a[ab]\)\1

というと、ababaaaaにはマッチするが、aaabにはマッチしない。

多くの人が理解していない部分として、文字列は量指定子の対象である。だから

% print ababababbbabab | grep -o '\(a[ab]\)\1*'
abababab
abab

という結果になる。

位置

位置は「ここがこういう場所である」ということを条件とするものである。

普通のBREの場合、「行頭」(^)と「行末」($)の2種類しかない。 GNU grepだと「単語協会」(\b)もあったりする。

Perlの正規表現にあるのは位置をパターンで表すことができる幅ゼロアサーションだ。 幅ゼロアサーションはパターンであるために混乱を招くかもしれないが、「…という場所」ということを述べていると理解すれば難しくない。

パターン (強敵)

さて、最大の強敵がパターンだ。Perlだと5.14から追加され、Onigurumaにも目玉機能として入っている。

正規表現そのものがパターンだから、パターンの中にパターンを表現するということは、「再利用」や「再帰」ができるということである。

再帰だと混乱する人が多いので再利用について述べると、例えば

nnn-ACnnn-Xnnn-n

という文法が存在するとしよう。(nは数字)。もちろんこれは、

\d{3}-AC\d{3}-X\d{3}-\d

で表現できるものである。

パターンを正規表現中で利用すること自体は、「グループの量指定」によって可能である。 ところが、これはあくまで「グループで示されるパターンが連続する」場合であり、「グループで示されるパターンが断続する」場合や、「グループで示されるパターンが内包される」場合には利用できない。

これを可能にするのが部分式呼び出しである。 これを使うとOnigmoでは

(\d{3})-AC\g<1>-X\g<1>-\d

と書くことができる。 \g<1>というところが1番目にキャプチャされたパターン(\d{3})を意味する。

ここでは\d{3}なんていうものすごく単純なパターンであり、別に嬉しくないように思うが、これはものすごく複雑なパターンが繰り返す場合には相当タイピングを節約できる。

Onigmoでは名前付き捕獲式を使うことができる。

(?<num>\d{3})-AC\g<num>-X\g<num>-\d

\g\kで悩む人もいるが、\kは単純に捕獲された文字列である。 これは既にあったものだ。だから、

% ruby -e 'ARGV.first =~ /([ab])\k<1>/ and puts $&' abaaa
aa

である。([ab]\k<1>)([ab])\1に等しい。

そして、部分式呼び出しはパターンであるから、

ruby -e 'ARGV.first =~ /([ab])\g<1>/ and puts $&' abaaa
ab

であり、これは結果的には([ab]){2}と書いても同じである。もちろん、その場合は間に何かをはさむことはできないが。

つまり、パターンが利用できる正規表現エンジンにおいては、「文字列の再利用」だけでなく「パターンの再利用」が可能になっている。 だから結果的にグループが「パターンの宣言的定義」にもなっているのだ。

正規表現の考え方

正規表現は文法を示すものである、ということを忘れてはいけない。 「Xはこのような文法を持っている」ということを記述するものである。

正直なところ、BNFなんかよりは遥かにわかりやすく、簡単だと思う。

例えばウェブアドレスの場合、httpまたはhttpsスキームを持つわけだが、そのあとはあまり変わらない。だから

https?://(?:\g<user>:\g<password>@)?\g<domain>/\g<path>(?:\g<param>(?:\g<param>)*)?

のように書くことができる。

「一致する文字列」というふうに考え出すとドツボにハマる。 正規表現はその概念を定義するものだ。

プログラミング上では、値よりもクラスの定義のほうが近いと思えば良いだろう。 クラスの定義は「Xというクラスは以下のようなものである。メソッドa、メソッドb、インスタンス変数xをもち…」というように行うものだが、 正規表現もまた「re1は以下のように正規表現によって表すことができるものである」と述べているわけだから。

ちなみに、この場合では各捕獲式がこの正規表現中で定義されていないため、正規表現のコンパイルエラーになる。 だが、パターンに名前をつけられるのであれば、事前に部分的文法を定義しておきたいものだ。 Perl6のRulesはそのようになっており、「部分式呼び出し」を中核に据えたものになっている。なので、よりわかりやすく宣言的だ。

ただし、Perl6のRulesは正規表現とは記法に互換性が 全くない ので注意してほしい。

Wrote on: 2019-08-25T00:00:00+09:00