読者です 読者をやめる 読者になる 読者になる

Perlの正規表現の修行の経過

この記事ではおもに以下の点について触れます。

  • \b{wb}というアンカーについて
  • m修飾子および\A\zアンカーについて

先に書いておくと、このブログの読者なら大丈夫とは思いますが、あくまで勉強過程のメモなので、くれぐれも鵜呑みにはされませんようお願いします。

では前者から。

前回の記事を書きながら、note103.hateblo.jp

そういえば先日のYAPCで、リカルドさんが積年の懸案だった「ワード」のマッチングおよび概念に関して、v5.22でようやく改善が果たされた、みたいなことゆってなかったっけ、とおぼろげに考えていた。

具体的には、たしか単語の切れ方として、本来であれば「That's」のようなアポストロフィを含めて1語にすべきところ、これまでは「That」で切れてしまっていたのが「That's」で一つとみなせるようになったよ、みたいな話があったはず・・という。

で、昨日YAPCサイトに行って当該ヴィデオを見直してみたら、

11:00あたりからその話をしていた。(以下の埋め込み動画はほぼその時点から始まります)

www.youtube.com

ただ、ここでは概説みたいなことに留まっていて、サンプルコードなどはなかったのでperldocを見に行ったら、この項目の下の方にザックリ書いてあった。

Using character classes

The word anchor \b matches a boundary between a word character and a non-word character \w\W or \W\w :

$x = "Housecat catenates house and cat";
$x =~ /\bcat/;  # matches cat in 'catenates'
$x =~ /cat\b/;  # matches cat in 'housecat'
$x =~ /\bcat\b/;  # matches 'cat' at end of string

In the last example, the end of the string is considered a word boundary.
For natural language processing (so that, for example, apostrophes are included in words), use instead \b{wb}

"don't" =~ / .+? \b{wb} /x;  # matches the whole string

で、これを見て、自分でもv5.22を入れてサンプルを書いてみた。

#!/usr/bin/env perl
use 5.022;
use strict;
use warnings;

# \bを使う(従来のワード境界アンカー)
my $str = "Don't think twice.";
if ($str =~ /(.+?)\b/) {
    say $1;
}

# \b{wb}を使う(新たに追加されたもの)
my $str2 = "Don't think twice.";
if ($str2 =~ /(.+?)\b{wb}/) {
    say $1;
}

実行。

Don
Don't

なるほど。リカルドさんの言うとおり、たしかに後者はアポストロフィ入りになっている。

ただ、日本語はとくに関係ないみたい。元はと言えば、前回記事の最後に書いたような、より意図に合った日本語抽出正規表現を求めてのことだったのだけど、それはまた継続勉強ということで・・

追記:

「日本語はとくに関係ないみたい」とあっさり書いたけど、動画の中でリカルドさんも触れているように、ここで望んでいる意図とは別の意味では日本語にも\b{wb}の有無の違いは出る。
具体的には、このようなコードで比べると、

#!/usr/bin/env perl
use 5.022;
use strict;
use warnings;
use utf8;
binmode STDOUT => ':utf8';

# \bを使う
my $str = "二度考えるな。大丈夫。";
if ($str =~ /(.+?)\b/) {
    say $1;
} else {
    say 'not match!';
}

# \b{wb}を使う
my $str2 = "二度考えるな。大丈夫。";
if ($str2 =~ /(.+?)\b{wb}/) {
    say $1;
} else {
    say 'not match!';
}

このような結果になる。

二度考えるな

リカルドさんのDEMOのとおりですね。(たぶん)

(追記ここまで)

次、頭尾アンカー&修飾子の話。

上の\b{wb}を理解するためにはまず\bアンカーについて理解しなければならず、そのために『初めてのPerl』を紐解いたのだけど(P174)、そうしたら本自体、文章自体が今までに思っていた以上に面白くて、そのまま前後のくだりも結構読んでしまった。

初めてのPerl 第6版

初めてのPerl 第6版

で、その中に\A\zというのも出てきたのだけど、これはPerl入学式だと通常^$として教えているもので、ようはどちらもそれぞれ文字列の頭と末尾にマッチさせるためのものなのだけど、僕が以前に読んだ説明だと小飼弾さんによる以下がわかりやすかった。

