17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PerlAdvent Calendar 2019

Day 18

ここ数年の間で便利に使っているPerlの正規表現〜2019年版〜

Last updated at Posted at 2019-12-18

Perl は21世紀になる前から活用されており、バージョン間の互換性も保たれているため、ともすると2000年代からPerlを書いているプログラマーはその延長線上でコードを書いていることも多いと思います。

とはいえ、Perl 本体も Perl 5.10 以降のメジャーバージョンアップで私のような現場のプログラマーが便利に使える新機能も時々追加されています。

この記事では正規表現にフォーカスを当てて、私 @xtetsuji がここ数年、具体的には2010年代中盤から後半にかけて便利に使い始めた正規表現を中心にご紹介します。

[PR] WEB+DB PRESS Vol.113 「Perl Hackers Hub」

2019年10月24日発売の WEB+DB PRESS Vol.113 の連載「Perl Hackers Hub」に「正規表現の勘所」というタイトルで寄稿しました。この記事で扱っている先読みと後読み(先後読み)などについて詳しく解説しています。気になった方は手にとって頂けると嬉しいです。

ちょうど記事公開日の2019年12月18日にウェブでも公開されました。

そして12月18日は Perl の誕生日(1987/12/18〜)ですね(参考)!二度めでたい!

2019/12/19 公開分。

2019/12/20 公開分。

昔からあるけれど便利に使っている機能

「ここ数年の間で」というわけではありませんが、昔からあって便利に使っている機能だけど、世間では詳しく取り上げられることが多くない(ように感じる)機能をご紹介します。

\Q \E

正規表現リテラルで \Q と書くと、以後 \E が出てくるまでメタ文字はその効果を失ったものと扱われます。言い換えると、 \Q\E の間のメタ文字は全てバックスラッシュでエスケープされたものとみなされます

if ( $str =~ /\Q******\E/ ) { # /\*\*\*\*\*\*/ と同じ
    ...
}

内部的には \Q\E の間は組み込み関数 quotemeta で評価された結果になるようです。

同種のものとして、\E が登場するまでの間、小文字にする \L と大文字にする \U があります。詳細は perlop の「クォートとクォート風の演算子」を参照下さい。

まとめると以下のようになります。

記法 効果 対応する文字列関数
\Q \E が登場するまでの間、メタ文字をエスケープ quotemeta
\L \E が登場するまでの間、文字列を小文字にする lc
\U \E が登場するまでの間、文字列を大文字にする uc

なお、これら \Q などは正規表現リテラルの中だけでなく文字列リテラルの中でも使えます。

print "Today is \Ufine\E. \Q......\E";
    # => Today is FINE. \.\.\.\.\.\.

これの何が便利かというと、文字列変数を各リテラル内で展開させた時、その内容に作用することでしょう。これは 外部からの入力を検索文字列とする際、様々な理由で正規表現のメタ文字を使わせたくない場合に有効 です。

my $search = shift @ARGV; # 外部から検索文字列を得る
if ( $text =~ /\Q$search\E/ ) { # 文字列変数 $search の中の文字列にメタ文字があればエスケープしてくれる
    ...
}

上記例からも分かる通り、 \Q$ をエスケープする前にスカラー変数のシジル $ の文字列変数のリテラル内展開が発生します。これは配列変数のシジル @ でも同様です。

グローバル置換で大きなコード評価をする

マッチするだけ複数回置換をするグローバル置換 s///g ですが、置換文字列を Perl コードとして評価する /e 修飾子を合わせて使い、まとまったコードを評価するすることをしばしば行います。

この際、まとまったコードが多くなるとスラッシュが区切り子では見づらいので、コードブロックであることの強調の意味を込めて波括弧を区切り子として s{}{}eg とすると見やすくなります。

colorsyslogより
while (<>) {
    # Nov  1 23:03:21 hostname postfix/pickup[12345]: D24EAA9510B: uid=500 from=<noreply@example.com>
    s{$syslog_re}{
        join " ", map {
              ref $color{$_} eq 'CODE' ? $color{$_}->($+{$_}, +{%+})
            : $color{$_}               ? colored($+{$_}, $color{$_})
            : $+{$_}
        } qw(date time host process message)
    }e;
    print;
}

