◇ 正規表現実験室

正規表現ってばりばりと使えたらかっこいいですよね。複雑な検索条件、置換条件が一行にまとめられたソースを眺めていると、正規表現を手足のように使いこなしている方々の頭の中っていったいどーなっているのだろうと、つくづく感心してしまいます。しかし、感心してばかりじゃおもしろくないので、なんとかして自分も正規表現を使いこなせるようになろうと、とりあえず初歩の初歩から順番に勉強しながらこのページを作っていきます。ちなみに、執筆段階の私の正規表現レベルは…2くらいです。書式、文法等は別ページの正規表現リファレンスにまとめています。こちらの方も参考にして下さい。尚、ここでは「Perl」環境の正規表現ということで、「Perl」は当たり前に使っています。正規表現だけ使えても仕方が無いので … Perl の意味が解らない方は初歩程度の Perl の知識を手に入れてから以下の文章を読むことをお奨めします。

まず、正規表現とはなにかから話を進めていきます。正規表現は英語で、Regular Expressionとなっています。直訳しますと、規則正しい式となるのですが、この規則正しい式を、文書作成のとき、検索のとき、置換ときなどに使用していくというわけです。文字列のパターンを規則正しく表す言い回しと解釈してもよさそうです。つまり、文字列を直接的ではなくて、式であらわすための表現方法ということとなっています。

ここで、以下の話を進めやすくするために、

  VALUE =~ s/ PATTERN / REPLACE /

という構文を紹介しておきます。VALUE の中に、指定された PATTERN を見つけたならば、それを REPLACE で置き換えた文字列を返します。この構文を使いつつ正規表現の実験を行っていきたいと思いますので頭にいれておいてください。

では、一番簡単な正規表現を紹介します。一番簡単な正規表現は、例えば「 a 」や「 b 」です。別にばかにしてるわけではなく…これらも正規表現の表現方法の一つです。例えば、「a」という文字列は、「a」という文字列にマッチします。

my ($str) = 'a';

$str =~ s/a//;

print "結果>$str";

結果>

この例では、最終的に $str には空白文字が入ります。「a」という文字列の中から「a」という文字列を検索し、該当すれば空白文字と置き換えなさいと言う命令なので、こんなことは言うまでも無く当たり前なのですが、次の例ではどうでしょう。

my ($str) = 'a+';

$str =~ s/a+//;

print "結果>$str";

結果>+

ここで注意しておきたいのは、「+」は「+」にマッチングするわけではありません。正規表現において「+」は直前の文字の一回以上の繰返しを表しています。即ち、「a」、「aa」、「aaa」などが「a+」にマッチすると言うわけです。例をあげると、以下のようになります。

my (@str) = ('aaaa', 'aabbcc', 'abc', 'bbcc');

print '結果>';
foreach (@str){
  s/a+//;
  print "$_,";
}

結果>,bbcc,bc,bbcc

また、似たような表現の方法に、「*」があります。正規表現において「*」は直前の文字のゼロ回以上の繰返しを表しています。ゼロ回の繰返しと言いますとわかりにくいですが、空文字も含むと言い直すと少しわかりやすいでしょうか。以上をふまえて、下の例を見てください。

my (@str) = ('aaaa', 'aabbcc', 'abc', 'bbcc');

print '結果>';
foreach (@str){
  s/ab*//;
  print "$_,";
}

結果>aaa,abbcc,c,bbcc

「@str[0]」と、「@str[3]」では、予想通りの結果が返ってきたのですが、「@str[1]」では「a」だけ空文字と置き換わり、「@str[2]」では「ab」が空文字と置き換わっています。どちらかに統一されていれば予測もつくのですが、何でこんな結果になったのでしょう。どうも Perl における正規表現では、「+」「*」などのように繰返し表現を行った際に、何も指定をしなかったら最長マッチとなるらしいです。なるほど、それならば「@str[2]」で、「abc」が「c」と置き換わったのも納得がいきます。「a」とマッチという結果よりも「ab」とマッチという結果を優先したのでしょう。では、「@str[1]」の「aabbcc」置換結果が「abbcc」になったのは…。調べた結果、置換処理では、最初にマッチした文字列のみが置換の対象になるということでした。ということは、「aabbcc」から、一番最初の「a」が削除された結果が「abbcc」だったのでしょう。確認のために以下のような実験を行ってみました。

