| Top Page | プログラミング | Perl 目次 | prev | next | 索引 | 別表 |

19. 正規表現とパターンマッチ(その2)
後方参照・文字列置換

※以下の説明を読むときには,正規表現を整理した 別表 を別ウインドウで開いておくと便利かもしれません. >別表を別ウインドウで開く

'同じパターンが再現する'というパターンは後方参照で

前のページで,正規表現を使ったパターンマッチについて簡単に説明しました. ここまでの知識を使って,いろんなパターンの表現方法を考えてみてください. たとえばメールアドレスをどう表現するか,電話番号をどう表現するか, 自分の扱うデータのなかに出てくる文字列パターンをどう表現するか. パズル的な工夫で,いろんなことが表現できます.

後方参照というものを使わないと表現がむずかしいパターンもあります. 後方参照は別表のエスケープシーケンスの終わりの ほうに載っています.パターンの中で,() で囲った部分にマッチした文字列を 意味するものです.() の出てきた順番に,\1, \2,...と書いて指定します. 特殊変数 $1, $2,...と似ていますが,パターンの記述の中に書き込んで 使うという点が違います(特殊変数 $1, $2,...は,パターンマッチが終わった あとで使う).

たとえば,数字の列のなかに,同じ数字が 5 個以上連続している部分があるか どうかを調べたかったらどうしましょうか.まず,失敗例から.

$numbers = "123456777777890";

if ($numbers =~ m/\d{5,}/) {  # \d (数字) が5個以上連続
    print $&, "\n";           #  $&は,パターンにマッチした部分が入ってる特殊変数
}

$& は別表の特殊変数のところに載ってますが, チェックした文字列全体のなかで,パターンにマッチした部分文字列が 自動的に代入される特殊変数です.

パターンマッチのところに書いてあるパターン中の,{n, } という表現は, 直前の文字が n 個以上連続することを意味します. だから,\d{5,} は数字が 5 個以上連続するという意味です. $numbers は数字だけが 15 個並んだ文字列なので,全体がマッチします. したがって,上のプログラムを実行すると 123456777777890 と表示されます.

…これでは数字の連続を探してるだけで,_同じ数字_の連続を探したことになってませんね. どうしたらよいでしょうか? 最初の \d にマッチした数字を覚えておいて,それと同じものが続けて 出てくるかどうかを調べればよいはずです.その「マッチした数字を 覚えておいて」そのあとのパターンの指定に使うのが後方参照です.

$numbers = "123456777777890";

if ($numbers =~ m/(\d)\1{4,}/) { 
    print $&, "\n";     #  $&は,パターンにマッチした部分が入ってる特殊変数
}

こんどは,数字ひとつにマッチされるエスケープシーケンス \d を () で囲って, \1 という名前で後方参照できるようにしました.\1 は,\d にマッチした特定の文字 そのものを意味することに注意してください. というわけで,/(\d)\1{4,}/ は,数字ひとつのあとに,それと同じ数字が 4個以上続いているパターン,すなわち全体としては同じ数字が5つ以上連続している パターンを意味します.

上のプログラムを実行すると,こんどはちゃんと "777777" と表示されます.


DNA シーケンスから繰り返しパターンを探し出す

これを応用すれば,DNA シーケンス中から TATATA…とか CGCG… といった 2塩基のペアの反復を探し出すこともできますね.

※(DNA にあまりなじみがない人向けの補足)

遺伝子の本体であるDNA は長い長い分子で,そこには T, A, G, C と略記される 4種類の構成要素(塩基)がずらずらと並んでいます.その配列に応じてタンパク質が 作られ,そのタンパク質がいろいろな働きをして生き物が出来上がります. ところが,DNA の塩基配列のなかにはタンパク質に翻訳されない部分も たくさん混じっています.そこには,短いパターンが何回も繰り返えされた 特徴的な部分もしばしば見つかります. ここで紹介する例はこれを踏まえたものです.

2塩基のペアが5回以上繰り返されるパターンを探しだすプログラムはこんなふうに書けます.

