5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Delphi】ゆるく正規表現を使う

Last updated at Posted at 2023-05-03

はじめに

Delphi で正規表現による検索/置換を使っていますか?

正規表現を使う

普通に正規表現を使うには、手前味噌ですが次の記事を参考にしてください。

検索

Delphi で文字列検索と言うと Pos() ではないでしょうか?

program Search; 
{$APPTYPE CONSOLE}
begin
  var s := 'Hello, world.';
  if Pos('world', s) > 0 then
    Writeln('Match.')
  else
    Writeln('No match.');
end.

正規表現で書くとこうなります。

program Search; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  if TRegEx.IsMatch(s, 'world') then
    Writeln('Match.')
  else
    Writeln('No match.');
end. 

大文字/小文字を無視してヒットさせるようにするには TRegExOption を指定できるオーバーロードメソッドを使います。

program Search; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  if TRegEx.IsMatch(s, 'WORLD', [roIgnoreCase]) then
    Writeln('Match.')
  else
    Writeln('No match.');
end. 

TRegExOption は正規表現に使えるオプションです。

列挙値 意味
roNone 何もオプションが設定されないことを指定します。
roIgnoreCase 大文字小文字を区別しないマッチングを指定します。
roMultiLine 複数行モード。 ^ と $ の意味が、文字列全体の先頭と末尾だけでなく、各行の先頭と末尾でも一致するように変更されます。
roExplicitCapture 明示的に名前または番号が付けられた (?...) 形式のグループだけが有効なキャプチャになるように指定します。 これによって、名前なしのかっこ表現 (?:...) は、構文的に間違いがなくても、非キャプチャ グループとして動作します。
roCompiled 正規表現をアセンブリにコンパイルするように指定します。 これによって、実行速度は速くなりますが、起動時間が長くなります。 CompileToAssembly メソッドを呼び出している場合は、この値を Options プロパティに割り当てないでください。
roSingleLine 単一行モードを指定します。 ドット(.)の意味が、(\n 以外のすべての文字ではなく)すべての文字にマッチするように変更されます。
roIgnorePatternSpace エスケープされていないホワイト スペースをパターンから除去し、# でマークされたコメントを有効にします。 ただし、IgnorePatternWhitespace 値は、文字クラス内のホワイト スペースには影響しません(文字クラス内のホワイト スペースは除去されません)。

See also:

検索 (その2)

文字列の出現位置を知りたいのであれば、

program Search;
{$APPTYPE CONSOLE}

begin
  var s := 'Hello, world.';
  var Idx := Pos('world', s);
  Writeln(Idx);
end.

TRegEx.Match().Index で取得できます。

program Search;
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  var Idx := TRegEx.Match(s, 'world').Index;
  Writeln(Idx);
end.

ヒットした文字列の長さは TRegEx.Match().Length で取れます。

See also:

置換

Delphi で文字列置換と言うと StringReplace() ではないでしょうか?

program Search; 
{$APPTYPE CONSOLE}

uses
  System.SysUtils;
  
begin
  var s := 'Hello, world.';
  s := StringReplace(s, 'world', 'delphi', [rfReplaceAll, rfIgnoreCase]);
  Writeln(s);
end. 

正規表現で書くとこうなります。

program Replace; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;
  
begin
  var s := 'Hello, world.';
  s := TRegEx.Replace(s, 'world', 'delphi', [roIgnoreCase]);
  Writeln(s);
end. 

全置換したくない場合には TRegEx のインスタンスを生成した上で、Count を指定できるオーバーロードメソッドを使う必要があり、ちょっとだけ面倒になります。

次のコードは文字列中の l の最初の一つだけを A に置換します。

program Replace; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  var RegEx := TRegEx.Create('l', [roIgnoreCase]);
  s := RegEx.Replace(s, 'A', 1);
  Writeln(s);
end.

あるいはこうなります。

program Replace; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  s := TRegEx.Create('l', [roIgnoreCase]).Replace(s, 'A', 1);
  Writeln(s);
end.

