So-net無料ブログ作成
検索選択
Regular Expressions ブログトップ

公開 [Regular Expressions]

前回提示した暗号(?) は、正規表現のパターン。Web を探すと記事として紹介しているところも多い。要は、数値文字列を 3 桁ずつ区切って、カンマを挿入するためのパターン。

(?<=\d)(?=(?:\d\d\d)+(?!\d))

このパターン、いくつかのバリエーションがあり、その選択は主に動作環境と趣味嗜好に委ねられている。私が提示したパターンは、.NET Framework に実装されている正規表現を、特にオプションを指定しないで使うためのもの。他の環境で使うためには注意が必要になる場合もある。

正直なところ、あまり実用的なパターンだとは思わないけど、正規表現のゼロ幅アサーションの説明をするのに便利なので、たまにサンプルとして使ってる。

正規表現のグループ化構成体には、パターンにマッチする位置を取得するための構文が 4 つ含まれている。正規表現はパターンにマッチする文字の並びを探すものと考えてしまうとチョット混乱するかもしれない。提示されたサンプルの使い方はなんとなく解るけど、実際、どうして出来るのかなかなか理解しづらいというのも解らないでもない。ただ、この訳を見せられて一度で理解するってのも無理な話かもしれない。


 ・肯定後読みゼロ幅アサーション

(?<=\d)

パターンの一部を切り出してきた。意訳すると、「数値(\d) が先行する場合、その位置にマッチする」となる。実際に動かしてみると解りやすいかもしれない。PowerShell の構文はこんな感じ。

"123456789" -replace "(?<=\d)","|"

 実行結果のバーチカルバー "|" が挿入された位置を確認したら納得できると思う。マッチした文字が書き換えられるわけではなく、該当位置にバーチカルバーが挿入される動作となる。

・(肯|否)定先読みゼロ幅アサーション

(?=(?:\d\d\d)+(?!\d))

チョット長いけど、「数値が 3 の倍数だけ並んだあとに、数値がこない場合、その位置にマッチする」となる。これだけ読むと、目標達成してしまいそうだけど、実はちょっとした落とし穴がある。

"123456789" -replace "(?=(?:\d\d\d)+(?!\d))","|"

実行結果を見ると、余計なところにバーチカルバーがついてしまっているのが解る。なので、先ほどのパターンと合わせて利用すると、課題が達成できるようになる。ゼロ幅アサーションはパターンがマッチした位置を探し出すという動作のため、挿入動作のために使うのが(まだ)解りやすい。なかなか実感できないもののひとつにバックトラックの回避(こちらは主に検索で使用する)というものもあるけど、機会があったら紹介します。

ちなみに look ahead は前読み、look behind は後読みと訳すのが一般的。そして、positive は肯定、negative は否定だろう。正のとかって言われたら解るものまで解らなくなる。


予告 [Regular Expressions]

(?<=\d)(?=(?:\d\d\d)+(?!\d))

 


バランス [Regular Expressions]

正規表現にはいくつかの方言(仕様)が存在しているため、常にどの環境に持っていっても動作するとは限らない。割と多いと思われるのが PCRE (Perl 5 Compatible Regular Expressions) や ECMAScript と呼ばれているもの。.NET Framework は 「Perl 5 の正規表現と互換性がある」とは謳っているけど、独自の拡張機能をも盛り込んでいる。

手間暇かけて作った正規表現のパターン、小細工をすればするほど移植性が悲しいものになってくる。ノータッチで移行できたことなんて殆どない。 

.NET Framework の仕様の正規表現で、何をするためのものなのか解らない機能の筆頭は、きっと「グループ定義の均等化」だと思う。タイトル見ただけじゃ意味不明だし、解説読んでも謎は深まるだけ。そして、原文読んでわかったのは、、、これ、誤訳じゃない?

左記のサイトには、下記のサンプルが提示されている。このサンプルを探ってみようとおもう。

