Perl Hackers Hub

第58回正規表現の勘所―わかりづらい記法の覚え方、先読みや後読みの実践(2)

前回の(1)こちらから。

正規表現にまつわる修飾子─⁠─マッチや置換方法の変更指示

(1)で解説した/g修飾子も含む、よく使う修飾子を表2にまとめます。

表2 正規表現演算子でよく使われる修飾子
修飾子意味Perlの採用バージョン
/m複数行モード。^や$のマッチ方法を変える(詳しくは後述)-
/s単一行モード。.が改行にもマッチする(詳しくは後述)-
/i大文字/小文字を区別しない-
/x空白を無視したうえでコメントを許可して正規表現を読みやすくする-
/pマッチ文字列、またその前後の文字列を取得する方法を提供する5.14
/n丸括弧()でのキャプチャを抑制する5.22
/gマッチを複数回行う-
/o正規表現の結果をキャッシュする-
/e置換文字列をPerlコードとして評価する-
/r置換を非破壊で行う(詳しくは後述)5.14

採用バージョンに-が入っている場合、すべてのPerl 5環境で使用できる

修飾子は複数指定可能で、その場合は/msiなどと列挙します。列挙順序は任意で、/imsでも同義です。

以降では、表中で「詳しくは後述」と記した、使うと効果的なもののわかりづらい修飾子について解説します。

/m修飾子─⁠─冒頭や末尾のメタ文字を、改行文字の直後や直前にもマッチさせる

/m修飾子は冒頭と末尾のメタ文字^$のマッチ方法を変えます。この修飾子を指定した状態は複数行モードmultiple line modeと呼ばれます。

/m修飾子を指定しない場合、^$は対象文字列の冒頭または末尾にマッチします。冒頭も末尾も1ヵ所ですので、/g修飾子を指定しても、2回以上マッチすることはありません。

/m修飾子を指定した場合、^$は対象文字列の冒頭または末尾だけでなく、対象文字列に含まれている改行文字の直後と直前にもそれぞれマッチします。/g修飾子を合わせて指定すると、対象文字列に改行文字が含まれている場合、2回以上マッチします。

言葉ではわかりづらいので、例を見てみましょう。

my $str1 = <<END_STRING;  ――(1)
  Alice Smith
    Bob Johnson
  Carol Williams
Dave Brown
END_STRING
my $str2 = $str1;  ――(2)

$str1 =~ s/^\s+//g;  ――(3)
print "=== str1 ===\n", $str1;
$str2 =~ s/^\s+//gm;  ――(4)
print "=== str2 ===\n", $str2;

(1)では、ヒアドキュメントを使って改行を含んだ文字列を定義して$str1に代入しています。(2)$str1のデータを$str2にコピーしたあと、/m指定なしの(3)/m指定ありの(4)それぞれで、^直後にある空白類文字\s+を空文字で置換、つまりマッチした空白類文字列を削除しています。出力結果は下記になります。

=== str1 ===
Alice Smith
    Bob Johnson
  Carol Williams
Dave Brown
=== str2 ===
Alice Smith
Bob Johnson
Carol Williams
Dave Brown

/mなしの場合は文字列の冒頭Aliceの前にある空白のみが削除されますが、/mありの場合は冒頭以外に改行の直後、つまり行頭の空白も削除されます。

末尾のメタ文字$も同様に、/mによって末尾だけでなく行末にもマッチします。

/m修飾子の指定によって、^$は厳密な冒頭や末尾の意味を失います。その代わり、/m修飾子の有無にかかわらず冒頭にのみマッチする\A末尾にのみマッチする\Z\zが用意されています[1]⁠。(4)のs/^\s+//gms/\A\s+//gmに置き換えた場合、出力結果はAliceの左側にある空白のみが削除されます。

/s修飾子─⁠─ドットを改行文字にもマッチさせる

/s修飾子は、任意の1文字にマッチするメタ文字.を改行にもマッチするよう変更します。この修飾子を指定した状態は単一行モードsingle line modeと呼ばれます。

つまり、/s修飾子を指定しない.「任意の1文字」と説明されますが、唯一改行文字にマッチしません

ドットが標準で改行にマッチしない理由