TRegEx はクラスではなく、高度なレコード型です。

See also:

置換 (その2)

検索にヒットした文字列を元に置換文字列を作るには、Evaluator を指定できるオーバーロードメソッドを使います。

次のコードは、カンマかピリオドがあれば、その後ろに改行コードを追加します。

program Replace;
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

type
  TDmyCls = class
    class function ReplaceMethod(const Match: TMatch): string;
  end;

{ TDmyCls }

class function TDmyCls.ReplaceMethod(const Match: TMatch): string;
begin
  result := Match.Value + sLineBreak;
end;

begin
  var s := 'Hello, world.';
  s := TRegEx.Replace(s, '[,\.]', TDmyCls.ReplaceMethod);
  Writeln(s);
end.

Evaluator は TMatchEvaluator 型 (メソッドポインタ) なので、VCL / Firemonkey アプリケーションからはともかく、コンソールアプリケーションだと書きにくいですね。

エスケープ処理

検索/置換を正規検索とプレーンな検索の両方で行ないたい場合、TRegEx.IsMatch() / Pos(), TRegEx.Replace() / StringReplace() と使い分けるのは面倒です。とはいえ、検索したい文字列に、正規表現では特殊な意味を持つ文字が含まれていると、使うメソッドを変えるか検索したい文字列を変更するしかありません。

例えば、次のような検索はエラーになります。

program Search; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;
  
begin
  var s := '*** Hello ***';
  if TRegEx.IsMatch(s, '***') then // '*' は特殊な意味を持つ (=正規表現検索)
    Writeln('Match.')
  else  
    Writeln('No match.');
end.                   

正規表現検索をプレーン検索に使いたい場合には TRegEx.Escape() を使います。

program Search; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;
  
begin
  var s := '*** Hello ***';
  if TRegEx.IsMatch(s, TRegEx.Escape('***')) then // エスケープ処理を通す (≒プレーン検索)
    Writeln('Match.')
  else  
    Writeln('No match.');
end.                   

これで使うメソッドを統一する事ができます。

See also:

エスケープ処理 (その2)

置換文字列では次の $ から始まるメタ文字を使う事ができます。

名前 説明
$number グループ番号 number (10 進数) と一致した部分文字列に置換します。グループ番号が存在しない場合は何もしません。
${name} グループ名 name と一致した部分文字列に置換します。グループ名が存在しない場合は何もしません。
$$ "$" に置換します。
$& 一致したパターン全体と同じパターンに置換します。
$` 一致した場所より前にある入力文字列のすべてに置換します。
$’ 一致した場所より後にある入力文字列のすべてに置換します。
$+ キャプチャした最後のグループに置換します。
$_ 入力文字列全体に置換します。

つまり、置換したい文字列にメタ文字が含まれる場合には $$ のようにエスケープする必要があります。次のコードは o$_$+ に置換しようとしています。

program Replace;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  var rStr := '$_$+';
  s := TRegEx.Replace(s, 'o', rStr, [roIgnoreCase]);
  Writeln(s);
end.

しかしながら結果は次のようになります。

HellHello, world.o, wHellHello, world.o, world.orld.

次のようにして置換文字列をエスケープすると、

  var rStr := '$_$+';
  rStr := StringReplace(rStr, '$', '$$', [rfReplaceAll]); // 追加
  s := TRegEx.Replace(s, 'o', rStr, [roIgnoreCase]);

希望通りの動作になります。

Hell$_$+, w$_$+rld.

TRegEx.Escape() は $ から始まるメタ文字以外もエスケープしてしまうので、置換文字列のエスケープには使えません。

  var rStr := '$_$+';
  rStr := TRegEx.Escape(rStr); // NG
  s := TRegEx.Replace(s, 'o', rStr, [roIgnoreCase]);

+ が消えてしまいます。

Hell$_$o, w$_$orld.

おわりに

正規表現を使った所で、大したコード量の違いにはなりませんので、積極的に使ってみてはいかがでしょうか?

See also:

5
0
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
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?