my ($str) = 'abbbcc';

$str =~ s/a*//;

print "結果>$str";

結果>cc

最初にマッチし、その中の最長マッチが削除されました。

ここで新たにわいてくる疑問があります。では最小マッチはどう表すの?指定しなければ勝手に最長マッチになると言うことは、何か指定したならば最小マッチになるのでしょう。「Perl」、「正規表現」、「置換」、「最小マッチ」でぐぐった結果が以下の例です。

my (@str) = ('aaaa', 'aabbcc', 'abc', 'bbcc');

print '結果>';
foreach (@str){
  s/ab*?//;
  print "$_,";
}

結果>aaa,abbcc,bc,bbcc

最小マッチにするためには、「+」や「*」(量指定のメタキャラと言うらしい)の後に「?」をつけるらしいのです。

さらに疑問はつきないわけで…MS Excel 等であいまい検索をする場合に「*」を使いますよね。ここでの意味は任意の文字列 … だったはずです。例えば、「山*県」であいまい検索を行うと、「山形県」や、「山梨県」、「山口県」他にもあるのでしょうか…これらが検索する集合の中にありましたらマッチするはずです。しかし、正規表現で同じ結果が得られるとは思えなく、実験してみれば当然惨敗でした。

my (@str) = ('山形県', '山梨県', '山口県', '山県');

print '結果>';
foreach (@str){
  s/山*県/OK/;
  print "$_,";
}

結果>山形県,山梨県,山口県,OK,

ここで、さらに新たな疑問が … なぜ結果が

結果>山形OK,山梨OK,山口OK,OK,

とならなかったのでしょう。「*」はゼロ回の表現、つまり、「県」も上の例ではマッチするはずです。もしかして、全角表現がまずいのかと思い、懲りずに実験。

my (@str) = ('abc', 'ab', 'bc', 'ac');

print '結果>';
foreach (@str){
  s/a*c//;
  print "$_,";
}

結果>ab,ab,b,,

今度は「a」がゼロ回の表現も1回の表現も共に成功しました。ということは、やっぱり日本語全角では繰返し表現を使うことはできないのでしょうか。いやいや、そんなわけあるはずがないと … また調べまくったところ自分のサイト内で気になる表現をみつけました(笑)「カッコは、文字や正規表現をグループにまとめる」早速、さらに実験を行ってみました。

my (@str) = ('山形県', '山梨県', '山口県', '山県');

print '結果>';
foreach (@str){
  s/(山)*県/OK/;
  print "$_,";
}

結果>山形OK,山梨OK,山口OK,OK,

うまくいったのでしょうか。なんとなく腑に落ちないと言いますか…ただ、この件に関する文献を他に見つけることができなかったので、とりあえず保留にしておきます。

で、大分話がそれましたが、あいまい検索に話を戻します。実は、上のことを調べてるうちに解決しましたので答えを紹介します。

my (@str) = ('山形県', '山梨県', '山口県', '山県');

print '結果>';
foreach (@str){
  s/山.+県/OK/;
  print "$_,";
}

結果>OK,OK,OK,山県,

ここで新たに「.」と「?」を紹介します。まず、「.」は任意の一文字を表します。当然、「a」、「b」は「.」で表されますし、さらに、改行、空白、全角文字も「.」で表されます。上の例で「.+」というのは、任意の一文字の1回以上の繰返しを表しているわけです。また、「?」ですが、これは、直前の文字の一回またはゼロ回の繰り返しを表しています。「a?」としましたら、「」もしくは「a」にマッチしますし、「.?」は任意の一文字もしくは空文字にマッチします。ということは、正規表現において、MS Excel の「*」と同じ意味になるのは「.*」なのです。

ここでまた疑問が(笑)例えば、正規表現でHTMLファイル名のみをマッチングさせたい場合にはどうすればいいのでしょう。どうも「.*.html」では具合が悪そうです。これは以下のように記述をするらしいです。