Perlの誕生以前からあるUNIXコマンドのgrepsedなどは、検索文字列を行ごとに受け取ります。つまり、検索文字列に改行が入ることはないため、.が改行文字にマッチするか否かは未定義だったとも言えます。これらUNIXコマンドでは、正規表現として.*を指定したとしても、行末を越えてマッチすることはありません。

そのあと誕生したPerlなどの正規表現を備えたプログラミング言語では、.*が行末を越えてマッチすることがないUNIXコマンドユーザーの直感を尊重するため標準では.は改行にマッチしない方針を取りました。

/s修飾子の有無による違い

しかし、Perlでは改行入りの文字列を正規表現で処理することも普通です。この場合、.が改行にマッチしないと不都合であるため、/s修飾子を用います。

実際の例を見てみましょう。

my $str1 = <<END_STRING;  ――(1)
<a href="https://gihyo.jp/dp/">
Gihyo Digital Publishing
</a>
END_STRING
my $str2 = $str1;  ――(2)

if ( $str1 =~ m|<a (.*?)>(.*?)</a>| ) {  ――(3)
    print "str1: リンクを発見しました\n";
    print "属性一覧は $1\n内容は $2\n";
} else{
    print "str1: リンクを発見できませんでした\n";  ――(4)
}

if ( $str2 =~ m|<a (.*?)>(.*?)</a>|s ) {  ――(5)
    print "str2: リンクを発見しました\n";  
    print "属性一覧は $1\n内容は $2\n";    ┛(6)
} else{
    print "str2: リンクを発見できませんでした\n";
}

(1)(2)のヒアドキュメントによる文字列定義と文字列コピーは、先ほどの例と同様です。上記は、(1)のHTMLの断片から最初に見つかったa要素の属性一覧と内容にマッチするサンプルです。a要素の内容に、改行を含むことに注意してください。

(3)/s修飾子がないため、.が改行文字とマッチしません。よって(4)の文字列が出力されます。

一方、(5)/s修飾子があるため、.は改行文字にもマッチします。よって(6)の文字列が次のように出力されます。

str2: リンクを発見しました
属性一覧は href="https://gihyo.jp/dp/"
内容は
Gihyo Digital Publishing

/s修飾子とドットを使用しない方法

メタ文字.が修飾子/sの有無によって意味を変えることが混乱につながるのであれば、マッチさせたい文字列の直後にある文字以外の任意の1文字[^ ■ ]を、.の代わりに使う方法もあります。

my $str1 = <<END_STRING;
<a href="https://gihyo.jp/dp/">
Gihyo Digital Publishing
</a>
END_STRING

if ( $str1 =~ m|<a ([^>]*)>([^<]*)</a>| ) {  ――(1)
    print "str1: リンクを発見しました\n";  
    print "属性一覧は $1\n内容は $2\n";    ┛(2)
} else{
    print "str1: リンクを発見できませんでした\n";
}

(1)では/s修飾子もメタ文字.も使用していないことに注目してください。[^>][^<]はそれぞれ><以外の任意の1文字を表す正規表現ですが、この任意の1文字の中には改行\nも含まれます。よって、マッチが成功して(2)の文字列が出力されます。

この[^■]で説明するなら、/s修飾子が指定されていない場合の.[^\n]と同等です

マッチを期待する文字列によっては、メタ文字.を使わずに正規表現を組み立てるほうが、/s修飾子の有無を気にすることなく読みやすい場合があります。

2つの修飾子/mと/sの覚え方

2つの修飾子/m/sを紹介しましたが、すでにこれらを学んだことがあっても、ややこしさを感じる人が多いのではないでしょうか。

複数行モードと単一行モード。その名前から対比される概念かと思えば、そもそも双方が意味変更の対象とするメタ文字が違います。

筆者も勉強したてのころはなかなか覚えられず、⁠英語だと自然なのだろうか」と思ったりもしました。しかし、正規表現の名著『詳説 正規表現 第3版』注2でさえ、⁠不適切な名前」⁠p.108⁠⁠、⁠始末が悪い」⁠p.109)と書く有様。英語圏でもややこしさは同じようです。

そうであれば別の覚え方をすればよいのではと、筆者は次の語呂合わせで覚えています。

