CodeZine(コードジン)

特集ページ一覧

.NETでの正規表現の使用法

.NETに新しく装備された正規表現の概要と使用例

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/07/22 12:00

正規表現については既に多くの記事が書かれていますが、本稿の目的は.NETに新しく装備された正規表現の概要を解説し、これらの用途および使用法に関する簡単なガイドラインを示すことです。本稿の読者としては、正規表現についての基礎知識がある方を想定しています。

はじめに

 正規表現については既に多くの記事が書かれていますが、本稿の目的は.NETに新しく装備された正規表現の概要を解説し、これらの用途および使用法に関する簡単なガイドラインを示すことです。本稿の読者としては、正規表現についての基礎知識がある方を想定しています。

 正規表現の初心者の方は、Regular Expressions Article Indexをご覧ください。入門者レベルの解説記事としては、An Introduction to Regular Expressions with VBScriptをお勧めします。

 私は以前、VBScriptやJScriptのコード内で正規表現を使用できるくらいの基礎知識は持っていても、いざサンプルやドキュメントで正規表現が使われていると、その理解に苦労することがたびたびありました。たとえば前後読み(lookaround)や名前付きキャプチャ(named capture)などという新機能には、かなり悩まされたのを憶えています。これに加えて、正規表現に関するドキュメントそのものが数少なく、そのほとんどにはサンプルコードが付いていませんでした。そのため、以前は.NETプロジェクトで正規表現を使用することを最初から避けていました。

 本稿では、このような新機能をいくつか紹介するとともに、読者の皆さんがかつての私と同じ苦労をせずに済むよう、わかりにくい部分を解説したいと思います。

マッチング: Groupsと名前付きキャプチャ

 正規表現を実際に使った経験があれば、カッコで指定されたキャプチャを$1...$Nという表記で参照するという手法はお馴染みのものでしょう。この手法は後方参照と呼ばれます。次のVB.NETのサンプルコードは、この手法の例を示しています。

Dim userName As String = "Neimke, Darren"
Dim re As New RegEx( "(\w+),\s(\w+)" )
userName = re.Replace( userName, "$2 $1" )
Response.Write( userName )

 ここで使用したパターンは、コンマとスペースで区切られた2つの英単語にマッチし、ユーザーの名前と名字をキャプチャした上で、名字と名前の順番を入れ換えたフォーマットで出力します。私の名前であれば、ブラウザには「Darren Neimke」と表示されるはずです。

 このReplaceステートメントで使われている$Nという表記は、N番目のカッコ部分(キャプチャ)を参照します。特に.NETの場合に注意してほしいのは、ゼロ番目の要素($0)はマッチしたテキスト全体を参照するという点です。上記の例で言えば「Neimke, Darren」というテキストがこれに該当します。

 現行のRegexクラスには、単純なステートメントをインライン化できる便利な共有(静的)メンバが用意されており、これを使用すると、先のサンプルで見たような無駄に長いコードを書かなくても済みます。こうした有用な静的メンバには、IsMatchMatchMatchesReplaceSplitがあります。この構文を用いると、先のコードは次のように簡素化されます。

Dim userName As String = "Neimke, Darren"
userName = Regex.Replace( userName, "(\w+),\s(\w+)", "$2 $1" )

 こうした静的メンバのもたらすメリットは、次のようなケースでも実感できます。このコードでは、何らかの処理を施す前に、処理対象の文字列中に数値が含まれているかどうかをIsMatch()を用いて確認しています。

If Regex.IsMatch( userInputString, "\d+(\.?\d+)" ) Then
    ' perform some conversion and math operations here
End If

 .NET以前の正規表現では、Matchオブジェクト中に多くのSubMatchesが含まれていました。こうした状況は.NETでも変わりませんが、.NETではGroupsと呼ばれます。GroupsMatchオブジェクトのコレクションプロパティの1つであり、キャプチャした個々のグループについては、次のようにインデックス指定でアクセスできます(インデックス0はマッチした文字列全体を参照します)。