my (@str) = ('dummy.html', 'samp.htm', 'dummy.txt', 'samphtml');

print '結果>';
foreach (@str){
  s/.*\.html//;
  print "$_,";
}

結果>,samp.htm,dummy.txt,samphtml

この「\」記号がポイントです。「\」はエスケープシーケンスと言い、直後の文字(記号のみ)が特殊な意味を持っていようが持ってなかろうが、常にその直後の記号自体を表します。ですから、「*」という文字を検索したい場合には「\*」とすればよいのです。通常、エスケープしなければならない文字は「.」「?」「*」「+」「{」「[」「^」「$」「|」「(」「)」「\」となっています。まぁ、覚えてなくて不安に思う記号があったら、その前に「\」をつけて記述すれば問題ないと思います。

2004/09/10 追記)

「-」「}」「]」はエスケープしなくて良いのか?という指摘を受けたので追加実験しました。

#!/usr/bin/perl

print "Content-type: text/html\n\n";
$smp1 = "1-2";
$smp2 = "}]";
$smp1 =~ s/-/OK/;
$smp2 =~ s/]/OK/;
$smp2 =~ s/}/OK/;

print "$smp1/$smp2";
exit;

1OK2/OKOK

「-」「}」「]」は基本的にはエスケープしなくてよさそうですね。

もう少し欲張って、「htm」ファイルも一緒にマッチングさせてみましょう。ここまで色々な参考文献を読み漁って…私の正規表現レベルは3くらいになりました(笑)当然、「or」検索も実際に試してないだけで既に学習済みなはず。早速実験。

my (@str) = ('dummy.html', 'samp.htm', 'dummy.txt', 'samphtml');

print '結果>';
foreach (@str){
  s/.*\.htm(l|)//;
  print "$_,";
}

結果>,,dummy.txt,samphtml

少し不安はあったんですがうまくいきました。実験した後に気がついたのですが、「.*\.html?」でもおっけでした。そんなことよりも今回初登場なのが「|」という記号です。例が悪くアルファベットの「l」と紛らわしいのですが、106キーボード上の「BS」キーの左隣を「Shift」と同時に押せば出せます。しかし、これなんて読むのでしょう(笑)「記号の読み方」でぐぐった結果、「パイプ」、「パイプライン」、「バーチカルライン」などと呼ばれてるみたいです。このパイプなんですが、左右どちらかの表現がマッチすれば成功となるのです。例えば、「html|htm」とすれば、「html」または、「htm」という文字列がマッチします。また、「html|htm|shtml」このような表現の仕方も可能らしいです。

「or」検索の方法として、さらに「[○△]」といった表現も可能らしいです。「[○△]」では、「○」または「△」がマッチするということです。ところで、この「[  ]」という記号なんですが、さらに面白い使い方ができるらしいです。例えば、「[0-9]」としましたら「0から9の間のいずれかの数字」にマッチするらしいです。マッチするというならばマッチするのでしょうから・・・別の事例で実験してみましょう。

my (@str) = ('10', '05', '4', '21');

print '結果>';
foreach (@str){
  s/[3-11]/OK/;
  print "$_,";
}

Invalid [] range "3-1" in regex; marked by <-- HERE in m/[3-1 <-- HERE 1]/

上のようなエラーが出てしまいました。そもそもこんなことは出来ないみたいです。「[3-11]」と表してしまいますと、「[3-1]|1」という事になっているのでしょうね。

my (@str) = ('10', '05', '4', '21');

print '結果>';
foreach (@str){
  s/[3-7]+/OK/;
  print "$_,";
}

結果>10,0OK,OK,OK1,

これならうまくいきました。他にも「[A-Za-z]」でアルファベット、「[あ-お]」で平仮名のあ行、などといった表現もできるらしいです。これらの範囲は Shift-JIS の文字コードで決まるとのことなので、多分漢字もいけるのでしょうね。実験しようかとも思いましたが、漢字を「S-jis」のコードで表すこともまずないだろうと思い、妥協しました。補足として、「[^あお]」としましたら、「あ」「お」以外の全ての一文字、「[^あ-お]」としましたら、平仮名のあ行以外の全ての一文字を表すことができます。