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

18. 正規表現とパターンマッチ:融通無碍な文字列処理

文字列を比較するのは eq だけじゃない

これまで,文字列を比較して判断するプログラムが何度か出てきました. 引いたおみくじが凶か? 2つの個体 ID が等しいか? この文字列は 空文字列 "" か?といった具合です.比較には,eq という演算子を 使いました.

eq を使ってできることは,2つの文字列がピッタリ同じかどうかを判断する ことです. でも,もうちょっとファジーな比較がしたいこともあるでしょう. 空文字列でなくとも空白やタブだけを含む文字列なのか, それともふつうの数字や文字が入った文字列なのかを判断したいとか, 数字だけが並んだデータなのかアルファベットも入ってるのかとか.

正規表現という文字列の表現方法を使うと, ある文字列そのものではなくて, 文字列の一部にある言葉を含んでいるパターンとか, 数字2文字のあとにアルファベットが続いたパターンといった具合に, パターンを指定することができます.

さらに,ある文字列がこのパターンと合ってるかどうかを判断するだけでなく, パターンと一致した部分を取り出したり,その部分をほかの文字列に置き換えたり といった操作も可能です.

これをメインに使って研究に必要なプログラムを書く場合はあまりないかも 知れませんが,ちょっとした作業がささっとできてありがたい, という場面ははよくあります.そんな例はあとで紹介するとして, まずは文法を簡単に紹介します.

あるパターンとある文字列が合っている(「マッチする」と呼ぶ)かどうかを 調べるときは m// という演算子を使います.これは,

文字列 =~ m/正規表現で書かれたパターン/

と書きます.m はマッチ(match)の m と覚えてください. パターンが文字列とマッチしたらこの式は真,マッチしなかったら偽になります.

=~パターン結合演算子と呼ばれる演算子です. 代入演算子 = とは関係ありません.左側の文字列が,右側のパターンと マッチするかどうか調べろ,という意味です(あとででてくる置換だの文字変換だのの場合は, 左側の文字列を対象に置換しろ,あるいは文字変換しろ,という意味).

正規表現では,普通のアルファベットや数字をそのまま書くと,その文字自体を 意味します.(ようするに「まんま」ということ). たとえば,こんな風に書いて,文字列のなかに take という文字列が 含まれるかどうか調べることができます.

if ("mistake" =~ m/take/) {   #  マッチする.
    print "mistake matches /take/\n";
}

if ( "cake" =~ m/take/) {   #  マッチしない.
    print "cake matches /take/\n";
}

$string = "takenaka";  # 変数を調べることもできる.

if ($string =~ m/take/) {   #  マッチする.
    print "$string matches /take/\n";
}

eq の場合と比べて,文字列全体の一致ではなくて,部分の一致も調べられる点が 便利です.

これだけの機能でも,たとえば DNA の塩基配列中に特定のシーケンスがあるか どうか調べるといった応用がすぐ考えられますね.


ファイルからのデータの抜き出し

データファイルをどんどん読み込んで,特定の文字列を含むものだけ書き出す というプログラムも簡単に書けます.

while ($line = <>) {
    if ($line =~ m/Quercus/) {  #  Quercus という文字列を含むか?
        print $line;
    }
}

m/パターン/ のパターン中に変数名があると,""で囲んだ文字列の場合と 同様に変数展開してくれる (その変数の_値_に置き換わる)ので, テストするパターンをコマンドラインから与えることもできます.

if (@ARGV != 2) {  #  コマンドライン引数は2つ必要.
    die "usage: perl p_grep.pl word file_name\n";
}

$word = shift @ARGV;  # コマンドライン引数のひとつめはパターン

while ($line = <>) {  # 二つめの引数でしたファイルから一行ずつ読む.
    if ($line =~ m/$word/) {  #  パターンとこの行がマッチするか?
        print $line;         #  マッチしてたら出力.
    }
}

「まんま」を指定するだけじゃ,まだ正規表現の実力の数パーセントしか使ってません.ネットの検索エンジンで,"Perl" と "正規表現"で検索するとたくさんのページが 見つかるので,正規表現の文法の詳細はそれらのページや書籍に譲ります. ここではおもなものだけ別表にまとめておきます (>別表). こんな表をズラッと見せられてもすぐに覚えられるものではありませんが, ぼちぼち試しながらなじんでください.

ここから先の説明は,この別表を別ウインドウで開いておいて見比べながら読むと 分かりやすいでしょう. >別表を別ウインドウで開く

<練習>


比較の結果をあとで活用する

正規表現中に () で囲まれた部分があると,こことマッチした部分文字列は, () の出現順に $1, $2, $3...という特別な名前の変数に自動的に代入されます. これを利用すると,文字列中から特定の部分を取り出すことができます.

$word = "Since 1999. Updated on 13 November 2002.";

if ($word =~ m/updated .* (\d\d\d\d)/i) { # 末尾の i は case insensitive の意
    $update_yr = $1;
    print "Updated in $update_yr.\n";
} 

このプログラムを実行すると,最終更新の年が表示されます.updated という語 のあとに4ケタの数字があったら(両者のあいだに他の文字があってもよい), それが更新の年だろうと判断しています.なお,m//i の i のようにうしろに オプションを付けることができます.i は大文字小文字の区別をしないという オプションです.上の例では,i を指定しているので updated と Updated がマッチしています.

文字による記述を含む調査データから情報を取り出すときなどに使えそうです. また,多数のデータファイルがあって,そのファイル名が調査地点,実験の処理区, 繰り返し番号,実験や調査の日時などの情報を含んでいるときに,それらを 取り出すのにも使えるでしょう.