上記は syslog フォーマットを変換する colorsyslog というスクリプトの断片です。 <> で1行を読み込んだ後のロジックを s{RE}{CODE}eg の CODE (実際の置換文字列を得るためのコード)部分にまるまる突っ込んでいます。

s{RE}{CODE}eg の CODE の中でさらに m//s/// を使うこともできます。括弧の対応があっていればいれば、CODE の中で {} 自体を区切り子として再度使っても問題ありません。やりすぎると読みづらくなるので、複雑な処理になりそうであれば別のサブルーチンに抜き出しておくと良いでしょう。

当然やりすぎる(CODEの分量が多くなる)と読みづらくなりますが、この処理の良い点の一つはグローバル置換をループとして見た場合に無限ループが起こらないという点でしょう。無限ループが意図せず起こったらまずいところのロジックを、大きなテキストの s///eg 処理に書き換えることがもしできるのであれば、無限ループの意図しない発生を考えなくて良くなります。

なお上記は s///eg/g で検索カーソルが必ず進んでいき(停止や逆進することなく)対象文字列の終端で終わることが前提となっています。逆に言うと、カーソルを前進させないような /gc 修飾子やカーソル位置を修正する pos 関数への左辺値代入を行うとこの前提が崩れます。対象文字列の何らかの構文解析を正規表現で行う際、 /gcpos を使うなら、無限ループに気をつける必要があるとも言えるでしょう。

/gcpos については prelrequick などが参考になります。

s///, s///, s/// for $string

Perl の for の文法は

for my $name ("alice", "bob", "carol") {
    say $name;
}

というものです。上記の一時変数 my $name を省略することもでき、この場合は暗黙の一時変数が Perl お馴染みの $_ になります。

for ("alice", "bob", "carol") {
    say $_;
    # say は引数がないと $_ を表示しようとするので `say;` のみでもOK
}

上記 for の一時変数 $name$_ は別名変数となっており、取得した元が文字列リテラルではなく配列の各要素の場合は、破壊が配列に及びます。

# これは破壊が発生しない。取得した元が文字列リテラルのリストだから
for ("alice", "bob", "carol") {
    _ .= " -san"; # 末尾に敬称 -san を付けている
    say $_;
}
# これは @numbers の破壊が発生する。$number を取得した元が配列の各要素だから
my @numbers = (1, 2, 3, 4, 5);
for my $number (@numbers) {
    $number *= 10; # 10をかけたものを新たに $number にする
    say "next time is $number";
}
say "@numbers"; # => 10 20 30 40 50

この破壊は、別名変数のことを知らないとドキッとしてしまいますね。

別名変数と破壊については冒頭でご紹介した Perl Hackers Hub の記事や他の文献を参照頂くとして、この破壊を肯定的に使ってみましょう。

先ほどだと my $name といった一時変数が for で定義されない場合、つまり一時変数が暗黙の $_ の場合は後置 for を使うことができます。後置 for は一時変数を選べない代わりに、リストや配列を囲う丸括弧を省略することができます。

say for "alice", "bob", "carol";

上記は say が引数無しの場合は say($_) と解釈されることを利用しています。

破壊が発生することは通常の前置の for と同じです。

@names = ("alice", "bob", "carol");
s/^(.).*/$1/ for @names;
say "@names"; # => a b c

また、必ずしも対象が複数である(上記だと "alice", "bob", "carol" )必要は無いので、意図して破壊したい1個のスカラー変数を後置 for の右側に置いて、$_ を破壊すると元のスカラー変数も破壊することができます。

my $todays_guest = "xtetsuji";
s/^(.).*/$1/ for $todays_guest;
say $todays_guest; # => x

