Perl Hackers Hub

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

この記事を読むのに必要な時間:およそ 4 分

前回の(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の左側にある空白のみが削除されます。

注1)
対象文字列の末尾に改行がある場合に限り,$\Zは末尾だけでなく,その改行の直前にもマッチします。この挙動を望まない場合,厳密に末尾にのみマッチするメタ文字\zを使います。

/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修飾子の有無を気にすることなく読みやすい場合があります。

著者プロフィール

尾形鉄次(おがたてつじ)

大学院卒業後の2003年,Webメール開発会社に入社。以後10年以上,Perlを使ったWeb開発に携わる。

2015年,株式会社ガイアックスに入社。オンプレやクラウドの構築や保守運用を担当するインフラチームに所属しつつ,2018年頃から社外イベント運営やプログラミング教育などにも携わっている。

教育に関しては大学でのTAの経験などから課題感を持っていたが,2013年以降にPerl入学式などのコミュニティで教える側に立つことにつながり,そこで得られた知見を社内外で活かしている。

2019年よりJapan Perl Association理事,およびPerl入学式2代目代表を務める。