^[^<>]*(((?'Open'<)[^<>]*)+((?'Close-Open'>)[^<>]*)+)*(?(Open)(?!))$

 このサンプルを見て最初に思ったのは、、、妙に括弧が多いなぁ、、、ってこと。実際 6 つのグループ化構成体と代替構成体を使用しているわけで、それだけで括弧のペアが 8 つ出てくることの説明になってしまう。サイトでは以下の入力例でサンプルの動作を説明しているのだけど、鉤括弧を使った形にしているのは、単に括弧の数を減らしたかっただけなのかもしれない。

<abc><mno<xyz>>
  • ^[^<>]*
    これは、入力の先頭から鉤括弧が現れるまでの文字を読み飛ばすためのパターン。今回の入力には余分なものはついていないので、気にしなくても大丈夫。まあ、和訳(?) するなら「< か > が現れるまで読み飛ばす」という事になる。このパターンと同じものが、サンプルで 3 箇所も使われている。顔文字に見えないこともない。
  • ((?'Open'<)[^<>]*)+
    ここからが本文。左鉤括弧 (<) が現れたら、これを Open という名称を付けてキャプチャする。.NET Framework では名前付部分式と定義されている機能。その後に続くのは、鉤括弧が見つかるまで読み飛ばす、、、これが大事。さらに、これらを繰り返す。
    結果的に、ここでは左鉤括弧を収集する動作となる。
  • ((?'Close-Open'>)[^<>]*)+
    これが本題のグループ定義の均等化(誤訳?)
    上にある名前付き部分式とよく似た構文。よく見ると、シングルクォーテーションで囲まれたパターンが引き算のようになっているだけ。
    右鉤括弧にマッチしたときに、Open が定義されていたら、Open から 現在位置までを Close にキャプチャし、Open の定義を削除する。
    右鉤括弧にマッチしたときに、Open が定義されていなければ、それは不一致として処理(バックトラック)される。
    後続のシーケンスは、前項と同様なので省略。
  • (((?'Open'<)[^<>]*)+((?'Close-Open'>)[^<>]*)+)*
    そして、上記のシーケンスを繰り返す。
  • (?(Open)(?!))$
    最後のシーケンスは、左鉤括弧と右鉤括弧のが一致しなかったときの処理が記述されている。意味的には、Open という名前のグループが存在していたら(最後まできて、左鉤括弧に対応する右鉤括弧が現れなければ)強制的にエラーにするという感じ。

.NET Framework の正規表現の実装は、結構便利に出来ている(と私個人は思っている)。名前付き部分式で(単純な部分式でも同様です)、同じ名称のキャプチャ要因が現れた場合、.NET Framework の実装では、出現順に全てのパターンを記憶していきます。実装によっては、パターンが出現するたびにキャプチャが上書きされてしまうものもあるようですが、キャプチャされたパターンを使いたい場合には .NET Framework のような実装が便利だとも思います。

グループ定義の均等化とは、上記の機能を活かすための実装のようです。私自身が開発に携わったわけではありませんので、単に勘違いの可能性もありますが、、、。キャプチャすることで作り上げたコレクションをスタックとして扱い、ペアとなるパターンを収集する。

A balancing group definition、これが原文におけるこの機能の命名のようです。決して均等化する(均す?)わけではなく、ペアを見つける(今回の例では < と > です)ための機能なんだと思います。

そういえば、カテゴリーを PowerShell にしていたなぁ zzz


蛇足

例の正規表現をマッチングさせてみると Close と命名されたグループにはこのような結果が得られる。

Index Length Value
----- ------ -----
1 3 abc
10 3 xyz
6 8 mno<xyz>

また、Open と命名されたグループは、結果だけを見ると存在しないことになっているし、他のグループ化構成体によってキャプチャされたデータはあまり使い道がない、と言うか、キャプチャを意識したものではないと思う。また、グループ定義の均等化では Close の部分がオプション扱いとなるので、ここを -Open という書き方をすることもできる。キャプチャ操作が入らなくなるので多少処理が速くなるのかもしれない(未検証)けど、その場合の使い道って、、、「鉤括弧の数があっているかを確認する」ためになるんでしょうか?