Dim userName As String = "Neimke, Darren"
Response.Write( Regex.Match( username, "(\w+),\s(\w+)" ).Groups(2).ToString() )

 この結果、キャプチャしたGroupsのインデックス2に該当するテキスト「Darren」が表示されます。

名前付きキャプチャ

 さらに(?<nameOfGroup>...)または(?'nameOfGroup'...)という構文を用いて、Groups内の要素に名前を割り当てることができます。私としては、Perlなどで使われている正規表現との整合性を保つ意味から、最初の構文の方が好みであり、一般的にもこちらの構文の方が多く用いられています。名前を割り当てておくと、コードの意味が把握しやすくなり、メンテナンスの面でも有利に働きます。次のサンプルでは、2つのキャプチャに名前を割り当てています。

Dim userName As String = "Neimke, Darren"
Dim pattern As String = "(?<surname>(\w+)),\s(?<firstname>(\w+))"
Response.Write( Regex.Match( userName, pattern ).Groups("firstname").ToString() )

 この結果、「Darren」が表示されます。

キャプチャの回避

 マッチングは有用な機能である反面、パフォーマンスの悪化を伴います。VBScriptやJScriptでは、正規表現パターン中にカッコを使用すると必ずキャプチャが行われます。しかし、カッコを使用する必要はあるが、該当データをキャプチャする必要はない、というケースも考えられます。たとえば「Let's go this way」か「Let's go that way」のいずれかにマッチさせたいのであれば、次のような正規表現で検索することになるでしょう。

Let\'s go th(is|at) way

 カッコと縦棒の組み合わせは、オプション指定であることを意味し、この場合で言えば「th」の後に「is」か「at」のいずれかが続く場合にマッチします。ここで問題なのは、キャプチャしたテキスト(つまり「is」または「at」)が後方参照用に記憶される分だけ、パフォーマンスが悪化するという点です。

 幸い、.NETの正規表現では(?:...)という構文が用意されています。この構文を使用すると、検索条件のグループ化だけを行い、該当テキストを後方参照用にメモリに格納せずに済むので、パフォーマンスの悪化を回避できます。この構文を用いると、前述の例は次のようになります。

Let\'s go th(?:is|at) way

 このパターンは、次のどちらのテキストにもマッチします。

  • "Let's go this way"
  • "Let's go that way"

 ただし、マッチしたテキスト全体を参照するGroups(0)だけはメモリに格納されます。それでも、この構文を用いるとパフォーマンスの悪化を回避することができ、特に、長めのテキストを複雑なパターンを適用する場合には効果が大きいはずです。

前後読み

 前後読み(先読み/戻り読み)の機能は、JScriptでは部分的に実装されていますが、VBScriptには実装されていません。前後読みでは、先読み(lookahead)と戻り読み(lookbehind)という2種類の方向を指定でき、それぞれの方向に対して肯定表明(positive assertion)と否定表明(negative assertion)を指定できます。各々の構文は次のとおりです。

  • (?=...) - 先読みの肯定
  • (?!...) - 先読みの否定
  • (?<=...) - 戻り読みの肯定
  • (?<!...) - 戻り読みの否定

 先読みと戻り読みの使い方を把握するには、マッチテキストとマッチ位置の意味を理解しなければなりません。最初に覚えておいてほしいのは、前後読みとは無駄を省くための機能であるという点です。この意味を説明するために、まずは次の単純なサンプルを見てみましょう。

