正規表現(8) 検索オプション /g

Perl での正規表現の勉強を続けます。検索オプションは、これまでに /i/m について勉強しましたが、まだまだあります。
普通、パターンマッチングというのは文字列中で一番最初にマッチした結果を返します。これはつまり、同じ文字列に対して同じパターンで検索を行なった場合、「マッチ結果は常に同じ」という意味です。

use strict;
 
my $text = "xaaax,xbbbx,xcccx,xdddx";
 
for (1..5) {
    my $result = $text =~ m/x(\w+)x/;
    if ($result) { print "$1\n" }
}

スカラーコンテキストで、同じ文字列に対して同じパターンで5回マッチングを行なっています。マッチした場合はキャプチャした文字列を表示します。
実行結果は以下の通りです。

aaa
aaa
aaa
aaa
aaa


/g オプションを指定すると、最後にマッチした文字列の位置を覚えておいてくれます。そして、次回の検索では覚えておいた位置の後ろから検索を始めるのです。(プログレッシブマッチと呼ぶそうです)
なので先ほどのコード中のパターンに /g を付けると……

use strict;
 
my $text = "xaaax,xbbbx,xcccx,xdddx";
 
for (1..5) {
    my $result = $text =~ m/x(\w+)x/g;
    if ($result) { print "$1\n" }
}

以下のような実行結果になります。

aaa
bbb
ccc
ddd

1回目のマッチングで「xaaax」に、2回目で「xbbbx」に……とマッチしていき、5回目ではもうマッチするものがありませんので $result には偽が入ります。
実際に使う場合は while と組み合わせると「検索パターンが見つかる限り何度も繰り返す」という事ができて、大変便利です。なんだか each 関数を利用したループに似ていますね。(^_^)
実際に使ってみます。

use strict;
 
my $tongue_twister = <<'TT';
She sells seashells by the seashore.
The shells she sells are surely seashells.
So if she sells shells on the seashore,
I'm sure she sells seashore shells.
TT
 
my %s_words = ();
while ($tongue_twister =~ m/(s\w*)/ig) {
    $s_words{ lc($1) }++;
}
 
for (sort keys %s_words) {
    print "$s_words{$_} $_\n";
} 

「Tongue Twister」とは「早口言葉」の事です。「She sells seashells by the seashore.」はとても有名な早口言葉なのですが、その全文が $tongue_twister です。ヒアドキュメント(Here-Document)を使って、文字列を埋め込んでいます。(ヒアドキュメントについては後述後日書いたヒアドキュメントの解説
この文字列に対して「m/(s\w*)/ig」(「s」で始まる英単語、大文字小文字は区別しない)で検索を繰り返しています。while の条件部はスカラーコンテキストで評価されるので、次々とマッチする英単語を検索します。(パターンに /g オプションを指定しないと、無限ループになってしまいます)
ハッシュ %s_words は lc で小文字にした英単語をキーとして、登場した回数を数えています。「++」はインクリメント(1増やす)ですね。
あとは、%s_words のキーの配列をソートして for して全て print して……で、実行結果は以下のようになりました。

2 seashells
3 seashore
4 sells
4 she
3 shells
1 so
1 sure
1 surely

どうやら、Perl でコードを書くのに慣れてきたようです。思いのほか、上のコードをスラスラ書く事ができました。
どんどん勉強して、Perl を Native Tongue (母国語)のように使いこなせるようになりたいですね。(^_^)

リストコンテキストの場合

ところで、上記の説明は全て「スカラーコンテキスト」で評価した場合の話です。キャプチャを含む正規表現をリストコンテキストで評価した場合は、キャプチャされた文字列がリストで返ってきましたが、これは「一番最初のマッチ」だけでした。

use strict;
 
my $text = "aaa:bbb:ccc ddd:eee:fff ggg:hhh:iii jjj:kkk:lll";
 
my @result = $text =~ m/(\w+):(\w+):(\w+)/;  # no /g option
print "@result\n";

配列変数(@result)への代入なので、このマッチングはリストコンテキストで評価されます。「/g」オプションは付けていません。この場合、実行結果は以下の通りです。

aaa bbb ccc

つまり、「aaa:bbb:ccc」が最初にマッチしたので、その部分文字列をキャプチャして返しているわけですね。これは勉強しました。
「/g」オプションを付けると、「文字列の終わりまで検索して」「全てのキャプチャが入ったリスト」が返ってきます。

use strict;
 
my $text = "aaa:bbb:ccc ddd:eee:fff ggg:hhh:iii jjj:kkk:lll";
 
my @result = $text =~ m/(\w+):(\w+):(\w+)/g;  # /g option used
print "@result\n";

今度は「/g」オプションを付けています。これを実行すると…

aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll

と、全てのキャプチャが入ったリストが返ってきました。
これはスゴい便利。(なんだか Perl に感心しっぱなしですケド (^_^;) )


これを利用すると、キャプチャの時に勉強した「INIファイルの解析」がとても簡単になります。

use strict;
 
my $ini_string = <<'INI';
name=Palmo
lang=Perl
hobby=programming
INI
 
my %values = $ini_string =~ m/^([^=]+)=(.+)$/mg;
 
print "Hi! I am $values{name}, studying $values{lang}.\n";
print "And my hobby is $values{hobby}.\n";

まだファイルの読み込みは勉強していないので、とりあえず $ini_string をファイルの内容と見立ててみました。早く勉強しなくちゃ。(^_^;)
実行結果はこうなります。

Hi! I am Palmo, studying Perl.
And my hobby is Programming.

注目すべきは %values への代入文。$ini_string に対して「m/^([^=]+)=(.+)$/mg」でマッチングを行なった結果を %values に代入しています。どういう意味なのか解説しますね。
利用した正規表現についてですが、「オプション /m」の効果を思い出せばわかるはずです。「^」が行頭、「$」が行末にマッチするので、このパターンは「1行にマッチするパターン」と言えます。あとはキャプチャの時と全く同じです。つまり、このパターンは「名前=値」という一行にマッチする事になります。
「/g」オプションがついているので、全てのキャプチャのリストが返ってくる事になるわけですが、ここでキャプチャされているのは「名前」と「値」です。つまり、返ってくるリストは「(名前, 値, 名前, 値, 名前, 値……)」というリストになるはずです。

($ini_string =~ m/^([^=]+)=(.+)$/mg)("name", "Palmo", "lang", "Perl", "hobby", "programming")

このリストをハッシュに代入すると……。そうなんです。なんと「名前」が「キー」に、「値」はそのまま「値」として、ハッシュが生成されるんです!

my %values = ("name", "Palmo", "lang", "Perl", "hobby", "programming");

なので、$values{name} や $values{lang} で、INIファイルの値が出てくる、というわけです。
正規表現を使わずにINIファイルを解析しようとすると、なかなか大変です。Perl正規表現がどれだけパワフルかがわかりますね。(^_^)