今回、パターンの検証をするのに、こんなスクリプトを作ってみた。

function global:RegEx(
[string] $Pattern,
[switch] $CaseSensitive
) {
Begin {
$RegOpt= [Text.RegularExpressions.RegexOptions];
$opt= $RegOpt::Compiled;
if (!$CaseSensitive) {
$opt= $opt -bor $RegOpt::IgnoreCase;
}
$re= New-Object Text.RegularExpressions.RegEx(
$Pattern,
$opt
);
}
Process { $re.Match($_); }
}

 使い方はこんな感じ

"<abc><mno<xyz>>" |
regex "^[^<>]*(((?'Open'<)[^<>]*)+((?'Close-Open'>)[^<>]*)+)*(?(Open)(?!))$" |
%{ $_.Groups["Close"].Captures }

Select-String でも同様のことはできるのかもしれないけど、、、なので、蛇足でした。


否定 [Regular Expressions]

正規表現ネタ 3 本目。

バグのない正規表現のパターンを記述するのはかなり難しい・・・いや、面倒くさいことなのかもしれない。サポートされている機能も環境によってマチマチだったりするし、正確な記述よりもさっさと結果を取り出して終わりにしてしまいたいこともあるし。

ここで使用している正規表現は PowerShell で使用出来るもの(= .NET Framework が提供するもの)なので、もしかすると他の環境で動作させたら異なる結果になる可能性がないわけではない。

一昨日投稿分の文末のサンプルで使用しているパターンでは、ゼロ幅否定先読みアサーションを用いたものになっている。で、ここで何をやっているかというと、

昔々に他人が作ったソースコード (Visual Basic) を改造することになってしまい、ソースコードの中から特定の単語を探し出すことから作業を始めることにした。ソースコードはおよそ 30 本あり、探したいのは DropDown という単語。.NET Framework に慣れている方だと察しがつくかもしれないけれど、ComboBox のあたりです。

実際に DropDown で検索してみたら、(今回の作業には関係ない)DropDownWidth というシーケンスが何件か含まれていることに気が付いた。一度ファイルに吐き出してからメモ帳かなにかで当該箇所を削除してしまえば目的は達成するはずなんだけど、ちょっと気になることがあり、少しばかり試行錯誤してみた。

正規表現を使った否定・・・結構鬼門だったりする。

「DropDown で始まり Width が続かない」ものを探すだけであれば、ゼロ幅否定先読みアサーションが利用できるなら大して苦労はいらない。 DropDown(?!Width) と記述すれば問題ないはず。

今回悩んだのはその先、(?!Width) に相当する部分をキャプチャしたい!?ところが、ゼロ幅肯定先読みアサーションでは、そこで指定されたパターンをキャプチャしてくれない。なので直後に [A-Z]+ というパターンを付加して無理やり拾ってくるようにしてしまった。また、 \b を配置することで単語境界をパターンに含めている。

一応、この段階で目的とする情報を取得することができたので、検証は打ち切ったのだけれど、本当にこれで正しいのか今一つ自信がない。ちなみに、一緒に公開したスクリプト(MyGrep)は上記作業中に即興で作ったものです。

 


grep [Regular Expressions]

PowerShell のスクリプトを使用し、grep もどきを作成してみました。 