これなら「普通に $string =~ s/// と書けばいいのでは?」と思ってしまいますが、これを応用できそうな興味深い点もあります。

  • for を後置されるロジック部分には1文しかかけない(セミコロンで区切れない)ものの、処理をカンマ , で区切ることで複数文を模倣することができる
  • 置換対象文字列がたまたま長い名前 $very_very_very_long_text$storage->{path}->{to}->{text} だった場合、複数回置換が必要な場合にいちいち それ =~ s/// を書くのは面倒だが、後置 for で別名変数を $_ とすれば問題が解決する

これを応用すると、例えば 3つの置換 s/RE1/STR1/ s/RE2/STR2/ s/RE3/STR3/$storage->{path}->{to}->{text} に行う場合

s/RE1/STR1/, s/RE2/STR2/, s/RE3/STR3/ for $storage->{path}->{to}->{text};

と書くことができます。英文として読んでも意外に自然?かもしれません。

単語の出現数を数える

単語の出現数を正規表現マッチと置換で数えることができます。

マッチで数える方法

マッチでは、グローバルマッチとキャプチャを使います。

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';

my $text = <<END_TEXT;
Beethoven was born in Bonn, the capital of the Electorate of Cologne,
and part of the Holy Roman Empire.
He displayed his musical talents at an early age
and was vigorously taught by his father Johann van Beethoven,
and was later taught by composer
and conductor Christian Gottlob Neefe.
At age 21, he moved to Vienna and studied composition with Joseph Haydn.
Beethoven then gained a reputation as a virtuoso pianist,
and was soon courted by Karl Alois,
Prince Lichnowsky for compositions,
which resulted in Opus 1 in 1795.
END_TEXT

my @words = $text =~ /(Beethoven)/g;
say "@words"; # => Beethoven Beethoven Beethoven

# スカラーコンテキストで @words の要素数を得ることができる
say scalar @words; # => 3
my $count = @words; # 右辺を scalar @words としても OK

題材テキストは Ludwig van Beethoven - Wikipedia より。

グローバルマッチ m//g はマッチを複数回試みますが、その中にキャプチャがあると、全部のマッチのキャプチャ内容を順番にリストとして返します。これを利用して、キャプチャを検索したい単語の1つだけ持つ正規表現を使って単語の数を数えることができます。

$text =~ /(WORD)/g の評価値をコンテキストごとにまとめると以下のようになります。

コンテキスト 効果
リスト キャプチャした文字列(単語)のリスト
スカラー キャプチャした個数
真偽値 キャプチャできたか
(=マッチが成功したか)

なお、カーソルを初期化せず m// の評価を繰り返すことができる /gc 修飾子と while ループを使った単語数カウントもできますが、ここでは触れません。

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';

my $text = <<END_TEXT;
Beethoven was born in Bonn, the capital of the Electorate of Cologne,
and part of the Holy Roman Empire.
He displayed his musical talents at an early age
and was vigorously taught by his father Johann van Beethoven,
and was later taught by composer
and conductor Christian Gottlob Neefe.
At age 21, he moved to Vienna and studied composition with Joseph Haydn.
Beethoven then gained a reputation as a virtuoso pianist,
and was soon courted by Karl Alois,
Prince Lichnowsky for compositions,
which resulted in Opus 1 in 1795.
END_TEXT

my $count = 0;
while ( $text =~ /Beethoven/gc ) {
    $count++;
    say "position is " . pos($text);
}
say $count;
output
position is 9
position is 214
position is 370
3

置換で数える方法

マッチで数えられれば問題ありませんが、置換で数えることもできます。

グローバル置換 s///g は置換を行った回数を返します。つまり s/WORD/WORD/g と全く同じ単語でグローバル置換を行うことで単語 WORD の数を数えることができます。

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';

my $text = <<END_TEXT;
Beethoven was born in Bonn, the capital of the Electorate of Cologne,
and part of the Holy Roman Empire.
He displayed his musical talents at an early age
and was vigorously taught by his father Johann van Beethoven,
and was later taught by composer
and conductor Christian Gottlob Neefe.
At age 21, he moved to Vienna and studied composition with Joseph Haydn.
Beethoven then gained a reputation as a virtuoso pianist,
and was soon courted by Karl Alois,
Prince Lichnowsky for compositions,
which resulted in Opus 1 in 1795.
END_TEXT