$dna_seq = "TTACGCGCGCGCGCGGTA";

if ($dna_seq =~ m/([ATCG][ATCG])\1{4,}/) { 

    # マッチした部分の前 ($`) の文字数を length() で調べる.
    print "from ", length ($`) + 1, "\t"; 

    # マッチした部分を表示.
    print $&, "\n"; 
}

[ATCG][ATCG] で,任意の2塩基のペアを表現し,これを () で囲んで後方参照して, 同じものがさらに4回以上続いているところ (すなわち同じペアが5回以上続いているところ)を探しています.

$` は文字列中のマッチした部分より_前_の部分が入っている特殊変数です (>別表の特殊変数を参照). 文字列の長さを教えてくれる関数 length を利用して $` の文字数を調べれば, マッチした部分が文字列のどこから始まってるのかが分かります.

うえのプログラムを実行すると,こんな出力が得られます.

from 4	CGCGCGCGCGCG

"TTACGCGCGCGCGCGGTA" の4番めから始まる,CG の 6 回の繰り返しが /([ATCG][ATCG])\1{4,}/ にマッチしていることが分かります.

6 回の繰り返しの最初の "CG" が ([ATCG][ATCG]) にマッチし(それが \1 に入る), 残りの 5 回の繰り返し "CGCGCGCGCG" が \1{4,} にマッチするから, 全体として "CGCGCGCGCGCG" が /([ATCG][ATCG])\1{4,}/ にマッチする,という わけです.

もうちょっとかしこいプログラムにしてみましょう. 繰り返しパターンが複数あってもちゃんと全部見つけるようにします. また,2塩基ペアの繰り返し回数を正規表現中に 4 なんてじかに書いてますが, これはあまり誉められた書き方ではありません. マジックナンバーと呼ばれる, 無作法な書き方の典型的な例です. 意味のある名前の変数に,みつけるべき繰返し回数を代入してから探索してもらいましょう.

$dna_seq = "TTACGCGCGCGCGCGGTTTATATACGT";

$repetition = 3;           #  繰り返し回数
$rep_1 = $repetition - 1;  #  正規表現のなかで使う.

$head_loc = 0; #  探索対象シーケンスの先頭は,もとのシーケンスの
               #  何文字めからに相当するかを記憶する変数

while (1) {    #  何度でも繰り返す

    if ($dna_seq =~ m/([ATCG][ATCG])\1{$rep_1,}/) { 

        # マッチした部分の前 ($`) の文字数を length() で調べる.
        print "from ", $head_loc + length ($`) + 1, "\t"; 

        # マッチした部分を表示.
	    print $&, "\n"; 

        $dna_seq = $';   #  見つけた繰り返しシーケンスのうしろが次の対象
        $head_loc += length($`) + length($&);  # その頭はもとのシーケンスの
                                               # 何文字めに相当するか.
    }

    else {
        last; # もう目指すパターンはないので while (1) から脱出
    }
}

こんどはこんな出力が得られます.

from 4	CGCGCGCGCGCG
from 19	TATATA

ちゃんと2つ見つかりましたね.

重要なポイントはふたつ.まず,後方参照 \1 の繰り返し回数のところで,$repetition から 1を引いた $rep_1 という変数をつかってます. 前にも書いたように,正規表現で書かれたパターンのなかに変数名があったら, それはダブルクオート " で囲まれた文字列の場合とおなじように, 変数展開してくれるので,このように繰り返し回数を 変数で与えることもできます.

もうひとつのポイントは,特殊変数 $` を使っていることです. パターンマッチの対象とする文字列が同じままにマッチングを繰り返すと, 同じところが見つかるばかり.マッチしたところより後ろの部分を表す特殊変数 $`を 次回のマッチングの対象とすることで,うしろへ,うしろへと探索を進めていく ことができます.


文字列中の文字を置き換える

文字列がパターンとマッチするかどうかを調べるには m// という演算子を 使いました.今度は,パターンにマッチする部分を他の文字列に置き換える演算子 s/// を紹介します.書き方は,

