M.Hiroi's Home Page

Linux Programming

お気楽 Perl プログラミング超入門

[ PrevPage | Perl | NextPage ]

正規表現 (後編)

前回は正規表現と文字列の検索について説明しました。今回は後方参照と文字列の置換について説明します。

●後方参照

Perl では、カッコ ( ) で正規表現をグループにまとめることができました。このほかに、もうひとつ機能があります。カッコはその中の正規表現と一致した文字列を覚えていて、あとからそれを使うことができるのです。これを「後方参照」といいます。正規表現の中では、「\ + 数字」でカッコと一致した文字列を参照することができます。いちばん左側にある ( が \1 に対応し、次の ( が \2、その次の ( が \3 というように、順番に数字がつけられます。カッコの数に制限はありません。簡単な例を示しましょう。

/(\w+)\s+\1/

これは同じ単語が続けて出現する場合、たとえば "abcd abcd" のような文字列と一致します。まず (\w+) と abcd が一致します。このとき、abcd が記憶されます。次に、\s+ と空白文字が一致し、\1 と abcd が照合されます。\1 は最初のカッコですから、その値は abcd ですね。したがって、"abcd abcd" と一致するのです。では、次の正規表現と一致する文字列はどうでしょうか。

/(\w+)\s+(\w+)\s+\2\s+\1/

かなり複雑になりましたが、これは "abc def def abc" のような文字列と一致します。\2 と \1 の位置に注意してください。これを逆に \1\s+\2 とすれば、"abc def abc def" と一致することになります。

カッコで記憶した文字列は、正規表現の外でも使うことができます。この場合、特殊変数 $1, $2, $3, ... で取り出すことができます。変数の値は、それぞれ \1, \2, \3, ... に対応します。次の例を見てください。

if( /(\d{1,2}):(\d\d):(\d\d)/ ){
    my $hour = $1;
    my $min  = $2;
    my $sec  = $3;
    .....
}

コマンド date の時刻表示はコロン ( : ) で区切られています。このような文字列から、時、分、秒、を取り出します。最初のカッコが時間を表します。8:00:00 のように 1 文字しかない場合もあるので \d{1,2} と表しました。分、秒は必ず 2 文字あるので \d\d と表すことができます。2 番目のカッコが分、3 番目のカッコが秒を表します。照合が成功したら、$1, $2, $3 には時間、分、秒、の値がセットされています。

さて、今までの説明では、m 演算子をスカラーコンテキストで使っていました。それでは、リストコンテキストではどうなるのでしょうか。次の例を見てください。

if (@result = /(\d{1,2}):(\d\d):(\d\d)/) {
    my $hour = $result[0];
    my $min  = $result[1];
    my $sec  = $result[2];
      .....
}

m 演算子はリストコンテキストで評価されると、カッコ内の正規表現と一致した文字列をリストにまとめて返します。上の例では、配列 @result に代入しましたが、次のようにすると、変数にまとめて代入することができます。

if (my ($hour, $min, $sec) = /(\d{1,2}):(\d\d):(\d\d)/) {
      .....
}

これは Perl らしい便利な機能ですね。ただし、m 演算子をリストコンテキストで使うと、特殊変数 $1, $2, ... や $', $&, $` には値がセットされません。ご注意くださいませ。

●文字列の置換

次は文字列の置換を行う演算子を説明しましょう。

s/pattern/replace/

s 演算子は文字列の中から pattern を探し、見つかれば一致した部分を replace に置き換えます。文字列の指定は m 演算子と同じく =~ や !~ を使います。指定がなければ、特殊変数 $_ に格納された文字列が対象となります。

s 演算子は置換した回数を返します。通常、最初に見つけた pattern を置換するだけですが、後ろに g を付けると pattern と一致する部分をすべて置換します。次の例を見てください。

$_ = "abc def abc ghi";
s/abc/123/;             # $_ => "123 def abc def"
s/abc/123/g;            # $_ => "123 def 123 def"

abc を 123 に置換します。g をつけないと最初の abc だけを 123 に置換しますが、g をつけるとすべての abc を 123 に置換します。

s 演算子は m 演算子と同様に、デリミタを変更することができます。また、pattern と replace は変数展開が行われます。したがって、文字列の置換プログラムも簡単に作成できます。

リスト : 文字列の置換 (gres.pl)

use strict;
use warnings;

my $pattern  = shift;
my $replace  = shift;
my $filename = shift;
open my $in, "<", "$filename" or die "Can't open file: $filename\n";
while (<$in>) {
    s/$pattern/$replace/og;
    print;
}
close $in;

このプログラムは、次のように実行します。

$ perl gres.pl 検索文字列 置換文字列 ファイル名

shift で @ARGV からデータを取り出して変数にセットします。検索文字列 $pattern は、1 回だけコンパイルすればいいので、s 演算子の後ろに o を付けています。ただし、replace 部は o が付いていても、置換前に必ず変数展開が行われます。これは、照合が成功したときにセットされる特殊変数 $& や $1, $2 などを有効にするためです。

ところで gres.pl では、置換文字列に特殊変数をセットしても、その値に置換されることはありません。変数 $replace は変数展開されますが、その結果はコマンドラインから与えられた文字列になってしまいます。このため、置換文字列に特殊変数を書いても、その値に置換されることはありません。ご注意くださいませ。

●タブを空白に展開する

s 演算子は後ろに e を付けると、置換部分を Perl の式として評価し、その結果を置換文字列とします。次の例を見てください。

$_ = "abcd1234efgh";
s/\d+/$&*2/e;       # $_ = "abcd2468efgh"

\d+ とマッチするのは 1234 ですね。特殊変数 $& には "1234" がセットされます。e が付いているので、置換が行われる前に $&*2 が評価されます。$& の文字列は数値に変換されて 2 倍され 2468 となります。これが文字列に変換され、置換後には "abcd2468efgh" となるわけです。これはとても便利ですね。数値と文字列を自在にあやつる Perl ならではの機能といえるでしょう。

この機能を使うと、タブを空白に展開するプログラムを簡単に作ることができます。ここではプログラムを簡単にするため、タブストップは 8 桁の固定とします。たとえば "ABC\tDEFG\tH" を表示すると、文字の位置は次のようになるでしょう。

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
---------------------------------------------------
 A B C                D E F G             H

タブは欄飛ばしの機能ですから、文字 D は次の欄の開始位置 9 桁目に、文字 H は 17 桁目に表示されます。変換する空白の個数は、次の式で求めることができます。

空白数 = タブの長さ * 8 - その前の文字列の長さ % 8

たとえば、最初のタブを変換してみましょう。

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
----------------------------------------------------
 A B C \t D E F G \t H
          ↑
         発見!

タブは 1 文字で、その前の文字列の長さは 3 ですから 8 - 3 = 5 個の空白を出力します。すると、文字 D の位置は次のようになります。

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
----------------------------------------------------
 A B C                D E F G \t H
                         ↑
                    次のタブストップまで

ちゃんとタブストップの位置になっていますね。では、次のタブを変換してみましょう。

1 * 8 - 12 % 8 = 8 - 4 = 4

今度は 4 個の空白に置き換えます。すると、文字 H の位置は次のようになります。

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
----------------------------------------------------
 A B C                D E F G             H

きちんとタブストップの位置にきていますね。これをプログラムします。文字列の長さを求めるには関数 length を使います。

length 文字列

Perl の場合、文字列の長さは文字数ではなく、バイト数を返すので注意してください。たとえば、ASCII コードの文字列 "abcde" の長さは 5 になりますが、UTF-8 の文字列 "あいうえお" であれば 15 になります。引数が省略されると、特殊変数 $_ に格納されている文字列が対象となります。

複数個の空白を作るには、文字列を複数回繰り返す x 演算子を使います。

my $a = 'a' x 10;  # $a => aaaaaaaaaa
my $b = 'ab' x 10; # $b => abababababababababab

この演算子を使うと、タブを空白に置き換える式は次のようになります。

s/\t+/' ' x (length($&) * 8 - length($') % 8/e;

これをタブがなくなるまで繰り返し適用すればいいわけです。プログラムは次のようになります。

リスト : タブを空白に展開 (expand.pl)

use strict;
use warnings;

while (<>) {
    1 while s/\t+/' ' x (length($&) * 8 - length($`) % 8)/e;
    print;
}

while 文がおもしろい形になっていますね。while の前にある 1 は必要で、ないとエラーになります。これは次のプログラムと同じ意味になります。

while( s/\t+/' ' x (length($&) * 8 - length($`) % 8)/e ){ 1; }

この場合、1; に意味はありません。なくても動作します。Perl では「while 式」という形を修飾子 [*1] といいます。このほかにも、次のような修飾子があります。

if 式
unless 式
until 式

until は while とは逆の動作を行う繰り返し文です。つまり、条件部が偽の間だけループするわけです。修飾子は、式とセミコロン ; の間にひとつだけ置くことができます。つまり、次に示すような使い方ができるのです。

式2 if 式1;        # 式1が成り立てば式2を実行
式2 unless 式1;    # 式1が不成立であれば式2を実行
式2 while 式1;     # 式1が成立している間、式2を実行
式2 until 式1;     # 式1が不成立の間、式2を実行

C言語プログラマから見ると何とも変わった構文ですが、Perl では頻繁に用いられます。

s 演算子は置換した回数を返すので、タブがある間は置換を繰り返すことになります。ところで、このプログラムはタブを置換したあと、再び文字列の先頭からタブを探すことになるので効率的ではありません。ですが、簡単な仕事は簡単にできる Perl らしいプログラムといえるでしょう。

-- Note --------
[*1] m 演算子や s 演算子の後ろにつく g, i, o も修飾子といいます。

●空白をタブに置換する

今度は expand.pl とは逆に、空白をタブに変換するプログラム unexpand.plを作りましょう。これも簡単です。文字列を 8 文字ずつ分解し、末尾まで連続している空白をタブに変換すればいいのです。これは s/ +$/\t/ と表すことができますが、このときメタ文字 $ の使い方に注意が必要です。

$ は行末と一致しますが、文字列の末尾にある改行文字も行末と認識します。したがって、/abc$/ は "abc" と一致しますが、"abc\n" とも一致するのです。8 文字ずつに分解していくと、最後は 8 文字に満たない部分文字列が取り出される場合があります。このとき、文字列の最後に空白文字があると、それを無条件にタブへ置換してしまいます。これを回避するように工夫しましょう。プログラムは次のようになります。

リスト : unexpand.pl

use strict;
use warnings;

while (<>) {
    chop;
    1 while s/\t+/' ' x (length($&) * 8 - length($`) % 8)/e;
    for (my $i = 0; $i <= length($_); $i += 8) {
        my $tabs = substr($_, $i, 8);
        if (length($tabs) == 8) {
            $tabs =~ s/ +$/\t/;
            print $tabs;
        } else {
            print "$tabs\n";
        }
    }
}

まず chop で邪魔になる改行文字を取り外します。次に、タブが含まれている場合に備え、それを空白に展開しておきます。次は文字列を 8 文字ずつに分解します。Perl の場合、正規表現を使って文字列を切り出すこともできますが、今回は関数 substr を使うことにします。

substr string, n, len

substr は string の n バイト目から len バイトの部分文字列を取り出します。len を省略すると n バイト目から文字列の最後までが取り出されます。n に負の数を指定すると、文字列の末尾から n バイト戻ったところが部分文字列の先頭となります。

次の for ループで、文字列を 8 文字ずつに分解していきます。substr で、$i から 8 バイト分の部分文字列を取り出して、部分文字列の長さをチェックします。8 バイトあれば、空白をタブに置換して print で出力します。そうでなければ、いちばん最後の部分文字列なので、置換を行わずに改行を付加して出力します。これで、最後に空白が残っていても、それをタブに置換することはありません。

●文字の置換

今度は文字列ではなく、文字の置換を行う tr 演算子を説明します。

tr/search-list/replace-list/
 y/search-list/replace-list/

これは Unix 系 OS のコマンド tr と同じ動作をする演算子です。tr は search-list にある文字を、replace-list の対応する文字に置き換えます。文字列の指定は =~ か !~ を使います。省略された場合は、特殊変数 $_ に格納されている文字列が対象となります。y は sed ユーザーのために用意された演算子で、tr とまったく同じ動作をします。

tr は正規表現ではありませんが、便利な使い方ができます。ここで詳しく説明しましょう。まずは簡単な文字の置換からです。

$_ = 'abcdabcdabcd';
tr/a/A/;             # $_ => 'AbcdAbcdAbcd'

文字 a に対応する文字は A ですね。したがって、文字列中の a はすべて A に置き変わります。変換する文字は、複数個指定することができます。

$_ = 'abcdabcdabcd';
tr/ab/AB/;           # $_ => 'ABcdABcdABcd'

文字 a は A に、b は B に置き変わります。文字を並べるのはめんどうなので、ハイフン ( - ) 使って変換する文字の上限から下限までを示すことにします。

$_ = 'abcdabcdabcd';
tr/a-z/A-Z/;         # $_ => 'ABCDABCDABCD'

この場合は、英小文字を英大文字に置き換えることになります。実は、" で囲まれた文字列の先頭に \U や \L をつけると、同じように変換することができます。

$_ = 'abcdABCD';
print "\U$_";    # ABCDABCD と表示
print "\L$_";    # abcdabcd と表示

\U は小文字を大文字に、逆に \L は大文字を小文字に変換します。これも便利な機能ですね。

ある一連の文字を同じ文字に置換したい場合もあるでしょう。

tr/a-z/a/;
tr/0-9/n/;
tr/a-z/A-G/;

最初の例は a から z の文字はすべて a に置き変わります。次の例は、0 から 9 までの文字が n に置き変わります。最後の例では、a から g が A から G に変わり、h から z の文字が G に変更されます。

置換後に同じ文字が続くとき、1 文字にまとめた方が都合がよい場合があります。tr 演算子は修飾子 s をつけると、変換後に同じ文字が続いた場合、1 文字に圧縮します。たとえば、$_ = 'abcdABCDefghEFGH' とすると、次のようになります。

tr/a-z/a/;     # $_ => aaaaABCDaaaaEFGH
tr/a-z/a/s;    # $_ => aABCDaEFGH

連続している a が 1 文字に押しつぶされていることがわかりますね。

いままでは文字を置換するだけでしたが、文字を削除したい場合もあるでしょう。修飾子 d をつけると、search-list にあって、replace-list に対応する置き換え文字が指定されていない文字を削除します。次の例を見てください。

$_ = 'abcABCdefDEFghi';
tr/a-z//d;       # $_ => 'ABCDEF'
tr/a-z/ABC/d;    # $_ => 'ABCABCDEF'

最初の例では、replace-list に文字を指定していないので、英小文字をすべて削除することになります。tr/a-z//; だけでは削除は行われないので注意してください。次の例では、a, b, c は A, B, C に置換されますが、それ以外の英小文字は replace-list に対応する文字がないので削除されます。一般の tr コマンドでは、指定した文字をすべて削除するのが普通なので、Perl の tr 演算子はそれよりも柔軟性が高いといえます。

修飾子 c をつけると、search-list で指定した文字以外の文字を replace-listの 1 文字に置き換えます。次の例を見てください。

$_ = 'abcABCdefDEFghi';
tr/a-z/A/c;      # $_ => 'abcAAAdefAAAghi'
tr/a-z/A/cs;     # $_ => 'abcAdefAghi'

最初の例では、英小文字以外はすべて A に置き換わります。次は c と s をいっしょに使った例で、置き変わった A を 1 文字にまとめることができます。

tr 演算子は置換した文字数を返します。これを利用して、特定の種類の文字数を数えることもできます。

$_ = 'abc123def456';
$num = tr/0-9//;      # $num => 6

replace-list に文字を指定しないと、実際に置換は行われませんが、search-list に含まれている文字を数えることができます。

●正規表現で日本語を使う場合

通常、Perl は文字列をバイト単位で取り扱います。これを「バイト文字列」と呼ぶことにしましょう。一般に、日本語の 1 文字は複数のバイトを使って表されています。正規表現を使うとき、このままでは文字単位で照合することができません。Perl の場合、内部では文字列をユニコードで表しています。バイト文字列を内部形式の文字列に変換すると、正規表現でも日本語を使うことができるようになります。

まず、プログラムに書かれている文字列ですが、何も指定しなければ「バイト文字列」として扱われます。ファイルの先頭で use utf8; を設定すると、プログラム中の文字列は内部形式の文字列として扱われます。なお、プログラムを UTF-8 で記述したとしても、use utf8; を指定しないかぎり、文字列はバイト文字列として扱われます。

このとき、lenght などの組み込み関数は動作が変化することに注意してください。次の例を見てください。

リスト : :utf8 の動作 (1)

use strict;
use warnings;
use utf8;

my $s1 = "abcde";
my $s2 = "あいうえお";
print length($s1), "\n";
print length($s2), "\n";

このプログラムを実行すると、文字列 $s1, $s2 はどちらも長さが 5 になります。この場合、length は文字数を返していることがわかります。use utf8; を削除すると、length "あいうえお" は 15 (byte) を返します。

そして、正規表現にも日本語が使えるようになります。次の例を見てください。

リスト : :utf8 の動作 (2)

use strict;
use warnings;
use utf8;

print "ok\n" if "あいうえお" =~ /あ.う/;
print "ok\n" if "あいうえお" =~ /あ..え/;
print "ok\n" if "あいうえお" =~ /あい*/;
print "ok\n" if "あいいいうえお" =~ /あい*/;
print "ok\n" if "あいいいうえお" =~ /あ[いうえ]+お/;

このプログラムを実行すると、すべて OK と表示されます。日本語でも文字単位で照合されていることがわかります。

ただし、use utf8; で内部形式の文字列になるのはプログラム中に記述された文字列だけです。外部 (ファイルハンドルやコマンドライン引数など) からの入力された文字列は「バイト文字列」として扱われます。

バイト文字列を内部形式の文字列に変換する方法はいくつかありますが、まず最初にモジュール Encode の関数 enocde, decode を使う方法を説明します。

use Encode;
Encode::decode(バイト文字列の文字コード, バイト文字列) => 内部形式の文字列
Encode::encode(バイト文字列の文字コード, 内部形式の文字列) => バイト文字列

バイト文字列は関数 decode で内部形式の文字列に変換することができます。逆に、内部形式の文字列をバイト文字列に変換するのが関数 encode です。どちらの関数も第 1 引数にはバイト文字列の文字コードを指定します。

たとえば、文字列を検索する grep.pl を日本語に対応させると、次のようになります。

リスト : grep.pl

use strict;
use warnings;
use Encode;
my $pattern = Encode::decode("UTF-8", shift);
my $filename = shift;

open my $in, "<", $filename or die "Can't open file: $filename\n";
while (<$in>) {
    $_ = Encode::decode("UTF-8", $_);
    if (/$pattern/o) {
        print Encode::encode("UTF-8", "$.: $_");
    }
}
close $in;

モジュール内の関数は モジュール名::関数名 で呼び出すことができます。コマンドラインからパターンを取り出して、それを decode で内部形式の文字列に変換します。M.Hiroi が使用している Ubunts の文字コードは UTF-8 なので、第 1 引数には UTF-8 を指定します。なお、open に渡すファイル名はバイト文字列でなければいけません。

次に、while 文の中で $_ を encode で内部形式の文字列に変換し、$pattern と照合します。照合に成功したら print で出力しますが、内部形式の文字列をそのまま出力するとワーニングが表示されます。そこで、関数 encode で内部形式の文字列を UTF-8 でエンコードされたバイト文字列に変換します。これで、日本語でも正規表現を使って検索することができます。

このほかの方法として、ファイルをリードオープンするとき、open の第 2 引数に :utf8 を指定すると、内部形式に変換された文字列を受け取ることができます。

open my $in, "<:utf8", $filename;

すでにオープンされているファイルハンドルの場合、関数 binmode で :utf8 を指定すると、内部形式の文字列とバイト文字列の変換を行うことができます。

binmode STDIN, ":utf8";
binmode STDOUT, ":utf8";

binmode で STDOUT に :utf8 を設定すると、内部形式の文字列をバイト文字列 (OS の文字コード) に変換して出力します。

この機能を使うと文字列を置換する gres.pl は次のようになります。

リスト : 文字列の置換 (gres.pl)

use strict;
use warnings;
use Encode;

binmode STDOUT, ":utf8";

my $pattern  = Encode::decode("UTF-8", shift);
my $replace  = Encode::decode("UTF-8", shift);
my $filename = shift;

open my $in, "<:utf8", $filename or die "Can't open file: $filename\n";
while (<$in>) {
    s/$pattern/$replace/og;
    print;
}

close $in;

ファイルの入出力に関しては、明示的に enocde や decode を呼び出さなくても動作するようになります。

このほかに 参考 URL 1 によると、『openプラグマを使って、入出力のデフォルトのエンコーディングを指定することができます。』 とのことです。興味のある方は 参考 URL をお読みください。有益な情報を公開されている加藤さんと木本さんに深く感謝いたします。

今回はここまでです。今までの例題では、ズラズラとプログラムを書き並べましたが、もっと複雑な処理を実現しようとすると、一度に全部の処理を作るのは難しくなります。そこで、全体を小さな処理に分割して、個々の処理を作成します。そして、それらを組み合わせることで全体のプログラムを完成させます。このときに必須となる機能が「関数」です。次回は関数について説明します。

●参考 URL

  1. Perl 5.8.x Unicode関連, 加藤敦さん
  2. Encode - 日本語などのマルチバイト文字列を適切に処理する, 木本裕紀さん

初版 2015 年 4 月 12 日
改訂 2023 年 3 月 19 日

Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Perl | NextPage ]