my $count = $text =~ s/Beethoven/Beethoven/g;
say $count; # => 3

グローバル置換で数える方法はパフォーマンスも悪そうですよね(計測していないですが)。グローバルマッチを使えば数えることはできるので、グローバル置換での挙動は頭の片隅にとどめておくで良いと思います。

/m/s の理解

正規表現を少し勉強すると、先輩や上司から「 /m/s は常時指定しておくべきだ」と教わる機会が出てくるかもしれません。

確かにそうなのですが、個人的には「意味がわからず漫然と指定してもあまり効果的ではない」という感じもします。

この2つの修飾子 /m/s については、それらが意味変更の対象とするメタ文字が違います。

/m /s
正式名称 複数行モード 単一行モード
意味変更対象のメタ文字 ^$ .
意味変更の内容 冒頭・末尾だけでなく行頭・行末(改行文字の右左)にもマッチ 改行文字にもマッチ
メタ文字の元々の意味を持った代替メタ文字 \A\Z
\z は少し特殊)
[^\n] または \N\Nは実験的)

それらメタ文字がないのにこれらを指定している場合、「将来的な目的かな」「コーディングスタイルの一環なのかな」「意味もわからず漫然としていしているのかな」と読み手に考えさせてしまう可能性もありそうです。

この2つの修飾子については別記事で解説しています。

先読みと後読みの積極活用と、その理解

先読みと後読み、あわせて先後読み(せんごよみ)と呼ばれたりもします。正規表現使いの初学者から中級者へ進むための知識の一つではないでしょうか。

先後読みについては冒頭でも紹介した記事でも解説しました。

記事の通りに理解すれば文法は分かります。とはいえ先後読みの本質を一言で言うなら何なのだろうと考えを巡らせていました。

特に肯定の先後読みはキャプチャを併用すれば幅ありマッチでも自然に代用できるわけで、初学者も無理に先後読みを使わなくても多くの要望を叶えることができます。否定の先後読みも厳密に何も存在すらしない場合を別の正規表現で書く必要はありますが、幅ありマッチとキャプチャでなんとかなります。

一つの側面として、先後読みは位置の指定 なのだと思います。位置といえば、冒頭 ^ や末尾 $ の他、単語境界 \b などが有名です。これらは具体的な(1文字のみを含む)文字列をマッチ対象にしないので、長さや幅のないマッチという意味合いで ゼロ幅マッチ、または(ゼロ幅の)言明などと呼ばれます。

位置の指定とゼロ幅マッチは同一視できるわけですが、ここで重要なことは先後読みもゼロ幅マッチであるということです。実際、上記で挙げた基本的な正規表現のゼロ幅マッチは先後読みを使った別の表現が可能です。

正規表現 意味 先後読みで書いた場合 どう読むか
\z 厳密な末尾 /(?!.)/s カーソルの前に何の文字も無い位置
^/m無し)、\A 先頭 /(?<!.)/s カーソルの後ろに何の文字がない位置
$/m無し)、\Z 末尾 /(?!.)|(?=\n(?!.))/s カーソルの前に何の文字も無いか、改行文字1文字のみあってその次に何の文字もない位置
^/mあり) 行頭 /(?<!.)|(?<=\n)/s /m 無し ^ に加え、カーソルの後ろに改行文字がある位置
$/mあり) 行末 /(?=\n)|(?!.)|(?:(?=\n(?!.)))/ /m 無し $ に加え、カーソルの前に改行文字がある位置
\b 単語境界 /(?<!\w)(?=\w)|(?<=\w)(?!\w)/ カーソルの後ろに単語文字がなくて前に単語文字がある場合、またはその入れ替え