ディレクトリ中のファイルを検索する

次の例では,カレントディレクトリ中のファイルすべてのなかから 特定の形式のファイル名のものを探しています. そして,そのファイル名に含まれる情報を取り出して表示します. ファイル名は,調査年(4ケタの数字),調査区(数文字のアルファベット), そして ".dat" という形式をしているものとします.

opendir (DIR, ".");     # カレントディレクトリ '.' を開く.DIR はディレクトリハンドル
@files = readdir(DIR);  # その中のすべてのファイル名を配列に取得する.
closedir (DIR);         # ディレクトリを閉じる.

foreach $file (sort @files) {            # 各ファイル名について…
    next unless ($file =~ m/^(\d{4})([a-zA-Z]+)\.dat$/i); # マッチしなかったら次
    ($year, $site) = ($1, $2);           # リストとして一度に代入.
    print "Year, $year; Site, $site\n";  # 年とサイトを表示
}

はじめの,ディレクトリ内のファイル一覧の取得は便利そうですね. ファイルをあけるのにはopen 関数を使い,ファイルハンドルという「窓口」 と対応付けました.同じように,ディレクトリ(フォルダ)も開いてその中を 見ることができます.ディレクトリの窓口はディレクトリハンドルと呼びます. ディレクトリを開けて窓口を対応付けるには opendir という関数, ディレクトリハンドルを介してディレクトリのなかのファイル名を読むには readdir という関数を使います.

readdir 関数を配列に代入する,すなわちreaddir 関数をリストコンテキストに置く (リストを返すことを期待されるような文脈に置く)と, ディレクトリ内のすべてのファイルとサブディレクトリ 名を読み取ってリストとして返します. opendir と readdir のいちおうの使い方は上の例を見れば分かるでしょう. 詳しくは参考書で勉強してください.

ファイル名とマッチさせるパターン /^(\d{4})([a-zA-Z]+)\.dat$/ をちょっと 詳しく見てみましょう.


最初の ^ は文字列の最初という意味でした.あたまに余計な文字列がついてる ファイルを拾ってしまわないように,これで始めてます.

(\d{4}) は,数字4つの連続です.(\d\d\d\d) と書いても同じことです. () で囲んであるので,全体のマッチに成功したら,この部分(4ケタ の数字)にマッチした部分文字列は $1 という変数に自動的に記録されます.

つぎの ([a-zA-Z]+) は,アルファベットが1文字以上を意味します. [] のなかではこのようにハイフンを使って文字の範囲を指定できます. () で囲んであるので,全体のマッチに成功したら,この部分(1文字以上の アルファベット)にマッチした部分文字列は $2 という変数に自動的に記録されます.

次の \. は,ピリオドです.ただのピリオドは任意の文字という意味を持つ メタ文字です.ピリオドそのものを表すために,エスケープ文字である \ を あたまに付けています.

dat$ は,"dat"という文字列があってそこで文字列全体が終わっていることを しめします.$ が文字列の末尾を意味するメタ文字でした.


このパターンとファイル名とがぴったりマッチしたら,調査年に相当する4ケタ数字と 調査区名に相当するアルファベット1文字以上とは,$1 と $2 に自動的に 代入されるので,そのあとこれを意味の分かる名前の変数に代入してから 表示しています.この代入は別段必須ではありませんが,分かりやすいように あえて書いてあります.


split 関数とパターンマッチ

ところで,正規表現は前にもちらっと出てきました. split の使い方のページ で,文字列を分割する区切りパターンを書き表すのにに正規表現を使うと 書きました.実例として,ひとつ以上の空白文字(空白やタブ)で区切られた データを分割するために, /\s+/ というパターンを使ってみました. また,カンマ区切りのデータファイルの場合の区切りパターンも紹介しました.

データが詰め込まれた文字列の末尾に余計な区切りパターンがある場合, split はこれを無視するので何も問題はありません. いっぽう,文字列のあたまの区切りパターンは無視せず,split が返すリストの 最初には空文字列がひとつ入ります. 空白区切りファイルで,各行の先頭に余計な空白があるかもしれない場合には, これを削除してしまってから処理するとよいでしょう. これも,ここまでの知識で書けますね.

while ($line = <>) {       #  一行づつ読み込む.
    chomp $line;                 #  末尾の改行を削除
    if ($line =~ m/^\s+(.*)/) {  #  先頭に空白がひとつ以上ある場合
        $line = $1;              #  それ以降の文字列を $line に代入しなおす.
    }

    @data = split /\s+/, $line;  #  そのうえで分割.
    # 以下,必要な処理
}

m/^\s+(.*)/ のところを,空白のあとに空白じゃない文字が来るから…と思って 非空白文字を表す \S(大文字のS)を使って m/^\s+(\S*)/ としてしまうと, たとえば " a 123 11" というデータの場合, $1 には "a" しか代入されないことに注意してください. a と 123 のあいだの空白のためです.

パターンマッチは,文字列の先頭から調べはじめて,なるべく長くマッチする ところを探していく(貪欲アルゴリズム)ので,文字列のあたまにいくらたくさん 空白があっても,それらはまとめてパターンの最初の \s+ にマッチします. したがって次の (.*) にマッチする部分文字列の先頭は空白文字では_ない_ ことが保証されますから,\S などを使う必要はありません.


だいぶ長くなったのでここらでページを変えましょう. 次のページでは,パターンマッチの使い方の例をもう少し紹介します. 文字列の置換も説明します.



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