Perl5 Perl の正規表現のFAQやTIPS等
[DESCRIPTION][正規表現][Version 8 正規表現][FAQやTIPS]
どうすれば正規表現を判読し難い、保守できないようなものにすることなく使うことができるでしょうか?
正規表現を保守可能なものにし、理解できるようにするための三つの技法があります。
- 正規表現の外側にコメントを付ける
- 通常のPerlのコメントを使って、あなたが何を、どのようにしているかを説明します。
# 行を、その最初の単語、コロン、行の残りの文字数に
# 変換します
s/^(\w+)(.*)/ lc($1) . ":" . length($2) /ge;
- 正規表現の内側にコメントを付ける
/x
修飾子は、正規表現中にある空白を無視するようにし、
(キャラクタクラスの中にあるものを除く)、通常のコメントが使えるようにします。あなたの想像できるように、空白とコメントは非常に助けに
なります。
/x
によって
s{<(?:[^>'"]*|".*?"|'.*?')+>}{}gs;
この正規表現を以下の様に記述できます:
s{ < # 開きのangle bracket
(?: # 後方参照なしのグルーピング
[^>'"] * # > でも ' でも "でもないものの0回以上の繰り返し
| # あるいは
".*?" # 二重引用符に囲まれたセクション (stingy match)
| # あるいは
'.*?' # 引用符に囲まれたセクション (stingy match)
) + # それらの一回以上の繰り返し
> # 閉じのangle bracket
}{}gsx; # 空に置き換え、つまり削除
訳注: stingy けちな、しみったれた; 少ない、不十分な
これでもまだ散文(prose)程には明確にはなっていませんが、パターンの各部分の意味を説明するには非常に便利なものです。
- Different Delimiters 異なった区切り
- 私たちは通常、
/
で区切られたものをパターンであると考えていますが、パターンはほとんどすべてのキャラクタを使って区切ることが可能
です。perlreはこれを説明しています。たとえば、先に挙げたs///
では、区切りとしてカーリーブレースを使っています。スラッシュ以外の区切りを選択することによって、パターンの中に存在する区切り記号
と同じものをクォートする手間を省くことができます。
s/\/usr\/local/\/usr\/share/g; # 良くない区切りの選択
s#/usr/local#/usr/share#g; # これは良い
▲このページのインデックスへ
二行以上に対するマッチングでトラブルがありました。何が悪いのでしょう?
文字列の中に改行がないか、パターンで正しい修飾子 (modifier)を 使っていないかのいずれかでしょう。
複数行のデータを一つの文字列にする方法はたくさんあります。これを、入力を読み込んでいる間自動で行なわせたいというのであれば、一度に二行以上読ませるために
$/を(パラグラフ単位で読み込みたいなら '' を、ファイル全体を読み込みたいならundef
を)設定したくなるでし
ょう。
あなたが使いたいのは /s
か/m
のいずれなのか(あるいはこれら両
方なのか)を決めるのを助けるために、perlreを読んでください: /s
はドットが改行を含むようにしますし、/m
はキャレットとドル
記号が文字列の両端だけでなく改行の前後でマッチするようにします。そして、複数行に渡る文字列を取得するようにさせる必要があります。
たとえば、以下に挙げるプログラムは重複した単語を、たとえそれが行をまたがっていても(ただしパラグラフはまたがっていない)探し出すも
のです。この例では、 /s
の必要はありません。なぜなら、この行を
またがらせたい正規表現でドットを使っていないからです。 /m
を使
う必要もありません。それは、キャレットやドル記号をレコードの中にある改行の前後でマッチさせることは望んでいないからです。しかし、
$/をデフォルト以外のものに設定することは避けられませんし、そうしなければ複数行レコードを読み込むことはできないのです。
$/ = ''; #一行ずつではなく、パラグラフ全体を読み込む
while ( <> ) {
while ( /\b(\w\S+)(\s+\1)+\b/gi ) {
print "Duplicate $1 at paragraph $.\n";
}
}
以下の例は、“From ”で始まるセンテンス(多くのメイラーによって変形されるであろうもの)を検索するものです。
$/ = ''; #一行ずつではなく、パラグラフ全体を読み込む
while ( <> ) {
while ( /^From /gm ) { # /m によって ^ が \nの直後にマッチするようになる
print "leading from in paragraph $.\n";
}
}
次の例は、パラグラフ中のSTARTとENDに挟まれた部分を検索するもので す:
undef $/; # 一行とか一パラグラフではなくファイル全体を読み込む
while ( <> ) {
while ( /START(.*?)END/sm ) { # /s は . が行境界をまたぐようにします
print "$1\n";
}
}
▲このページのインデックスへ
異なる行にある二つのパターンに挟まれている行を取り出すのはどうやればできますか?
Perlの ..
演算子を使えます(perlopに説明があります)。
perl -ne 'print if /START/ .. /END/' file1 file2 ...
行ではなく、テキストが必要なら次のようにします
perl -0777 -pe 'print "$1\n" while /START(.*?)END/gs' file1 file2 ...
しかし、START
とEND
が現れるのを入れ子にさせたいというのであれば、このセクションにある質問で説明されている問題に直面することになります。
▲このページのインデックスへ
正規表現を$/に設定したけど、駄目でした。何が悪かったのですか?
$/は正規表現ではなく、文字列でなければなりません。この点に関してはawkの方が良いですね
:-)
実際のには、ファイル全体をメモリーへ読み込んでしまうことを気にしないのであればお望みのことを行うことができます。
undef $/;
@records = split /your_pattern/, <FH>;
Net::Telnet モジュール(CPANで入手可能)は、入力ストリームであるパターンを待ったり、それが特定の時間内に現れなかったときにはタイム
アウトする機能を持っています。
## 三行からなるファイルを作成します。
open FH, ">file";
print FH "The first line\nThe second line\nThe third line\n";
close FH;
## それに対するread/writeファイルハンドルを取得します。
$fh = new FileHandle "+<file";
## それを“ストリーム”オブジェクトにアタッチします。
use Net::Telnet;
$file = new Net::Telnet (-fhopen => $fh);
## 二番目の行を探し、三番目の行を出力します。
$file->waitfor('/second line\n/');
print $file->getline;
▲このページのインデックスへ
LHSでは大小文字を無視して、RHSでは元の大小文字を保存しておくような置換はどうやるの?
それは、あなたが“元の大小文字”(preserving case)をどのような意味で使っているかによります。以下に挙げるスクリプトは、大小文字の違いを保ったまま、文字毎に置換を行ないます。置換対象の文字列より
も多くのキャラクターが置換後の文字列にあるのであれば、最後のキャ ラクターの大小文字の種別が置換後の文字列の残りの部分のキャラクターに対して使われます。
# Original by Nathan Torkington, massaged by Jeffrey Friedl
#
sub preserve_case($$)
{
my ($old, $new) = @_;
my ($state) = 0; # 0 = no change; 1 = lc; 2 = uc
my ($i, $oldlen, $newlen, $c) = (0, length($old), length($new));
my ($len) = $oldlen < $newlen ? $oldlen : $newlen;
for ($i = 0; $i < $len; $i++) {
if ($c = substr($old, $i, 1), $c =~ /[\W\d_]/) {
$state = 0;
} elsif (lc $c eq $c) {
substr($new, $i, 1) = lc(substr($new, $i, 1));
$state = 1;
} else {
substr($new, $i, 1) = uc(substr($new, $i, 1));
$state = 2;
}
}
# 新しい文字列の残りの部分を仕上げる (newがoldより長い場合)
if ($newlen > $oldlen) {
if ($state == 1) {
substr($new, $oldlen) = lc(substr($new, $oldlen));
} elsif ($state == 2) {
substr($new, $oldlen) = uc(substr($new, $oldlen));
}
}
return $new;
}
$a = "this is a TEsT case";
$a =~ s/(test)/preserve_case($1, "success")/gie;
print "$a\n";
この出力は以下のようになります:
this is a SUcCESS case
▲このページのインデックスへ
perllocaleを参照してください。
▲このページのインデックスへ
/[a-zA-Z]/
の locae-smartなバージョンでマッチさせるには?
あなたの置かれているロカールに関りなく、alphabetic キャラクター は/[^\W\d_]/
となります。非
alphabeticキャラクターは /[\W\d_]/
です(あなたがアンダースコアを文字と考えないと仮定しています)。
▲このページのインデックスへ
正規表現の中で使う変数をクォートするには?
Perlの構文解析器(parser)は、区切りがシングルクォーテーションでない限り、正規表現の中にある
$variableや@variableといったものを展 開します。s///
による置換の右側にあるものはダブルクォーテーションで括られた文字列とみなされるということを忘れないでください。また、すべての正規表現演算子はその前に
\Qを置いておかないと、正規表現演算子として振る舞うということも忘れないでください。以下に例を挙げます。
$string = "to die?";
$lhs = "die?";
$rhs = "sleep no more";
$string =~ s/\Q$lhs/$rhs/;
#ここで$stringは"to sleep no more"となる
\Qがないと、この正規表現は“di”にマッチします。
▲このページのインデックスへ
/o
は実際なんのためのものなのですか?
正規表現マッチングで変数を使うと、そこを通る度に再評価(とおそら くは再コンパイル)が強制的に発生します。
/o
修飾子は正規表現を最 初に使ったものにロックします。これは常に正規表現定数
(constant regular expression)に起きるもので、実際、パターンはプログラム全体がコンパイルされたときと同時に内部表現にコンパイルされます。
/o
の使用は、変数展開(variable interpolation)がパターンの中で使われていなければ的外れなものになります。もし変数展開があると、正規表現エンジンはパターンが非常に早い段階で評価された後で変数が変更されたことを知ることもないし、気にかけることもありません。
/o
は、変数の変更がないことがわかっていたり(なぜならあなた自身が変数を変更しないことを知っているから)、変更されたことを正規表現に通知したくないような場合に余計な評価を行なわないことによって効率を上げるために良く使われます。
For example, here's a ``paragrep'' program: uno以下に挙げるのは“paragrep”プログラムです:
$/ = ''; # パラグラフモード
$pat = shift;
while (<>) {
print if /$pat/o;
}
▲このページのインデックスへ
ファイルから、C形式のコメントを剥ぎ取る(strip)するにはどのように正規表現を使えば良いのでしょうか?
実際これは可能なのですが、あなたが考えているよりも非常に難しいも のです。たとえば次の一行野郎
(one-liner)はほとんどの場合にうまく行きますが、すべての場合ではありません。
perl -0777 -pe 's{/\*.*?\*/}{}gs' foo.c
そう、これはCのプログラムを簡単に考えすぎているのです。特に、クォートされた文字列にコメントが出現するということを考慮していません。このため、Jeffrey
Friedlが作成した次の例のようなことが必要に なります。
$/ = undef;
$_ = <>;
s#/\*[^*]*\*+([^/*][^*]*\*+)*/|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|\n+|.[^/"'\\]*)#$2#g;
print;
もちろんこれは、/x
修飾子を使って空白やコメントを付加することで、より読みやすくすることが可能です。
▲このページのインデックスへ
Perlの正規表現をテキストのバランスが取れているかを 検査するために使えますか?
Perlの正規表現は、後方参照(\1
など)のような便利な機能があるこ
とで“数学的” (mathematical)な正規表現よりも強力であるにもかかわらず、この問題に対処するには能力が足りません。たとえば括弧やブレースに挟まれているテキストのようなもののバランスが取れているかを解析するための、正規表現を使わないテクニックを使う必要がありま
す。
ネストする可能性のある`
と '
, {
と }
, (
と )
のような単一キャラクタのバランスを検査するための精巧なサブルーチンが、http://www.perl.com/CPAN/authors/id/TOMC/scripts/pull_quotes.gz
にあります(7-bit ASCII専用)。
CPANにある C::Scanモジュールはこのようなサブルーチンを内部的に使っているのですが、ドキュメントには載っていません。
▲このページのインデックスへ
正規表現が貪欲(greedy)であるとはどういうことですか?
ほとんどの人が、貪欲な正規表現(greedy regexps)は可能な限りマッチすると考えています。技術的には、量指定子(?
,
*
, +
, {}
) はパターン全体よりも貪欲です。Perlは
local greedであることを好み、全体の要求を即座に満足させます。同じ量指定子のnon-greedyバージョ
ンを得るには、 ??
, *?
, +?
, {}?
を使います。
例:
$s1 = $s2 = "I am very very cold";
$s1 =~ s/ve.*y //; # I am cold
$s2 =~ s/ve.*?y //; # I am very cold
二番目の置換が、“y ”を見つけてすぐにマッチングを中断していることに注目してください。量指定子
*?
は正規表現エンジンに対して、あなたが熱いジャガイモを扱っているときのように、可能な限り早くマッチするのもを見つけて制御を次の行に渡すように効果的に指示します。
▲このページのインデックスへ
各行の、各単語毎に処理をするにはどうすれば良いですか?
split関数を使います。
while (<>) {
foreach $word ( split ) {
# $word に対する処理をここで行う
}
}
これは実際には英語でいうところの語ではないことに注意してください。これは、単なる連続した空白でないキャラクターの塊です。
アルファベットもしくは数字の並びのみを対象とするには以下のようにしてできます。
while (<>) {
foreach $word (m/(\w+)/g) {
# ここで$wordに対する処理をする
}
}
▲このページのインデックスへ
語の出現頻度や行の出現頻度のまとめをどうやれば出力できますか?
これを行うためには、入力ストリームにある単語のそれぞれについて解 析する必要があります。私たちはここで、一つ前の質問と同様に、非空
白キャラクターの塊を語とするのではなくアルファベット、ハイフン、アポストロフィ、の塊を語とします:
while (<>) {
while ( /(\b[^\W_\d][\w'-]+\b)/g ) { # misses "`sheep'"
$seen{$1}++;
}
}
while ( ($word, $count) = each %seen ) {
print "$count $word\n";
}
同じことを行に対して行ないたいのであれば、正規表現は必要ないでし ょう。
while (<>) {
$seen{$_}++;
}
while ( ($line, $count) = each %seen ) {
print "$count $line";
}
ソートされた順序で出力したいのなら、ハッシュのセクションを参照してください。
▲このページのインデックスへ
曖昧なマッチング (approximate matching)はどうやればできますか?
CPANで入手できる String::Approx モジュールを参照してください。
▲このページのインデックスへ
たくさんの正規表現を一度に効率良くマッチングするには?
次のようなやり方は非常に効率が悪いものです:
while (<FH>) {
foreach $pat (@patterns) {
if ( /$pat/ ) {
# 何か処理を行う
}
}
}
こういったやり方ではなくCPANにある 実験的な正規表現拡張モジュールの一つを使うか(ひょっとしたらあなたの目的にはおおげさすぎるかもしれませんが)、あるいは、Jeffrey
Friedlの本にあったルーチンに影響を受けた以下の例のように、(正規表現を)まとめてしまう必要があるでしょう:
sub _bm_build {
my $condition = shift;
my @regexp = @_; #これはlocal()ではいけない! my()が必要。
my $expr = join $condition => map { "m/\$regexp[$_]/o" } (0..$#regexp);
my $match_func = eval "sub { $expr }";
die if $@; # propagate $@; this shouldn't happen!
# $@を伝播する。これは起きてはいけない!
return $match_func;
}
sub bm_and { _bm_build('&&', @_) }
sub bm_or { _bm_build('||', @_) }
$f1 = bm_and qw{
xterm
(?i)window
};
$f2 = bm_or qw{
\b[Ff]ree\b
\bBSD\B
(?i)sys(tem)?\s*[V5]\b
};
# feed me /etc/termcap, prolly
while ( <> ) {
print "1: $_" if &$f1;
print "2: $_" if &$f2;
}
▲このページのインデックスへ
なぜ\b
を使った語境界の検索がうまく行かないのでしょうか?
二つの良くある勘違いとは、\b
をC<\s>と同じと考えてしまうという
ことと、\b
が空白キャラクターと非空白キャラクターの間にあるも
のと考えてしまうことです。これは両方とも間違いです。 \b
は\w
のキャラクターと、\W
のキャラクターとの間にマッチします(つまり、
\b
は“語”の境界なのです)。これは^
、$
などのアンカーと同
じく幅がありません。ですから、これは何のキャラクターも消費しません。perlreでは、すべての正規表現メタキャラクターの振る舞いを
解説しています。
以下の例は、\b
の間違った使い方と、それを直したものです。
"two words" =~ /(\w+)\b(\w+)/; # *間違い*
"two words" =~ /(\w+)\s+(\w+)/; # 正しい
" =matchless= text" =~ /\b=(\w+)=\b/; # *間違い*
" =matchless= text" =~ /=(\w+)=/; # 正しい
これらの演算子はあなたが思ったようには動作しないかもしれませんが、 それでも\b
と\B
は実に便利に使えるのです。\b
の正しい使い
方の例は、複数行に渡る重複単語のマッチングの例を見てください。
\B
を使った例は、\Bis\B
というものです。これは“this”や“island”ではなく、“thistle”のように単語の中に収まっている“is”という並びだけを見つけ出します。
▲このページのインデックスへ
なぜ $&, $`, $' を使うとプログラムが遅くなるのでしょうか?
プログラムのどこかでそういった変数が使われているのを見つけてしまうと、Perlはすべてのパターンマッチに対してそれに対処することをや
らなければなりません。同様のからくりが、$1、$2などを使ったときにも行なわれます。このためすべての正規表現において、部分正規表現を
捕捉するために同じコストがかかることになります。しかし、スクリプト中で
$&などを全く使っていないのであれば、正規表現は部分正規表現を捕捉して不利になるようなことはしません。ですから、可能であれ
ば $&や$'、$`を使わないようにすべきなのですが、それができないのであれば(一部のアルゴリズムはこれを使うのが便利なのです)、一度これらの変数を使ってしまったら好きなように使いましょう。なぜなら、罰金はすでに払ってしまったのですから。
▲このページのインデックスへ
正規表現の中で\G
を使うと何が良いのですか?
\G
記法は、最後にマッチしていた場所(つまり pos()の場所)がどこ
なのかを示すために正規表現につける目印で、/g
修飾子と組み合わ
せてマッチングや置換で使われます。
例として、標準的なメイルやusenetのやり方で引用されているテキスト (つまり、先頭に
>
がある)があって、先頭で連続している>
を同じ数の
:
に変換したいという状況を考えてみましょう。これは以下のようにして実現できます。
s/^(>+)/':' x length($1)/gem;
これを、\G
を使ってより単純(かつ)高速にできます:
s/\G>/:/g;
より精巧な使い方は tokenizerに関連したものでしょう。以下に挙げた lexに似た例は、Jeffrey
Friedlの好意によるものです。これは 処理系 のバグのために 5.003では動作しませんが、5.004以降では動作します
(/g
を使ったマッチングが失敗して検索位置が文字列の先頭にリセットされることを防ぐために、/c
を使っていることに注意すること)。
while (<>) {
chomp;
PARSER: {
m/ \G( \d+\b )/gcx && do { print "number: $1\n"; redo; };
m/ \G( \w+ )/gcx && do { print "word: $1\n"; redo; };
m/ \G( \s+ )/gcx && do { print "space: $1\n"; redo; };
m/ \G( [^\w\d]+ )/gcx && do { print "other: $1\n"; redo; };
}
}
もちろん、以下のように書くこともできます
while (<>) {
chomp;
PARSER: {
if ( /\G( \d+\b )/gcx {
print "number: $1\n";
redo PARSER;
}
if ( /\G( \w+ )/gcx {
print "word: $1\n";
redo PARSER;
}
if ( /\G( \s+ )/gcx {
print "space: $1\n";
redo PARSER;
}
if ( /\G( [^\w\d]+ )/gcx {
print "other: $1\n";
redo PARSER;
}
}
}
しかし、これでは正規表現の垂直方向の揃えがなくなってしまいます。
▲このページのインデックスへ
Perlの正規表現ルーチンはDFAですかNFAですか? また、それはPOSIXに従ってますか?
Perlの正規表現はegrep(1)のDFA (deterministic finite automata, 決 定性有限オートマトン)と似たものではあるのですが、実際のところはバックトラックや後方参照
(backreferencing)のために NFAとして実装 されています。そして、Perlの正規表現は
POSIX形式のものでもありません。なぜなら、それはすべてのケースにおいて最悪の振る舞いを行うからです(一部の人は、それが遅さをもたらすにもかからわず、一貫性をもたらすという点を好んでいるようです)。上記のことなどに関して
の詳細はJeffrery Friedlによる O'Reillyから出版されている ``Mastering Regular
Expressions'' という本を参照してください。
▲このページのインデックスへ
voidコンテキストでgrepやmapを使うことのどこが間違っているのでしょうか?
厳密に言えばなにもありません。形式的に言えば、それは保守しやすいプログラムを書く良い方法ではありません。なぜなら、あなたはそこで
grepやmapの戻り値を使うのではなく、副作用のために使っているから です。副作用はあいまいになりやすいのです。
for
(技術的には foreache
も)ループとしてvoidコンテキストのgrepを使うことは良い
ことではありません。
▲このページのインデックスへ
複数バイトキャラクタを使った文字列のマッチングは どうすればできますか?
これは難しく、そしていい方法がありません。Perlは幅広文字 (wide characters)を直接はサポートしておらず、一バイトと一キャラクターとが同一であることを要求しています。以下に、The
Perl Journalの第五号でこの問題についてより詳しい記事を書いたJeffery Friedlにより提案されたアプローチの幾つかを挙げます。
さて、ここでASCIIの大文字二文字で火星語の符号化をしていると仮定 しましょう(たとえば、
``CV'', ``SG'', ``VS'', ``XX''などといった二バイトの並びが火星語の一文字を表わすということです)。
ですから、火星語の符号化をしている 12バイトの ``I am CVSGXX!'' 文 字列は、'I',
' ', 'a', 'm', ' ', 'CV', 'SG', 'XX', '!' という九 文字で構成されます。
ここで、/GX/
という一文字検索をしたいと考えてみましょう。Perl
は火星語については何も知りませんから、``I am CVSGXX!'' という文字 列にある
``GX''二バイトを見つけ出してしまうでしょうが、これは文字 としてそこにあるものではありません。つまり、``SG''に続けて``XX''があるのでそう見えるだけであって、本当に``GX''があるわけではないのです。これは大きな問題です。
この問題に対処する方法が、うんざりするようなものですが、幾つかあ ります:
$martian =~ s/([A-Z][A-Z])/ $1 /g; #“火星語”のバイト並びが隣接しないよう
# にする
print "found GX!\n" if $martian =~ /GX/;
あるいは:
@chars = $martian =~ m/([A-Z][A-Z]|[^A-Z])/g;
# 上の行は次のものと考えは同じ: @chars = $text =~ m/(.)/g;
#
foreach $char (@chars) {
print "found GX!\n", last if $char eq 'GX';
}
あるいは:
while ($martian =~ m/\G([A-Z][A-Z]|.)/gs) { # \Gは多分不要
print "found GX!\n", last if $1 eq 'GX';
}
あるいは:
die "sorry, Perl doesn't (yet) have Martian support )-:\n";
(申し訳ない。Perlは(まだ)火星語をサポートしてません )-:)
それに加え、(Shift-JISやEUCの)半角かたかなを全角かたかなに変換するサンプルプログラムがCPANから入手できます。
今日一般的に使われている多くのダブルバイト(とマルチバイト)エンコーディングがあります。
出典:Perlについていたドキュメントだったと思う。