文字列 =~ s/正規表現で書かれたパターン/置換する文字列/

です.

s は置換 (substitution)の s と覚えてください. ’置換する文字列’は文字列であって正規表現で書かれたパターンではないことに 注意してください.\d (数字一文字)に置換せよと言われてもどの数字にしたらいいか 決められませんから,当然ですね.

m// は,うしろにオプションを付けることができました(たとえば m//i は大文字 小文字の区別なし). s/// も同様です. i (case insensitive, 大文字小文字の区別なし)や g (global, パターンにマッチするもの全部を置換.これを指定しないと 最初に見つけた一ヶ所しか置換しない)のほか,いくつかのオプションがあります.

ずっとまえに使ったこのデータは,名前や数値がいくつかの空白文字で 区切られています.

Atsushi 177.0   75.0
Hide    175.0   68.0
Yutaka  180.0   78.0
Koji    182.0   74.0
Shinji  175.0   74.0
Mitsuo  173.0   68.0

こんなデータファイルを読んで,区切りはすべてカンマ ',' に変換する プログラムを書いてみます.

while ($line = <>) {
    $line =~ s/\s+/,/g;
    print $line;
}

このプログラムで上のデータを処理すると,こんな出力が得られます.

Atsushi,177.0,75.0
Hide,175.0,68.0
Yutaka,180.0,78.0
Koji,182.0,74.0
Shinji,175.0,74.0
Mitsuo,173.0,68.0

出来合いのソフト間でのデータのやり取りなどに使えそうです. オブションの g を忘れると,最初の一ヶ所しか置換されません.


置換文字列を変数で与える

置換する文字列のなかには,変数を含めることもできます.パターンと同様, 変数はその値に置き換えられます(変数展開という機能でしたね). 変数には,もちろん $1, $2 などの特殊変数も使えます.

では,"2002/11/23" という形式の文字列を "2002年11月23日"に変換してみます.

$date = "2002/07/13";
$date =~ s/(\d{4})\/(\d\d)\/(\d\d)/$1年$2月$3日/;
print $date;

パターン中に / が出てくると,演算子 s/// の / だと解釈されてしまいます. これを避けるために,/ の頭に \ をつけて \/ と書きます.これで / そのものを 表現できます.

このプログラムを実行すると,2002年07月13日 と出力されます.


s/// が返す置換回数を利用する

s/// 演算子は,置換した回数を返します. どこもマッチせず,置換が行われなかったらゼロが返ります. ゼロは真か偽かというと偽なので, if (文字列 =~ s/…/…/) というように, 置換が行われたかどうかの条件判断にそのまま使えます.

さらに,置換回数を積極的に利用すると,文字列中にあるパターンが 何回出てくるかを簡単に数えることができます. たとえば 1 と 0 が混在した文字列の中に何個の 1 が含まれてるかは こんなプログラムで数えられます.

$data = "100010010101001101010";

$num_one = ($data =~ s/1/1/g);  # 1 を 1 で置換してるので $data は変化しない

print "There are $num_one 1's in $data\n";

このプログラムでは,1 を 1 で置換して,$data の内容が変化しないように していますが,$data の内容を保持する必要がないなら,置換する文字列は 何も書かずに s/1//g と書いてもかまいません.


もうなんでもあり:置換文字列を式で指定

s/// の強力なオプションに e があります. これは,置換文字列が式として評価できる場合にはこれを実行時に評価し, その結果を置換文字列とするというオプションです. 置換文字列のところに関数だって書けます. たとえば sprintf を使えば,文字列中に含まれる小数点付きの数値を すべて小数点以下3ケタ表示にするなんてことができます.

$line = "4.725 + 3.2 + 0.075 = 8.0";

$line =~ s/(\d+\.\d+)/sprintf("%.3f", $1)/eg;
print $line;

出力はこうなります.

4.725 + 3.200 + 0.075 = 8.000

みごとに小数点以下3ケタまで表示されていますね.