pattern = "test"
text = "testing"

 上記のパターンとテキストでマッチングを実行すると、最終的に正規表現パーサのコンテキストは文字列「testing」の2番目の「t」と「i」の間に移動します。これは、正規表現パーサが次のように文字列の先頭から順次マッチングを進めていくからです(^はパーサの処理位置を示します)。

  1. マッチの開始 - ^testing
  2. 「t」のマッチ - t^esting
  3. 「e」のマッチ - te^sting
  4. 「s」のマッチ - tes^ting
  5. 「t」のマッチ - test^ing

 いったんパーサが通過した後に、位置をさかのぼってマッチングの開始位置に戻るような方法は存在しません。こうした制限がどのような問題を引き起こすかは、「tested」という文字列の中の「test」だけを検索したいが「tester」などの文字列は検索対象外にしたい、というケースを考えてみればわかるでしょう。しかし、ここで先読みを用いれば、(?=tested\b)testというパターンを指定して、こうしたマッチングを実行することができます。

 これが機能する理由は、前後読みの場合の正規表現パーサは、マッチングの開始位置を文字列の先頭に限定しないためです。この機能が特に役立つのは、先読みと戻り読みを組み合わせて、ドキュメント中の位置検索をするような場合です。具体的な例として、文字列「protested」の中の「test」を検索したいが文字列「detested」は検索の対象外としたい、というケースを考えてみましょう。これを実行するには、戻り読みの否定に「de」を指定し、先読みの肯定に「tested」を指定して、(?<!de)(?=tested\b)testとします。

 このタイプの検索は、「テキスト中の指定位置からマッチを始めさせる機能」と言い換えることもできるでしょう。上記のパターンであれば、正規検索パーサの位置は、対象となる文字列「protested」の中を次のように移動していきます。

  1. マッチの開始 - pro^tested
  2. 「t」のマッチ - prot^ested
  3. 「e」のマッチ - prote^sted
  4. 「s」のマッチ - protes^ted
  5. 「t」のマッチ - protest^ed

 前後読みの使用例としては、たとえば「パスワードには8から20個のキャラクタを使用できるが、最低2個の文字および2個の数値を含んでいる必要がある。使用できるのは文字と数字だけである」というように、特殊なパスワード指定を検証する場合も挙げられます。

 このようなパスワード制限に合致している文字列であれば、次のパターンでマッチするはずです。^(?=.*?\d.*?\d)(?=.*?\w.*?\w)[\d\w]{8,20}$

信頼性と保守性

 私が個人的に気に入っている新機能の1つに、正規表現中にコメントを埋め込むというものがあります。たとえば、次のような正規表現パターンに遭遇したとします。

