旧燈明日記

自分史と英語学習と映画感想と健康志向と警備一般

Perlの謎(その8):正規表現

今回の謎は、正規表現です。
はじめての人のために解説をしてみました。


他言語からの人は、正規表現を使う機会がなかったかもしれませんが・・・。
Perlでは、正規表現を使うための言語と言っても過言ではありません。
正規表現を使うことによりテキスト(文字列)編集処理が飛躍的に楽になるのです。


そもそも正規表現は、人体の神経回路網を数学的に説明するための方法として開発されました。その後、UNIXのケン・トンプソンによってコンピュータでの検索アルゴリズムに引き継がれました。ちなみに、正規表現を初めて使用したアプリケーションは、UNIX のエディタedやexの祖となったQEDというエディタのようです。QED、ed、grep等を経てPerlに引き継がれたようです。 (そもそもの正規表現は、参考リンクのリンク先を参照しました)


では、まずはじめにテストデータ、family.txt を用意します。
このテストデータを元にしてサンプルプログラムを踏まえて解説をしていきますね。

・family.txtファイルの内容
macha koike
yachu koike
chaichan koike
hiro koike
mama koike
papa koike
koike 6
ni-bo- horie yasashii
nee-nee horie chottokowai
60 horie
ma-kun murai
sachi murai
kazumasa murai
hisa

◆マッチング

まず始めに、苗字が『koike』の人をマッチさせてみましょう。(サンプル01 sample01.pl)

while($_ = <STDIN>){
   if($_ =~ /koike/){
      print $_;
   }
}

上記は、標準入力からデータを読み込んで一行づつループし、その行データに『koike』がマッチするか調べ、マッチした時に行データを標準出力しています。
『$_ =~ /koike/』の式はマッチした時は、真を返し、マッチしない時は、偽を返します。
では、実行してみましよう。

% sample01.pl < family.txt
macha koike
yachu koike
chaichan koike
hiro koike
mama koike
papa koike
koike 6
・補足1

逆にマッチしない時は真を返し、マッチした時は偽を返すには『$_ !~ /koike/』とします。

◆アンカー

まずいことに、名前以外の koike 6 もマッチてしまいました。
そこで、koikeという文字列がデータの末尾に一致する場合のみに限定します。(サンプル02 sample02.pl)

while($_ = <STDIN>){
   if($_ =~ /koike$/){
      print $_;
   }
}

/koike$/の『$』は、文字列の末尾にマッチする記号で、アンカーと言います。
アンカーには、他に文字列の先頭にマッチする『^』と単語の境界(正確には、単語の先頭あるいは末尾)にマッチする『\b』、それ以外の部分にマッチする『\B』があります。
では、実行してみましよう。

% sample02.pl < family.txt
macha koike
yachu koike
chaichan koike
hiro koike
mama koike
papa koike

今度は上手く行きました。

・アンカー『^』の検索(サンプル03 sample03.pl)

つぎに、アンカー『^』を使って『k』で始まる人を検索してみましょう

while(<STDIN>){
   if(/^k/){
      print;
   }
}

尚、『$_』は省略可能です。

% sample03.pl < family.txt
koike 6
kazumasa murai
・補足2

また、アンカーには、 文字列の先頭にマッチする 『\A』 と文字列の末尾にマッチする 『\Z』 があります。
『\A』 と 『^』 、『\Z』 と 『$』 は機能がよく似ていますが、複数の行がある文字列を対象に正規表現を行った場合、『^』 は改行の直後にもマッチしますが、『\A』 は文字列の先頭にしかマッチしません。
また、『$』は改行の直前にもマッチしますが、『\Z』は文字列の末尾にしかマッチングしません。


◆文字クラス

また、よけいな koike 6 がマッチしてしまいました。
そこで、koike 6 には数字が入っていますので、数字があるとNGにすればいいわけです。
ですが、発想を変えて、数字以外の文字が連続するとしても同じです。

・数字以外の文字が連続の検索(サンプル04 sample04.pl)
while(<STDIN>){
   if(/^k[^0-9]+$/){
      print;
   }
}
% sample04.pl < family.txt
kazumasa murai