上記記事でも、日本語文字列の場合は単語境界 \b は無力であることが書かれていますが、よくあるケースを先後読みで指定することで、限定的な単語境界を先後読みで代用することができます。頻出の単語である「東京都」が「京都」という検索キーワードにマッチしないようにしたい場合、 /(?<!東)京都/ という正規表現マッチを書くことで対応できます。

このように、先後読みが位置指定の手段であるということを推し進めると、所望の場所に文字を挿入するためにその場所にマッチする先後読みを組み合わせた正規表現を書くこともできます。

#!/usr/bin/env perl
use strict;
use warnings;
use feature qw(say);

my $price = "2509800";
my $text = "この自動車は $price 円です";

# この正規表現は先後読みのみで構成されているので、全体でもゼロ幅マッチ
# 結果的にカンマを挿入する位置を指定している
$text =~ s/(?<=\d)(?=(?:\d\d\d)+(?!\d))/,/g;
say $text; # => この自動車は 2,509,800 円です

その他にも、先後読みはマッチ対象ではなくなるので置換のときの取り回しが良くなったり、否定の先後読みはその方向の文字集合から一部の文字を取り除く差集合的演算的役割があったりと、いくつかの側面があると思います。とはいえ、ゼロ幅マッチであり位置を指定するものという見方は、先後読みを理解する上で興味深い見方ではないかと思います。

Perl 5.10 以降の機能

ここからは Perl 5.10 以降で加わった機能のうち、私がよく使っているものです。

名前付きキャプチャ (?<name>RE)%+

キャプチャの括弧を使うと $1 $2 といった番号変数でマッチの部分文字列を取得することができますが、キャプチャの括弧が多くなってくるとわかりづらくなってしまいます。括弧が入れ子になった場合は開き括弧の登場順に番号変数が振られるというルールを覚えておけば、ややこしい程度で済みますが、ややこしいことに変わりありません。また、改修の際にキャプチャの間に新たにキャプチャを入れる場合、番号がずれる問題があります。

上記のような場合、Perl 5.10 以降で使うことができる名前付きキャプチャを使うべきでしょう。

以前の章で例に出した colorsyslog が対象とする syslog の一行を表す正規表現は以下のようになっています。

my $syslog_re = qr/^(?<date>\w\w\w [ \d]\d) (?<time>\d\d:\d\d:\d\d) (?<host>\S+) (?<process>\S+): (?<message>.*)/;

名前付きキャプチャは (?<name>RE) という部分で、これは name という名前で正規表現 RE にマッチした部分をキャプチャするというもの。この名前のキャプチャにマッチした文字列は特殊ハッシュ %+ から $+{name} という記法で取り出すことができます。これだとキャプチャの順番も関係ありません。

キャプチャの数が1個2個程度であれば今まで通り $1 $2 と書いても問題ないですが、ある程度キャプチャの量が多くなってきたら名前付きキャプチャの使用を考えてみても良いでしょう。

/p 修飾子から $& 復興まで

特に Perl 5.10 以前から Perl を書いている人は、 $& がもたらすパフォーマンス上の致命的な問題を避けるため、 $& を忌避することが普通です。しかし Perl のバージョンアップとともに改善が図られ、Perl 5.20 以降では $& をソースコードに書いても性能劣化が起こらないようになっています。

マッチした文字列全体が欲しい時に正規表現全体をキャプチャすることが当たり前になっている人も多いと思いますが、 $& が使えるのであればそれで参照したほうが楽です。長年の癖で書くことに抵抗があるかもしれませんが、Perl 5.20 以降を確約できる個人利用プログラムなどは $& を活用していって問題ないでしょう。

詳細は以下の記事を参照下さい。

/r 修飾子で非破壊置換

Perl 5.14 から非破壊置換ができる /r 修飾子が追加されました。

元の文字列を保持したい場合は、文字列代入でコピーを取ってから置換を行っていましたが、この /r 修飾子でその手間が省けます。

この /r 修飾子、特に相性が良いのは formap で別名変数が設定される場合じゃないでしょうか。

17
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?