Dim re As New Regex( "(?<=(#|@))(?=\w+)\w+\b", RegexOptions.Multiline )

 運が良ければ、この正規表現パターンの使用目的についてのコメントが書かれている場合もあるかもしれませんが、このパターンを修正するような場合、途中で何が何を意味しているのか訳がわからなくなって、最初から書き直した方が早い、という事態に陥ってしまうのではないでしょうか。しかし.NETでは、RegExOptions.IgnorePatternWhitespaceコンパイラオプションと(?#...)構文を用いることで、正規表現パターンにコメントを埋め込むことができます。

 この機能を利用すると、疑似コード的にコメントを埋め込んで、コードの読みやすさを向上させることができます。

Dim re As New Regex ( _
    "(?<=        (?# Start a positive lookBEHIND assertion ) " & _
    "(#|@)       (?# Find a # or a @ symbol ) " & _
    ")           (?# End the lookBEHIND assertion ) " & _
    "(?=         (?# Start a positive lookAHEAD assertion ) " & _
    "    \w+     (?# Find at least one word character ) " & _
    ")           (?# End the lookAHEAD assertion ) " & _
        "\w+\b   (?# Match multiple word characters leading up to a word boundary)", _
    RegexOptions.Multiline Or RegexOptions.IgnoreCase Or RegexOptions.IgnoreWhitespace _
)

デリゲート

 最後に.NET Frameworkの追加機能の中で最も有用なものとして、Regex.Replace()メソッドの置換引数としてデリゲートが使えることを紹介しておきます。これが何を意味するのかについては、まず次のコードを見てみましょう。

Dim myString As String = RegEx.Replace( "a true taste of the temperature", "t.*?e\b", "a" )

 この置換処理を行うと、myStringの値は「a a a of a a」となるはずですが、この場合どのような処理が行われたのかについては明らかでしょう。正規表現パーサは、検索対象の文字列内でパターンに該当する文字列を検出するごとに、その部分を「a」に置換したのです。このように単純な置換をするだけでよいのならば、特に問題はありませんが、何らかのビジネスロジックを組み込んだり、部分的にマッチした箇所にある種の操作を加えたい場合はどうでしょうか?

 これに該当する事例としては、英文中のすべての単語の先頭文字を大文字に変更するという処理が考えられます。その場合は、おそらく\b(\w)(\w+)?\bという正規表現のパターンを組むことになるでしょう。そして検索条件に一致した文字列のうち、先頭文字として部分マッチしたアルファベットを対応する大文字に変更し、残りの部分と合わせてStringBuilderにより文字列として再構成する、という手順をとるのではないでしょうか。

mc = re.Matches( bodyOfText )
Dim m As Match
For Each m In mc

   sb.AppendFormat("{0}{1}", m.Groups(1).Value.ToUpper(), m.Groups(2).Value)
Next

 変換対象となる文字列がアルファベットだけを含んでいるならばこの方式でも問題はないはずですが、「~~~ This %%% is ### a chunk of text」というように、アルファベット以外の記号類も使われている場合はどうでしょうか。置換を実行してみると、記号類がすべて消え去り、「ThisIsAChunkOfText」という文字列ができてしまうはずです。こうした難点を回避する方法は多数存在しますが、通常はより複雑なパターンを指定して、アルファベットの置き換え処理を多重化することで対策するといったところでしょう。

 よりエレガントな方式は、MatchEvaluatorデリゲートを利用することです。MatchEvaluatorとは、「OnMatchイベント」の発生に応じてトリガーされる一種のイベントハンドラだと見なすことができます。MatchEvaluatorにハンドラ関数へのポインタ(参照)を渡しておくと、検索がヒットするたびに、この関数が呼び出されることになります。この関数の唯一の引数には、Matchパラメータを指定し、正規表現を行う呼び出し元のReplaceメソッドにはString値を返す必要があります。この方法による置換は、Replaceメソッドで行う処理を把握しやすいというメリットがあり、Replaceメソッド呼び出し内部で完結できるので、先の例のように文字列を再構成する手間も省けます。

 この方式の具体例として、先のサンプルコードを書き換えて、デリゲートを用いて単語の先頭部を大文字化するように変更してみましょう。

Sub Page_Load(sender as Object, e as EventArgs)
    Dim myDelegate As New MatchEvaluator( AddressOf MatchHandler )
    Dim sb As New System.Text.Stringbuilder()
    Dim bodyOfText As String = _
        "~~~ This %%% is ### a chunk of text."
        
    Dim pattern As String = "\b(\w)(\w+)?\b"
    Dim re As New Regex( _
        pattern, RegexOptions.Multiline Or _
        RegexOptions.IgnoreCase _
    )
    Dim newString As String = re.Replace(bodyOfText, myDelegate)
        
    Response.Write( bodyOfText & "<hr>" & newString )
End Sub

Private Function MatchHandler( ByVal m As Match ) As String
    Return m.Groups(1).Value.ToUpper() & m.Groups(2).Value
End Function

 デモページを表示

 このように、コードの構造が非常に明確なものになり、置換ロジックは独立したハンドラメソッドで処理するようになっています。このように複雑な置換処理でも、可読性や保守性を損なわずに実装することができ、何よりもありがたいことに、文字列を再構成する際にデータの欠落が生じる危険性も減少するので、データの完全性を保証する上でも有益です。

まとめ

 文字列を処理する場合、初心者プログラマは、鈍重で込み入った処理のソリューションを組んでしまいがちですが、言語に精通しているプログラマであれば、たいがいのテキスト処理を、正規表現を用いた操作でこなしてしまうものです。

 .NETには、効率的かつ洗練された手法で正規表現を行うための機能が用意されています。たしかに正規表現の学習には時間がかかりますが、ひとたびマスターしてしまえば、正確かつ効率的なソリューションを構築できるようになります。

 サンプルASP.NET Webページにアクセスすると、本稿で解説した各種の新機能を用いたデモを使用することができます。このサンプルページでは、リモートのWebサーバーからHTMLデータを抽出して、先頭部がhttp://で始まっていないURLを使っているハイパーリンク部を探し、プレフィックスを挿入します。



  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

バックナンバー

連載:japan.internet.com翻訳記事

もっと読む

All contents copyright © 2005-2020 Shoeisha Co., Ltd. All rights reserved. ver.1.5