また、そのアンカー2組に加えて/m, /sという修飾子の問題もあるのだけど、それについてはPerl入学式でいつもお世話になっています@xtetsujiさんの以下が参考になりまして、

これらをまとめて僕なりの解釈で言い換えると、

  • \A\zと^$はそれぞれ対象文字列の最初と最後にマッチするけど、修飾子にmが入っていた場合、挙動が変わる。
    • 具体的には、^$を使っているときに/m修飾子を使うと、対象文字列に改行が含まれていた場合、その文字列全体における最初と最後ではなく、文字列中の改行前後がマッチ候補になる。
  • よって弾さん的には、「文字列の最初と最後にマッチさせるつもりのアンカーはつねに\A\zを使って、かつm修飾子もつねに付けることにすれば、想定外の結果が出なくなる」とおっしゃっている(はず)。
  • また同時に言えることとして、s修飾子が付いていないと「.(ドット)」が改行(\n)にマッチしないので、「s修飾子もつねに付ける前提として、.(ドット)は全ての文字にマッチすると覚えておけばいい」と弾さんはおっしゃっている(はず)。

ということかと思った。

それらを示すサンプルコードを、これまた僕なりに作ってみると、まず2組のアンカー&m修飾子の違いを見るのはこんな感じ。

#1. ^$使用
##1-1. m未使用
$_ = "apple\norange";
if ($_ =~ /^(orange)$/) {
    say 'match!';
} else {
    say 'not match!';
}
##1-2. m使用
$_ = "apple\norange";
if ($_ =~ /^(orange)$/m) {
    say 'match!';
} else {
    say 'not match!';
}
#2. \A\z使用
##2-1. m未使用
$_ = "apple\norange";
if ($_ =~ /\A(orange)\z/) {
    say 'match!';
} else {
    say 'not match!';
}
##2-2. m使用
$_ = "apple\norange";
if ($_ =~ /\A(orange)\z/m) {
    say 'match!';
} else {
    say 'not match!';
}

結果。

not match!
match!
not match!
not match!

つまり、m修飾子とその有無の影響を受ける^$が揃った2番目のサンプルの時だけ、2行目にあたる「orange」がマッチしている。

じゃあ次、「.(ドット)」が改行にもマッチするようにs修飾子を使った場合と、使わない場合を比べてみる。これはアンカー関係ないっぽいので、どちらも\A\zにて。

#3-1. \A\z使用, s未使用
$_ = "apple\norange";
if ($_ =~ /\A(.+)\z/) {
    say 'match!';
} else {
    say 'not match!';
}
#3-2. \A\z使用, s使用
$_ = "apple\norange";
if ($_ =~ /\A(.+)\z/s) {
    say 'match!';
} else {
    say 'not match!';
}

結果。

not match!
match!

はい。つまり前者は対象文字列の中に改行(\n)が含まれているけど、正規表現内のパターンは「改行を含まない文字列が欲しい」と言っているので、マッチしない。
でも後者は、s修飾子を入れることで「改行あってもいいよ」という意味になっているので、マッチしている。

ということで、あらためてまとめると、

  1. 「対象文字列の最初と最後」を見たい、という意図なら\A\z組の一択でOK。
  2. そうではなく数行を対象とした際の「(どの行でもいい)行頭と行末」を見たいならば、その時は^$を使う。
  3. で、いずれにおいてもmsを使いましょう。

ということかなと。

以上、後で同じことでハマっても、この記事じたいが僕の勉強のアンカーになるようにというか(べつに上手いこと言いたいわけじゃないのだけど)、ようは学習って下りエスカレーターをのぼり続けるような行為なので、いずれきっとまた今の理解より下に下がってしまうこともあるかもしれないけど、その時にもまったくのゼロ地点まで下がりきってしまいづらいように、書いてみました。

しかしそんなちょっとした目的から書き始めてみたものの、なかなかのボリュームになってしまった&上記の小飼弾さんの記事、ほんと濃い・・「わかりやすかった」などと書いたけど、実際にはまだわからない内容の方が多いですね・・

ってそれが「宿題」ということなのでしょうけど・・道は深く、長く、そして険しい。