置換文字列のところに計算式を書くこともできます. hh:mm:ss 形式の時刻データをすべて 夜中の0時からの経過秒に置換してみましょう.

$line = "started 07:32:02 arrived 12:01:23";

$line =~ s/(\d\d):(\d\d):(\d\d)/($1 * 3600 + $2 * 60 + $3)/eg;
print $line;

出力はこうなります.

started 27122 arrived 43283

こういう仕事は,split して各データを配列にしまってから処理してもよい わけですが,どこに時刻データが出てくるか分からない場合には s///eg を 使った置換のほうが融通がききます.いずれにせよ,Perl のモットーは "There's more than one way to do it" です.


tr/// で1対1の文字変換

最後に,文字の置換の方法をもうひとつ紹介します. tr/// という演算子で,このように書きます.

文字列 =~ tr/探す文字のリスト/置換する文字のリスト/

見てのとおり,s/// 演算子に似てますが,パターンを指定するのではなくて 探す文字そのものを指定するところが大きく違います.

探す文字,置換する文字,それぞれ複数並べて書けます.探す文字リスト中の 1番めの文字が見つかったら置換する文字の1番めの文字で置き換え, 2番めの文字が見つかったら置換する文字の2番めの文字で置き換え(以下同様) という動作をします.

さっそく例をひとつ.

$dna = "GCTTAGTACT";    # こんな DNA 断片があったとして,
print $dna, "\n";

$dna =~ tr/TAGC/ATCG/;  # それと相補的なシーケンスを求める.
print $dna, "\n";

※(ふたたびDNA にあまりなじみがない人向けの補足)

遺伝子の本体であるDNA は2本の長い長い鎖を並べて捻りあわせたような構造を しています.それぞれの鎖には,4種類の構成要素 (T, A, G, C) がずらずらと 並んでいますが,となりの鎖の配列と一定の対応関係があり, T の向かい側には A, G の向かい側には C が来ます.だから,一本の鎖が あれば,おのずとそれのペアになる鎖の構成も決まります. うえの例はこれを踏まえたものです.

探す文字のリストよりも置換する文字のリストのほうが短かったら, 不足分はぜんぶ置換する文字のリストの最後の文字で補われます. tr/\t\n/ / と書けば,タブも改行も空白文字に置き換えられます.

文字のリストは,- を使って範囲指定することができます. tr/a-z/A-Z/ と書けば,小文字をすべて大文字に変換できます. また, tr/0-9/x/ と書けば,文字列中の数字をすべて伏せ字にできます.

tr/// 演算子は置換した文字の数を返します. たとえば 1 と 0 が混在した文字列の中に何個の 1 が含まれてるかは こんなプログラムで数えられます.

$data = "100010010101001101010";

$num_one = ($data =~ tr/1/1/);  # 1 を 1 で置換してるので $data は変化しない

print "There are $num_one 1's in $data\n";

これはさっき s/// でやったこととまったく同じような気がしますが… そう,この場合,どちらでも同じことです. ただし,置換したい対象が正規表現を使わないと表現できない場合には 否応なく s/// を使うことになります.


ここまででもじゅうぶん強力

正規表現のパターンマッチの話はここまでです. 正規表現をつかっていろんなパターンを表現することにはパズル的なおもしろさも ありますね.

もちろん,これですべてを説明し尽くしたわけではありません. 本を見れば,ほかにどんなことができるか書いてあります. でも,ここで紹介したことだけでもかなりのことができるでしょう.

ところで,日本語の文字列のパターンマッチは,コード体系や文字に よってうまくいく場合といかない場合があります. 世の中には,日本語の文字も自由に扱えるように perl を改変した jperl とか, 問題なく扱える EUC コードに変換するなど日本語処理用の関数を用意した jcode.pl や Jcode.pm とかいったものがあります. 詳しくはネットで検索するなどして調べてください (たとえば 日本語の扱い - Perlで日本語を扱うためのメモ Jcode.pm - jcode.pl の後継 など).


| Top Page | プログラミング | Perl 目次 | prev | next | 別表 |