function global:MyGrep(
[string] $Pattern,
[string] $Path=".\",
[string[]] $Include=@("*.txt"),
[string[]] $Exclude=@("*.exe","*.dll","*.msi"),
[string] $Encoding="UTF8",
[switch] $CaseSensitive,
[switch] $AbsolutePath
) {
if ($Pattern -ne "") {
$Path= Convert-Path $Path;
$uri= New-Object System.Uri("$Path\");
$opt= if ($CaseSensitive) {
[System.Text.RegularExpressions.RegexOptions]::None
}
else {
[System.Text.RegularExpressions.RegexOptions]::IgnoreCase;
}
$re= New-Object System.Text.RegularExpressions.RegEx($Pattern, $opt);
Get-ChildItem
-Path $Path -Include $Include -Exclude $Exclude -Recurse |
%{ $line= 1;
$filename= $uri.MakeRelativeUri($_.FullName).ToString();
foreach ($text in @(Get-Content $_.FullName -Encoding $Encoding)) {
if (($m= $re.Match($text)).Success) {
New-Object PSObject -Property @{
FileName=
if ($AbsolutePath) { $_.FullName; }
else { $filename; }
Line= $line;
Text= $text;
Groups= $m.Groups;
};
}
$line++;
}
}
}
}

もともと業務で使用する専用ツールとして作り始めたものでしたので、若干の癖が残っていますがそこそこ使えるのではないでしょうか。$Include や $Exclude の省略時の値は適当に変更して頂ければよいかと思います。

PowerShell で作成していますので、出力はテキストではなくオブジェクトになります。整形するには Format-Table コマンドレットあたりをしようしたらいいと思います。

オリジナルの grep は指定したパターンを含む行を表示するためのユーティリティですが、それに加えマッチしたパターンを抽出できるように変更しています。Groups プロパティからキャプチャした情報にアクセス可能です。

MyGrep "DropDown(?!Width\b)[A-Z]+\b" |
ft FileName,
@{L="Line"; E={$_.Line}; W=5; },
@{L="Match"; E={$_.Groups[0].Value} }

 


Regular Expressions [Regular Expressions]

PowerShell では正規表現を扱う演算子が提供されている。正規表現を一般的な用途(検索や置換)に使用するならほぼ問題ないレベルになっていると考えてもいいだろう。ただ、見つけたパターンを抽出したいような場合、多少のコードを記述をしなければならないようなこともある。

 Visual Basic で作成したソリューションがある。ソリューションに含まれるプロジェクトはおよそ 100、ソースファイルはおよそ 1000 本ほどある。ここにレポート作成用クラス、40 程度が定義されている筈なので、その定義の一覧を作成したい。一覧には当該クラスのクラス名を含むものとする。プロジェクトの命名規則によりレポート作成クラスの名称は「英字のみを使用していて Report で終わる」となっている。

レポート作成クラスにはいくつかの検証用クラスが含まれている。検証用クラスはの名称は「英字のみを使用していて TestReport で終わる」となっている。可能なら、上記一覧に検証用クラスは含まないようにしたい。

 正規表現を用いて検索を実行し、さらにマッチした文字列を取得するにはキャプチャという機能を使用する必要がある。ただ、PowerShell の実装だけではキャプチャを活用することが難しいので、.NET FrameworkSystem.Text.RegularExpressions.RegEx クラスを直接使ってしまうのが便利。

正規表現の文法についてはある程度標準のようなものがあり、実装レベルさえ一致していれば異なる環境でも互換性が保てるようだけれど、キャプチャの扱いに関しては実装依存が多いようです。.NET Framework の実装は(慣れもあるとは思うけど)、結構高機能で使いやすくできている気がする。

さて。上記の例題、前半部分だけでいいならこのように RegEx オブジェクトを構築すればいい。

$re = New-Object System.Text.RegularExpressions.RegEx(
"([A-Za-z]+Report)"
);

この表現は下記の動作を規定している、、、はず。

  • 1 字以上の英字からなる接頭辞があり
  • Reportというパターンが続く場合にマッチとする
  • マッチした場合に上記パターンをキャプチャする

正規表現は大文字/小文字を区別するいわゆるケースセンシティブがデフォ。なので接頭辞の部分には大文字/小文字の両方の範囲を含むように記述している。オプションを指定して下記のようにも記述可能だけれど、返って長くなっちゃうので私はあまり利用していない、、、補完も効かないしね。

$re = New-Object System.Text.RegularExpressions.RegEx(
"([A-Z]+Report)",
[System.Text.RegularExpressions.RegExOptions]::IgnoreCase
);

あとは RegEx オブジェクトを使用して...

$m = $re.Match("Class TechnicalReport ' demo");

のように記述すればいい。実際には "TechnicalReport" の部分にファイルから読み込んだテキストを当てながらマッチングを繰り返していくことになるのだけれど、明示的なコンパイル作業が不要なスクリプトならその場で動作確認もできてしまう。

さて、タイプミスしていなければ、変数 $m には比較結果(System.Text.RegularExpressions.Match)が含まれてる。Success プロパティは成否($True なら成功)が、そして、 Groups コレクションにはキャプチャされたパターンが含まれる。Groups コレクションはインデックスを用いて参照することが可能で、1 から始まるインデックスは検索文字列に表れる左かっこ "(" の順番が該当する。0 をインデックスにして Groups コレクションを参照すると検索文字列全体が対象となるパターンを表す。

$m.Groups[1].value;

ここでは TechnicalReport のパターンが表示されるはず。

正規表現に使用されるメタキャラクタには様々な機能が与えられているけれど、意外にも否定を表すことができるものは多くない。今回の例では割と上手くハマルのだけど、そうもいかないことも結構ある。例題の後半部分も盛り込んだ、解答例はこんな具合になる。

$re = New-Object System.Text.RegularExpressions.RegEx(
"([A-Za-z]+(?<!Test)Report)"
);

前出の例に対して Test というパターンを追加している。ざっくりな説明をするなら、「Report で終わるパターンはマッチするけど、TestReport で終わるものは除外する」となる。

今回追加した (?<!Test) となっている部分はゼロ幅否定後読みアサーションと呼ばれるもので、ここでは Report の前が Test でなければにマッチするという感じで使用している。.NET Framework のオンラインドキュメントでは、「ゼロ幅の負の後読みアサーション」などと翻訳されている。

検索がうまく動作するかは、下記を実行してみるとすぐわかる。

$m = $re.Match("Class TechnicalTestReport ' demo");

 Test が含まれている場合、Success プロパティは $False となるはず。

ゼロ幅(肯定|否定)(先読み|後読み)アサーションと表記される構文(4 種類ある)は置換に応用されることが多いようだ。指定されたパターンが見つかるとその位置にマッチしたと判断するものなので、なかなか検索に応用するのは難しいのかもしれない。さらに、キャプチャを表すグループ化構成体と同様に括弧 "( )"を使った構文になるのだけれど、こちらはキャプチャ動作を伴わない。今回は否定のパターンを使用しているので、パターン全体がマッチするなら当該箇所のパターンは必ず空になるのが前提なので問題ないけど、別の用途で肯定のパターンを使ったとしても当該パターンに対応する文字はキャプチャできない。また、特定の文字列以外にマッチした時にそのパターンが取れたらいいのにと思うことも少なくない。

グループ化構成体を多用するようになると、括弧に対応するインデックスを見つけるのが大変になってくることがある。.NET Framework の実装ではグループ化構成体に名前を割り当てることが可能になっており、さらにキャプチャ対象のパターンを全て記憶してくれる。

後半部分について「当然でしょ!?」と思われる方も少なくないかもしれない、、、私もそう思ってたのだけれど、、、。キャプチャが繰り返し実行される場合に、最後のキャプチャ結果以外が破棄されてしまう実装も珍しくはないようです。

こんな構文について考察したい。

$re = New-Object System.Text.RegularExpressions.RegEx(
"(?<name>[A-Za-z]+)(?:\s+(?<name>[A-Za-z]+))*"
);

(?<name>[A-Za-z]+) となっている部分は、英字だけからなるパターンにマッチしたら、name という名前でキャプチャするという意味。さらに、同じパターン 2 回登場していて、後半部分が 0 回以上の繰り返しとなっているため、全体としては「スペースで区切った英字からなるパターン全てを name という名前でキャプチャする」となる。

$m = $re.Match("AAAAA BBBBB CCCCC DDDDD");
$m.Groups["name"].Captures

 名前を付けてキャプチャした場合には、当然名前によって参照が可能になる。上記のような構文を使用すると、パターンの定義位置に拘らず同一の名前で参照できることがわかる。


Regular Expressions ブログトップ

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。