今度は上手く行きました。
まず、[〜]は文字クラスといって、[0-9]の場合は 0から9までの1文字にマッチします。
[^0-9]は文字クラスの中に ^が付くことによって文字クラスの否定を意味します。
文字列のアンカーの『^』とは別物です。
[^0-9]+の+は1回以上の『繰り返し』という意味です。
つまり、0-9以外の一文字の繰り返しになります。
$は末尾にマッチのアンカーの$です。

・1語の名前を検索(サンプル05 sample05.pl)
while(<STDIN>){
   if(/^\S+$/){
      print;
   }
}
% sample05.pl < family.txt
hisa

\Sも文字クラスで、空白文字以外を表します。
つまり、^\S+$ は先頭が空白文字以外で、これがかつ一回以上の繰り返しで、末尾までになります。

・3語の名前を検索(サンプル06 sample06.pl)
while(<STDIN>){
   if(/^\S+\s+\S+\s+\S+$/){
      print;
   }
}
% sample06.pl < family.txt
ni-bo- horie yasashii
nee-nee horie chottokowai

\sも文字クラスで、空白文字(空白,タブ,改行)を表します。\Sの反対です。
つまり、^\S+\s+\S+\s+\S+$ は先頭が空白文字以外で、これがかつ一回以上の繰り返した後、空白文字を一回以上の繰り返し、空白文字以外一回以上の繰り返し、空白文字を一回以上の繰り返し、空白文字以外一回以上の繰り返しで、末尾までになります。

・文字クラス一覧
    [ABC]           A,B,Cのいずれか1文字    
    [A-Z]           A〜Zまでのいずれか1文字
    [A-Za-z0-9]     A〜Z, a〜z, 0-9までのいずれか1文字    
    [^ABC]          A,B.C以外の文字
    [^A-Z]          A〜Z以外の文字  
    \w              英数文字。[a-zA-Z0-9]と同様    
    \W              \w以外の文字
    \d              数値文字。[0-9]と同等    
    \D              \d以外の文字
    \s              空白文字(空白,タブ,改行)     
    \S              \s以外の文字
    \b              単語の区切り
    .               任意の一文字
・補足3

/koike/ だと koikesでもマッチしてしまいますが、/\bkoike\b/だと回避できます。

・補足4

\wと\Wは、アスキー系以外では機能しない場合があるらしい。



◆メタキャラクタとエスケープ

正規表現において特殊な意味を持つ文字をメタキャラクタと呼びます。

    + * ? . ( ) [ ] { } | \ $ ^

\は、メタキャラクタの本来の作用をエスケープし、
メタキャラクタ以外では書かないのと同じになります。

たとえば、


^\^ は、^という文字で始まる行にマッチ
\\ は、\自体にマッチ


また、\Qと\Eの間に挿入された文字列は全部の文字の前に \ が挿入されたものと同じです。
/\o\)\+\>/ と /\Qo)+<\E/ は同じ。

◆繰り返し

    A+              1個以上連続したA(A, AA, AAA, ...)
    A*              0個以上連続したA(  , A, AA, AAA, ...)
    A?              0または1つの任意文字(  , A, B, C, ...)
    A{5}            5回繰り返し。 AAAAAと同じ
    A{3,}           3回以上繰り返し。 AAA+と同じ
    A{3,5}          3回以上5回以下繰り返し。 AAAA?A?と同じ

◆ 最大マッチと最小マッチ

たとえば、以下のようなデータがあったとします。

<h1>Koike family</h1> <h2>Home Page</h2> <h3>By chaichanpapa</h3>

このデータからタグのみ除去してみましょう。

タグは < ではじまり、 > で終わる文字列です。
その文字列の中を「.」の任意の一文字で「*」で0回以上の繰り返しとして指定します。
ですので、以下のようなプログラムになります。

・サンプル(sample10.pl)
while(<DATA>){
      s/<.*>//g;
      print;
}
__END__
<h1>Koike family</h1> <h2>Home Page</h2> <h3>By chaichanpapa</h3>

では、実行してみましょう。

% sample10.pl

%

改行のみの表示だけになってしまいました。