/mはmultiple match mountain mode

^$が冒頭や末尾だけでなく改行の前や後にもマッチする/mは、multiple match mountain modeと覚えます。山の形の記号^が複数ヵ所にマッチする端的な説明であり、覚え方に頭文字mが何度も登場することで、/mの意味が記憶に刻まれることを期待しています。

$については上記のフレーズ内では言及されませんが、^が複数ヵ所にマッチするのであれば$も同様に連想できるでしょう。

/sはsuper dot mode

メタ文字.が改行にもマッチする/sは、super dot modeと覚えます。今までの.「任意の1文字にマッチする」とされつつも改行にはマッチしない性質がありますが、/sを指定すると.がマッチする文字対象がスーパーになる[3]と連想できるでしょう。

/r修飾子─⁠─非破壊で置換を行う

前項までで見てきたとおり、通常の置換演算子s///は置換対象文字列を破壊します。破壊するとは、変数の内容を再代入することなく変更することを言います。

本項で解説する/r修飾子の登場以前の古いPerlで破壊を避けたい場合、代入で文字列のコピーを作成し、それに対して置換を実行していました。

my $input = "アリスさんとボブくん";  ――(1)
my $output = $input;  ――(2)
$output =~ s/君|くん|さん/さま/g;  ――(3)
print "$input → $output\n";

(1)で定義した文字列を破壊することなく置換結果を得たい場合、(2)で代入によって文字列のコピーを別の変数に確保したうえで、(3)でコピーを確保した変数に対して置換を行います。(2)でコピーを確保せず(3)$inputに対して置換を行うと、(1)の内容が失われます。

しかし、上記手順は煩雑なこともあり、Perl 5.14で置換を非破壊で行う/r修飾子が登場しました。

my $input = "アリスさんとボブくん";
my $output = $input =~ s/君|くん|さん/さま/gr;  ――(1)
print "$input → $output\n";

(1)/r修飾子を指定することによって、$input自体は破壊されず、$outputに置換結果の文字列が代入されます。

別名変数

置換演算子s///が破壊的であることで特に不都合な場合があるのは、formapの一時変数です。

my @names = ("Alice", "Bob", "Carol", "Dave");
print "ループ内: ";
for my $name (@names) {
    $name =~ s/^(.).*(.)$/$1...$2/;  ――(1)
    print "$name ";
}
print "\n元の配列: @names\n";  ――(2)

(1)では、"Alice""A...e"となるよう、名前を一部伏せる置換を施しています。しかし、forの一時変数$nameを破壊しただけだと思ったら、(2)で元の配列も破壊されていることがわかります。

ループ内: A...e B...b C...l D...e
元の配列: A...e B...b C...l D...e

この$nameのような変数は別名変数と呼ばれ、繰り返しの配列の特定の要素表記$names[0]などの、文字どおり別名となります。つまり、たとえば繰り返しの初回であれば、$name$names[0]双方を評価した値は同一、一方を破壊すると他方も破壊されます[4]⁠。

forやmapと相性が良いs///r

前項のような場合、変数を破壊しない/r修飾子が有用です。前項の例のfor文を書き換えると以下になります。

for my $name (@names) {
    print $name =~ s/^(.).*(.)$/$1...$2/r, "\n";
}

特にmapは、一時変数が$_固定であることと、s///=~を伴わない場合の置換対象が$_となるため、無意識に元の配列を破壊する場合があります。

my @names = ("Alice", "Bob", "Carol", "Dave");
my @snips = map { s/^(.).*(.)$/$1...$2/; $_ } @names;
print "@snips\n";
print "@names\n"; # 破壊されている!

/r修飾子によって、mapの中で$_を破壊せず、自然に書くことができます。

my @names = ("Alice", "Bob", "Carol", "Dave");
my @snips = map { s/^(.).*(.)$/$1...$2/r } @names;
print "@snips\n";
print "@names\n"; # 元のデータは無事

/r修飾子を使うことができない場合、mapのブロック内にて$_のコピーを作成することで対応します。

my @snips = map {
    my $x = $_; $x =~ s/^(.).*(.)$/$1...$2/; $x
} @names;

<続きの(3)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