実は、perlでは、パターンマッチがもっとも長い文字列とマッチする最大マッチなのです。
ですから、<h1>Koike から chaichanpapa</h3> まで削除されてしまいました。
では、タグのみ削除するにはどうしたらいいでしょう?
それには最小マッチの ? を指定します。

・サンプル(sample11.pl)
while(<DATA>){
      s/<.*?>//g;
      print;
}
__END__
<h1>Koike family</h1> <h2>Home Page</h2> <h3>By chaichanpapa</h3>

では、実行してみましょう。

% sample11.pl
Koike family Home Page By chaichanpapa

今度は、タグが除去され、上手くいきました。

・補足5

最大マッチと最小マッチは、英語(perlretut.pod)での"maximal match"と"minimal match"の訳です。
しかし、訳者によって、最長マッチと最短マッチや最多マッチと最少マッチになっていますね。
ちなみに、私は、すP派です。

◆グループ化と選択

文字列を繰り返すときは()を使ってグループ化します。

    koi(ke)+        koike, koikeke, koikekekeなどにマッチします。
     
    いくつかのパターンのどれかにマッチさせるときは | を使います。
    (選択はパフォーマンスがあまり良くないとのことです。)

    macha|yachu     machaかyachuにマッチします。
    koike(X|Y)      koikeXかkoikeYにマッチします。

◆カッコを使った記憶

また、グループ化した部分は後から参照することができます。
これを後方参照といい、 同じ正規表現内で後方参照を行うには、\1, \2... を用います。

    /(koike)\1/    koikeが2回連続する文字列にマッチします。

また、正規表現外で後方参照を行うには、マッチ特殊変数 $1, $2... を用います。

$str = 'koike chaichan';
$str =~ /^(k.+) (c.*)$/i;
print $1, "\n"; # koikeと表示される
print $2, "\n"; # chaichanと表示される

◆マッチ特殊変数

マッチ特殊変数には、$1,$2,・・・の他に、$` , $& , $'等があります。
これらは、パターンマッチしたときに「マッチする前までの文字列」、「マッチした文字列」、「マッチした後の文字列」が各々、$` , $& , $' に格納されます。では、プログラムで確かめてみましょう。

サンプル sample14.pl
  $n = 0;
  while(<STDIN>){
        chomp;
        / \S+ | /;
        ++$n;
        print "*** $n ***\n";
        print "AAA: $`\n";
        if($& ne " "){
           print "BBB:" . substr($&, 1, length($&) -2) . "\n";
        } 
        print "CCC: $'\n";
        print "\n";
  }
% cat family3.txt
macha koike
sayuri koike chaichan
%
%
% sample14.pl < family3.txt
*** 1 ***
AAA:macha
CCC:koike

*** 2 ***
AAA:sayuri
BBB:koike
CCC:chaichan

まず、chompは $_ の末尾の改行コードを切り落とします。
/ \S+ | /; は「左右に空白を持つ非空白1文字以上か、または、1文字の空白」のマッチです。
つまり、今回の場合は、 *** 1 ***は「1文字の空白」のマッチで、 *** 2 ***は「左右に空白を持つ非空白1文字以上か」にマッチします。
そして、$` と $& と $' に各々の値が格納されます。尚、$&には前後にスペースが入っていますので、substr($&, 1, length($&) -2)で削除します。

◆置換処理

置換演算子(s///)を用いると、文字列の置換を行うことができます。

my $str = 'koike sayuri';
$str =~ s/(koike) sayuri/$1 chaichan/; # $strは 'koike chaichan' になる
print $str, "\n";
・修飾子

以下の修飾子を指定することにより、パターンマッチの振る舞いを変えることができます。

  • i 大文字小文字の同一視 (case-insensitive)
  • s 「.」が改行にもマッチするようにする (single line)
  • m 複数行として扱う (multi-line)
  • x 拡張正規表現を使う (extended)
  • e Perlのコードとして評価する (evaluation)(並べてた数だけevalします)
  • g 連続して何回もマッチ (global)
  • o 一度だけコンパイルする (only once)
・i修飾子

i修飾子は、正規表現がアルファベットの大文字小文字を区別せずにマッチするようにするために指定します。

m/^chaichan$/i; # chaichanにもChaichanにもCHAICHANにもChAichanにもマッチする。
・s修飾子
#「.」が改行にもマッチするようになる
my $str = "aaa bbb\nccc ddd";
print "No Match1\n" if ($str !~ /bbb.*ccc/);
print "Match1\n"    if ($str =~ /bbb.*ccc/s);
・m修飾子
#m修飾子は、複数行として扱う
my $str = "aaa bbb\nccc ddd";
print "No Match2\n" if ($str !~ /^ccc/);
print "Match2\n"    if ($str =~ /^ccc/m);
・x修飾子

正規表現内の空白や改行が無視され、「#」以降はコメントとして扱われます。

# 1と出力
print 1 if 'Chaichan' =~ /
C
h
a
i
# コメント
c
h
a
n
/x;
・e修飾子

以下はあまり意味がないプログラムですが、3回『e』しています。

my $str = "hello world";
$str =~ s/\w+/subx()/eeeg;
print $str;

sub subx {
    return 'suby()';
}
sub suby {
    return 'subz($&)';
}
sub subz {
    ucfirst($_[0]);
}
C:\perltest>perl reg_e.pl
Hello World
・g修飾子

連続して何回もマッチの gですが、上記で『s/\w+/subx()/eeeg』を『s/\w+/subx()/eee』にすると、表示は『Hello world』となり、worldはそのままで、Helloだけ『h』が『H』になり、連続しなくなります。
逆にいうと、g修飾子を指定すると連続して何回もマッチするわけです。

・o修飾子

変数展開が最初の1回だけ行われます。
逆にいうと、o修飾子を指定しない場合は、毎回変数展開される。

my $str = "aaa bbb\nccc ddd";
my $regex = 'aaa';
for (1..10) {
    print "Match3\n" if ($str =~ /$regex/o);
}

◆拡張構文

Perlには、以下のような拡張構文があります。
拡張構文は、キャプチャ(カッコを使った記憶)を行わないため、マッチした部分を正規表現の中で\1、\2のように参照したり、後から$1、$2のような変数で参照したりすることができません。

  • (?# コメント) 正規表現内にコメントを入れます。
  • (?=式) 前方一致検索です。
  • (?!式) 後続の文字列が式に一致しなければマッチします。
  • (?ismx) 正規表現内にパターンマッチ修飾子を埋めみます。
  • (?<=式) 後方一致検索で先行する文字列が式に一致すればマッチします。
  • (?<!式) 後方一致検索の否定形で先行する文字列が式に一致しなければマッチします。


以下は、拡張構文のサンプルです。すべてマッチします。

$bbb = '東京都庁';
$ccc = 'とうきょう都庁';
print "Match1", "\n" if ($bbb =~ /東京(?=都庁)/);  # 東京の後に都庁が続いている東京にマッチ
print "Match2", "\n" if ($bbb =~ /東京(?!議会)/);  # 東京の後に議会が続いていない東京にマッチ
print "Match3", "\n" if ($bbb =~ /(?<=東京)都庁/); # 東京の後に都庁が続いている都庁にマッチ
print "Match4", "\n" if ($ccc =~ /(?<!東京)都庁/); # 東京でないの後に都庁が続いている都庁にマッチ

ちなみに、拡張構文の「東京(?=都庁)」と普通構文の「東京(都庁)」は同じ結果になりますが、すべての拡張構文には後方参照がありません。
また、拡張構文では検索文字にパターンマッチ修飾子(imsx)が埋め込めます。

$_ = 'TOKYO';
# 普通構文
$ddd = 'Tokyo';
if (/$ddd/i) {
  print "$ddd\n";
}
# 拡張構文
$ddd = "(?i)Tokyo";
if (/$ddd/) {
  print "$ddd\n";
}

正規表現初心者卒業

上記がマスター出来たら、とりあえず、正規表現初心者卒業です。
しかし、正規表現は奥が深く、また、Perlが独自に拡張しています。
ですので、まだまだ、正規表現を極めようという人は、とりあえず以下で勉強をしてみてください。
再帰的な正規表現正規表現の中にPerlコードを記述できたりと、高度なテクニックが書いてありますよ。